DynamicProperty和propertyWrapper介绍和深入理解

本文分两部分,第一部分是介绍常用的属性包装器,第二步部分是自定义属性包装器 + 动态属性分析

一、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 : DynamicProperty {
// 包装属性
    public var wrappedValue: Value { get nonmutating set }
// 投影属性 可使用 $ 加在属性前来使用
    public var projectedValue: Binding { 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运作原理

  1. @propertyWrapper:声明,声明了包装值和投影值,
    • 需要包装类
    • 需要包装值
  2. DynamicProperty:动态属性协议,包装器和动态属性并不一样,但大多是关联在一起的,
    • update(可省略)函数,更新发布器的被订阅值,
    • _makeProperty: 该函数的默认实现只在包装器内生效,包装器有确定的包装值即动态值,该函数将动态属性加入到视图的动态属性池并与视图状态关联,因而更新动态值可以更新视图;
      一般propertyWrapper用于修饰继承DynamicProperty(动态属性协议)的struct,

DynamicProperty协议:

public protocol DynamicProperty {
// 关联动态属性和视图
  static func _makeProperty(in buffer: inout _DynamicPropertyBuffer, container: _GraphValue, fieldOffset: Int, inputs: inout _GraphInputs)
// 属性行为
  static var _propertyBehaviors: UInt32 { get }
// 更新属性值
  mutating func update()
}
  1. @propertyWrapper:声明,声明了包装值和投影值,
  • 需要包装类
  • 需要包装值
  1. DynamicProperty:动态属性协议,包装器和动态属性并不一样,但大多是关联在一起的,
  • update(可省略)函数,更新发布器的被订阅值,
  • _makeProperty,该函数的默认实现只在包装器内生效,包装器有确定的包装值即动态值,该函数将动态属性加入到视图的动态属性池并与视图状态关联,因而更新动态值可以更新视图;
动态属性包装器:

从包装属性到更新视图的流程:

  1. View初始化
  2. 数据依赖实例化: 包装器/动态属性初始化:
  3. 获取视图状态
  4. 更新动态属性,关联视图状态
  • 调用当前struct的动态属性对应的_makeProperty函数;
  • 若动态属性a内也有其他动态属性b,则调用属性b的_makeProperty函数,
  1. 构建视图
  2. ObservableObject:被订阅的发布器;修改动态属性值,
  3. objectWillChange.send() 发送 值即将变更通知,
  4. struct-View-DynamicPropertyBuffer:动态属性池接收到 值即将变更通知,视图状态 收到 视图状态值 即将变更,
  5. struct-DynamicProperty: 调用DynamicProperty-update()主动更新动态值,若有需要的话,
  6. struct-View(body): 动态属性值发生变更,视图状态值变更,
  7. 视图(状态)更新
模拟 @AppStorage 属性包装器:
@propertyWrapper
struct MyUserDefault: DynamicProperty {
    private var manager: RecordManager = .shared // 8
    private let defaultValue: Value // 16
//    @ObservedObject private var record: RecordValues2 // 16
    @StateObject private var record: RecordValue // // 16(结构体) + 8(对象)
    var wrappedValue:Value {
        get {
            return record.wrappedValue ?? defaultValue
        }
        nonmutating set {
            record.wrappedValue = newValue
        }
    }
    
    var projectedValue: Binding {
        Binding {
            wrappedValue
        } set: {
            wrappedValue = $0
        }
    }
    
    init(wrappedValue value: Value,_ key: String) {
        defaultValue = value
        let rec = manager.pressRecord(key: key) as! RecordValue
        // @StateObject包装的属性是只读属性,无法正常赋值
        self._record = StateObject(wrappedValue: rec)
        // @ObservedObject包装可以直接赋值
//        record = rec
    }
}

public class RecordValue: 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(key: String) -> RecordValue {
        var obj = info[key]
        if obj == nil {
            obj = RecordValue(key: key)
            info[key] = obj
        }
        return obj as! RecordValue
    }
}
  • RecordValue:用于在 UserDefaults 中存取数据,并手动发送 值变更通知
  • RecordManager:使用键值对纳用发布器,不同包装器只要key值一样,就会使用同一个发布器(RecordValue)

你可能感兴趣的:(DynamicProperty和propertyWrapper介绍和深入理解)