RealmSwift,如何在加载时对属性列表进行排序并在重新排序期间保持更新?

问题描述 投票:0回答:2

我在 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 上写入了更改,但除非我执行重新加载视图的操作(例如切换编辑模式),否则更改不会发生。

ios sorting swiftui realm
2个回答
0
投票

这是绝对可行的。假设您有两个如下所示的模型,您应该能够在更新模型对象的位置属性时添加、删除和移动行。我一直这样做。

假设您的模型对象如下所示:

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

0
投票

如果您已经在使用列表,则只需使用

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 个独立设备上创建项目的场景。

© www.soinside.com 2019 - 2024. All rights reserved.