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

Posted

技术标签:

【中文标题】如何在 SwiftUI 列表中显示领域结果?【英文标题】:How to display Realm Results in SwiftUI List? 【发布时间】: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 对象。标记为重复,因为发布的解释将提供答案。 How to use Realm with 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+Realm。 enum 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() 
        
    

【讨论】:

以上是关于如何在 SwiftUI 列表中显示领域结果?的主要内容,如果未能解决你的问题,请参考以下文章

当带有过滤器选项的 ForEach 在 Swiftui 中没有显示结果时,如何显示文本?

如何正确地将 RealmDB 结果对象映射到 SwiftUI 列表?

如何在 SwiftUI 列表中显示 MPMediaItem 图稿?

如何在 SwiftUI 中使用 .fileimporter 保存音频文件并检索文件并在 SwiftUI 列表中显示它们的文件名?

如何在 SwiftUI 中清除列表项上的默认突出显示灰色?

从 SwiftUI 列表和领域中删除记录时出错