I've got the following Object schema that I'm working with in Realm (I'm describing it using Typescript notation cause I'm comfortable with it):
Publisher {
name: String (primary key),
position: Int,
games: RealmSwift.List<Game>,
}
Game {
name: String (primary key),
description: String,
cover: String,
position: Int,
}
So, every publisher has its own list of games and onAppear of the outermost view of the ContentView.swift I load all the publishers from Realm, and sort them by their position
keyPath. However, I would also like the games
list to be sorted by position
for each publisher and I'm not sure if you can do it directly. For now this is the code I wrote under the onAppear
method:
Relevant parts of 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")
})
}
}
}
When I run the code above I get the following error at @main, apparently caused by the code inside the onAppear
block (since if I remove it everything works fine):
"This method may only be called on RLMArray instances retrieved from an RLMRealm"
Also I would like that when in editing mode, on swapping position of two games, the changes should be written on the Realm database and immediately updated in my app, as in I would like to "reload" on change. The way it is, it already writes changes on Realm but changes don't happen unless I do something that reloads the View, like toggling the edit mode.
This is absolutely doable. Assuming you had two models that looked like this you should be able to add, delete, and move rows around while updating the position properties of your model objects. I do this all the time.
Let's assume your model objects look like this:
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
}
}
Now let's create a protocol that defines a position
property.
protocol Positionable {
var position: Int { get set }
}
Now let's make our models conform to it:
extension Game: Positionable {}
extension Publisher: Positionable {}
This Results
extension will allow us to "move" objects sorted by position
. Which means that it will update the position
properties for all elements that need updating. Keep in mind, this requires that your result set is already sorted by 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
}
}
}
Now we need to do something similar for List
. This one is a bit simpler because List offers a move(fromOffsets: toOffsets:)
method. However, as you recall, you also need to update the position
property for all affected elements.
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
}
}
}
Just for kicks let's add a delete method that updates the position
property. Now we can use this method on List
and Results
since they both conform to RealmCollection
. Again, this method assumes that the collection is already sorted by 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
}
}
}
}
As for the UI, make sure that you are using the @ObservedResults
property wrapper for your Results<Publisher>
property and using the @ObservedRealmObject
property wrapper for your Publisher
properties.
Also, remember these property wrappers will freeze all of your objects so you will need to call thaw()
in order to unfreeze them before any realm writes.
Here is a sample UI for you:
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)
}
}
}