Search code examples
iosswiftswiftuinsuserdefaultsuserdefaults

Crash (SIGABRT) when writing data to UserDefaults after Sheet disappears


I got three similar crash reports that I can't reproduce (all on iOS 14.4). The stracktrace says the following (I only pasted the part where my app is starting):

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  0

Thread 0 name:
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x00000001c077d414 __pthread_kill + 8
1   libsystem_pthread.dylib         0x00000001de2d8b50 pthread_kill + 272 (pthread.c:1392)
2   libsystem_c.dylib               0x000000019bc5bb74 abort + 104 (abort.c:110)
3   libswiftCore.dylib              0x0000000196795f20 swift::fatalError(unsigned int, char const*, ...) + 60 (Errors.cpp:393)
4   libswiftCore.dylib              0x0000000196796078 swift::swift_abortRetainUnowned(void const*) + 36 (Errors.cpp:460)
5   libswiftCore.dylib              0x00000001967e5844 swift_unknownObjectUnownedLoadStrong + 76 (SwiftObject.mm:895)
6   SwiftUI                         0x00000001992b0cdc ViewGraph.graphDelegate.getter + 16 (ViewGraph.swift:234)
7   SwiftUI                         0x00000001997e4d58 closure #1 in GraphHost.init(data:) + 80
8   SwiftUI                         0x00000001997e6550 partial apply for closure #1 in GraphHost.init(data:) + 40 (<compiler-generated>:0)
9   AttributeGraph                  0x00000001bbcc9b88 AG::Graph::Context::call_update() + 76 (ag-closure.h:108)
10  AttributeGraph                  0x00000001bbcca1a0 AG::Graph::call_update() + 56 (ag-graph.cc:176)
11  AttributeGraph                  0x00000001bbccfd70 AG::Subgraph::update(unsigned int) + 92 (ag-graph.h:709)
12  SwiftUI                         0x00000001997e1cdc GraphHost.runTransaction() + 172 (GraphHost.swift:491)
13  SwiftUI                         0x00000001997e4e1c GraphHost.runTransaction(_:) + 92 (GraphHost.swift:471)
14  SwiftUI                         0x00000001997e37a8 GraphHost.flushTransactions() + 176 (GraphHost.swift:459)
15  SwiftUI                         0x00000001997e2c78 specialized GraphHost.asyncTransaction<A>(_:mutation:style:) + 252 (<compiler-generated>:0)
16  SwiftUI                         0x00000001993bd2fc AttributeInvalidatingSubscriber.invalidateAttribute() + 236 (AttributeInvalidatingSubscriber.swift:89)
17  SwiftUI                         0x00000001993bd1f8 AttributeInvalidatingSubscriber.receive(_:) + 100 (AttributeInvalidatingSubscriber.swift:53)
18  SwiftUI                         0x00000001993bd914 protocol witness for Subscriber.receive(_:) in conformance AttributeInvalidatingSubscriber<A> + 24 (<compiler-generated>:0)
19  SwiftUI                         0x000000019956ba34 SubscriptionLifetime.Connection.receive(_:) + 100 (SubscriptionLifetime.swift:195)
20  Combine                         0x00000001a6e67900 ObservableObjectPublisher.Inner.send() + 136 (ObservableObject.swift:115)
21  Combine                         0x00000001a6e670a8 ObservableObjectPublisher.send() + 632 (ObservableObject.swift:153)
22  Combine                         0x00000001a6e4ffdc PublishedSubject.send(_:) + 136 (PublishedSubject.swift:82)
23  Combine                         0x00000001a6e76994 specialized static Published.subscript.setter + 388 (Published.swift:0)
24  Combine                         0x00000001a6e75f74 static Published.subscript.setter + 40 (<compiler-generated>:0)
25  MyApp                           0x00000001005d1228 counter.set + 32 (Preferences.swift:0)
26  MyApp                           0x00000001005d1228 Preferences.counter.modify + 120 (Preferences.swift:0)
27  MyApp                           0x00000001005ca440 MyView.changeCounter(decrease:) + 344 (MyView.swift:367)
28  MyApp                           0x00000001005cf110 0x100584000 + 307472
29  MyApp                           0x00000001005e65d8 thunk for @escaping @callee_guaranteed () -> () + 20 (<compiler-generated>:0)
30  MyApp                           0x00000001005a8828 closure #2 in MySheet.body.getter + 140 (MySheet.swift:0)

What is happening is, that I have a Sheet with a button and when clicking on it the sheet disappears and in the onDisappear the changeCounter method in the main View MyView is called to change the counter. The method changeCounter is passed to the Sheet from MyView when calling/opening the Sheet.

This is the .sheet method in MyView:

.sheet(item: $activeSheet) { item in
    switch item {
    case .MY_SHEET:
        MySheet(changeCounter: {changeCounter(decrease: true)}, changeTimer, item: $activeSheet)
    }
}

This is the (important part of the) sheet:

struct MySheet: View {
    var changeCounter: () -> Void
    var changeTimer: () -> Void
    @Binding var item: ActiveSheet?
    @State var dismissAction: (() -> Void)?
    
    var body: some View {
        GeometryReader { metrics in
            VStack {
                Button(action: {
                    self.dismissAction = changeCounter
                    self.item = nil
                }, label: {
                    Text("change_counter")
                })
                Button(action: {
                    self.dismissAction = changeTimer
                    self.item = nil
                }, label: {
                    Text("change_timer")
                })
            }.frame(width: metrics.size.width, height: metrics.size.height * 0.85)
        }.onDisappear(perform: {
            if self.dismissAction != nil {
                self.dismissAction!()
            }
        })
    }
}

Here is changeCounter with the preferences object:

struct MyView: View {
    @EnvironmentObject var preferences: Preferences

    var body: some View {...}

    func changeCounter(decrease: Bool) {
        if decrease {
            preferences.counter -= COUNTER_INTERVAL
        }
    }
}

The Preferences is an ObservableObject with the counter variable:

class Preferences: ObservableObject {
    let userDefaults: UserDefaults

    init(_ userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
        self.counter = 0
    }

    @Published var counter: Int {
        didSet {
            self.userDefaults.set(counter, forKey: "counter")
        }
    }
}

It changes a value in the userDefaults that are UserDefaults.standard.

Anyone has an idea how that crash can happen and in what situations? Because it only happened three times now on users devices and I can't reproduce it.


Solution

  • Let's analyze

        Button(action: {
            self.dismissAction = changeCounter       1)
            self.item = nil                          2)
        }, label: {
    

    Line 1) changes internal sheet state initiating update of sheet's view Line 2) changes external state initiating close of sheet (and probably update of parent view).

    It even sounds as two conflicting process (even if there are no dependent flows, but looking at your code second depends on result of first). So, this is very dangerous logic and should be avoided.

    In general, as I wrote in comment, changing two states in one closure is always risky, so I would rewrite logic to have something like (sketch):

    Button(action: {
        self.result = changeCounter  // one external binding !!
    }, label: {
    

    , ie. the one state change that initiates some external activity...

    Possible workaround for your code (if for any reason you cannot change logic) is to separate changes of those states in time, like

    Button(action: {
        self.dismissAction = changeCounter // updates sheet
        DispatchQueue.main.async {         // or after some min delay
          self.item = nil                  // closes sheet after (!) update
        }
    }, label: {