点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新
本文是 《Swift 100 Days》系列的的第 15 天, Swift 100 Days 是笔者记录自己 Swift 学习的记录,欢迎各位指正。
结构和类(统称为“类型”)可以有自己的变量和常量,这些统称为属性。
允许将值附加到类型上并以唯一地表示。
在结构体与对象 三个小节中,我们提及了属性的使用。
编程离不开对象,对象离不开属性!
Swift 中有两种属性:存储属性,它们将状态和对象相关联;计算属性,则根据该状态执行计算。
属性观察器
当你声明一个存储属性,你可以使用闭包定义一个 属性观察器,该闭包中的代码会在属性被设值的时候执行。Swift 允许你观察属性即将更改或已更改。
有两种类型的属性观察器:willSet 和 didSet,它们在属性更改之前或之后被调用。在 willSet 中 Swift 为你的代码提供了一个叫做 newValue 的特殊值,表明属性即将变成的新值,而在 didSet 中你会得到 oldValue 来表示更新之前的值。
struct Product {
var price: Double {
willSet {
print("changing from \(price) to \(newValue)")
}
didSet {
print("changed from \(oldValue) to \(price)")
}
}
}
var product = Product(price: 30.0)
product.price = 28.0
product.price = 35.0
//changing from 30.0 to 28.0
//changed from 30.0 to 28.0
//changing from 28.0 to 35.0
//changed from 28.0 to 35.0
任何功能都离不开实践,没有实践意义的功能,可能不是迫切的等待你学习。
增加值约束
使用属性观察器来对类型接受的值增加额外的约束。
我们为我们的产品添加一个最低/高价。当值小于/大于最低/高价时,将其重置为最低/高价。
struct Product {
var price: Double {
willSet {
print("changing from \(price) to \(newValue)")
}
didSet {
print("changed from \(oldValue) to \(price)")
if self.price < 30.0 {
self.price = 30.0
}
}
}
}
var product = Product(price: 30.0)
product.price = 28.0
product.price = 35.0
//changing from 30.0 to 28.0
//changed from 30.0 to 28.0
//changing from 30.0 to 35.0
//changed from 30.0 to 35.0
在观察器内部设置属性不会触发额外的回调,上面的代码不会产生无限循环。
我们之所以不使用 willSet 观察器是因为即使我们在其回调中进行任何赋值,都会在属性被赋予 newValue 时覆盖。
当然,这次赋值最终还是执行了的。只不过赋值成为我们想要的值。如果我们想要指定一个如果设置错误就报错的机制的话。使用 didSet 观察器达不到这个效果。
避免不需要的刷新
使用属性观察器来对避免不需要的刷新。或者说我们总在需要的时候才需要更新数据。
如果我们的 UI 会因为某个值而改变。那么我们应该尽量当且仅当值改变的时候才去更新这个页面,而不是约束在变量的赋值操作。
what new
在 Swift 5.3 中,属性观察器可以附加到惰性属性上,这使得我们可以自动地观察何时给给定属性分配了新值,即使它的值在第一次访问时是惰性加载的。
总结
使用 willSet 和 didSet 进行属性观察提供了一种非常强大的方法,可以轻松观察值的变化,而无需任何其他类型的抽象。它们可用于确保我们基于单一的事实来源来驱动逻辑,并允许我们在给定属性更改时以反应方式更新其他值和状态。
计算属性
计算属性是一个计算并返回值的属性,而不仅仅只是存储它。
计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
计算属性的 getter 和 setter
简要介绍一下我们使用的语法:
var property:type {
get {
code
}
set(value) {
code
}
}
计算属性首先是一个变量,如果一个常量,并没有必要当做一个计算属性。
其次,set 方法中的 value 既可以显示声明,也可以隐式声明
set {
// Use `newValue` in here (隐式)
}
set(newString) {
// Use `newString` in here (显示)
}
只读计算属性
只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
var sellPrice: Double {
get {
return Double(count) * price
}
}
var description: String {
return "iOS 成长指北"
}
var name: String {
"iOS 成长指北"
}
自 Swift 5.1 以来,为了清晰、简洁和表达,单行表达式可以省略显式返回。
使用函数返回值,同样也能达到相同的效果。在 Swift 中,在计算属性和方法之间进行选择时,我会在以下情况下选择使用只读计算属性:
- 我所定义的 API 返回某种形式的关于对象或值状态的信息。
- 其计算属性的时间复杂度为
O(1)——不需要进行过于冗杂的逻辑计算
let sellPrice = product.sellPrice()
//or
let sellPrice = product.sellPrice
惰性/延迟加载计算属性
惰性属性可以使你的 iOS 开发更高效,更容易阅读。
Day9 结构体和对象 中 我们介绍了如何在 Swift 中使用 lazy 处理存储属性。但是如果简单使用 lazy 关键字来修饰计算属性,毫无作用
class Circle {
var radius:Double = 0
lazy var circumference:Double {
Double.pi * 2 * radius
}
}
当我们兴致昂昂的写下上述代码时,编译器报错
// Output: 'lazy' may not be used on a computed property
// Output: Lazy properties must have an initializer
那我们怎么实现一个惰性/延迟加载的计算属性呢
我们可以这么操作。
class Circle {
var radius:Double = 0
lazy var circumference:Double = {
Double.pi * 2 * self.radius
}()
}
惰性/延迟加载的计算属性现在使用闭包来进行延迟计算。
惰性/延迟加载计算的属性仅计算一次!
你需要时刻注意什么情况下才需要使用这种方式。
一般我们推荐将其作为语法糖来实现代码级别的 UI 绘制
lazy var label:UILabel = {
UILabel.init()
}()
延迟加载延迟初始化属性,直到它第一次被访问。计算属性不允许使用 lazy 关键字,因此可以使用隐式调用的闭包来获得相同的效果。
属性包装器(Property Wrappers)
在 WWDC 2019 上,随着对 SwiftUI 的介绍,我们第一次在 Swift 中看到了属性包装器。
从 Swift 5.1 以后,Swift 家族有了一系列 @s。
属性包装器除了在 SwiftUI 中十分有用,在实际的 Swift 编程中也很有作用。
在我们学习 SwiftUI 之前,我们先对属性包装器有一个具象的认知。
什么是属性包装器?
Swift 中的属性包装器允许你在不同的包装器对象中提取公共逻辑。
属性包装器可以看作是一个额外的层,它定义了如何在读取时存储或计算属性。它对于替换属性的 getter 和 setter 中的重复代码特别有用。
透明包装(Transparently Wrapping)
我们定义一个用来实现名称首字母大写的属性包装器
@propertyWrapper struct Capitalized {
var wrappedValue: String {
didSet {
wrappedValue = wrappedValue.capitalized
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.capitalized
}
}
注意:我们需要显式地将传入初始化器的任何字符串大写,因为属性观察者只有在值或对象完全初始化之后才会触发。
我们定义两个结构体,一个是 User,一个是Book。并且这两种结构 name 属性的首字母都需要大写。
如果不使用属性包装器,我们可以使用属性观察器,在 didSet 中重新赋值。
但我们可以这么使用
struct User {
@Capitalized var name: String
}
let user = User(name: "iOS 成长指北")
struct Book {
@Capitalized var name: String = "iOS 成长指北"
}
let book = Book()
print(user.name)
print(book.name)
//Ios 成长指北
//Ios 成长指北
这样,当我们需要我们的属性具备这个效果时,我们只需要在定义属性时加一个 @Capitalized 前缀就行了。
属性包装器和 UserDefaults
属性包装器也可以拥有自己的属性,从而可以进行进一步的自定义,甚至可以将依赖项注入到我们的包装器类型中。
Swift 中有一种存储功能 UserDefaults,一个用户默认数据库的接口,你可以在应用程序的启动过程中持久化存储键值对。
let defaults = UserDefaults.standard
defaults.set(25, forKey: "Age")
defaults.set("iOS 成长指北", forKey: "name")
let age = defaults.integer(forKey: "Age") // 25
let name = defaults.string(forKey: "name") //iOS 成长指北
这样做通常需要编写某种形式的映射代码,以便将每个值与其底层的 UserDefaults 存储进行同步——通常这需要为我们要存储的每个数据段进行复制。并且根据类型的不同需要使用不同的获取方法。
但是,通过属性包装器,可以使我们的方法更加的优雅
@propertyWrapper struct UserDefaultsBacked<Value> {
let key: String
var storage: UserDefaults = .standard
var wrappedValue: Value? {
get { storage.value(forKey: key) as? Value }
set { storage.setValue(newValue, forKey: key) }
}
}
当我们需要使用时,我们可以为需要存储的数据
@UserDefaultsBacked<String>(key: "name") var name
@UserDefaultsBacked<Int>(key: "age") var age
这样,当我们赋值时,会存储当前的对象,读取值时,也很方便,并且使用是,可以直接点语法获取。
现在,这些知识已经足够我们编写足够简洁的代码了。
感谢你阅读本文! 🚀