本文分两部分,第一部分是介绍常用的属性包装器,第二步部分是自定义属性包装器 + 动态属性分析
一、SwiftUI常用的属性包装器:
@AppStorage
: 全局生效(除App层级),全局发送更新通知,直接操作UserDefaults
生效;可存储配置(轻量)数据;
@SceneStorage
: 作用域位为所有SwiftUI视图,可在界面内存储轻量数据,界面注销(非app关闭)则数据清除;
@State
: 作用域位为SwiftUI视图模块内,仅支持值类型;
@ObservedObject
:作用域位为SwiftUI视图模块内,支持class对象,作为小范围内的初始数据源,视图刷新会销毁重建;
@StateObject
: 同@ObservedObject
,但是属于静态变量,视图刷新不会销毁;
@EnvironmentObject
: 自定义环境对象,使用时需注入给具体视图,可减少初始化,方便切换不同数据源等;
@Environment
: 系统环境变量,不需要注入(若手动增加变量,则仍需注入给具体视图)
@FocusedValue
: 用于输入框的绑定/读取输入内容
由于结构体内的属性不可变,所以当想创建可变属性时,需要使用mutating关键字,但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 }
}
// 创建包装属性
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协议且RawValue为Int或String的数据类型。
@SceneStorage
@SceneStorage("selected") var index = 0
- 同@AppStorage十分类似,不过其作用域仅限于当前Scene
- Scene退出时若app未退出,则数据失效
- app退出时数据会持久化
@ObservedObject
class Person:ObservableObject{
@Published var name = "张三"
}
@ObservedObject var p = Person()
- ObservableObject 协议要求实现类型是 class,它需要实现一个属性:
objectWillChange = ObservableObjectPublisher()
,使用@Published可以自动实现。 - 在数据将要发生改变时,会向外进行“广播”
- 只是作为View的数据依赖,包装器被动态属性池持有,但是包装器内的动态属性不被持有,View更新视图时视图状态(值)重新获取,ObservedObject对应的动态属性可能会被销毁重建,
@StateObject
- StateObject行为类似ObservedObject对象
- 动态属性为只读属性,只能修改动态属性的子属性
- 针对引用类型设计,当View更新时,实例不会被销毁,和视图的动态属性池绑定,
...
写了几个小时文章,发布的时候内容不见了,吐了,不想写了,直接贴代码看吧!!!
注意:
避免非必要的包装器声明:只要声明了,就算不使用,其发送的更新通知,会导致View发生不必要的更新。
二、DynamicProperty运作原理
-
@propertyWrapper
:声明,声明了包装值和投影值,- 需要包装类
- 需要包装值
-
DynamicProperty
:动态属性协议,包装器和动态属性并不一样,但大多是关联在一起的,-
update
(可省略)函数,更新发布器的被订阅值, -
_makeProperty
: 该函数的默认实现只在包装器内生效,包装器有确定的包装值即动态值,该函数将动态属性加入到视图的动态属性池并与视图状态关联,因而更新动态值可以更新视图;
一般propertyWrapper用于修饰继承DynamicProperty(动态属性协议)的struct,
-
DynamicProperty协议:
public protocol DynamicProperty {
// 关联动态属性和视图
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
:动态属性协议,包装器和动态属性并不一样,但大多是关联在一起的,
-
update
(可省略)函数,更新发布器的被订阅值, -
_makeProperty
,该函数的默认实现只在包装器内生效,包装器有确定的包装值即动态值,该函数将动态属性加入到视图的动态属性池并与视图状态关联,因而更新动态值可以更新视图;
动态属性包装器:
从包装属性到更新视图的流程:
- View初始化
- 数据依赖实例化: 包装器/动态属性初始化:
- 获取视图状态
- 更新动态属性,关联视图状态
- 调用当前struct的动态属性对应的_makeProperty函数;
- 若动态属性a内也有其他动态属性b,则调用属性b的_makeProperty函数,
- 构建视图
-
ObservableObject
:被订阅的发布器;修改动态属性值, -
objectWillChange.send()
发送 值即将变更通知, - struct-View-DynamicPropertyBuffer:动态属性池接收到 值即将变更通知,视图状态 收到 视图状态值 即将变更,
- struct-DynamicProperty: 调用DynamicProperty-update()主动更新动态值,若有需要的话,
- struct-View(body): 动态属性值发生变更,视图状态值变更,
- 视图(状态)更新
模拟 @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(结构体) + 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
中存取数据,并手动发送 值变更通知 -
RecordManager
:使用键值对纳用发布器,不同包装器只要key值一样,就会使用同一个发布器(RecordValue)