NSManagedObject events: handling state in Core Data

An NSManagedObject lifecycle goes from insertion and updates until deletion in the end. All those events come with their own common related modifications and can be used in many different ways.

Managing state in Core Data from within the NSManagedObject class itself is a great way to keep logic centralized. Updating a creation date, modified date, or cleaning up resources upon deletion. Without handling those events from within the managed object class you can easily end up copying a lot of code throughout your project.

In this blog post I’ll go over three NSManagedObject lifecycle events that occur when using Core Data in your app:

  • Insertion
  • Updates
  • Deletion

Each of those events has their own common scenarios that you’re likely going to need one day. I’ve been building the Collect app for several years now with Core Data and I can tell you that we’re using many of the following methods in our app to manage state.

Settings defaults upon insertion using awakeFromInsert

The awakeFromInsert method is called when an NSManagedObject is created. It’s only getting called once for each object as an object can only be created once.

You can use this method to set special default property values. The model editor allows us to set static default values but does not allow us to, for example, set the current date as a default value.

Setting a date from the awakeFromInsert method is a common use-case:

final class Content: NSManagedObject, Identifiable {
    @NSManaged var name: String?
    @NSManaged var creationDate: Date!

    override func awakeFromInsert() {
        super.awakeFromInsert()

        setPrimitiveValue(Date(), forKey: #keyPath(Content.creationDate))
    }
}

This is all the code we need to make sure that the creation date is set upon creating a new Content instance.

Note that we’re making use of setPrimitiveValue here which has a specific reason I’m going to explain to you now.

Primitive accessors in Core Data

Primitive accessors in Core Data allow us to set and get values without noticing key-value observations (KVO). You could describe them as private property handlers as nobody will notice.

This is great in cases that you’d like to adjust a NSManagedObject value without letting anyone know. A common example could be adjusting the modification date in which you don’t want any fetched results controllers to reload again after a modification.

Updating state upon saving using willSave

The willSave method is getting called for each modified NSManagedObject every time the context is saved. You can use this method to update properties in your model before it’s getting persisted. You can use it to, for example, compute persistent values from other transient or scratchpad values or use it to set a last modified date.

Once again, it’s important to think about using primitive accessors. If you change properties using the standard accessor methods, Core Data will see it as a change and will trigger another willSave. Potentially, you could end up in an infinite loop.

In our Content example we could add a lastModifiedDate property:

final class Content: NSManagedObject, Identifiable {
    @NSManaged var name: String?
    @NSManaged var creationDate: Date!
    @NSManaged var lastModifiedDate: Date!

    override func awakeFromInsert() {
        super.awakeFromInsert()

        setPrimitiveValue(Date(), forKey: #keyPath(Content.creationDate))
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))
    }

    override func willSave() {
        super.willSave()
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))
    }
}

Note that we’re updating it both in the awakeFromInsert and the willSave method. This is to make sure a value is always set and we can keep it as non-optional. In fact, insertion can be seen as an initial modification to the object.

Cleaning up resources and canceling ongoing events using prepareForDeletion

The prepareForDeletion method is getting called once your object is about to get deleted. This method can be used to notify related objects about the deletion so that they can, for example, cancel any running network requests.

final class Content: NSManagedObject, Identifiable {
    @NSManaged var name: String?
    @NSManaged var creationDate: Date!
    @NSManaged var lastModifiedDate: Date!

    override func awakeFromInsert() {
        super.awakeFromInsert()

        setPrimitiveValue(Date(), forKey: #keyPath(Content.creationDate))
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))
    }

    override func willSave() {
        super.willSave()
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))
    }

    override func prepareForDeletion() {
        super.prepareForDeletion()
        NetworkProvider.shared.cancelAllRequests(for: self)
    }
}

In this case, we’re letting our network provider know about the deletion. This is a great alternative to setting up didSave notifications in all objects that need to know about specific deletions and keeps your code much more readable.

Using willSave instead of prepareForDeletion

Many will likely use the prepareForDeletion method to clean up local resources. In most cases, this is indeed a good place to perform local file deletions but in case you use rollbacks anywhere in your code you could end up losing files.

It’s important to know that the prepareForDeletion method is getting called directly after a NSManagedObjectContext.delete(_:) method is called and before a save occurred. In case you’re deletion local resources from the prepareForDeletion method it could happen that an object is not saved yet while the local resource is already deleted.

Once a rollback occurs at that moment, the managed object will be restored and pointing to a local resource that no longer exists. Instead, it’s safer to clean up resources in the willSave method by checking for isDeleted:

final class Content: NSManagedObject, Identifiable {
    @NSManaged var name: String?
    @NSManaged var creationDate: Date!
    @NSManaged var lastModifiedDate: Date!
    @NSManaged var localResource: URL?

    override func awakeFromInsert() {
        super.awakeFromInsert()

        setPrimitiveValue(Date(), forKey: #keyPath(Content.creationDate))
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))
    }

    override func willSave() {
        super.willSave()
        setPrimitiveValue(Date(), forKey: #keyPath(Content.lastModifiedDate))

        if isDeleted, let localResource = localResource {
            do {
                try FileManager.default.removeItem(at: localResource)
            } catch {
                print("Removing local file failed with error: \(error)")
            }
        }
    }

    override func prepareForDeletion() {
        super.prepareForDeletion()
        NetworkProvider.shared.cancelAllRequests(for: self)
    }
}

We’re first checking for the isDeleted property to be true. Note that this property is only set to true when the deletion is not yet saved. If we also find a local resource, we try to delete it using the file manager.

We skip checking to verify whether the file exists as it’s recommended by the documentation of the fileExists method to rather perform the operation and let it fail instead of relying on the system to determine whether a file still exists.

Conclusion

That’s it! We’ve implemented an example NSManagedObject class and update its property using several life cycle events. We kept its state up to date and cleaned up any resources upon deletion.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!