blwinters blwinters - 2 months ago 14
iOS Question

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

Answer

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.