How to observe NSManagedObject changes in Core Data using Combine

Observing changes in Core Data NSManagedObject instances with Combine publishers can be a great solution to keep your user interface in sync with the latest changes. After reading through my posts in the Combine and Core Data categories, you might know more about those individual frameworks, but how do you “combine” them together?

Techniques like using a NSFetchedResultsController often gives us a lot of control in updating lists of NSManagedObject instances with inserts and deletions, but you might have run into issues when using Diffable Data Sources with Core Data in which reloads aren’t correctly triggered. You might also be interested in reloading only a single label in your collection view cell instead of redrawing the complete cell for a single value change. In this article, I will explain to you how you can benefit from using Combine to solve this problem.

Observing NSManagedObject value changes in Core Data objects using Combine

Core Data’ NSManagedObject type inherits from NSObject which allows us to use Key-Value Observation (KVO) to observe value changes. Combine comes with a Publisher for KVO observations which you can use with any NSObject, as well as with your Core Data managed object instances.

In the following example, we’ve created an Article managed object which we’ll pass into our cell.

final class Article: NSManagedObject {
    @NSManaged var title: String!
    @NSManaged var summary: String!
}

final class ArticleCollectionViewCell: UICollectionViewCell {
    
    let titleLabel = UILabel()
    let summaryLabel = UILabel()
    
    private var titleSubscription: AnyCancellable?
    private var summarySubscription: AnyCancellable?
    
    func setup(with article: Article) {
        titleSubscription = article.publisher(for: \.title)
            .assign(to: \.text, on: titleLabel)
        summarySubscription = article.publisher(for: \.summary)
            .assign(to: \.text, on: summaryLabel)
    }
    
    override func prepareForReuse() {
        titleSubscription?.cancel()
        summarySubscription?.cancel()
        
        super.prepareForReuse()
    }
}

We’re making use of the publisher(for:) method on NSObject which gives us a publisher for a given key path. We store the subscription to cancel monitoring updates as soon as our cell is being prepared for reuse. If we didn’t manage the subscriptions like this, we might run into weird crashes as observations keep working while cells aren’t even visible anymore.

By using the following code we can see that NSManagedObject updates are passed through to our labels as expected:

let cell = ArticleCollectionViewCell()
let article = Article() // Setup using your context
article.title = "Combine and Core Data"
article.summary = "Combining combine with Core Data is awesome"
cell.setup(with: article)

print(cell.titleLabel.text) // Prints: "Combine and Core Data"
print(cell.summaryLabel.text) // Prints: "Combining combine with Core Data is awesome"

/// Change the title and see that the label changes accordingly
article.title = "Another title"
print(cell.titleLabel.text) // Prints: "Another title"

/// After the subscription has ended, the value is no longer updated:
cell.prepareForReuse()
article.title = "Our last title"
print(cell.titleLabel.text) // Prints: "Another title"

Using regular NSObject instances with a KVO publisher in Combine

In case you’re reading this post and writing the above code in combination with a non- NSManagedObject type, unrelated to Core Data, you might run into the following error:

Fatal error: Could not extract a String from KeyPath

In this case, you need to mark your properties with @objc dynamic to make them work with KVO. @objc is required to make KVO work while dynamic tells the compiler to use dynamic dispatching and allows updates to pass through. As an example, the above NSManagedObject example for an article could be rewritten as follows:

final class Article: NSObject {
     @objc dynamic var title: String?
     @objc dynamic var summary: String?
 }

Updating SwiftUI Views with Core Data

NSManagedObject conforms to ObservableObject since the introduction of Combine in iOS 13. This is a hidden gem many might not know but it allows us to update our SwiftUI Views whenever a value in Core Data changes.

The following code example is enough to keep our ArticleView up to date with any changes in the linked Core Data Article instance:

struct ArticleView: View {
    
    // Your Core Data entity NSManagedObject instance
    @ObservedObject var article: Article
    
    var body: some View {
        VStack {
            Text(article.title)
            Text(article.summary)
        }
    }
}

Observed objects will publish changes using an ObservableObjectPublisher that emits before the object has changed. The View updates and the text will be updated to reflect the latest values.

Conclusion

Working with NSManagedObject instances combined with Combine in Core Data makes it easy to update views whenever a value is updated. Keep your user interface up to date without maintaining hard-to-write managed object context observe notifications.

If you like to improve your Core Data knowledge, even more, check out the Core Data category page. The Combine category allows you to learn more about the Combine framework. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!