本文将对 @Published 与符合 ObservableObject 协议的类实例之间的沟通机制做以介绍,并经过三个示例:@MyPublished( @Published 的拷贝版别 )、@PublishedObject(包装值为引证类型的 @Published 版别)、@CloudStorage(相似 @AppStorage ,但适用于 NSUbiquitousKeyValueStore ),来展现怎么为其他的自界说特点包装类型添加可访问包裹其的类实例的特点或办法的才能。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

何为 @Published 的才能

@Published 是 Combine 结构中最常用到的特点包装器。经过 @Published 符号的特点在产生改动时,其订阅者(经过 $projectedValue 供给的 Publisher )将收到即将改动的值。

不要被它称号尾缀的 ed 所利诱,它的发布机遇是在改动前( willSet

class Weather {
    @Published var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}
let weather = Weather(temperature: 20)
let cancellable = weather.$temperature
    .sink() {
        print ("Temperature now: \($0)")
}
weather.temperature = 25
// Temperature now: 20.0
// Temperature now: 25.0

而在符合 ObservableObject 协议的类中,经过 @Published 符号的特点在产生改动时,除了会告诉自身 Publisher 的订阅者外,也会经过包裹它的类实例的 objectWillChange 来告诉类实例( 符合 ObservableObject 协议)的订阅者。这一特性,也让 @Published 成为 SwiftUI 中最有用的特点包装器之一。

class Weather:ObservableObject {  // 遵循 ObservableObject
    @Published var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}
let weather = Weather(temperature: 20)
let cancellable = weather.objectWillChange // 订阅 weather 实例的 obejctWillChange
    .sink() { _ in
        print ("weather will change")
}
weather.temperature = 25
// weather will change

仅从调用包裹其类的 objectWillChange 的机遇来讲,下面的代码与上面的代码的表现是相同的,但在 @Published 的版别中,咱们并没有为 @Published 供给包裹其类的实例,它是隐式取得的

class Weather:ObservableObject {
    var temperature: Double{  // 没有运用 @Published 进行符号
        willSet {  // 改动前调用类实例的 objectWillChange 
            self.objectWillChange.send()  // 在代码中明确地引证了 Weahter 实例
        }
    }
    init(temperature: Double) {
        self.temperature = temperature
    }
}
let weather = Weather(temperature: 20)
let cancellable = weather.objectWillChange // 订阅 weather 实例
    .sink() { _ in
        print ("weather will change")
}
weather.temperature = 25
// weather will change

长期以来,我一向将 @Published 调用包裹其类的实例办法的行为视为天经地义,从未仔细想过它是怎么完成的。直到我发现除了 @Published 外,@AppStorage 也具有相同的行为(参阅 @AppStorage 研究),此刻我意识到或许咱们能够让其他的特点包装类型具有相似的行为,创立更多的运用场景。

本文中为其他特点包装类型添加的相似 @Published 的才能是指 —— 无需显式设置,特点包装类型便可访问包裹其的类实例的特点或办法

@Published 才能的隐秘

从 Proposal 中找寻答案

我之前并不习惯于看 swift-evolution 的 proposal,由于每逢 Swift 推出新的言语特性后,许多像例如 Paul Hudson 这样的优秀博主会在第一时间将新特性提炼并整理出来,读起来又快又轻松。但为一个言语添加、修改、删去某项功用事实上是一个比较漫长的进程,期间需要对提案不断地进行讨论和修改。proposal 将该进程汇总成文档供每一个开发者来阅览、分析。因而,假如想详细了解某一项 Swift 新特性的来龙去脉,最好仍是要仔细阅览与其对应的 proposal 文档。

在有关 Property Wrappers 的文档中,关于怎么在特点包装类型中引证包裹其的类实例是有特别提及的 —— Referencing the enclosing ‘self’ in a wrapper type。

提案者提出:经过让特点包装类型供给一个静态下标办法,以完成对包裹其的类实例的自动获取(无需显式设置)。

// 提案主张的下标办法
public static subscript<OuterSelf>(
        instanceSelf: OuterSelf,
        wrapped: ReferenceWritableKeyPath<OuterSelf, Value>,
        storage: ReferenceWritableKeyPath<OuterSelf, Self>) -> Value

虽然此种方式是在 proposal 的未来方向一章中提及的,但 Swift 现已对其供给了支持。不过,文档中的代码与 Swift 其时的完成并非完全共同,幸好有人在 stackoverflow 上供给了该下标办法的正确参数称号:

public static subscript<OuterSelf>(
        _enclosingInstance: OuterSelf, // 正确的参数名为 _enclosingInstance
        wrapped: ReferenceWritableKeyPath<OuterSelf, Value>,
        storage: ReferenceWritableKeyPath<OuterSelf, Self>
    ) -> Value

@Published 便是经过完成了该下标办法从而取得了“特别”才能。

特点包装器的运作原理

考虑到特点包装器中的包装值( wrappedValue )众多的变体形式,Swift 社区并没有选用标准的 Swift 协议的方式来界说特点包装器功用,而是让开发者经过声明特点 @propertyWrapper 来自界说特点包装类型。与 把握 Result builders 一文中介绍的 @resultBuilder 相似,编译器在终究编译前,首要会对用户自界说的特点包装类型代码进行转译。

struct Demo {
    @State var name = "fat"
}

上面的代码,编译器将其转译成:

struct Demo {
    private var _name = State(wrappedValue: "fat")
    var name: String {
        get { _name.wrappedValue }
        set { _name.wrappedValue = newValue }
    }
}

能够看出 propertyWrapper 没有什么特别的魔法,便是一个语法糖。上面的代码也解释了为什么在运用了特点包装器后,无法再声明相同称号(前面加下划线)的变量。

// 在运用了特点包装器后,无法再声明相同称号(前面加下划线)的变量。
struct Demo {
    @State var name = "fat"
    var _name:String = "ds"  // invalid redeclaration of synthesized property '_name'
}
// '_name' synthesized for property wrapper backing storage

当特点包装类型仅供给了 wrappedValue 时(比方上面的 State ),转译后的 getter 和 setter 将直接运用 wrappedValue ,不过一旦特点包装类型完成了上文介绍的静态下标办法,转译后将变成如下的代码:

class Test:ObservableObject{
    @Published var name = "fat"
}
// 转译为
class Test:ObservableObject{
    private var _name = Published(wrappedValue: "fat")
    var name:String {
        get {
            Published[_enclosingInstance: self,
                                 wrapped: \Test.name,
                                 storage: \Test._name]
        }
        set {
            Published[_enclosingInstance: self,
                                 wrapped: \Test.name,
                                 storage: \Test._name] = newValue
        }
    }
}

当特点包装器完成了静态下标办法且被所包裹时,编译器将优先运用静态下标办法来完成 getter 和 setter 。

下标办法的三个参数分别为:

  • _enclosingInstance

    包裹其时特点包装器的类实例

  • wrapped

    对外计算特点的 KeyPath (上面代码中对应 name 的 KeyPath )

  • storage

    内部存储特点的 KeyPath (上面代码中对应 _name 的 KeyPath )

在实际运用中,咱们只需运用 _enclosingInstance 和 storage 。虽然下标办法供给了 wrapped 参数,但咱们现在无法调用它。读写该值都将导致应用锁死

经过上面的介绍,咱们能够得到以下结论:

  • @Published 的“特别”才能并非其独有的,与特定的特点包装类型无关
  • 任何完成了该静态下标办法的特点包装类型都能够具有本文所探讨的所谓“特别”才能
  • 由于下标参数 wrapped 和 storage 为 ReferenceWritableKeyPath 类型,因而只有在特点包装类型被类包裹时,编译器才会转译成下标版别的 getter 和 setter

能够在此处取得 本文的典范代码

从仿照中学习 —— 创立 @MyPublished

实践是检验真理的唯一标准。本节咱们将经过对 @Published 进行复刻来验证上文中的内容。

由于代码很简单,所以仅就以下几点做以提示:

  • @Published 的 projectedValue 的类型为 Published.Publisher<Value,Never>
  • 经过对 CurrentValueSubject 的包装,即可轻松地创立自界说 Publisher
  • 调用包裹类实例的 objectWillChange 和给 projectedValue 的订阅者发送信息均应在更改 wrappedValue 之前
@propertyWrapper
public struct MyPublished<Value> {
    public var wrappedValue: Value {
        willSet {  // 修改 wrappedValue 之前
            publisher.subject.send(newValue)
        }
    }
    public var projectedValue: Publisher {
        publisher
    }
    private var publisher: Publisher
    public struct Publisher: Combine.Publisher {
        public typealias Output = Value
        public typealias Failure = Never
        var subject: CurrentValueSubject<Value, Never> // PassthroughSubject 会短少初始话赋值的调用
        public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
            subject.subscribe(subscriber)
        }
        init(_ output: Output) {
            subject = .init(output)
        }
    }
    public init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
        publisher = Publisher(wrappedValue)
    }
    public static subscript<OuterSelf: ObservableObject>(
        _enclosingInstance observed: OuterSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
    ) -> Value {
        get {
            observed[keyPath: storageKeyPath].wrappedValue
        }
        set {
            if let subject = observed.objectWillChange as? ObservableObjectPublisher {
                subject.send() // 修改 wrappedValue 之前
                observed[keyPath: storageKeyPath].wrappedValue = newValue
            }
        }
    }
}

现在,@MyPublished 拥有与 @Published 完全相同的功用与行为表现:

class T: ObservableObject {
    @MyPublished var name = "fat" // 将 MyPublished 替换成 Published 将取得相同的结果 
    init() {}
}
let object = T()
let c1 = object.objectWillChange.sink(receiveValue: {
    print("object will changed")
})
let c2 = object.$name.sink{
    print("name will get new value \($0)")
}
object.name = "bob"
// name will get new value fat
// object will changed
// name will get new value bob

下文中咱们将演示怎么将此才能应用到其他的特点包装类型

@PublishedObject —— @Published 的引证类型版别

@Published 只能胜任包装值为值类型的场景,当 wrappedValue 为引证类型时,仅改动包装值的特点内容并不会对外发布告诉。例如下面的代码,咱们不会收到任何提示:

class RefObject {
    var count = 0
    init() {}
}
class Test: ObservableObject {
    @Published var ref = RefObject()
}
let test = Test()
let cancellable = test.objectWillChange.sink{ print("object will change")}
test.ref.count = 100
// 不会有提示

为此,咱们能够完成一个适用于引证类型的 @Published 版别 —— @PublishedObject

提示:

  • @PublishedObject 的 wrappedValue 为遵循 ObservableObject 协议的引证类型
  • 在特点包装器中订阅 wrappedValue 的 objectWillChange ,每逢 wrappedValue 产生改动时,将调用指定的闭包
  • 在特点包装器创立后,系统会立刻调用静态下标的 getter 一次,选择在此刻机完成对 wrappedValue 的订阅和闭包的设置
@propertyWrapper
public struct PublishedObject<Value: ObservableObject> { // wrappedValue 要求符合 ObservableObject
    public var wrappedValue: Value
    public init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }
    public static subscript<OuterSelf: ObservableObject>(
        _enclosingInstance observed: OuterSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
    ) -> Value where OuterSelf.ObjectWillChangePublisher == ObservableObjectPublisher {
        get {
            if observed[keyPath: storageKeyPath].cancellable == nil {
                // 只会履行一次
                observed[keyPath: storageKeyPath].setup(observed)
            }
            return observed[keyPath: storageKeyPath].wrappedValue
        }
        set {
            observed.objectWillChange.send() // willSet
            observed[keyPath: storageKeyPath].wrappedValue = newValue
        }
    }
    private var cancellable: AnyCancellable?
    // 订阅 wrappedvalue 的 objectWillChange 
    // 每逢 wrappedValue 发送告诉时,调用 _enclosingInstance 的 objectWillChange.send。
    // 运用闭包对 _enclosingInstance 进行弱引证
    private mutating func setup<OuterSelf: ObservableObject>(_ enclosingInstance: OuterSelf) where OuterSelf.ObjectWillChangePublisher == ObservableObjectPublisher {
        cancellable = wrappedValue.objectWillChange.sink(receiveValue: { [weak enclosingInstance] _ in
            (enclosingInstance?.objectWillChange)?.send()
        })
    }
}

@PublishedObject 为咱们供给了愈加灵活的才能来驱动 SwiftUI 的视图,比方咱们能够这样运用 @PublishedObject :

@objc(Event)
public class Event: NSManagedObject { // Core Data 的保管目标符合 ObservableObject 协议
    @NSManaged public var timestamp: Date?
}
class Store: ObservableObject {
    @PublishedObject var event = Event(context: container.viewContext)
    init() {
        event.timestamp = Date().addingTimeInterval(-1000)
    }
}
struct DemoView: View {
    @StateObject var store = Store()
    var body: some View {
        VStack {
            Text(store.event.timestamp, format: .dateTime)
            Button("Now") {
                store.event.timestamp = .now
            }
        }
        .frame(width: 300, height: 300)
    }
}

为自定义属性包装类型添加类 @Published 的能力

@CloudStorage —— @AppStorage 的 CloudKit 版别

在 @AppStorage 研究 一文中,我介绍过,除了 @Published 外,@AppStorage 也相同具有引证包裹其的类实例的才能。因而,咱们能够运用如下的代码在 SwiftUI 中统一管理 UserDefaults :

class Defaults: ObservableObject {
    @AppStorage("name") public var name = "fatbobman"
    @AppStorage("age") public var age = 12
}

Tom Lokhorst 写了一个相似 @AppStorage 的第三方库 —— @CloudStorage ,完成了在 NSUbiquitousKeyValueStore 产生变化时能够驱动 SwiftUI 视图的更新:

struct DemoView: View {
    @CloudStorage("readyForAction") var readyForAction: Bool = false
    @CloudStorage("numberOfItems") var numberOfItems: Int = 0
    var body: some View {
        Form {
            Toggle("Ready", isOn: $readyForAction)
                .toggleStyle(.switch)
            TextField("numberOfItems",value: $numberOfItems,format: .number)
        }
        .frame(width: 400, height: 400)
    }
}

咱们能够运用本文介绍的办法为其添加了相似 @Published 的才能。

在撰写 在 SwiftUI 下运用 NSUbiquitousKeyValueStore 同步数据 一文的时候,我没有把握本文介绍的办法。其时只能选用一种比较笨拙的手法来与包裹 @CloudStorage 的类实例进行通讯。现在我已用本文介绍的方式重新修改了 @CloudStorage 代码。由于 @CloudeStorage 的作者没有将修改后的代码合并,因而大家现在能够暂时运用我 修改后的 Fork 版别。

代码要点:

  • 由于设置的 projectValue 和 _setValue 的作业是在 CloudStorage 结构器中进行的,此刻只能捕获为 nil 的闭包 sender ,经过创立一个类实例 holder 来持有闭包,以便能够经过下标办法为 sender 赋值。
  • 留意 holder?.sender?() 的调用机遇,应与 willSet 行为共同
@propertyWrapper public struct CloudStorage<Value>: DynamicProperty {
    private let _setValue: (Value) -> Void
    @ObservedObject private var backingObject: CloudStorageBackingObject<Value>
    public var projectedValue: Binding<Value>
    public var wrappedValue: Value {
        get { backingObject.value }
        nonmutating set { _setValue(newValue) }
    }
    public init(keyName key: String, syncGet: @escaping () -> Value, syncSet: @escaping (Value) -> Void) {
        let value = syncGet()
        let backing = CloudStorageBackingObject(value: value)
        self.backingObject = backing
        self.projectedValue = Binding(
            get: { backing.value },
            set: { [weak holder] newValue in
                backing.value = newValue
                holder?.sender?() // 留意调用机遇
                syncSet(newValue)
                sync.synchronize()
            })
        self._setValue = { [weak holder] (newValue: Value) in
            backing.value = newValue
            holder?.sender?()
            syncSet(newValue)
            sync.synchronize()
        }
        sync.setObserver(for: key) { [weak backing] in
            backing?.value = syncGet()
        }
    }
    // 由于设置的 projectValue 和 _setValue 的作业是在结构器中进行的,无法仅捕获闭包 sender(其时仍是 nil),创立一个类实例来持有闭包,以便能够经过下标办法装备。
    class Holder{
        var sender: (() -> Void)?
        init(){}
    }
    var holder = Holder()
    public static subscript<OuterSelf: ObservableObject>(
        _enclosingInstance observed: OuterSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
    ) -> Value {
        get {
            // 设置 holder 的机遇和逻辑与 @PublishedObject 共同
            if observed[keyPath: storageKeyPath].holder.sender == nil {
                observed[keyPath: storageKeyPath].holder.sender = { [weak observed] in
                    (observed?.objectWillChange as? ObservableObjectPublisher)?.send()
                }
            }
            return observed[keyPath: storageKeyPath].wrappedValue
        }
        set {
            if let subject = observed.objectWillChange as? ObservableObjectPublisher {
                subject.send()
                observed[keyPath: storageKeyPath].wrappedValue = newValue
            }
        }
    }
}

运用修改后的代码,能够将 @AppStorage 和 @CloudStorage 统一管理,以方便在 SwiftUI 视图中运用:

class Settings:ObservableObject {
       @AppStorage("name") var name = "fat"
       @AppStorage("age") var age = 5
       @CloudStorage("readyForAction") var readyForAction = false
       @CloudStorage("speed") var speed: Double = 0
}
struct DemoView: View {
    @StateObject var settings = Settings()
    var body: some View {
        Form {
            TextField("Name", text: $settings.name)
            TextField("Age", value: $settings.age, format: .number)
            Toggle("Ready", isOn: $settings.readyForAction)
                .toggleStyle(.switch)
            TextField("Speed", value: $settings.speed, format: .number)
            Text("Name: \(settings.name)")
            Text("Speed: ") + Text(settings.speed, format: .number)
            Text("ReadyForAction: ") + Text(settings.readyForAction ? "True" : "False")
        }
        .frame(width: 400, height: 400)
    }
}

为自定义属性包装类型添加类 @Published 的能力

总结

许多东西在咱们对其不了解时,常将其视为黑魔法。但只需穿越其魔法屏障就会发现,或许并没有幻想中的那么玄奥。

希望本文能够对你有所协助。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】