RealmSwift.List support for new editable List in SwiftUI

I have currently implemented this and it works. Here flowers is a List of Flower which is
a Realm.Object.

NavigationSplitView {
    List(selection: $selectedFlower) {
        ForEach($viewModel.flowers) { $flower in
            NavigationLink(value: flower) {
                FlowerRow(flower: flower)
            }
        }
        .onDelete { indexSet in
            withAnimation {
                selectedFlower = nil
                $viewModel.flowers.remove(atOffsets: indexSet)
            }
        }
    }
} detail: { /* Omitted */ }

SwiftUI (xCode14, beta 4) now provides an editable SwiftUI.List, making this more compact code possible:

NavigationSplitView {
    List($viewModel.flowers, editActions: [.delete], selection: $selectedFlower) { $flower in
        NavigationLink(value: flower) {
            FlowerRow(flower: flower)
        }
    }
} detail: { /* Omitted */ }

Attempting to do this with a RealmSwift.List gives the following error message:
‘delete’ is unavailable: Delete is available only for collections conforming to RangeReplaceableCollection.

RangeReplaceableCollection conformance requires an empty initializer and the
func replaceSubrange(Range<Self.Index>, with: C) method. These are both implemented by RealmSwift.List.

By declaring compliance to RangeReplaceableCollection in an extension to List I can get the code to
compile and run, unsurpisingly I get this error message.
‘Cannot modify managed RLMArray outside of a write transaction.’

If I then make a copy of replaceSubrange and for debugging purposes simplify it to the delete of a single Element
of the list and try to wrap that operation in a write transation like this:

extension List {

    public func replaceSubrange<C: Collection, R>(_ subrange: R, with newElements: C)
        where C.Iterator.Element == Element, R: RangeExpression, List<Element>.Index == R.Bound {
            let subrange = subrange.relative(to: self)
            
            let thawed = self.thaw() ?? self
            if let realm = thawed.realm, !realm.isInWriteTransaction {
                try! realm.write {
                    remove(at: subrange.lowerBound)
                }
            } else {
                remove(at: subrange.lowerBound)
            }
            
    }
}

I get this error message:
‘Object is already managed by another Realm. Use create instead to copy it into this Realm.’

If I check the realm objects of self and thawed it seems the thawing creates and new object with a different Realm.

(lldb) po thawed.realm
▿ Optional<Realm>
  ▿ some : Realm
    - rlmRealm : <RLMRealm: 0x60000018c6e0>

(lldb) po self.realm
▿ Optional<Realm>
  ▿ some : Realm
    - rlmRealm : <RLMRealm: 0x600000188370>

At this point I am stumped. Any suggestions?

Also in other places of my app I have found that if I directly use $list.append or remove
methods, things work, but as soon as I deviate from this I tend to end up with different list objects and multiple
realms. I would greatly appreciate an advanced tutorial on this topic!

I guess many developers would like to use the new cleaner approach to NavigationSplitView and List so it would
be good if you could get this to work.

As a side comment I have implemented my app on two different branches one for CoreData and one for Realm.
I very much prefer Realm with its bettter fit to Swift. However I found the topic of updating elements and
appending / removing from lists so complicated that at one point I gave up work on the Realm branch.

To summarize several days of investigation …

If I create a managed object in my App view (StateRealmObject) and pass it as an
argument to ContentView (ObservedRealmObject) the application fails with
“Object is already managed by another Realm”.

If I create the same StateRealmObject directly in ContentView the application actually works!

Very confusing to me. I do not see a reason for dual Realms to be created because a managed object
is passed in as an argument.

I have created a simplfied version of my app for this investigation. I include it here in full.
Any hint on how to proceed would be much appreciated.

SwiftListApp:

@main
struct SwiftListApp: SwiftUI.App {
    @StateRealmObject var viewModel: ViewModel = {
        try! Realm().write {
            let model = ViewModel()
            try! Realm().add(model)
            model.flowers.append(objectsIn: [
                Flower(index: 1, name: "one"),
                Flower(index: 2, name: "two")
            ])
            return model
        }
    }()
    
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: viewModel)
        }
    }
}

ContentView:

struct ContentView: View {
    @ObservedRealmObject var viewModel: ViewModel
    
    @State var selectedFlower: Flower?
    
    var body: some View {
        NavigationSplitView {
            List($viewModel.flowers, editActions: [.delete], selection: $selectedFlower) { $flower in
                NavigationLink(value: flower) {
                    Text("\(flower.index)")
                }
            }
            .navigationTitle("Blommor")
            .toolbar {
                EditButton()
            }

        } detail: {
            if let flower = selectedFlower {
                Text(flower.name)
            } else {
                Text("Select a flower")
            }
        }
    }
}

ViewModel:

class Flower: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var index: Int
    @Persisted var name: String
    
    /// Default initializer
    override init() {
        super.init()
    }
    
    convenience init(index: Int, name: String) {
        self.init()
        self.index = index
        self.name = name
    }
}

class ViewModel: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var id: ObjectId
    @Persisted var flowers = RealmSwift.List<Flower>()
    
    /// Default initializer
    override init() {
        super.init()
    }
}

extension RealmSwift.List: RangeReplaceableCollection {
    
}

extension List {

    public func replaceSubrange<C: Collection, R>(_ subrange: R, with newElements: C)
        where C.Iterator.Element == Element, R: RangeExpression, List<Element>.Index == R.Bound {
            let subrange = subrange.relative(to: self)
            
            let thawed = self.thaw() ?? self
            if let realm = thawed.realm, !realm.isInWriteTransaction {
                try! realm.write {
                    thawed.remove(at: subrange.lowerBound)
                }
            } else {
                thawed.remove(at: subrange.lowerBound)
            }
            
    }
}

I am having a bit of trouble trying to replicate the issue. The gist of the problem is you’re instantiating a Realm Object, ViewModel, populating it with some data, returning that model and passing it to another view. That action throws the error. I wanted to try to address that part:

I’ve got a model TestModel

class TestModel: Object, ObjectKeyIdentifiable {
   @Persisted(primaryKey: true ) var id: ObjectId
   @Persisted var testField = ""
}

Here’s the @StateRealmObject that instantiates a model, stores it in Realm and then is used as an argument to the ContentView

struct Realm_SwiftUI_MacApp: SwiftUI.App {
    @StateRealmObject var testModel: TestModel = {
        try! Realm().write {
            let model = TestModel()
            try! Realm().add(model)
            model.testField = "Hello, World"
            return model
        }
    }()
    
    var body: some Scene {
        WindowGroup {
            ContentView(myTestModel: testModel)
        }
    }
}

and then the ContentView

struct ContentView: View {
    @ObservedRealmObject var myTestModel: TestModel
    .
    .
    .
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(testLabel) { labels in //this is just a list of a, b, c that are clickable
                        NavigationLink(destination: TestView(myObservedTestModel: myTestModel))
                        {
                            Text(testLabel.myId)
                        }

                    }
                }
            }

and then the TestView

struct TestView: View {
   @Binding var navigationViewIsActive: Bool
   @ObservedRealmObject var myObservedTestModel: TestModel

That all works correctly. The only little and probably not an issue thing is returning inside the write transaction?

Instead of this

try! Realm().write {
   return model
}

maybe this?

try! Realm().write {
   
}
return model

That’s probably not the issue.

Hi,
Thank you for your response. Passing the StateRealmObject from the app to the ContentView works OK. The error occurs deep in Realm when deleting a object from the List as described in my first post. The trace is below.

It seems that SwiftUI’s editable List, sets its data object with same list (same objectId) after doing the delete operation (to trigger notification?). As far as I have been able to trace down in the Realm code, it seems Realm does not recognize that this set operation is setting the list that is already set. I think this is the cause for the error. Why it only occurs when the object is an ObservedObject and not a StateObject I have no idea, still learning :thinking:

2022-08-10 10:07:46.158934+0200 SwiftList[55775:3565350] *** Terminating app due to uncaught exception 'RLMException', reason: 'Object is already managed by another Realm. Use create instead to copy it into this Realm.'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007ff800427380 __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007ff80004dbaf objc_exception_throw + 48
	2   SwiftList                           0x00000001079e64c5 _ZN18RLMAccessorContext12createObjectEP11objc_objectN5realm12CreatePolicyEbNS2_6ObjKeyE + 1829
	3   SwiftList                           0x00000001079e8083 _ZN18RLMAccessorContext5unboxIN5realm3ObjEEET_P11objc_objectNS1_12CreatePolicyENS1_6ObjKeyE + 83
	4   SwiftList                           0x00000001079f6e2e _ZZN5realm4List3addIRU8__strongP11objc_object18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyEENKUlS9_E_clIPNS_3ObjEEEDaS9_ + 94
	5   SwiftList                           0x00000001079f6701 _ZN5realmL14switch_on_typeINS_3ObjEZNS_4List3addIRU8__strongP11objc_object18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyEEUlSB_E_EEDaNS_12PropertyTypeEOS9_ + 337
	6   SwiftList                           0x00000001079f6492 _ZNK5realm4List8dispatchIZNS0_3addIRU8__strongP11objc_object18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyEEUlSA_E_EEDaSB_ + 66
	7   SwiftList                           0x00000001079f5fed _ZN5realm4List3addIRU8__strongP11objc_object18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyE + 253
	8   SwiftList                           0x0000000107aa89f5 _ZZN5realm4List6assignIRU8__strongKP7NSArray18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyEENKUlSA_E_clIRU8__strongP11objc_objectEEDaSA_ + 85
	9   SwiftList                           0x0000000107aa8863 _ZN27RLMStatelessAccessorContext20enumerate_collectionIZN5realm4List6assignIRU8__strongKP7NSArray18RLMAccessorContextEEvRT0_OT_NS1_12CreatePolicyEEUlSC_E_EEvP11objc_objectSC_ + 467
	10  SwiftList                           0x0000000107a9fd6f _ZN5realm4List6assignIRU8__strongKP7NSArray18RLMAccessorContextEEvRT0_OT_NS_12CreatePolicyE + 207
	11  SwiftList                           0x0000000107a9fc57 __48-[RLMManagedArray replaceAllObjectsWithObjects:]_block_invoke_2 + 119
	12  SwiftList                           0x0000000107aa4489 _ZL15translateErrorsIRU8__strongU13block_pointerFvvEEDaOT_ + 25
	13  SwiftList                           0x0000000107aa4aa2 _ZL11changeArrayIZL11changeArrayP15RLMManagedArray16NSKeyValueChange8_NSRangeU13block_pointerFvvEE4$_23EvS1_S2_S5_OT_ + 514
	14  SwiftList                           0x0000000107a9f291 _ZL11changeArrayP15RLMManagedArray16NSKeyValueChange8_NSRangeU13block_pointerFvvE + 81
	15  SwiftList                           0x0000000107a9fb19 -[RLMManagedArray replaceAllObjectsWithObjects:] + 825
	16  SwiftList                           0x0000000107a4c800 RLMAssignToCollection + 80
	17  SwiftList                           0x0000000107d6d4d4 $s10RealmSwift4ListC6assignyyypF + 404
	18  SwiftList                           0x0000000107d6d580 $s10RealmSwift4ListCyxGAA07MutableA10CollectionA2aEP6assignyyypFTW + 16
	19  SwiftList                           0x0000000107dcea04 $s10RealmSwift9PersistedV3set_5valueySo13RLMObjectBaseC_xtF + 580
	20  SwiftList                           0x0000000107dce73c $s10RealmSwift9PersistedV18_enclosingInstance7wrapped7storagexqd___s24ReferenceWritableKeyPathCyqd__xGAHyqd__ACyxGGtcSo13RLMObjectBaseCRbd__luisZ + 268
	21  SwiftList                           0x00000001079d32f6 $s9SwiftList9ViewModelC7flowers05RealmA00B0CyAA6FlowerCGvs + 134
	22  SwiftList                           0x00000001079cf601 $s9SwiftList9ViewModelC7flowers05RealmA00B0CyAA6FlowerCGvpACTk + 113
	23  libswiftCore.dylib                  0x00007ff80d70c3ca $ss26NonmutatingWritebackBufferCfD + 138
	24  libswiftCore.dylib                  0x00007ff80d910b50 _swift_release_dealloc + 16
	25  libswiftCore.dylib                  0x00007ff80d70dd8b swift_setAtReferenceWritableKeyPath + 219
	26  SwiftList                           0x0000000107e0d5f8 $s10RealmSwift23createCollectionBinding33_06F2B43D1E2DA64D3C5AC1DADA9F5BA7LL_10forKeyPath0B2UI0E0Vyq_Gx_s017ReferenceWritablerS0Cyxq_GtAA14ThreadConfinedRzSo08RLMSwiftD4BaseCRb_AaLR_r0_lFyq_cfU0_yxXEfU_ + 184
	27  SwiftList                           0x0000000107e0bded $s10RealmSwift9safeWrite33_06F2B43D1E2DA64D3C5AC1DADA9F5BA7LLyyx_yxXEtAA14ThreadConfinedRzlFyyXEfU_ + 61
	28  SwiftList                           0x0000000107e417d4 $s10RealmSwift9safeWrite33_06F2B43D1E2DA64D3C5AC1DADA9F5BA7LLyyx_yxXEtAA14ThreadConfinedRzlFyyXEfU_TA + 36
	29  SwiftList                           0x0000000107deb773 $s10RealmSwift0A0V5write16withoutNotifying_xSaySo20RLMNotificationTokenCG_xyKXEtKlF + 275
	30  SwiftList                           0x0000000107e0bca2 $s10RealmSwift9safeWrite33_06F2B43D1E2DA64D3C5AC1DADA9F5BA7LLyyx_yxXEtAA14ThreadConfinedRzlF + 1042
	31  SwiftList                           0x0000000107e0d519 $s10RealmSwift23createCollectionBinding33_06F2B43D1E2DA64D3C5AC1DADA9F5BA7LL_10forKeyPath0B2UI0E0Vyq_Gx_s017ReferenceWritablerS0Cyxq_GtAA14ThreadConfinedRzSo08RLMSwiftD4BaseCRb_AaLR_r0_lFyq_cfU0_ + 297
	32  SwiftUI                             0x0000000110945e97 get_witness_table 7SwiftUI4ViewRzAA10ShapeStyleRd__r__lAA15ModifiedContentVyxAA018_DefaultForegroundE8ModifierVyqd__GGAaBHPxAaBHD1__AhA0cJ0HPyHCHCTm + 3335
	33  SwiftUI                             0x00000001106357a5 __swift_memcpy74_8 + 30437
	34  SwiftUI                             0x00000001106357f4 __swift_memcpy74_8 + 30516
	35  SwiftUI                             0x0000000110634adc __swift_memcpy74_8 + 27164
	36  SwiftUI                             0x00000001109455d0 get_witness_table 7SwiftUI4ViewRzAA10ShapeStyleRd__r__lAA15ModifiedContentVyxAA018_DefaultForegroundE8ModifierVyqd__GGAaBHPxAaBHD1__AhA0cJ0HPyHCHCTm + 1088
	37  SwiftUI                             0x000000010feac3b2 get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA32_EnvironmentKeyTransformModifierVyAA06ScrollE10BackgroundVGGAaBHPxAaBHD1__AiA0cI0HPyHCHCTm + 7669
	38  SwiftUI                             0x000000010fbcbf4b get_witness_table 7SwiftUI4ViewRzlAA15ModifiedContentVyxAA26_PreferenceWritingModifierVyAA019PresentationDetentsF3KeyVGGAaBHPxAaBHD1__AiA0cH0HPyHCHCTm + 5396
	39  SwiftUI                             0x000000010fbcc585 get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 1152
	40  SwiftUI                             0x000000010fbcc44b get_witness_table 7SwiftUI18DynamicViewContentRzlAA08ModifiedE0VyxAA21_TraitWritingModifierVyAA08OnDeleteG3KeyVGGAaBHPxAaBHD1__AiA0dI0HPyHCHCTm + 838
	41  SwiftUI                             0x0000000110262698 objectdestroy.136Tm + 41111
	42  SwiftUI                             0x000000010ff85497 block_destroy_helper.15 + 46631
	43  SwiftUI                             0x000000010ff84df8 block_destroy_helper.15 + 44936
	44  SwiftUI                             0x00000001103747f0 __swift_memcpy56_4 + 171220
	45  SwiftUI                             0x0000000110375409 block_destroy_helper + 232
	46  SwiftUI                             0x00000001103741fc __swift_memcpy56_4 + 169696
	47  SwiftUI                             0x000000011037579d block_destroy_helper + 1148
	48  SwiftUI                             0x0000000110482df8 __swift_memcpy94_8 + 16562
	49  UIKitCore                           0x000000010cf99573 -[UIContextualAction executeHandlerWithView:completionHandler:] + 148
	50  UIKitCore                           0x000000010cfa8310 -[UISwipeOccurrence _executeLifecycleForPerformedAction:sourceView:completionHandler:] + 656
	51  UIKitCore                           0x000000010cfa89f0 -[UISwipeOccurrence _performSwipeAction:inPullView:swipeInfo:] + 620
	52  UIKitCore                           0x000000010cfaa4a3 -[UISwipeOccurrence swipeActionPullView:tappedAction:] + 62
	53  UIKitCore                           0x000000010cfb2bb4 -[UISwipeActionPullView _tappedButton:] + 148
	54  UIKitCore                           0x000000010cde1cbb -[UIApplication sendAction:to:from:forEvent:] + 95
	55  UIKitCore                           0x000000010c554fe3 -[UIControl sendAction:to:forEvent:] + 110
	56  UIKitCore                           0x000000010c5553e7 -[UIControl _sendActionsForEvents:withEvent:] + 345
	57  UIKitCore                           0x000000010c551507 -[UIButton _sendActionsForEvents:withEvent:] + 148
	58  UIKitCore                           0x000000010c553c3e -[UIControl touchesEnded:withEvent:] + 485
	59  UIKitCore                           0x000000010c7e82f7 _UIGestureEnvironmentUpdate + 9811
	60  UIKitCore                           0x000000010c7e584a -[UIGestureEnvironment _updateForEvent:window:] + 844
	61  UIKitCore                           0x000000010ce28f2f -[UIWindow sendEvent:] + 5282
	62  UIKitCore                           0x000000010cdfc722 -[UIApplication sendEvent:] + 898
	63  UIKitCore                           0x000000010cea3620 __dispatchPreprocessedEventFromEventQueue + 9383
	64  UIKitCore                           0x000000010cea5d3d __processEventQueue + 8355
	65  UIKitCore                           0x000000010ce9c1a0 __eventFetcherSourceCallback + 272
	66  CoreFoundation                      0x00007ff800386eed __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	67  CoreFoundation                      0x00007ff800386e2c __CFRunLoopDoSource0 + 157
	68  CoreFoundation                      0x00007ff800386629 __CFRunLoopDoSources0 + 212
	69  CoreFoundation                      0x00007ff800380de3 __CFRunLoopRun + 927
	70  CoreFoundation                      0x00007ff800380667 CFRunLoopRunSpecific + 560
	71  GraphicsServices                    0x00007ff809bfc28a GSEventRunModal + 139
	72  UIKitCore                           0x000000010cddb621 -[UIApplication _run] + 994
	73  UIKitCore                           0x000000010cde04fd UIApplicationMain + 123
	74  SwiftUI                             0x00000001108131b7 __swift_memcpy53_8 + 95801
	75  SwiftUI                             0x0000000110813064 __swift_memcpy53_8 + 95462
	76  SwiftUI                             0x000000010ff37140 __swift_memcpy195_8 + 12056
	77  SwiftList                           0x00000001079d555e $s9SwiftList0aB3AppV5$mainyyFZ + 30
	78  SwiftList                           0x00000001079d57c9 main + 9
	79  dyld                                0x000000010ba042bf start_sim + 10
	80  ???                                 0x000000011461552e 0x0 + 4636890414
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'RLMException', reason: 'Object is already managed by another Realm. Use create instead to copy it into this Realm.'
terminating with uncaught exception of type NSException
CoreSimulator 857.7 - Device: iPad Pro (11-inch) (3rd generation) (48AD3BF4-0841-40A0-901D-E6BD056D882A) - Runtime: iOS 16.0 (20A5339d) - DeviceType: iPad Pro (11-inch) (3rd generation)