如何在 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】:您传入List
或ForEach
的数据必须符合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<T>
。默认情况下,Results<T>
是“实时的”。当 @Published
属性 WILL 更新时,SwiftUI 会将更改发送到 View。当收到RealmCollectionChange<Results<T>>
通知时,Results<T>
已经更新;因此,由于索引超出范围,删除时会出现fatalError
。相反,我使用“实时”Results<T>
跟踪更改,使用“冻结”Results<T>
与视图一起使用。完整的工作示例,包括如何使用通用 View
和 RealmViewModel<T>
(如下所示),可以在此处找到:SwiftUI+Realm。 enum Status
用于在适用时显示ProgressView
、“未找到记录”等,如项目所示。另外,请注意“冻结”Results<T>
在需要计数或单个对象时使用。删除时,onDelete
的 IndexSet
将从“冻结”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 示例代码不再有删除问题,并以安全的方式处理实时更新。 冻结集合不一定直接解决问题。冻结的集合基本上是该集合在给定时间的副本,并且在领域更改时不会收到更新。要跟踪和应用更改,您必须跟踪实时集合并手动使您的冻结集合保持最新。分开保存冻结和实时集合允许您控制何时将更改发送到视图。目前,我无法从 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 列表中显示它们的文件名?