如何在 SwiftUI 列表中显示 Realm 结果?

How to display Realm Results in SwiftUI List?

我已经能够将数据保存在 Realm 数据库中,但无法在 SwiftUI 中显示结果 List

我知道我有数据并且在控制台中打印结果没有问题。

有没有办法将 Realm Result 转换成可以在 SwiftUI List 上显示的格式?

import SwiftUI
import RealmSwift
import Combine

class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0

    override static func primaryKey() -> String? {
        return "name"
    }
}

class SaveDog {
    func saveDog(name: String, age: String) {
        let dog = Dog()
        dog.age  = Int(age)!
        dog.name = name

        // Get the default Realm
        let realm = try! Realm()

     print(Realm.Configuration.defaultConfiguration.fileURL!)

        // Persist your data easily
        try! realm.write {
        realm.add(dog)
        }

        print(dog)
    }
}

class RealmResults: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()

    func getRealmResults() -> String{
        let realm = try! Realm()
        var results = realm.objects(Dog.self) { didSet 
 {didChange.send(())}}
        print(results)
        return results.first!.name
    }
}

struct dogRow: View {
    var dog = Dog()
    var body: some View {
        HStack {
            Text(dog.name)
            Text("\(dog.age)")
        }
    }

}

struct ContentView : View {

    @State var dogName: String = ""
    @State var dogAge: String = ""

    let saveDog = SaveDog()
    @ObjectBinding var savedResults = RealmResults()
    let realm = try! Realm()

    let dogs = Dog()

    var body: some View {
        VStack {
            Text("Hello World")
            TextField($dogName)
            TextField($dogAge)
            Button(action: {
                self.saveDog.saveDog(name: self.dogName, 
                age:self.dogAge)
//                self.savedResults.getRealmResults()
            }) {
                Text("Save")
            }
            //insert list here to show realm data

            List(0 ..< 5) { 
             item in
                Text(self.savedResults.getRealmResults())
            } //Displays the same thing 5 times
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

有些代码可能没有意义,因为我尝试了几种方法来查看是否有任何效果。

例如,这一行将在列表视图中显示结果。

return results.first!.name

如果我只是 return 结果,则列表文本视图中不会显示任何内容。

正如我在下面评论的那样,我将在有时间时尝试使用 ForEach 方法。看起来很有希望。

您在 ListForEach 中传递的数据必须符合 Identifiable 协议。

要么在你的 Realm 模型中采用它,要么使用 .identified(by:) 方法。


即便如此,如果数据发生变化,View 也不会重新加载。

您可以包装 Results 并使其成为 BindableObject,这样视图可以检测到更改并自行重新加载:

class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {

    var results: Results<Element>
    private var token: NotificationToken!

    init(results: Results<Element>) {
        self.results = results
        lateInit()
    }

    func lateInit() {
        token = results.observe { [weak self] _ in
            self?.objectWillChange.send()
        }
    }

    deinit {
        token.invalidate()
    }
}

并像这样使用它:

struct ContentView : View {

    @ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))

    var body: some View {
        List(dogs.results.identified(by: \.name)) { dog in
            DogRow(dog: dog)
        }
    }

}

我已经创建了一个通用解决方案来显示和 add/delete 任何 Results<T>。默认情况下,Results<T> 为“实时”。当 @Published 属性 WILL 更新时,SwiftUI 将更改发送到视图。当收到RealmCollectionChange<Results<T>>通知时,Results<T>已经更新;因此,由于索引超出范围,删除时将出现 fatalError。相反,我使用“实时”Results<T> 来跟踪更改,并使用“冻结”Results<T> 与视图一起使用。可以在此处找到完整的工作示例,包括如何将通用 ViewRealmViewModel<T>(如下所示)一起使用:SwiftUI+Realmenum Status 用于在适用时显示 ProgressView、“未找到记录”等,如项目中所示。另请注意,在需要计数或单个对象时使用“frozen”Results<T>。删除时,onDeleteIndexSet 将从“冻结”Results<T> return 移动到一个位置,因此它会检查该对象是否仍然存在于“活动”Results<T>.

class RealmViewModel<T: RealmSwift.Object>: ObservableObject, Verbose where T: Identifiable {

typealias Element = T

enum Status {
    // Display ProgressView
    case fetching
    // Display "No records found."
    case empty
    // Display results
    case results
    // Display error
    case error(Swift.Error)
    
    enum _Error: String, Swift.Error {
        case fetchNotCalled = "System Error."
    }
}

init() {
    fetch()
}

deinit {
    token?.invalidate()
}

@Published private(set) var status: Status = .error(Status._Error.fetchNotCalled)

// Frozen results: Used for View

@Published private(set) var results: Results<Element>?

// Live results: Used for NotificationToken

private var __results: Results<Element>?

private var token: NotificationToken?

private func notification(_ change: RealmCollectionChange<Results<Element>>) {
    switch change {
        case .error(let error):
            verbose(error)
            self.__results = nil
            self.results = nil
            self.token = nil
            self.status = .error(error)
        case .initial(let results):
            verbose("count:", results.count)
            //self.results = results.freeze()
            //self.status = results.count == 0 ? .empty : .results
        case .update(let results, let deletes, let inserts, let updates):
            verbose("results:", results.count, "deletes:", deletes, "inserts:", inserts, "updates:", updates)
            self.results = results.freeze()
            self.status = results.count == 0 ? .empty : .results
    }
}

var count: Int { results?.count ?? 0 }

subscript(_ i: Int) -> Element? { results?[i] }

func fetch() {
    
    status = .fetching
    
    //Realm.asyncOpen(callback: asyncOpen(_:_:))
    
    do {
        let realm = try Realm()
        let results = realm.objects(Element.self).sorted(byKeyPath: "id")
        self.__results = results
        self.results = results.freeze()
        self.token = self.__results?.observe(notification)
        
        status = results.count == 0 ? .empty : .results
        
    } catch {
        verbose(error)
        
        self.__results = nil
        self.results = nil
        self.token = nil
        
        status = .error(error)
    }
}

func insert(_ data: Element) throws {
    let realm = try Realm()
    try realm.write({
        realm.add(data)
    })
}

func delete(at offsets: IndexSet) throws {
    let realm = try Realm()
    try realm.write({
        
        offsets.forEach { (i) in
            guard let id = results?[i].id else { return }
            guard let data = __results?.first(where: { [=10=].id == id }) else { return }
            realm.delete(data)
        }
    })
}

}

这是另一个使用新的 Realm frozen() 集合的选项。虽然这是早期 UI 会在 'assets' 添加到数据库时自动更新。在此示例中,它们是从 NSOperation 线程添加的,该线程应该是后台线程。

在此示例中,侧边栏根据数据库中的不同值列出了不同的 属性 组 - 请注意,您可能希望以更稳健的方式实现它 - 但作为快速 POC,它工作正常。见下图。

struct CategoryBrowserView: View {
    @ObservedObject var assets: RealmSwift.List<Asset> = FileController.shared.assets
    @ObservedObject var model = ModelController.shared
    
    @State private var searchTerm: String = ""
    @State var isEventsShowing: Bool = false
    @State var isProjectsShowing: Bool = false
    @State var isLocationsShowing: Bool = false
    
    var projects: Results<Asset> {
        return assets.sorted(byKeyPath: "project").distinct(by: ["project"])
    }
    var events: Results<Asset> {
        return assets.sorted(byKeyPath: "event").distinct(by: ["event"])
    }
    var locations: Results<Asset> {
        return assets.sorted(byKeyPath: "location").distinct(by: ["location"])
    }
    @State var status: Bool = false
    
    var body: some View {
        VStack(alignment: .leading) {
        ScrollView {
            VStack(alignment: .leading) {
                
                // Projects
                DisclosureGroup(isExpanded: $isProjectsShowing) {
                    
                    VStack(alignment:.trailing, spacing: 4) {
                        
                        ForEach(filteredProjectsCollection().freeze()) { asset in
                            HStack {
                                    Text(asset.project)
                                    Spacer()
                                Image(systemName: self.model.selectedProjects.contains(asset.project) ? "checkmark.square" : "square")
                                        .resizable()
                                        .frame(width: 17, height: 17)
                                    .onTapGesture { self.model.addProject(project: asset.project) }
                            }
                        }
                    }.frame(maxWidth:.infinity)
                    .padding(.leading, 20)
                    
                } label: {
                    HStack(alignment:.center) {
                        Image(systemName: "person.2")
                        Text("Projects").font(.system(.title3))
                        Spacer()
                    }.padding([.top, .bottom], 8).foregroundColor(.secondary)
                }
                
                // Events
                DisclosureGroup(isExpanded: $isEventsShowing) {
                    
                    VStack(alignment:.trailing, spacing: 4) {
                        
                        ForEach(filteredEventsCollection().freeze()) { asset in
                            HStack {
                             Text(asset.event)
                                Spacer()
                            Image(systemName: self.model.selectedEvents.contains(asset.event) ? "checkmark.square" : "square")
                                    .resizable()
                                    .frame(width: 17, height: 17)
                                .onTapGesture { self.model.addEvent(event: asset.event) }
                            }
                        }
                    }.frame(maxWidth:.infinity)
                    .padding(.leading, 20)
                    
                } label: {
                    HStack(alignment:.center) {
                        Image(systemName: "calendar")
                        Text("Events").font(.system(.title3))
                        Spacer()
                    }.padding([.top, .bottom], 8).foregroundColor(.secondary)
                }
                
                // Locations
                DisclosureGroup(isExpanded: $isLocationsShowing) {
                    
                    VStack(alignment:.trailing, spacing: 4) {
                        
                        ForEach(filteredLocationCollection().freeze()) { asset in
                            HStack {
                             Text(asset.location)
                                Spacer()
                            Image(systemName: self.model.selectedLocations.contains(asset.location) ? "checkmark.square" : "square")
                                    .resizable()
                                    .frame(width: 17, height: 17)
                                .onTapGesture { self.model.addLocation(location: asset.location) }
                            }
                        }
                    }.frame(maxWidth:.infinity)
                    .padding(.leading, 20)
                    
                } label: {
                    HStack(alignment:.center) {
                        Image(systemName: "flag")
                        Text("Locations").font(.system(.title3))
                        Spacer()
                    }.padding([.top, .bottom], 8).foregroundColor(.secondary)
                }
                
            }.padding(.all, 10)
            .background(Color(NSColor.controlBackgroundColor))
        }
            SearchBar(text: self.$searchTerm)
                .frame(height: 30, alignment: .leading)
        }
    }
    
    func filteredProjectsCollection() -> AnyRealmCollection<Asset> {
        if self.searchTerm.isEmpty {
            return AnyRealmCollection(self.projects)
        } else {
            return AnyRealmCollection(self.projects.filter("project CONTAINS[c] %@ || event CONTAINS[c] %@ || location CONTAINS[c] %@ || tags CONTAINS[c] %@", searchTerm, searchTerm, searchTerm, searchTerm))
        }
    }
    func filteredEventsCollection() -> AnyRealmCollection<Asset> {
        if self.searchTerm.isEmpty {
            return AnyRealmCollection(self.events)
        } else {
            return AnyRealmCollection(self.events.filter("project CONTAINS[c] %@ || event CONTAINS[c] %@ || location CONTAINS[c] %@ || tags CONTAINS[c] %@", searchTerm, searchTerm, searchTerm, searchTerm))
        }
    }
    func filteredLocationCollection() -> AnyRealmCollection<Asset> {
        if self.searchTerm.isEmpty {
            return AnyRealmCollection(self.locations)
        } else {
            return AnyRealmCollection(self.locations.filter("project CONTAINS[c] %@ || event CONTAINS[c] %@ || location CONTAINS[c] %@ || tags CONTAINS[c] %@", searchTerm, searchTerm, searchTerm, searchTerm))
        }
    }
    func filteredCollection() -> AnyRealmCollection<Asset> {
        if self.searchTerm.isEmpty {
            return AnyRealmCollection(self.assets)
        } else {
            return AnyRealmCollection(self.assets.filter("project CONTAINS[c] %@ || event CONTAINS[c] %@ || location CONTAINS[c] %@ || tags CONTAINS[c] %@", searchTerm, searchTerm, searchTerm, searchTerm))
        }
    }
    func delete(at offsets: IndexSet) {
        if let realm = assets.realm {
            try! realm.write {
                realm.delete(assets[offsets.first!])
            }
        } else {
            assets.remove(at: offsets.first!)
        }
    }
    
}

struct CategoryBrowserView_Previews: PreviewProvider {
    static var previews: some View {
        CategoryBrowserView()
    }
}

struct CheckboxToggleStyle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        return HStack {
            configuration.label
            Spacer()
            Image(systemName: configuration.isOn ? "checkmark.square" : "square")
                .resizable()
                .frame(width: 22, height: 22)
                .onTapGesture { configuration.isOn.toggle() }
        }
    }
}

这是最直接的方法:

struct ContentView: View {
    @State private var dog: Results<Dog> = try! Realm(configuration: Realm.Configuration(schemaVersion: 1)).objects(Dog.self)

    var body: some View {
        ForEach(dog, id: \.name) { i in
        Text(String((i.name)!))
        }
    }
}

...就是这样,而且有效!