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

10

我已经能够将数据保存在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

如果我只返回结果,List Text View中没有显示任何内容。

正如我在下面评论中所述,当我有时间时,我将尝试使用ForEach方法。那看起来很有前途。


你遇到了什么错误? - Matteo Pacini
我无法遍历结果以显示它们。我可以显示 .first 或 .last,但无法以列表形式显示所有结果。似乎将其与 JSON 数据数组一起使用可以解决问题,但不知道如何处理 Realm 数据。也不确定 ForEach 是否可行。 - K. Law
您可以直接像使用数组一样使用 Result 或 List Realm 对象。但是,如果我们不知道您尝试了什么,我们将无法正确回答问题。另外请注意,您遇到的问题是 SwiftUI 和 Realm 都有一个 List 对象。由于已经发布了解释并提供了答案,因此标记为重复。 - Jay
可能是重复的问题:如何在SwiftUI中使用Realm - Jay
还要注意,如果您将数据从Results对象复制到其他地方,例如数组、列表等,它会“断开”与父对象的连接,因此它们将不再是实时更新的对象。如果您在结果上有一个监听器,它也会被破坏。 - Jay
我是那个关于在SwiftUI中使用Realm时出现List对象冲突的问题提问者。那个问题已经解决了。SwiftUI的问题在于它似乎不知道如何通过SwiftUI List视图迭代和显示Realm Results。 - K. Law
4个回答

18

您传递到 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)
        }
    }

}

Matteo,我一直无法让.identified(by:)方法正常工作。我还没有尝试过Identifiable协议。这可能需要一些时间来解决,而且今天可能无法完成。感谢您的回复。 - K. Law
@K.Law 更新了我的答案,展示了如何在你的特定情况下使用 BindableResults - 看看我如何在 List 中使用 .identified(by:) 方法。 - Matteo Pacini
1
Matteo Pacini,你现在正式成为我的英雄。它完美地运行了。我唯一的遗憾是我没有早点问这个问题。 - K. Law
@MatteoPacini - 我正在尝试遵循这种方法来在SwiftUI List中呈现Realm中的数据,除了从List中删除对象之外,一切都正常,我得到了一个“越界”错误。有趣的是,如果我将List更改为ForEach,它就可以正常工作,您可以添加、删除并更新UI。将List(dogs.results, id: \.name) { dog in更改为ForEach(dogs.results, id: \.name) { dog in即可解决此问题。请注意,我使用的是id:\.name而不是identified(by: \.name) - fs_tigre
@K.Law - 你有没有尝试从“List”中删除行?它对你起作用了吗? - fs_tigre
显示剩余4条评论

3
这是最直接的做法:
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)!))
        }
    }
}

就是这样,它可以运行!


2
我创建了一个通用的解决方案,可用于显示和添加/删除任何 Results<T>。默认情况下,Results<T> 是“实时”的。当 SwiftUI 发送更改到 View 时,@Published 属性将会更新。当接收到 RealmCollectionChange<Results<T>> 通知时,Results<T> 已经更新了;因此,在删除时会出现 fatalError,因为索引超出范围。相反,我使用“实时”的 Results<T> 来跟踪更改,并使用“冻结”的 Results<T> 来与 View 配合使用。其中一个完整的工作示例,包括如何使用具有 RealmViewModel<T> 的通用 View(如下所示),可以在此处找到:SwiftUI+Realm。当适用时,enum Status 用于显示 ProgressView,“未找到记录”等。此外,请注意,在需要计数或单个对象时,使用“冻结”的 Results<T>。在删除时,IndexSetonDelete 返回来自“冻结”的 Results<T> 的位置,因此它检查对象是否仍存在于“实时”的 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: { $0.id == id }) else { return }
            realm.delete(data)
        }
    })
}

}


谢谢您发布这个问题 - 您是否知道新的frozen()集合现在是否正确解决了这个问题?我假设5.2.0示例代码不再存在删除问题,并以安全的方式处理实时更新。 - Duncan Groenewald
冻结集合并不一定直接解决问题。冻结集合基本上是在给定时间的集合副本,当领域发生变化时不会接收更新。要跟踪和应用更改,您必须跟踪实时集合并手动使您的冻结集合保持最新状态。将冻结和实时集合分开保留了控制权,使您可以控制何时将更改发送到视图。目前,我还没有找到任何官方文档或来自Realm的代码,演示如何以其他方式实现SwiftUI和Realm的结合。 - shawnynicole
我按照他们的示例使用了冻结集合,并且它可以实时更新(我的意思是列表中添加/删除的项目)- 我认为它应该可以工作,因为视图正在接收更改通知,然后使用新的“冻结”集合与旧的“冻结”集合进行比较,所以它可以正常工作而不会抛出异常。我想如果集合中的对象属性发生更改,它将无法工作,因为这些属性更改不会在视图中显示。 - Duncan Groenewald
看起来你说的是你的冻结集合正在接收实时更新。冻结集合不应该接收更新,而我使用冻结集合时没有遇到这个问题。你能发一个出现这种情况的例子吗?关于属性更新,更改通知发送3种类型的更改:删除、插入和更新。因此,当属性更改时,您将收到一组对象的索引,这些对象已经更新。如果您想跟踪哪些属性已更改,您需要为每个对象拥有一个通知令牌。我在完整演示中也展示了这一点。 - shawnynicole
是的,当新项目添加到数据库中时,我的视图会自动更新。我将在下面作为另一个答案粘贴代码。 - Duncan Groenewald

0
这里有另一种选项,使用新的 Realm frozen() 集合。虽然现在还处于早期阶段,但当“资产”被添加到数据库中时,UI 会自动更新。在本例中,它们是从 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() }
        }
    }
}

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接