directory

  • Store attributes: Stores constants and variable values as part of an instance, provided only by classes and structs.
  • Computed properties: Computed values are not stored. Computed properties are provided by classes, structs, and enumerations.
  • Type attributes: Attributes associated with the type itself.
  • Property observer: Monitors changes in property values, which can be added to a custom storage property or to a property that a subclass inherits from a superclass. You can also reuse code in getters and setters for multiple properties using property wrappers.

Storage properties

Storage properties of a constant struct instance

If an instance of a structure is assigned to a constant, the instance’s properties cannot be modified, even if the properties are declared as variables:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property
Copy the code

This is because the structure is a value type. When an instance of a value type is marked as a constant, all of its attributes are marked as constants. This is not the case for classes that reference types. If you assign an instance of a reference type to a constant, you can still change the variable properties of that instance.

Lazy storage property

The initial value of the deferred storage property is not computed until it is first used. The lazy modifier is declared before the property is defined to indicate that it is a lazy storage property.

The ⚠️ lazy storage property must be declared as mutable using var because its initial value may not be checked after instance initialization is complete. Constant properties must always have a value until initialization is complete, so they cannot be declared lazy.

When to declare a property using lazy? Lazy is useful when the initial value of a property needs to be set up in a complex or computationally expensive way and is not executed until it is needed, similar to lazy loading when writing an OC, which is not initialized until it is used. As shown in the following example:

class DataImporter {
    /*
    DataImporter is a class to import data from an external file.
    The class is assumed to take a nontrivial amount of time to initialize.
    */
    var filename = "data.txt"
    // the DataImporter class would provide data importing functionality here
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // the DataManager class would provide data management functionality here
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property has not yet been created
Copy the code

The importer property uses the lazy keyword to indicate lazy loading, which is only loaded at access time because it may be a time-consuming operation and there is no need to initialize the DataImporter as soon as the DataManager is initialized. For example, when its filename property is queried:

print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"
Copy the code

If a property marked as lazy is accessed simultaneously by multiple threads and the property has not been initialized, there is no guarantee that the property will be initialized only once.

Calculate attribute

Computed properties do not store values. Instead, a getter and an optional setter are provided to retrieve and set other properties and values indirectly.

Struct Point {var x = 0.0, y = 0.0} struct Size {var width = 0.0, Height = 0.0} struct Rect {var origin = Point() var size = size () var center: Point { get { let centerX = origin.x + (size.width / 2) let centerY = origin.y + (size.height / 2) return Point(x: centerX, y: centerY) } set(newCenter) { origin.x = newCenter.x - (size.width / 2) origin.y = newCenter.y - (size.height / 2) } } } Var square = Rect(origin: Point(x: 0.0, y: 0.0), size: size (width: 10.0, height: 0) Square. Center = Point(x: 15.0, y: 15.0)) let initialSquareCenter = square.center square.center = Point(x: 15.0, y: 0) 16.0) print("square.origin is now at (\(square.origin), \(square.origine.y))") // Prints "square. Origin is now at (10.0, 10.0)"Copy the code

The Rect structure provides a calculated property called Center. As long as you know the origin and size of the Rect, you can determine the center position, so there is no need to store the center point as an explicit point value. Instead, Rect defines custom getters and setters for the computed variable named Center, which handles the center of the rectangle as if it were a stored property.

The above example creates a new Rect variable named square. The square variable has an initial value of (0,0) with a width and height of 10. This square is represented by the blue square in the figure below.

The center property of the square variable is then accessed via the dot syntax square.center, which causes the center getter to be called to retrieve the current property value. The getter does not return the existing value, but instead represents the center of the square by calculating and returning a new dot. As you can see from above, the getter correctly returns a center point (5,5).

The center property is then set to a new value (15,15) that moves the square up and right to the new position shown by the orange square in the figure. Setting the Center property calls the Setter for Center, which modifies the x and Y values of the stored Origin property and moves the square to its new location.

Shorthand Setter Declaration

If the setter for the evaluated property does not define a name for the newValue to set, the default name newValue is used. Here is an alternate version of the Rect structure that utilizes this shorthand notation:

struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}
Copy the code

Shorthand Getter declaration

If the entire body of the getter is an expression, the getter implicitly returns that expression. Here is another version of the Rect structure that makes use of this shorthand notation and the shorthand notation of the setter:

struct CompactRect { var origin = Point() var size = Size() var center: Point { get { Point(x: origin.x + (size.width / 2), y: origin.y + (size.height / 2)) } set { origin.x = newValue.x - (size.width / 2) origin.y = newValue.y - (size.height / 2) }}}Copy the code

Read-only This object indicates the compute attribute

A computed property with a getter but no setter is called a read-only computed property. Read-only computed properties always return a value and can be accessed via dot syntax, but cannot set a new value.

Computed properties (including read-only computed properties) must be declared as variable properties using the var keyword because their values are not fixed. The let keyword is used only for constant properties to indicate that they cannot be changed once their values are set as part of the instance initialization.

You can remove the get keyword and its curly braces to simplify the declaration of read-only computed attributes:

Struct Cuboid {var width = 0.0, height = 0.0, depth = 0.0 var volume: struct Cuboid {var width = 0.0, height = 0.0, depth = 0.0 Double {return width * height * depth}} let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 5.0) 2.0) print(" The volume of fourByFiveByTwo is \(fourbyfivebytwo. volume)") // Prints "the volume of fourByFiveByTwo is 40.0"Copy the code

In theory the size of this example is determined by the length, width and height, so it makes sense to set it to read-only.

Attribute viewer

Similar to KVO in OC, the property changes to listen for new and old values of the property. Attribute observers observe and respond to changes in attribute values. The property observer is called every time the value of the property is set, even if the new value is the same as the current value of the property.

Attribute observers can be added in the following locations:

  • Defining storage properties
  • Inherited storage properties
  • Inherited computed properties

For computed properties, use setters for properties to observe and respond to changes in value, rather than trying to create an observer.

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
Copy the code

Listen for changes in values using willSet and didSet.

  • willSetIs called before the value is stored.
  • didSetCalled immediately after the new value is stored.

When the attributes of the parent class are set in a subclass initializer, the willSet and didSet observers of the parent class attributes are called after the parent class initializer is called. When subclasses set their properties, they are not called until the parent class initializer is called.

Attribute wrapper

The property wrapper adds an intermediate layer between the code storing the property and the code defining the property, which handles logic such as thread-safety checks when the property is used.

define

The wrappedValue attribute needs to be defined in a structure, class, or enumeration, for example, a TwelveOrLess structure that returns a value that is always less than or equal to 12:

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}
Copy the code

Number uses the private declaration to ensure that externally accessible numbers can only be accessed through wrappedValue, not directly.

use

You can apply a wrapper to an attribute by writing the name of the wrapper to the attribute as an attribute. For example, the SmallRectangle structure, which stores the width and height of the rectangle:

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"
Copy the code

Although the height is set to 24, the result is still printed as 12.

The compiler synthesizes the logic behind the wrapper properties when applying them, as shown in the following example:

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}
Copy the code

The _height and _width attributes store an instance of the attribute wrapper TwelveOrLess. Getters and setters for height and width change access to the wrappedValue property.

Setting the initial value

TwelveOrLess is a TwelveOrLess structure, and SmallRectangle cannot specify a width and height. TwelveOrLess SmallNumber TwelveOrLess SmallNumber TwelveOrLess SmallNumber TwelveOrLess SmallNumber TwelveOrLess SmallNumber

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}
Copy the code

When no initial value is specified, Swift defaults to using the init() initializer to set the wrapper. Such as:

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
Copy the code

When specifying an initial value for a property, Swift uses the init(wrappedValue:) initializer to set the wrapper. Such as:

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"
Copy the code

When writing parameters in parentheses after custom attributes, Swift sets the wrapper with an initializer that accepts these parameters:

struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"
Copy the code

When the wrapper contains parameters and an initial value is specified for the property. Swift treats assignment as processing the wrappedValue argument and uses an initializer that accepts the contained argument. Such as:

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1"

mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12"
Copy the code

SmallNumber instances of the encapsulation height are created by calling SmallNumber(wrappedValue: 1), which uses the default maximum of 12. An instance of the wrapper width is created by calling SmallNumber(wrappedValue: 2, maximum: 9).

Map values from attribute wrappers

In addition to wrapping values, attribute wrappers can expose additional functionality by defining projected values. The following code adds a projectedValue property to the SmallNumber structure to track whether the property wrapper adjusts the new value of the property before storing the new value.

@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    init() {
        self.number = 0
        self.projectedValue = false
    }
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"
Copy the code

In this case, the attribute wrapper exposes a Boolean value as a projection value, indicating whether the number has been adjusted. The property wrapper can return any type of value as its projection value. If more information needs to be exposed, the wrapper can return an instance of some other data type directly, or it can return self to expose the instance of the wrapper as a projected value.

When a projected value is accessed from code belonging to this type, such as a property getter or instance method, self can be omitted. Before the attribute name, as if accessing any other attribute. In the code in the following example, the wrapper takes the projected height and width values as $height and $width:

enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}
Copy the code

If resize(to: At the end of resize(to:), the return statement checks $height and $width, To determine whether the property wrapper has adjusted the height or width.