作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.
In simple terms, 属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为. 如果需要约束可用的属性值,则使用它, 为读/写访问添加额外的逻辑(比如使用数据库或用户默认值), or add some additional methods.
This article is about a new Swift 5.一种包装属性的方法,它引入了一种新的、更简洁的语法.
假设您正在开发一个应用程序,并且您有一个包含用户配置文件数据的对象.
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.
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 @
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)
}
}
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. 您需要在圆括号中以与初始化式相同的顺序定义它们.
如果您需要访问包装器本身(而不是包装的值), 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
}
}
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.
新的属性包装器的语法看起来不错,但也有一些限制, the main ones being:
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.@CaseInsensitive
wrapper and combine it with an @Email
wrapper instead of making the @Email
wrapper case insensitive. 但是这样的结构是被禁止的,并且会导致编译错误.@CaseInsensitive
@Email
var email: String?
As a workaround for this particular case, we can inherit the Email
wrapper from the CaseInsensitive
wrapper. However, 继承也有限制——只有类支持继承, and only one base class is allowed.
@propertyWrapper
annotations simplify the property wrappers’ syntax, 我们可以用与普通属性相同的方式来操作包装属性. This makes your code, as a Swift Developer more compact and understandable. 同时,它也有一些我们必须考虑到的限制. 我希望在未来的Swift版本中能够修正其中的一些错误.
如果你想了解更多Swift属性,点击这里 the official docs.
属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为.
如果需要约束可用的属性值,则使用属性包装器, change read/write access (like using DB or other storage), or add some additional methods like value validation.
The @propertyWrapper annotation is available in Swift 5.1 or later.
They can’t participate in error handling, 并且不允许对属性应用多个包装器.
Located in Phuket, Thailand
Member since August 31, 2016
Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.
World-class articles, delivered weekly.
World-class articles, delivered weekly.
Join the Toptal® community.