Search code examples
iosswiftcore-datamagicalrecord

Core Data custom accessor crash when saving context with nil required attribute


After reading this excellent article about custom accessors in Swift, I refactored from NSDecimalNumber to use the new Decimal type. I have a fairly complex model and things have been working fine for several days, but I now see an EXC_BAD_ACCESS crash if I save the managed object context while a required (non-optional) property is nil, baseAmount in my case. When using NSDecimalNumber directly, before the refactor, saving the object would fail, but it wouldn't crash.

Not sure if this is important, but I'm saving the context using Magical Record's function: NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()

Any ideas on how I can modify the baseAmount accessors to achieve the same failed-save behavior that Core Data provides so that it doesn't crash?

Here is the relevant part of my NSManagedObject subclass, followed by the stack trace:

extension DFTransaction {

    @NSManaged private var primitiveBaseAmount: NSDecimalNumber

    var baseAmount: Decimal {
        get {
            willAccessValue(forKey: "baseAmount")
            defer { didAccessValue(forKey: "baseAmount") }
            return primitiveBaseAmount.decimalValue
        }
        set {
            willChangeValue(forKey: "baseAmount")
            defer { didChangeValue(forKey: "baseAmount") }
            primitiveBaseAmount = NSDecimalNumber(decimal: newValue)
        }
    }
}


[appname]`@objc DFTransaction.baseAmount.getter:
0x100343cd0 <+0>:   stp    x29, x30, [sp, #-16]!
0x100343cd4 <+4>:   mov    x29, sp
0x100343cd8 <+8>:   sub    sp, sp, #96               ; =96 
0x100343cdc <+12>:  stur   x0, [x29, #-32]
0x100343ce0 <+16>:  stur   x8, [x29, #-40]
0x100343ce4 <+20>:  bl     0x10051ca64               ; symbol stub for: objc_retain
0x100343ce8 <+24>:  sub    x8, x29, #24              ; =24 
0x100343cec <+28>:  ldur   x30, [x29, #-32]
0x100343cf0 <+32>:  str    x0, [sp, #48]
0x100343cf4 <+36>:  mov    x0, x30
0x100343cf8 <+40>:  bl     0x100343da4               ; [appname].DFTransaction.baseAmount.getter : __C.Decimal at DFTransaction.swift:64
0x100343cfc <+44>:  ldur   w9, [x29, #-24]
0x100343d00 <+48>:  ldurh  w10, [x29, #-20]
0x100343d04 <+52>:  ldurh  w11, [x29, #-18]
0x100343d08 <+56>:  ldurh  w12, [x29, #-16]
0x100343d0c <+60>:  ldurh  w13, [x29, #-14]
0x100343d10 <+64>:  ldurh  w14, [x29, #-12]
0x100343d14 <+68>:  ldurh  w15, [x29, #-10]
0x100343d18 <+72>:  ldurh  w16, [x29, #-8]
0x100343d1c <+76>:  ldurh  w17, [x29, #-6]
0x100343d20 <+80>:  ldur   x0, [x29, #-32]
0x100343d24 <+84>:  str    w9, [sp, #44]
0x100343d28 <+88>:  str    w10, [sp, #40]
0x100343d2c <+92>:  str    w11, [sp, #36]
0x100343d30 <+96>:  str    w12, [sp, #32]
0x100343d34 <+100>: str    w13, [sp, #28]
0x100343d38 <+104>: str    w14, [sp, #24]
0x100343d3c <+108>: str    w15, [sp, #20]
0x100343d40 <+112>: str    w16, [sp, #16]
0x100343d44 <+116>: str    w17, [sp, #12]
0x100343d48 <+120>: bl     0x10051ca58               ; symbol stub for: objc_release
0x100343d4c <+124>: ldr    w9, [sp, #44]
0x100343d50 <+128>: ldur   x8, [x29, #-40]
->  0x100343d54 <+132>: str    w9, [x8]    //EXC_BAD_ACCESS
0x100343d58 <+136>: ldr    w10, [sp, #40]
0x100343d5c <+140>: strh   w10, [x8, #4]
0x100343d60 <+144>: ldr    w11, [sp, #36]
0x100343d64 <+148>: strh   w11, [x8, #6]
0x100343d68 <+152>: ldr    w12, [sp, #32]
0x100343d6c <+156>: strh   w12, [x8, #8]
0x100343d70 <+160>: ldr    w13, [sp, #28]
0x100343d74 <+164>: strh   w13, [x8, #10]
0x100343d78 <+168>: ldr    w14, [sp, #24]
0x100343d7c <+172>: strh   w14, [x8, #12]
0x100343d80 <+176>: ldr    w15, [sp, #20]
0x100343d84 <+180>: strh   w15, [x8, #14]
0x100343d88 <+184>: ldr    w16, [sp, #16]
0x100343d8c <+188>: strh   w16, [x8, #16]
0x100343d90 <+192>: ldr    w17, [sp, #12]
0x100343d94 <+196>: strh   w17, [x8, #18]
0x100343d98 <+200>: mov    sp, x29
0x100343d9c <+204>: ldp    x29, x30, [sp], #16
0x100343da0 <+208>: ret    

Solution

  • In the article I mention that "Primitive accessors are always nullable Objective-C reference types". This is a truth that's not affected by the isOptional setting, as you've been finding with those crashes.

    As such, the primitive accessor must be declared like so:

    @NSManaged private var primitiveBaseAmount: NSDecimalNumber?

    With that out of the way, you've declared the baseAmount accessor to be Decimal (non-optional), so your custom get implementation must do something non-crashy when it encounters a null primitive value, like return primitiveBaseAmount?.decimalValue ?? 0.