本文分兩部分,第一部分是介紹常用的屬性包裝器,第二步部分是自定義屬性包裝器 + 動態(tài)屬性分析
一任斋、SwiftUI常用的屬性包裝器:
@AppStorage
: 全局生效(除App層級),全局發(fā)送更新通知耻涛,直接操作UserDefaults
生效;可存儲配置(輕量)數(shù)據(jù);
@SceneStorage
: 作用域位為所有SwiftUI視圖废酷,可在界面內(nèi)存儲輕量數(shù)據(jù),界面注銷(非app關(guān)閉)則數(shù)據(jù)清除抹缕;
@State
: 作用域位為SwiftUI視圖模塊內(nèi)澈蟆,僅支持值類型;
@ObservedObject
:作用域位為SwiftUI視圖模塊內(nèi)卓研,支持class對象趴俘,作為小范圍內(nèi)的初始數(shù)據(jù)源,視圖刷新會銷毀重建鉴分;
@StateObject
: 同@ObservedObject
哮幢,但是屬于靜態(tài)變量,視圖刷新不會銷毀志珍;
@EnvironmentObject
: 自定義環(huán)境對象橙垢,使用時需注入給具體視圖,可減少初始化伦糯,方便切換不同數(shù)據(jù)源等;
@Environment
: 系統(tǒng)環(huán)境變量柜某,不需要注入(若手動增加變量嗽元,則仍需注入給具體視圖)
@FocusedValue
: 用于輸入框的綁定/讀取輸入內(nèi)容
由于結(jié)構(gòu)體內(nèi)的屬性不可變,所以當(dāng)想創(chuàng)建可變屬性時喂击,需要使用mutating關(guān)鍵字剂癌,但swift不允許我們編寫mutating var body: some View,那怎么改變視圖呢翰绊,這里就需要屬性包裝器了
@AppStorage:
@frozen @propertyWrapper public struct AppStorage<Value> : DynamicProperty {
// 包裝屬性
public var wrappedValue: Value { get nonmutating set }
// 投影屬性 可使用 $ 加在屬性前來使用
public var projectedValue: Binding<Value> { get }
}
// 創(chuàng)建包裝屬性
let wrapped_age = AppStorage(wrappedValue: 12, "age");
@AppStorage("age") var age = "12"
- 用于操作UserDefaults的屬性包裝器佩谷,
- App層級注冊屬性無效,
- 在SwiftUI和UIView視圖中都可以生效,
- 直接修改UserDefaults可以對SwfitUI中綁定生效
- 目前僅支持:Bool监嗜、Int谐檀、Double、String裁奇、URL桐猬、Data(UserDefaults支持更多的類型)。
- @AppStorage還支持符合RawRepresentable協(xié)議且RawValue為Int或String的數(shù)據(jù)類型刽肠。
@SceneStorage
@SceneStorage("selected") var index = 0
- 同@AppStorage十分類似溃肪,不過其作用域僅限于當(dāng)前Scene
- Scene退出時若app未退出,則數(shù)據(jù)失效
- app退出時數(shù)據(jù)會持久化
@ObservedObject
class Person:ObservableObject{
@Published var name = "張三"
}
@ObservedObject var p = Person()
- ObservableObject 協(xié)議要求實現(xiàn)類型是 class音五,它需要實現(xiàn)一個屬性:
objectWillChange = ObservableObjectPublisher()
惫撰,使用@Published可以自動實現(xiàn)。 - 在數(shù)據(jù)將要發(fā)生改變時放仗,會向外進(jìn)行“廣播”
- 只是作為View的數(shù)據(jù)依賴润绎,包裝器被動態(tài)屬性池持有,但是包裝器內(nèi)的動態(tài)屬性不被持有诞挨,View更新視圖時視圖狀態(tài)(值)重新獲取,ObservedObject對應(yīng)的動態(tài)屬性可能會被銷毀重建呢蛤,
@StateObject
- StateObject行為類似ObservedObject對象
- 動態(tài)屬性為只讀屬性惶傻,只能修改動態(tài)屬性的子屬性
- 針對引用類型設(shè)計,當(dāng)View更新時其障,實例不會被銷毀银室,和視圖的動態(tài)屬性池綁定,
...
寫了幾個小時文章励翼,發(fā)布的時候內(nèi)容不見了蜈敢,吐了,不想寫了汽抚,直接貼代碼看吧Wハ痢!造烁!
注意:
避免非必要的包裝器聲明:只要聲明了否过,就算不使用午笛,其發(fā)送的更新通知,會導(dǎo)致View發(fā)生不必要的更新苗桂。
二药磺、DynamicProperty運(yùn)作原理
-
@propertyWrapper
:聲明,聲明了包裝值和投影值煤伟,- 需要包裝類
- 需要包裝值
-
DynamicProperty
:動態(tài)屬性協(xié)議癌佩,包裝器和動態(tài)屬性并不一樣,但大多是關(guān)聯(lián)在一起的便锨,-
update
(可省略)函數(shù)围辙,更新發(fā)布器的被訂閱值, -
_makeProperty
: 該函數(shù)的默認(rèn)實現(xiàn)只在包裝器內(nèi)生效鸿秆,包裝器有確定的包裝值即動態(tài)值酌畜,該函數(shù)將動態(tài)屬性加入到視圖的動態(tài)屬性池并與視圖狀態(tài)關(guān)聯(lián),因而更新動態(tài)值可以更新視圖卿叽;
一般propertyWrapper用于修飾繼承DynamicProperty(動態(tài)屬性協(xié)議)的struct桥胞,
-
DynamicProperty協(xié)議:
public protocol DynamicProperty {
// 關(guān)聯(lián)動態(tài)屬性和視圖
static func _makeProperty<V>(in buffer: inout _DynamicPropertyBuffer, container: _GraphValue<V>, fieldOffset: Int, inputs: inout _GraphInputs)
// 屬性行為
static var _propertyBehaviors: UInt32 { get }
// 更新屬性值
mutating func update()
}
-
@propertyWrapper
:聲明,聲明了包裝值和投影值考婴,
- 需要包裝類
- 需要包裝值
-
DynamicProperty
:動態(tài)屬性協(xié)議贩虾,包裝器和動態(tài)屬性并不一樣,但大多是關(guān)聯(lián)在一起的沥阱,
-
update
(可省略)函數(shù)缎罢,更新發(fā)布器的被訂閱值, -
_makeProperty
考杉,該函數(shù)的默認(rèn)實現(xiàn)只在包裝器內(nèi)生效策精,包裝器有確定的包裝值即動態(tài)值,該函數(shù)將動態(tài)屬性加入到視圖的動態(tài)屬性池并與視圖狀態(tài)關(guān)聯(lián)崇棠,因而更新動態(tài)值可以更新視圖咽袜;
動態(tài)屬性包裝器:
從包裝屬性到更新視圖的流程:
- View初始化
- 數(shù)據(jù)依賴實例化: 包裝器/動態(tài)屬性初始化:
- 獲取視圖狀態(tài)
- 更新動態(tài)屬性,關(guān)聯(lián)視圖狀態(tài)
- 調(diào)用當(dāng)前struct的動態(tài)屬性對應(yīng)的_makeProperty函數(shù)枕稀;
- 若動態(tài)屬性a內(nèi)也有其他動態(tài)屬性b询刹,則調(diào)用屬性b的_makeProperty函數(shù),
- 構(gòu)建視圖
-
ObservableObject
:被訂閱的發(fā)布器萎坷;修改動態(tài)屬性值凹联, -
objectWillChange.send()
發(fā)送 值即將變更通知, - struct-View-DynamicPropertyBuffer:動態(tài)屬性池接收到 值即將變更通知哆档,視圖狀態(tài) 收到 視圖狀態(tài)值 即將變更蔽挠,
- struct-DynamicProperty: 調(diào)用DynamicProperty-update()主動更新動態(tài)值,若有需要的話虐呻,
- struct-View(body): 動態(tài)屬性值發(fā)生變更象泵,視圖狀態(tài)值變更寞秃,
- 視圖(狀態(tài))更新
模擬 @AppStorage 屬性包裝器:
@propertyWrapper
struct MyUserDefault<Value: Equatable>: DynamicProperty {
private var manager: RecordManager = .shared // 8
private let defaultValue: Value // 16
// @ObservedObject private var record: RecordValues2<Value> // 16
@StateObject private var record: RecordValue<Value> // // 16(結(jié)構(gòu)體) + 8(對象)
var wrappedValue:Value {
get {
return record.wrappedValue ?? defaultValue
}
nonmutating set {
record.wrappedValue = newValue
}
}
var projectedValue: Binding<Value> {
Binding {
wrappedValue
} set: {
wrappedValue = $0
}
}
init(wrappedValue value: Value,_ key: String) {
defaultValue = value
let rec = manager.pressRecord(key: key) as! RecordValue<Value>
// @StateObject包裝的屬性是只讀屬性,無法正常賦值
self._record = StateObject(wrappedValue: rec)
// @ObservedObject包裝可以直接賦值
// record = rec
}
}
public class RecordValue<Value: Equatable>: ObservableObject {
let info: UserDefaults = .standard
let key: String
var wrappedValue: Value? {
get {
// 存儲于UserDefaults中偶惠,輕量緩存
return info.object(forKey: key) as? Value
}
set {
guard wrappedValue != newValue else { return }
objectWillChange.send()
// 存儲于UserDefaults中春寿,輕量緩存
self.info.set(newValue, forKey: key)
}
}
init(key: String) {
self.key = key
}
}
class RecordManager {
var info = [String: AnyObject]()
static let shared = RecordManager()
func pressRecord<T>(key: String) -> RecordValue<T> {
var obj = info[key]
if obj == nil {
obj = RecordValue<T>(key: key)
info[key] = obj
}
return obj as! RecordValue<T>
}
}
-
RecordValue
:用于在UserDefaults
中存取數(shù)據(jù),并手動發(fā)送 值變更通知 -
RecordManager
:使用鍵值對納用發(fā)布器忽孽,不同包裝器只要key值一樣绑改,就會使用同一個發(fā)布器(RecordValue)