1

I'd like to monitor property value change of a managed object in Core Data for data integrity purpose (for example, if a specific property changes, another object in a relationship should be changed accordingly). I'm thinking to implement it using KVO and let the managed object observe its own property change. Below is my code.

extension Book {
    override public func awakeFromInsert() {
        super.awakeFromInsert()
        observation = self.observe(\.date, options: [.old, .new]) { book, dateChange in
            print("date is changed!")
        }
    }

    override public func awakeFromFetch() {
        super.awakeFromFetch()
        observation = self.observe(\.date, options: [.old, .new]) { book, dateChange in
            print("date is changed!")
        }
    }
}

(Note: the observation variable is a transient attribute of the managed object to keep a strong reference to the NSKeyValueObservation object.)

My question is what's the appropriate place to remove observers? Or is it necessary at all? I did try the above code and it worked fine. But I'm not sure if it just happened to work and may have potential issue when code becomes complex.

I have figured out the following after some research:

1) App should remove KVO observers before a NSObject is deallocated. For example, the doc says:

An object that calls this method must also eventually call either the removeObserver(:forKeyPath:) or removeObserver(:forKeyPath:context:) method to unregister the observer when participating in KVO.

Note: the doc is about addObserver(), not the API I'm using, but I think the idea is same.

2) The new KVO API in Swift removes the observer automatically when NSKeyValueObservation object is deinitialized (i.e., when it's out of scope or not referenced by others).

So I thought I should do this:

extension Book {
    deinit {
        observation = nil
    }
}

But unfortunately that is impossible because XCode gave this error:

Deinitializers may only be declared within a class

So I wonder what's the usual way to do it? Could it be that, since the the observer and observee are same object, the underlying KVO framework code notices that observer object is also being deallocated, so it's OK without removing the observer explicitly?

Any suggestion will be much appreciated.

Update 1:

@pbasdf suggests using willTurnIntoFault. I google about it and find few discussions which indicate people using it this way. My main confusion: is fault a required a state before a NSManagemObject is deallocated? I don't find Apple doc says so (the doc doesn't have a state transition diagram).

EDIT: According to this post, when a NSManagedObjectContext is tore down, it turns all objects registered with it back into faults. If so, fault does always occur before deallocation.

BTW, while reading Apple doc, I also find this:

When Core Data turns an object into a fault, key-value observing (KVO) change notifications are sent to the object’s properties. If you are observing properties of an object that is turned into a fault and the fault is subsequently realized, you receive change notifications for properties whose values have not in fact changed.

That leads me to think using KVO with NSManagemObject is perhaps not a ideal approach because it is interfered by fault.

Update 2:

Since the observer and observee is the same object in my case, there is a much simpler approach than using KVO. That's defining a wrapper of property setter method and implementing all the logic in the wrapper. I'll use this approach.

That said, I still think my question is valid in general case where observer and observee are different objects.

Update 3:

@matt pointed out that it's OK to not remove KVO observers since iOS11. See another discussion here and official doc here.

Relaxed Key-Value Observing Unregistration Requirements

Prior to 10.13, KVO would throw an exception if any observers were still registered after an autonotifying object's -dealloc finished running. Additionally, if all observers were removed, but some were removed from another thread during dealloc, the exception would incorrectly still be thrown. This requirement has been relaxed in 10.13, subject to two conditions:

  • The object must be using KVO autonotifying, rather than manually calling -will and -didChangeValueForKey: (i.e. it should not return NO from +automaticallyNotifiesObserversForKey:)
  • The object must not override the (private) accessors for internal KVO state

If all of these are true, any remaining observers after -dealloc returns will be cleaned up by KVO; this is also somewhat more efficient than repeatedly calling -removeObserver methods.

rayx
  • 1,329
  • 10
  • 23
  • 1
    I think you want `willTurnIntoFault` [see here](https://developer.apple.com/documentation/coredata/nsmanagedobject/1506537-willturnintofault) though I've never used it myself so can't advise. – pbasdf Jan 30 '20 at 20:34
  • Hi @pbasdf Thanks for your suggestion. That seems to be how people do it indeed, though I don't think I fully understand it (see update 1 above). I think I'm going to use a simpler approach (as explained in update 2), instead of KVO. – rayx Jan 31 '20 at 02:37
  • 1
    Since iOS 11 it has been permitted for an observed object to go out of existence with observers still on it. So you are probably worrying needlessly about that. – matt Jan 31 '20 at 03:01
  • 1
    @matt Thanks for the information and glad to know it has been taken care of by the framework. For other people who are interested, I find another discussion (by @matt) [here](https://stackoverflow.com/questions/46591637/in-swift-4-how-do-i-remove-a-block-based-kvo-observer) and official document [here](https://developer.apple.com/library/archive/releasenotes/Foundation/RN-Foundation/index.html). – rayx Jan 31 '20 at 03:19
  • Thank all. I think I have a clear understanding of how to use KVO with NSManagedObject. I have added them to the question. – rayx Jan 31 '20 at 03:41
  • I found another way to set up KVO using Combine (intro in WWDC 19 - 230 [11:22]), likes .publisher(for: \.someKey), is relies on the object existing. Once the object deinit. The event chain is not canceled but no new value emits anymore. – user25917 Sep 07 '21 at 11:29

0 Answers0