3

Xcode 11.3,斯威夫特 5.1.3

我目前正在尝试创建一个自定义属性包装器,它允许我将变量链接到 Firebase 数据库。这样做时,为了更新视图,我首先尝试使用@ObservedObject @Bar var foo = []. 但是我收到一个错误,即不支持多个属性包装器。接下来我尝试做的事情,老实说是理想的,是尝试让我的自定义属性包装器在更改时更新视图本身,就像@Stateand一样@ObservedObject。这既避免了需要向下两层访问底层值,又避免了使用嵌套属性包装器。为此,我检查了 SwiftUI 文档,发现它们都实现了该DynamicProperty协议。我也尝试使用它但失败了,因为我需要能够更新视图(调用update()) 来自我的 Firebase 数据库观察者,我不能这样做,因为.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 { $0.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: { $0.id == decodedValue.id }) else {
                    return
                }
                self.objectWillChange.send()
                self.currentValue[changeIndex] = decodedValue
                self.childChangedActions.forEach { $0.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 { $0.id == decodedValue.id }
                self.childRemovedActions.forEach { $0.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 { $0.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: { $0.id == decodedValue.id }) else {
                    return
                }
                self.objectWillChange.send()
                self.currentValue[changeIndex] = decodedValue
                self.childChangedActions.forEach { $0.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 { $0.id == decodedValue.id }
                self.childRemovedActions.forEach { $0.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 { $0.id == action.id }
        }
        if action.event.contains(.childChanged) {
            childChangedActions.removeAll { $0.id == action.id }
        }
        if action.event.contains(.childRemoved) {
            childRemovedActions.removeAll { $0.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) { $0.sort { $0.name < $1.name } }])
    var schools: [School] = []

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

但是,当我尝试使用它时,即使我肯定.childAdded正在调用观察者,视图也不会使用 Firebase 的值进行更新。


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

class Session: ObservableObject {

    @DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.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 中的视图?

4

2 回答 2

0

对此的解决方案是对单例的解决方案进行微调。感谢 @user1046037 向我指出这一点。原始帖子中提到的单例修复的问题在于它没有在初始化程序中保留接收器的取消器。这是正确的代码:

class Session: ObservableObject {

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

    private var cancellers = [AnyCancellable]()

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

    static let current = Session()

}
于 2019-12-18T04:39:31.457 回答
0

利用该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()
        }
    }
}
于 2021-05-09T02:51:13.877 回答