作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Aleksandr Gaidukov
Verified Expert in Engineering

Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.

Expertise

PREVIOUSLY AT

Accenture
Share

In simple terms, 属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为. 如果需要约束可用的属性值,则使用它, 为读/写访问添加额外的逻辑(比如使用数据库或用户默认值), or add some additional methods.

Property Wrappers in Swift 5.1

This article is about a new Swift 5.一种包装属性的方法,它引入了一种新的、更简洁的语法.

Old Approach

假设您正在开发一个应用程序,并且您有一个包含用户配置文件数据的对象.

struct Account {
    var firstName: String
    var lastName: String
    var email: String?
}

let account = Account(firstName: "Test",
                      lastName: "Test",
                      email: "test@test.com")

account.email = "new@test.com"
print(account.email)

您需要添加电子邮件验证—如果用户电子邮件地址无效,则 email property must be nil. 这将是使用属性包装器封装此逻辑的好情况.

struct Email {
    private var _value: Value?
    
    init(initialValue value: Value?) {
        _value = value
    }
    
    var value: Value? {
        get {
            return validate(email: _value) ? _value : nil
        }
        
        set {
            _value = newValue
        }
    }
    
    private func validate(email: Value?) -> Bool {
        guard let email = email else { return false }
        let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}"
        let pred = NSPredicate(format: "SELF MATCHES %@", regex)
        return pred.evaluate(with: email)
    }
}

We can use this wrapper in the Account structure:

struct Account {
    var firstName: String
    var lastName: String
    var email: Email
}

现在,我们确定email属性只能包含有效的电子邮件地址.

Everything looks good, except the syntax.

let account = Account(firstName: "Test",
                      lastName: "Test",
                      email: Email(initialValue: "test@test.com"))

account.email.value = "new@test.com"
print(account.email.value)

With a property wrapper, the syntax for initializing, reading, and writing such properties becomes more complex. 那么,是否有可能避免这种复杂性并在不更改语法的情况下使用属性包装器? With Swift 5.1, the answer is yes.

The New Way: @propertyWrapper Annotation

Swift 5.1 提供创建属性包装器的更优雅的解决方案, where marking a property wrapper with a @propertyWrapper annotation is allowed. 与传统包装器相比,这种包装器具有更紧凑的语法, resulting in more compact and understandable code. The @propertyWrapper 注释只有一个要求:包装器对象必须包含一个名为a的非静态属性 wrappedValue.

@propertyWrapper
struct Email {
    var value: Value?

    var wrappedValue: Value? {
        get {
            return validate(email: value) ? value : nil
        }
        set {
            value = newValue
        }
    }
    
    private func validate(email: Value?) -> Bool {
        guard let email = email else { return false }
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = nspreate (format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}

要在代码中定义这样的包装属性,我们需要使用新的语法.

@Email
var email: String?

So, we marked the property with the annotation @. 属性类型必须与包装器的“wrappedValue”类型匹配. 现在,你可以像处理普通性质一样处理这个性质.

email = "valid@test.com"
print(email) // test@test.com
email = "invalid"
print(email) // nil

Great, it looks better now than with the old approach. 但是我们的包装器实现有一个缺点:它不允许包装值的初始值.

@Email
var email: String? = "valid@test.com" //compilation error.

要解决这个问题,我们需要向包装器添加以下初始化式:

init(wrappedValue value: Value?) {
    self.value = value
}

And that’s it.

@Email
var email: String? = "valid@test.com"
print(email) // test@test.com

@Email
var email: String? = "invalid"
print(email) // nil

The final code of the wrapper is below:

@propertyWrapper
struct Email {
    var value: Value?
    init(wrappedValue value: Value?) {
        self.value = value
    }
    var wrappedValue: Value? {
        get {
            return validate(email: value) ? value : nil
        }
        set {
            value = newValue
        }
    }
    
    private func validate(email: Value?) -> Bool {
        guard let email = email else { return false }
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = nspreate (format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}

Configurable Wrappers

Let’s take another example. 你正在编写一款游戏,你有一个存储用户分数的属性. 要求这个值大于等于0,小于等于100. You can achieve this by using a property wrapper.

@propertyWrapper
struct Scores {
    private let minValue = 0
    private let maxValue = 100
    private var value: Int
    init(wrappedValue value: Int) {
        self.value = value
    }
    var wrappedValue: Int {
        get {
            return max(min(value, maxValue), minValue)
        }
        set {
            value = newValue
        }
    }
}

@Scores
var scores: Int = 0

This code works but it doesn’t seem generic. 您不能使用不同的约束(不是0和100)来重用它。. Moreover, it can constrain only integer values. 最好有一个可配置的包装器,它可以约束任何符合Comparable协议的类型. 为了使包装器可配置,我们需要通过初始化器添加所有配置参数. If the initializer contains a wrappedValue 属性(属性的初始值),它必须是第一个参数.

@propertyWrapper
struct Constrained {
    private var range: ClosedRange
    private var value: Value
    init(wrappedValue value: Value, _ range: ClosedRange) {
        self.value = value
        self.range = range
    }
    var wrappedValue: Value {
        get {
            return max(min(value, range.upperBound), range.lowerBound)
        }
        set {
            value = newValue
        }
    }
}

To initialize a wrapped property, 我们在注释后面的括号中定义了所有配置属性.

@Constrained(0...100)
var scores: Int = 0

The number of configuration attributes is unlimited. 您需要在圆括号中以与初始化式相同的顺序定义它们.

Gaining Access to the Wrapper Itself

如果您需要访问包装器本身(而不是包装的值), you need to add an underscore before the property name. For instance, let’s take our Account structure.

struct Account {
    var firstName: String
    var lastName: String
    @Email
    var email: String?
}

let account = Account(firstName: "Test",
                      lastName: "Test",
                      email: "test@test.com")

account.email // Wrapped value (String)
account._email // Wrapper(Email)

我们需要访问包装器本身,以便使用我们添加到其中的附加功能. 例如,我们希望Account结构符合Equatable协议. Two accounts are equal if their email addresses are equal, and the email addresses must be case insensitive.

extension Account: Equatable {
    static func ==(lhs: Account, rhs: Account) -> Bool {
	 return lhs.email?.lowercased() == rhs.email?.lowercased()
    }
}

It works, 但这不是最好的解决方案,因为我们必须记住在比较电子邮件时添加一个小写()方法. A better way would be to make the Email structure equatable:

extension Email: Equatable {
    static func ==(lhs: Email, rhs: Email) -> Bool {
	 return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased()
    }
}

and compare wrappers instead of wrapped values:

extension Account: Equatable {
    static func ==(lhs: Account, rhs: Account) -> Bool {
	 return lhs._email == rhs._email
    }
}

Projected Value

The @propertyWrapper 注释提供了另一个语法糖——投影值. This property can have any type you want. To access this property, you need to add a $ prefix to the property name. 为了解释它是如何工作的,我们使用Combine框架中的一个示例.

The @Published 属性包装器为属性创建发布者,并将其作为投影值返回.

@Published
var message: String

print(message) // Print the wrapped value
$message.sink { print($0) } // Subscribe to the publisher

As you can see, we use a message to access the wrapped property, and a $message to access the publisher. What should you do to add a projected value to your wrapper? Nothing special, just declare it.

@propertyWrapper
struct Published {
    private let subject = PassthroughSubject()
    var wrappedValue: Value {
	didSet {
	    subject.send(wrappedValue)
	}
    }
    var projectedValue: AnyPublisher {
	subject.eraseToAnyPublisher()
    }
}

As noted earlier, the projectedValue property can have any type based on your needs.

Limitations

新的属性包装器的语法看起来不错,但也有一些限制, the main ones being:

  1. They can’t participate in error handling. 被包装的值是一个属性(而不是一个方法),我们不能将getter或setter标记为 throws. For instance, in our Email 例如,如果用户试图设置无效的电子邮件,则不可能抛出错误. We can return nil or crash the app with a fatalError() call, which could be unacceptable in some cases.
  2. Applying multiple wrappers to the property is not allowed. For example, it would be better to have a separate @CaseInsensitive wrapper and combine it with an @Email wrapper instead of making the @Email wrapper case insensitive. 但是这样的结构是被禁止的,并且会导致编译错误.
@CaseInsensitive
@Email
    	var email: String?

作为这种特殊情况的变通方法,我们可以继承 Email wrapper from the CaseInsensitive wrapper. However, 继承也有限制——只有类支持继承, and only one base class is allowed.

Conclusion

@propertyWrapper annotations simplify the property wrappers’ syntax, 我们可以用与普通属性相同的方式来操作包装属性. This makes your code, as a Swift Developer more compact and understandable. 同时,它也有一些我们必须考虑到的限制. 我希望在未来的Swift版本中能够修正其中的一些错误.

如果你想了解更多Swift属性,点击这里 the official docs.

Understanding the basics

  • What is a property wrapper in Swift?

    属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为.

  • Why do we need property wrappers?

    如果需要约束可用的属性值,则使用属性包装器, change read/write access (like using DB or other storage), or add some additional methods like value validation.

  • 哪个版本的Swift包含了@propertyWrapper注释?

    The @propertyWrapper annotation is available in Swift 5.1 or later.

  • What limitations does the wrapper have?

    They can’t participate in error handling, 并且不允许对属性应用多个包装器.

Hire a Toptal expert on this topic.
Hire Now
Aleksandr Gaidukov

Aleksandr Gaidukov

Verified Expert in Engineering

Phuket, Thailand

Member since August 31, 2016

About the author

Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

PREVIOUSLY AT

Accenture

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.