【问题标题】:How to display Realm Results in SwiftUI List?如何在 SwiftUI 列表中显示领域结果?
【发布时间】:2019-06-23 01:47:31
【问题描述】:

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

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

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

【问题讨论】:

  • 您遇到了什么错误?
  • 我无法遍历结果来显示它们。我可以显示 .first 或 .last 但不能在列表中显示所有结果。似乎它可以处理一组 json 数据,但不知道如何处理领域数据。我不知道 ForEach 是否也有效。
  • 您可以直接使用 Result 或 List Realm Object,其方式与数组非常相似。但是,如果没有看到您的尝试,我们将无法(正确地)回答问题。另请注意,您遇到的问题是 SwiftUI 和 Realm 都有一个 List 对象。标记为重复,因为发布的解释将提供答案。
  • 另请注意,如果您将数据从结果对象复制到其他对象、数组、列表等,它会“中断”与父对象的连接,因此它们将不再是实时更新对象。如果你有一个结果监听器,它也会破坏它。

标签: swift realm swiftui


【解决方案1】:

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

}

【讨论】:

  • 虽然这里有一些很好的信息,但它并没有解决问题。 OP 正在使用此答案未解决的 SwiftUI List 。另外真正的问题是这个 我无法遍历结果以显示它们 在 OP 发布他们尝试过的代码之前我们无法解决这个问题 - 我猜这是一个 SwiftUI/Realm 问题但是我们得看看。
  • 另请注意,Realm 结果符合标准协议,因此直接支持 forEach。因此,例如,如果您将一堆狗存储在 Realm 中并使用 let dogResults = realm.objects(DogClass.self) 加载,那么您可以使用 dogResults.forEach 遍历结果{ print($0.dog_name)} 打印他们的名字。所以不清楚为什么他们不能迭代一个 Results 类。
  • 谢谢杰。我将尝试使用 forEach 方法并进行报告。我也会努力让代码稍后发布。
  • Matteo,我无法让 .identified(by:) 方法工作。我还没有尝试过 Identifiable 协议。可能需要一些时间来解决这个问题,而我今天可能无法做到。感谢您的回复。
  • Matteo Pacini,你正式成为我的英雄。它就像一个魅力。我唯一的遗憾是我没有早点问这个问题。
【解决方案2】:

这是最直接的做法:

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)!))
        }
    }
}

...就是这样,它的工作原理!

【讨论】:

    【解决方案3】:

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

    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 示例代码不再有删除问题,并以安全的方式处理实时更新。
    • 冻结集合不一定直接解决问题。冻结的集合基本上是该集合在给定时间的副本,并且在领域更改时不会收到更新。要跟踪和应用更改,您必须跟踪实时集合并手动使您的冻结集合保持最新。分开保存冻结和实时集合允许您控制何时将更改发送到视图。目前,我无法从 Realm 中找到任何官方文档或代码来演示如何以其他方式同时实现 SwiftUI 和 Realm。
    • 我正在按照他们的示例使用冻结的集合,并且它会实时更新(我的意思是从列表中添加/删除的项目)-我认为它应该可以工作,因为视图正在获取更改通知然后使用新的“冻结”集合与旧的冻结集合进行比较,因此它可以正常工作而不会引发异常。我猜它不起作用的地方是如果集合中的对象的属性发生了更改,这些属性更改将不会显示在视图中。
    • 您似乎在说您的冻结收藏正在接收实时更新。冻结的集合不应该接收更新,并且在使用冻结的集合时我没有这个问题。你能发布一个发生这种情况的例子吗?关于属性更新,更改通知发送 3 种类型的更改:删除、插入和更新。因此,当属性发生更改时,您将收到一个包含已更新对象的索引数组。如果要跟踪更改了哪些属性,则需要为每个对象提供一个通知令牌。我还在我的完整演示中证明了这一点。
    • 是的,当新项目添加到数据库时,我的视图会自动更新。我将通过代码作为下面的另一个答案。
    【解决方案4】:

    这是使用新 Realm freeze() 集合的另一个选项。虽然这是早期的,但当“资产”添加到数据库时,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() }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多