realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Crash at `removeObserver` after dismiss a view that has passing down an ObservedRealmObject to multiple subviews. #8318

Open Ciyou opened 1 year ago

Ciyou commented 1 year ago

How frequently does the bug occur?

Almost always in my personal project, more than 95%

Description

I'm using realm swift in a SwiftUI project. In the project I have a view contains an ObservedRealmObject and passes it to multiple subviews, both subview also reference the object as an ObservedRealmObject. When I dismiss the parent view, the app always crash due to trying to remove an observer before it's registered as an observer.

I wrote a minimal demo project to reproduce the crash.

Stacktrace & log output

Thread 1 Queue : com.apple.main-thread (serial)
// <-- Cannot remove an observer <RLMSwiftUIKVO 0x600000d4d420> for the key path \"id\" from <RLM:Managed 1 MyObject 0x600003a0c0a0> because it is not registered as an observer.
#0  0x00000001052d8064 in objc_exception_throw ()
#1  0x000000010ac9bb7c in -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] ()
#2  0x000000010ac9bf5c in -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] ()
#3  0x0000000100f44ff0 in -[RLMObjectBase removeObserver:forKeyPath:] at /Users/ciyou/Library/Developer/Xcode/DerivedData/RealmObserverCrashDemo-ebckzsqigifrpkgjnsldhqtgdotk/SourcePackages/checkouts/realm-swift/Realm/RLMObjectBase.mm:398
#4  0x0000000101252830 in closure #1 in SwiftUIKVO.Subscription.removeObservers() at /Users/ciyou/Library/Developer/Xcode/DerivedData/RealmObserverCrashDemo-ebckzsqigifrpkgjnsldhqtgdotk/SourcePackages/checkouts/realm-swift/RealmSwift/SwiftUI.swift:146
#5  0x00000001012944b0 in partial apply for closure #1 in SwiftUIKVO.Subscription.removeObservers() ()
#6  0x00000001060a31b0 in Sequence.forEach(_:) ()
#7  0x00000001012526c0 in SwiftUIKVO.Subscription.removeObservers() at /Users/ciyou/Library/Developer/Xcode/DerivedData/RealmObserverCrashDemo-ebckzsqigifrpkgjnsldhqtgdotk/SourcePackages/checkouts/realm-swift/RealmSwift/SwiftUI.swift:145
#8  0x0000000101252494 in SwiftUIKVO.Subscription.cancel() at /Users/ciyou/Library/Developer/Xcode/DerivedData/RealmObserverCrashDemo-ebckzsqigifrpkgjnsldhqtgdotk/SourcePackages/checkouts/realm-swift/RealmSwift/SwiftUI.swift:137
#9  0x0000000101252b90 in protocol witness for Cancellable.cancel() in conformance SwiftUIKVO.Subscription ()
#10 0x00000001082b9374 in ___lldb_unnamed_symbol194644 ()
#11 0x00000001082b9740 in ___lldb_unnamed_symbol194663 ()
#12 0x0000000105d923cc in AnyCancellable.cancel() ()
#13 0x00000001086ef318 in ___lldb_unnamed_symbol223707 ()
#14 0x000000010848aae4 in ___lldb_unnamed_symbol209880 ()
#15 0x0000000108a65570 in ___lldb_unnamed_symbol251940 ()
#16 0x0000000108a64a7c in ___lldb_unnamed_symbol251920 ()
#17 0x0000000108575118 in ___lldb_unnamed_symbol212877 ()
#18 0x0000000108575154 in ___lldb_unnamed_symbol212878 ()
#19 0x000000010610ae20 in withUnsafePointer<τ_0_0, τ_0_1>(to:_:) ()
#20 0x00000001062b56cc in withUnsafeMutablePointer<τ_0_0, τ_0_1>(to:_:) ()
#21 0x0000000108574e84 in ___lldb_unnamed_symbol212876 ()
#22 0x0000000107a1d9e0 in ___lldb_unnamed_symbol122988 ()
#23 0x000000010f859750 in AG::Graph::UpdateStack::update() ()
#24 0x000000010f859ee0 in AG::Graph::update_attribute(AG::data::ptr<AG::Node>, unsigned int) ()
#25 0x000000010f8678d0 in AG::Subgraph::update(unsigned int) ()
#26 0x0000000107f15c0c in ___lldb_unnamed_symbol165455 ()
#27 0x00000001088a7f20 in ___lldb_unnamed_symbol237995 ()
#28 0x000000010889bbd0 in ___lldb_unnamed_symbol237790 ()
#29 0x0000000108c6cba8 in ___lldb_unnamed_symbol270012 ()
#30 0x0000000108c6cbec in ___lldb_unnamed_symbol270013 ()
#31 0x000000011a388120 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#32 0x0000000111ed3e48 in CA::Layer::layout_if_needed(CA::Transaction*) ()
#33 0x000000011a377778 in -[UIView(Hierarchy) layoutBelowIfNeeded] ()
#34 0x00000001083dc6f0 in ___lldb_unnamed_symbol203801 ()
#35 0x0000000107ea0528 in ___lldb_unnamed_symbol161227 ()
#36 0x0000000107ea0544 in ___lldb_unnamed_symbol161228 ()
#37 0x000000011a37d2f8 in +[UIView(Animation) performWithoutAnimation:] ()
#38 0x00000001083db160 in ___lldb_unnamed_symbol203795 ()
#39 0x0000000107ea0528 in ___lldb_unnamed_symbol161227 ()
#40 0x0000000107ea0544 in ___lldb_unnamed_symbol161228 ()
#41 0x000000011a37d2f8 in +[UIView(Animation) performWithoutAnimation:] ()
#42 0x00000001083dc824 in ___lldb_unnamed_symbol203802 ()
#43 0x0000000108264cc0 in ___lldb_unnamed_symbol191631 ()
#44 0x00000001088a7b38 in ___lldb_unnamed_symbol237995 ()
#45 0x000000010889bbd0 in ___lldb_unnamed_symbol237790 ()
#46 0x0000000108c6cba8 in ___lldb_unnamed_symbol270012 ()
#47 0x0000000108c6cbec in ___lldb_unnamed_symbol270013 ()
#48 0x000000011a388120 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#49 0x0000000111ed3e48 in CA::Layer::layout_if_needed(CA::Transaction*) ()
#50 0x0000000111edea1c in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#51 0x0000000111df98d0 in CA::Context::commit_transaction(CA::Transaction*, double, double*) ()
#52 0x0000000111e292c4 in CA::Transaction::commit() ()
#53 0x0000000111d2743c in CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long) ()
#54 0x0000000111d26e1c in CA::Display::DisplayLink::callback(_CADisplayTimer*, unsigned long long, unsigned long long, unsigned long long, bool, void*) ()
#55 0x0000000111e2e270 in display_timer_callback(__CFMachPort*, void*, long, void*) ()
#56 0x0000000106e9fc0c in __CFMachPortPerform ()
#57 0x0000000106ed6504 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
#58 0x0000000106ed5ad4 in __CFRunLoopDoSource1 ()
#59 0x0000000106ed00e4 in __CFRunLoopRun ()
#60 0x0000000106ecf450 in CFRunLoopRunSpecific ()
#61 0x000000010a3ffbc0 in GSEventRunModal ()
#62 0x0000000119e7a01c in -[UIApplication _run] ()
#63 0x0000000119e7dc94 in UIApplicationMain ()
#64 0x000000010893de08 in ___lldb_unnamed_symbol242334 ()
#65 0x000000010893dca8 in ___lldb_unnamed_symbol242332 ()
#66 0x0000000107efcffc in static App.main() ()
#67 0x0000000100e49a30 in static RealmObserverCrashDemoApp.$main() ()
#68 0x0000000100e49ae0 in main at /Users/ciyou/Developer/RealmObserverCrashDemo/RealmObserverCrashDemo/RealmObserverCrashDemoApp.swift:11
#69 0x0000000103bed558 in start_sim ()
#70 0x0000000103e92058 in start ()

Can you reproduce the bug?

Always (with demo code below)

Reproduction Steps

To reproduce the bug:

I personally think it's caused by having a ObservedRealmObject passing down to more than one subview, and some borken logic cause the value observed only once.

//
//  ContentView.swift
//  RealmObserverCrashDemo
//

import SwiftUI
import RealmSwift

/// To reproduce the bug:
///  - Run the ContentView
///  - Click 'Add Object' to present the AddObjectView
///  - Turn on 'Trigger Bug' toggle
///  - Edit the title, then click 'Save'
/// There's a high possibility that the app will crash with the following error:
///  `Thread 1: "Cannot remove an observer <RLMSwiftUIKVO 0x600000d4d420> for the key path \"id\" from <RLM:Managed 1 MyObject 0x600003a0c0a0> because it is not registered as an observer."`
///  I personally think it's caused by having a `ObservedRealmObject` passing down to more than one subview, and some borken logic cause the value observed only once.

class MyObject: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var id: ObjectId
    @Persisted var title: String = ""
}

struct ObjectCellCaption: View {
    @ObservedRealmObject var object: MyObject

    var body: some View {
        Text(object.id.description)
            .foregroundColor(.secondary)
            .font(.caption)
    }
}

struct AddObjectView: View {
    @ObservedRealmObject var object: MyObject
    @Environment(\.realm) var realm
    @Environment(\.dismiss) var dismiss

    @State var triggerBug = false

    var body: some View {
        NavigationView {
            Form {
                Section {
                    TextField("Title", text: $object.title)
                    ObjectCellCaption(object: object)
                }

                Section {
                    Toggle(isOn: $triggerBug) {
                        Text("Trigger Bug")
                    }
                    if triggerBug {
                        ObjectCellCaption(object: object)
                    }
                }
            }
            .navigationTitle("Add Object")
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") {
                        save()
                    }
                }
            }
        }
    }

    func save() {
        do {
            try realm.write {
                realm.add(object)
            }
            dismiss()
        } catch {
            print(error.localizedDescription)
        }
    }
}

struct ContentView: View {
    @ObservedResults(MyObject.self) var objects

    @State var showAddObject = false

    var body: some View {
        List {
            Section {
                Button("Add Object") {
                    showAddObject.toggle()
                }
                .sheet(isPresented: $showAddObject) {
                    AddObjectView(object: MyObject())
                }
            }
            Section {
                ForEach(objects) { object in
                    Text(object.title)
                }
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Version

10.41.1

What Atlas Services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

both iOS 16 and iOS 17, can be reproduced on both Xcode 14 and Xcode 15

Build environment

Xcode version: can be reproduced on both Xcode 14 and Xcode 15 Dependency manager and version: SPM bundled with Xcode

aminashi commented 3 months ago

Has anyone found a solution for this? I struggled with this issue in my app, I can't seem to find a way to get rid of the crash, which happens super often ~95% (but strangely not always, sadly I can't notice any pattern that could tell me why it doesn't crash 5% of the time)