我在 Realm 中使用了以下对象模式(我使用 Typescript 表示法来描述它,因为我对此感到满意):
Publisher {
name: String (primary key),
position: Int,
games: RealmSwift.List<Game>,
}
Game {
name: String (primary key),
description: String,
cover: String,
position: Int,
}
因此,每个发行商都有自己的游戏列表,并且在 ContentView.swift 最外层视图的 onAppear 上,我从 Realm 加载所有发行商,并按其
position
keyPath 对它们进行排序。但是,我还希望每个发布商的 games
列表按 position
排序,我不确定您是否可以直接这样做。现在这是我在 onAppear
方法下编写的代码:
ContentView.swift相关部分:
import SwiftUI
import RealmSwift
struct ContentView: View {
@State var publishers: Results<Publisher>? = nil
private var realm: Realm
init() {
self.realm = try! Realm()
}
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
if publishers != nil {
ForEach(publishers!, id: \.name) { publisher in
PublisherName(publisher: publisher.name)
List {
ForEach(publisher.games, id: \.name) { game in
HStack {
Text(String(game.position))
.font(.system(size:20))
.fontWeight(.bold)
GameRow(game: game)
}
}.onMove(perform: {
startIndex, destination in
do {
try self.realm.write {
publisher.games.move(fromOffsets: startIndex, toOffset: destination-1)
}
}catch {
print("Error: \(error)")
}
})
}
}
}
}
}
.onAppear(perform: {
let realm = try! Realm()
self.publishers
= realm.objects(Publisher.self)
.reduce(RealmSwift.List<Publisher>(), { publishersList, publisher -> RealmSwift.List<Publisher> in
let p = Publisher()
p.name = publisher.name
p.position = publisher.position
p.games = publisher.games.sorted(byKeyPath: "position")
.reduce(RealmSwift.List<Game>(), { gamesList, game -> RealmSwift.List<Game> in
gamesList.append(game)
return gamesList
})
publishersList.append(p)
return publishersList
})
.sorted(byKeyPath: "position")
})
}
}
}
当我运行上面的代码时,我在 @main 处收到以下错误,显然是由
onAppear
块内的代码引起的(因为如果我删除它,一切正常):
"This method may only be called on RLMArray instances retrieved from an RLMRealm"
我还希望在编辑模式下,在交换两个游戏的位置时,更改应写入领域数据库并立即在我的应用程序中更新,就像我想在更改时“重新加载”一样。事实是,它已经在 Realm 上写入了更改,但除非我执行重新加载视图的操作(例如切换编辑模式),否则更改不会发生。
这是绝对可行的。假设您有两个如下所示的模型,您应该能够在更新模型对象的位置属性时添加、删除和移动行。我一直这样做。
假设您的模型对象如下所示:
class Publisher: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var name = ""
@Persisted var position: Int = 0
@Persisted var games: RealmSwift.List<Game>
convenience init(name: String, position: Int, games: [Game]) {
self.init()
self.name = name
self.position = position
self.games.append(objectsIn: games)
}
}
class Game: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var name = ""
@Persisted var position: Int = 0
convenience init(name: String, position: Int) {
self.init()
self.name = name
self.position = position
}
}
现在让我们创建一个定义
position
属性的协议。
protocol Positionable {
var position: Int { get set }
}
现在让我们的模型符合它:
extension Game: Positionable {}
extension Publisher: Positionable {}
这个
Results
扩展将允许我们“移动”按 position
排序的对象。这意味着它将更新所有需要更新的元素的 position
属性。请记住,这要求您的结果集已按位置排序。
extension Results where Element: Object & Positionable {
func repositionElement(atOffset source: Int, toOffset destination: Int) {
if source < destination {
let nextElements = self[source+1..<destination]
var target = self[source]
for var element in nextElements {
element.position -= 1
}
target.position = destination - 1
} else {
let prevElements = self[destination..<source]
var target = self[source]
for var element in prevElements {
element.position += 1
}
target.position = destination
}
}
}
现在我们需要对
List
做类似的事情。这个稍微简单一些,因为 List 提供了一个 move(fromOffsets: toOffsets:)
方法。但是,您还记得,您还需要更新所有受影响元素的 position
属性。
extension List where Element: Object & Positionable {
func repositionElement(atOffset source: Int, toOffset destination: Int) {
move(fromOffsets: [source], toOffset: destination)
enumerated()
.forEach {
var element = $0.element
element.position = $0.offset
}
}
}
为了好玩,我们添加一个更新
position
属性的删除方法。现在我们可以在 List
和 Results
上使用这个方法,因为它们都符合 RealmCollection
。同样,此方法假设集合已按 position
排序。
extension RealmCollection where Element: Object & Positionable {
func deleteElement(atOffset offset: Int) {
guard let element = objects(at: [offset]).first else { return }
realm?.delete(element)
for i in offset..<count {
if var element = objects(at: [i]).first {
element.position -= 1
}
}
}
}
对于 UI,请确保您对
@ObservedResults
属性使用 Results<Publisher>
属性包装器,并对 @ObservedRealmObject
属性使用 Publisher
属性包装器。
另外,请记住这些属性包装器将冻结所有对象,因此您需要调用
thaw()
以便在任何领域写入之前解冻它们。
这是一个供您使用的示例 UI:
struct ContentView: View {
@ObservedResults(
Publisher.self,
configuration: .defaultConfiguration,
sortDescriptor: .init(keyPath: \Publisher.position)) var publishers
let names = ["Bob","Tom","Tim","Sally","Joe","Tammy","Tina","Jeff","Meghan","Don","Kirk","Justin","Jason"]
var body: some View {
NavigationView {
List {
ForEach(publishers) { publisher in
HStack {
Text("\(publisher.position):")
.font(.system(size: 20))
.bold()
Text(publisher.name)
.font(.system(size: 20))
}
GameList(publisher: publisher)
}
.onMove(perform: movePublisher(fromOffsets:toOffset:))
.onDelete (perform: deletePublisher(atOffsets:))
}.toolbar {
ToolbarItemGroup.init(placement: .navigationBarTrailing) {
EditButton()
Button("Add") {
addPublisher()
}
.disabled(publishers.count == names.count)
}
}
}
}
func addPublisher() {
let realm = try! Realm()
let pos = Publisher.nextPosition(in: realm)
guard let name = names.first(
where: { publishers.filter("name == %@", $0).count == 0 }) else {
return print("No more names")
}
let ordinalFormatter = NumberFormatter()
ordinalFormatter.numberStyle = .ordinal
let pub = Publisher(
name: name,
position: pos,
games: (0..<3)
.map {
.init(
name: "\(name).game.\(ordinalFormatter.string(from: NSNumber(value: $0 + 1)) ?? "")",
position: $0)
}
)
try! realm.write {
realm.add(pub)
}
}
func movePublisher(fromOffsets offsets: IndexSet, toOffset destination: Int) {
guard let offset = offsets.first else { return }
let realm = try! Realm()
try! realm.write {
publishers.thaw()?.repositionElement(
atOffset: offset,
toOffset: destination
)
}
}
func deletePublisher(atOffsets offsets: IndexSet) {
guard let offset = offsets.first else { return }
let realm = try! Realm()
try! realm.write {
guard let publishers = self.publishers.thaw() else { return }
realm.delete(publishers[offset].games)
publishers.deleteElement(atOffset: offset)
}
}
}
struct GameList: View {
@ObservedRealmObject var publisher: Publisher
var body: some View {
ForEach(publisher.games) { game in
HStack {
Text(String(game.position) + ":")
Text(String(game.name))
}
.padding(.leading)
}.onMove(perform: moveGame(fromOffsets:toOffset:))
.onDelete(perform: deleteGame(atOffsets:))
}
func moveGame(fromOffsets offsets: IndexSet, toOffset destination: Int) {
guard let offset = offsets.first else { return }
let realm = try! Realm()
try! realm.write {
publisher.thaw()?.games
.repositionElement(atOffset: offset, toOffset: destination)
}
}
func deleteGame(atOffsets offsets: IndexSet) {
guard let offset = offsets.first else { return }
let realm = try! Realm()
try! realm.write {
publisher.thaw()?.games
.deleteElement(atOffset: offset)
}
}
}
如果您已经在使用列表,则只需使用
list.move
。
class ItemGroup: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var items = RealmSwift.List<Item>()
}
@ObservedRealmObject var itemGroup: ItemGroup
List {
ForEach(itemGroup.items) { item in
ItemRow(item: item)
}
.onDelete(perform: $itemGroup.items.remove)
.onMove(perform: $itemGroup.items.move)
}
A
List
存储为对象 id 数组,因此您可以依赖对象 id 在数组中的位置。 $
将操作包装在写入事务中。
如果您想使用自定义字段来排序数据,这里是一些模板代码。如果您使用灵活同步,则需要处理使用相同顺序/位置在 2 个独立设备上创建项目的场景。