自定义 属性 更新视图的包装器 Swift

Custom Property Wrapper that Updates View Swift

Xcode11.3,Swift5.1.3

我目前正在尝试创建一个自定义 属性 包装器,它允许我将 link 变量添加到 Firebase 数据库。这样做时,为了让它更新视图,我首先尝试使用 @ObservedObject @Bar var foo = []。但是我得到一个错误,不支持多个 属性 包装器。接下来我尝试做的事情,老实说是理想的,是尝试让我的自定义 属性 包装器在更改时更新视图本身,就像 @State@ObservedObject 一样。这既避免了向下两层访问底层值的需要,又避免了使用嵌套 属性 包装器。为此,我查看了 SwiftUI 文档,发现它们都实现了 DynamicProperty 协议。我也尝试使用它但失败了,因为我需要能够从我的 Firebase 数据库观察器中更新视图(调用 update()),我不能这样做,因为 .update() 正在发生变化。

这是我目前的尝试:

import SwiftUI
import Firebase
import CodableFirebase
import Combine 

@propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
    typealias ObserverHandle = UInt
    typealias Action = RealtimeDatabase.Action
    typealias Event = RealtimeDatabase.Event

    private(set) var reference: DatabaseReference

    private var currentValue: [Element]

    private var childAddedObserverHandle: ObserverHandle?
    private var childChangedObserverHandle: ObserverHandle?
    private var childRemovedObserverHandle: ObserverHandle?

    private var childAddedActions: [Action<[Element]>] = []
    private var childChangedActions: [Action<[Element]>] = []
    private var childRemovedActions: [Action<[Element]>] = []

    init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
         actions: [Action<[Element]>] = []) {
        currentValue = wrappedValue
        reference = RealtimeDatabase()[keyPath: path].reference

        for action in actions {
            if action.event.contains(.childAdded) {
                childAddedActions.append(action)
            }
            if action.event.contains(.childChanged) {
                childChangedActions.append(action)
            }
            if action.event.contains(.childRemoved) {
                childRemovedActions.append(action)
            }
        }

        if events.contains(.childAdded) {
            childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                self.objectWillChange.send()
                self.currentValue.append(decodedValue)
                self.childAddedActions.forEach { [=12=].action(&self.currentValue) }
            }
        }
        if events.contains(.childChanged) {
            childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                guard let changeIndex = self.currentValue.firstIndex(where: { [=12=].id == decodedValue.id }) else {
                    return
                }
                self.objectWillChange.send()
                self.currentValue[changeIndex] = decodedValue
                self.childChangedActions.forEach { [=12=].action(&self.currentValue) }
            }
        }
        if events.contains(.childRemoved) {
            childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                self.objectWillChange.send()
                self.currentValue.removeAll { [=12=].id == decodedValue.id }
                self.childRemovedActions.forEach { [=12=].action(&self.currentValue) }
            }
        }
    }

    private func setValue(to value: [Element]) {
        guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
            fatalError("Could not encode value to Firebase.")
        }
        reference.setValue(encodedValue)
    }

    var wrappedValue: [Element] {
        get {
            return currentValue
        }
        set {
            self.objectWillChange.send()
            setValue(to: newValue)
        }
    }

    var projectedValue: Binding<[Element]> {
        return Binding(get: {
            return self.wrappedValue
        }) { newValue in
            self.wrappedValue = newValue
        }
    }

    var hasActiveObserver: Bool {
        return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
    }
    var hasChildAddedObserver: Bool {
        return childAddedObserverHandle != nil
    }
    var hasChildChangedObserver: Bool {
        return childChangedObserverHandle != nil
    }
    var hasChildRemovedObserver: Bool {
        return childRemovedObserverHandle != nil
    }

    func connectObservers(for event: Event) {
        if event.contains(.childAdded) && childAddedObserverHandle == nil {
            childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                self.objectWillChange.send()
                self.currentValue.append(decodedValue)
                self.childAddedActions.forEach { [=12=].action(&self.currentValue) }
            }
        }
        if event.contains(.childChanged) && childChangedObserverHandle == nil {
            childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                guard let changeIndex = self.currentValue.firstIndex(where: { [=12=].id == decodedValue.id }) else {
                    return
                }
                self.objectWillChange.send()
                self.currentValue[changeIndex] = decodedValue
                self.childChangedActions.forEach { [=12=].action(&self.currentValue) }
            }
        }
        if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
            childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
                guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
                    fatalError("Could not decode value from Firebase.")
                }
                self.objectWillChange.send()
                self.currentValue.removeAll { [=12=].id == decodedValue.id }
                self.childRemovedActions.forEach { [=12=].action(&self.currentValue) }                
            }
        }
    }

    func removeObserver(for event: Event) {
        if event.contains(.childAdded), let handle = childAddedObserverHandle {
            reference.removeObserver(withHandle: handle)
            self.childAddedObserverHandle = nil
        }
        if event.contains(.childChanged), let handle = childChangedObserverHandle {
            reference.removeObserver(withHandle: handle)
            self.childChangedObserverHandle = nil
        }
        if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
            reference.removeObserver(withHandle: handle)
            self.childRemovedObserverHandle = nil
        }
    }
    func removeAction(_ action: Action<[Element]>) {
        if action.event.contains(.childAdded) {
            childAddedActions.removeAll { [=12=].id == action.id }
        }
        if action.event.contains(.childChanged) {
            childChangedActions.removeAll { [=12=].id == action.id }
        }
        if action.event.contains(.childRemoved) {
            childRemovedActions.removeAll { [=12=].id == action.id }
        }
    }

    func removeAllActions(for event: Event) {
        if event.contains(.childAdded) {
            childAddedActions = []
        }
        if event.contains(.childChanged) {
            childChangedActions = []
        }
        if event.contains(.childRemoved) {
            childRemovedActions = []
        }
    }
}

struct School: Codable, Identifiable {
    /// The unique id of the school.
    var id: String

    /// The name of the school.
    var name: String

    /// The city of the school.
    var city: String

    /// The province of the school.
    var province: String

    /// Email domains for student emails from the school.
    var domains: [String]
}

@dynamicMemberLookup
struct RealtimeDatabase {
    private var path: [String]

    var reference: DatabaseReference {
        var ref = Database.database().reference()
        for component in path {
            ref = ref.child(component)
        }
        return ref
    }

    init(previous: Self? = nil, child: String? = nil) {
        if let previous = previous {
            path = previous.path
        } else {
            path = []
        }
        if let child = child {
            path.append(child)
        }
    }

    static subscript(dynamicMember member: String) -> Self {
        return Self(child: member)
    }

    subscript(dynamicMember member: String) -> Self {
        return Self(child: member)
    }

    static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
        return Self()[keyPath: keyPath]
    }

    static let reference = Database.database().reference()

    struct Event: OptionSet, Hashable {
        let rawValue: UInt
        static let childAdded = Event(rawValue: 1 << 0)
        static let childChanged = Event(rawValue: 1 << 1)
        static let childRemoved = Event(rawValue: 1 << 2)

        static let all: Event = [.childAdded, .childChanged, .childRemoved]
        static let constructive: Event = [.childAdded, .childChanged]
        static let destructive: Event = .childRemoved
    }

    struct Action<Value>: Identifiable {

        let id = UUID()
        let event: Event
        let action: (inout Value) -> Void

        private init(on event: Event, perform action: @escaping (inout Value) -> Void) {
            self.event = event
            self.action = action
        }

        static func on<Value>(_ event: RealtimeDatabase.Event, perform action: @escaping (inout Value) -> Void) -> Action<Value> {
            return Action<Value>(on: event, perform: action)
        }
    }
}

用法示例:

struct ContentView: View {

    @DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { [=13=].sort { [=13=].name < .name } }])
    var schools: [School] = []

    var body: some View {
        Text("School: ").bold() +
            Text(schools.isEmpty ? "Loading..." : schools.first!.name)
    }
}

当我尝试使用它时,视图永远不会使用来自 Firebase 的值进行更新,即使我确信正在调用 .childAdded 观察者。


我解决这个问题的尝试之一是将所有这些变量存储在一个本身符合 ObservableObject 的单例中。这个 解决方案 也很理想,因为它允许在我的应用程序中共享观察到的变量,防止同一日期的多个实例并允许单一事实来源。不幸的是,这也没有使用 currentValue.

的获取值更新视图
class Session: ObservableObject {

    @DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { [=14=].sort { [=14=].name < .name } }])
    var schools: [School] = []

    private init() {
        //Send `objectWillChange` when `schools` property changes
        _schools.objectWillChange.sink {
            self.objectWillChange.send()
        }
    }

    static let current = Session()

}


struct ContentView: View {

    @ObservedObject
    var session = Session.current

    var body: some View {
        Text("School: ").bold() +
            Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
    }
}

有没有什么方法可以制作自定义 属性 包装器,同时更新 SwiftUI 中的视图?

这个问题的解决方案是对单例的解决方案做一个小的调整。感谢@user1046037 向我指出这一点。原文 post 中提到的单例修复的问题是它没有在初始化器中保留接收器的取消器。这是正确的代码:

class Session: ObservableObject {

    @DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { [=10=].sort { [=10=].name < .name } }])
    var schools: [School] = []

    private var cancellers = [AnyCancellable]()

    private init() {
        _schools.objectWillChange.sink {
            self.objectWillChange.send()
        }.assign(to: &cancellers)
    }

    static let current = Session()

}

利用 DynamicProperty 协议,我们可以通过使用 SwiftUI 现有的 属性 包装器轻松触发视图更新。 (DynamicProperty 告诉 SwiftUI 在我们的类型中寻找这些)

@propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
    
    // A state object that we notify of updates
    @StateObject private var updater = Updater()
    
    var wrappedValue: T {
        get {
            // Your getter code here
        }
        nonmutating set {
            // Tell SwiftUI we're going to change something
            updater.notifyUpdate()
            // Your setter code here
        }
    }
    
    class Updater: ObservableObject {
        func notifyUpdate() {
            objectWillChange.send()
        }
    }
}