The side project mentioned in the previous post requires some data to be persistent. While I had looked at Realm in the past, eventually I drifted back to Core Data. This was mostly due to NSFetchedResultsController
and the fact that it enables animating changes to your table- and collectionviews. At the time, Realm's notification system only supported 'naive' notifications, which didn't give you many options other than reloading your entire UI when a change was detected.
Somewhat recently though, Realm added granular notifications, which was probably one of the most requested features on their backlog. Now that these have been added, it seemed like a good opportunity to re-evaluate realm.
The problem with Core Data
Initially, I had also started out with Core Data for this particular project and had it working fine for the most part. On a high level the setup was like this:
- Retrieve some JSON data from an API
- Map that JSON data to objects that inherit from
NSManagedObject
- Insert those objects into an
NSManagedObjectContext
and save the context
To map JSON output to objects, the project uses ObjectMapper. In order to make these NSManagedObject
s save-able, they need to be initialized within an NSManagedObjectContext
inside its initializer, so it needed to be injected into the mapping process somehow. ObjectMapper actually includes an (empty) protocol for exactly this reason. It's as simple as:
class ManagedMapContext: MapContext {
let managedObjectContext: NSManagedObjectContext
init(managedObjectContext: NSManagedObjectContext) {
self.managedObjectContext = managedObjectContext
}
}
let context = ManagedMapContext(managedObjectContext: managedObjectContext)
let mapper = Mapper<Article>(context: context)
let articles = mapper.mapArray(jsonString)
Inside the initializer, you'd then have access to this context so you could initialize the object:
convenience init?(_ map: Map) {
guard let context = map.context as? ManagedMapContext else { return nil }
let entity = NSEntityDescription.entityForName("Article", inManagedObjectContext: context.managedObjectContext)
super.init(entity: entity, insertIntoManagedObjectContext: context.managedObjectContext)
}
Presto! This worked great for insertions, but this app requires only non-existing objects to be inserted and existing ones to be updated by checking against some primary key instead.
Because the mapping process created these instances within the managed object context, performing an NSFetchRequest
on that same context to check for existing objects will always yield true, even if the underlying persistent store may not actually contain the object you're querying for.
There are multiple ways to work around this. For instance, you could add a non-persistent property which you can use to differentiate between objects that are persisted and those that aren't. Another approach is to map to unmanaged objects or structs instead and insert new or update corresponding existing managed objects with data from those structs. This last approach worked well, but still didn't feel quite right, which supplied enough incentive to see if Realm would make a better fit.
Converting to Realm
First, I deleted everything Core Data related from the project and added RealmSwift and. Next, it was time to convert any NSManagedObject
subclasses to work with Realm by letting them inherit from Object
instead (or RLMObject
in Objective-C) instead.
Realm does come with a few minor caveats:
- Realm doesn't support all types Core Data does, so you may need to persist some values as other types and use a computed property to do some conversion
- Optionals work out-of-the-box for most types, but not for numbers. Realm does provide a generic
RealmOptional<T>
you can use instead.
The updated classes look something like this:
// MARK: - Realm
final class Article: Object {
dynamic var articleId: RealmOptional<Int>?
dynamic var title: String?
dynamic var body: String?
dynamic var urlString: String?
var url: NSURL? {
guard let urlString = urlString else { return nil }
return NSURL(string: urlString)
}
override public class func primaryKey() -> String? {
return "articleId"
}
override public class func indexedProperties() -> [String] {
return ["articleId", "title"]
}
override public class func ignoredProperties() -> [String] {
return ["url"]
}
}
// MARK: - Mappable (ObjectMapper)
extension Article: Mappable {
convenience init?(_ map: Map) {
self.init()
}
func mapping(_ map: Map) {
articleId.value <- map["articleId"]
title <- map["title"]
body <- map["body"]
}
}
Conversion looked like this:
- Change
@NSManaged
intodynamic
properties - Change number properties into
RealmOptional<Int>
- Override the
primaryKey()
function - Override the
ignoredProperties()
andindexedProperties()
functions (optional) - Map the
articleId
property toRealmOptional
's inner value instead
So here's the kicker. The following will take care of both inserting and updating objects in one go:
ArticleService.shared.fetchArticles { (articles) in
do {
let realm = try Realm()
try realm.write {
realm.add(articles, update: true)
}
} catch {
// FIXME: Handle your errors
}
}
If objects with the same primary key exist, all its properties will be updated accordingly. Otherwise, a new object is created.
Animating changes
As stated initially, Realm now supports more fine grained notifications as well, which you can use to animate changes to your table- and collectionviews, though sections aren't currently supported.
let articles = Realm.objects(Article)
.sorted("articleId", ascending: false)
let token = articles.addNotificationBlock { (changes) in
switch changes {
case .Initial(_):
self.collectionView.reloadData()
case .Update(_, let deletions, let insertions, let modifications):
let deleteIndexPaths = deletions.map { NSIndexPath(forItem: $0, inSection: 0) }
let insertIndexPaths = insertions.map { NSIndexPath(forItem: $0, inSection: 0) }
let updateIndexPaths = modifications.map { NSIndexPath(forItem: $0, inSection: 0) }
self.collectionView.performBatchUpdates({
self.collectionView.deleteItemsAtIndexPaths(deleteIndexPaths)
self.collectionView.insertItemsAtIndexPaths(insertIndexPaths)
self.collectionView.reloadItemsAtIndexPaths(updateIndexPaths)
}, completion: nil)
case .Error(_):
// FIXME: Handle your errors
}
}
In order to accomplish this, Realm introduces a type Results<T>
, which you can use like any other CollectionType
. Next, we subscribe to changes using addNotificationBlock(_:)
, which returns a NotificationToken
. As long as you keep a reference to this token, you'll keep receiving updates. After that, all changes will be animated, just like they would using NSFetchedResultsController
.
Note that for tableviews, using reloadRowsAtIndexPath
in conjunction with insertRowsAtIndexPath
or deleteRowsAtIndexPaths
may result in some potentially odd animations. This isn't related to realm though. Apparently UICollectionView
don't suffer from the same issue.
Conclusion
Converting from Core Data to Realm took less than 30 minutes including replicating NSFetchedResultsController
's behavior (though this app is still in the beginning stages; YMMV). It's also admittedly much simpler. While Core Data has proven itself to be a solid choice, it's difficult to not like the simplicity of Realm. Like Core Data, it comes with a few (minor) caveats, but all of them are easy to overcome.