Xcode 11.3,斯威夫特 5.1.3
我目前正在尝试创建一个自定义属性包装器,它允许我将变量链接到 Firebase 数据库。这样做时,为了更新视图,我首先尝试使用@ObservedObject @Bar var foo = []
. 但是我收到一个错误,即不支持多个属性包装器。接下来我尝试做的事情,老实说是理想的,是尝试让我的自定义属性包装器在更改时更新视图本身,就像@State
and一样@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 中的视图?