肇鑫的技术博客

业精于勤,荒于嬉

SwiftUI Preview Crashed Issue With Exception `RLMException`. Reason: This method may only be called on RLMArray instances retrieved from an RLMRealm.

After investigation, I found that this issue was caused by sorted method with keyPath.

List { // this list crashes
    ForEach(dayList.items.sorted(by: \.startDate, ascending: true)) { currentItem in
        ItemRowSwiftUIView(currentItem: currentItem)
    }.onDelete(perform: { indexSet in
        removeItems(atOffsets: indexSet)
    })
}
    
List { // this list doesn't crash
    ForEach(dayList.items.sorted(by: {
        $0.startDate < $1.startDate
    })) { currentItem in
        ItemRowSwiftUIView(currentItem: currentItem)
    }.onDelete(perform: { indexSet in
        removeItems(atOffsets: indexSet)
    })
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(dayList: DayList())
    }
}

When using the keyPath to sort, the preview crashed. That because sorted by keyPath used realm and for a unmanaged object dayList, there is no realm.

So we need to create a realm first.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(dayList: createRealmAndGetDayList())
    }

    static func createRealmAndGetDayList() -> DayList {
        let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "for debug"))
        let dayList = DayList()
        
        try! realm.write({
            realm.add(dayList, update: .all)
        })

        return dayList
    }
}

Then everything will work.

Trump Realm With SwiftUI

You may have already known how to using Realm with Swift. In case you were not, we would like to have a quick review.

Realm With Swift

Model

import Foundation
import RealmSwift

class Item:Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var title = ""
    @Persisted(originProperty: "items") var group: LinkingObjects<Group>
}

class Group:Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var items = RealmSwift.List<Item>()
}

Get Results

let realm = try! Realm()
let groups = realm.objects(Group.self)

Mutating Data

try! realm.write {
    let group = Group()
    realm.add(group, update: .all)
    group.items.append(Item())
}

Realm With SwiftUI

Model in Realm with SwiftUI is the same. However, how to getting results and mutating data are much different.

Get Results

Results are got implicitly.

import SwiftUI
import RealmSwift

struct ContentView: View {
    @ObservedResults(Group.self) var groups
    
    var body: some View {
        if let group = groups.last {
            MainSwiftUIView(group: group)
        } else {
            ProgressView().onAppear(perform: {
                $groups.append(Group())
            })
        }
    }
}

You can think @ObservedResults as some kind of Binding struct, its wrappedValue is the original Realm results.

public var wrappedValue: RealmSwift.Results<ResultType> { get }

When you call @ObservedResults(Group.self) var groups, Realm automatically created implicitly.

public init(_ type: ResultType.Type, configuration: RealmSwift.Realm.Configuration? = nil, filter: NSPredicate? = nil, keyPaths: [String]? = nil, sortDescriptor: RealmSwift.SortDescriptor? = nil)

Mutating Data

@ObservedResults(Group.self) var groups

The variable groups and $groups are different. groups is a frozen object of Group. While $groups is a ObservedResults object, it is unfrozen and is used to do mutation.

Frozen object mean the state of the object is unchanged, the normal Realm object is live and unfrozen.

private func add() {
    $group.items.append(Item())
}

@ObservedRealmObject

@ObservedRealmObject is similar like @ObservedResults, but it is not results, but for single realm object.

You should take care of the spelling. As there are a lot similar spellings. Like @ObservableObject, @ObservedObject, they all look like @ObservedRealmObject, if you misspell them, the compiler won't notify and you app won't work.
Also, there are two common naming space issues. RealmSwift.List<T> and SwiftUI.App.

References

Pop View and Void Stacked NavigationBars in SwiftUI

We could use SwiftUI to pop a View to another, like this

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(
                destination: PopView(),
                label: {
                    Text("Pop View")
                })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct PopView:View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    var body: some View {
        Button(
            "Back",
            action: { self.presentationMode.wrappedValue.dismiss() }
        )
    }
}

This will create a button, when you press the button, a new pop up, like below.

Simulator Screen Shot - iPhone 12 Pro - 2021-08-12 at 15.49.44,

Stacked NavigationBars

import SwiftUI

struct PopSwiftUIView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    var body: some View {
        NavigationView {
            Button(
                "Back",
                action: { self.presentationMode.wrappedValue.dismiss() }
            ).toolbar(content: {
                Button(action: { }, label: {
                    Text("Done")
                })
            })
        }
    }
}

struct PopSwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        PopSwiftUIView()
    }
}

Simulator Screen Shot - iPhone 12 Pro - 2021-08-12 at 16.06.17

Above code will run like this, you can see the Done button is stacked to a lower level.

That is because you should only use NavigationView one time. You don't need to use it again if you previously used it.

However, in this case, you need to add NavigationView to preview yourself.

import SwiftUI

struct PopSwiftUIView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    var body: some View {
        Button(
            "Back",
            action: { self.presentationMode.wrappedValue.dismiss() }
        ).toolbar(content: {
            Button(action: { }, label: {
                Text("Done")
            })
        })
    }
}

struct PopSwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            PopSwiftUIView()
        }
    }
}

Hide Back Button

Use .navigationBarBackButtonHidden(true) to hide the back button.

Simulator Screen Shot - iPhone 12 Pro - 2021-08-12 at 16.12.52

References