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

时间:2019-12-18 00:11:47

标签: swift firebase firebase-realtime-database swiftui property-wrapper

Xcode 11.3,Swift 5.1.3

我目前正在尝试创建一个自定义属性包装器,该包装器允许我将变量链接到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 { $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中的视图?

2 个答案:

答案 0 :(得分: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()

}

答案 1 :(得分: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()
        }
    }
}