神刀安全网

Modern Core Data

Use Swift to breathe new life into old Objective-C APIs. In this talk from try! Swift , Daniel Eggert gives an example using Core Data, showing how to make the code more readable and less error-prone by using protocols and protocol extensions.

I am going to talk about modern Core Data . But this talk is not about Core Data; this talk is about how to use an old API in modern Swift code ( Swift is fun to use, and we want to make using old API is fun as well! ).

The two main goals of this talk are: 1) making code more readable, and 2) making code less error-prone. To this end, we will use two tools, protocol and protocol extensions.

We will use Core Data as an example; it is a good example because Core Data is twelve years old, written in Objective-C for Objective-C, dynamic and not type-safe. In Swift, we do not like things to be dynamic and you want things to be type-safe. We are going to look at how to bridge those two worlds. Last year I wrotethis book together with Florian; I will show a few examples from this book, because it is 100% in Swift, but about Core Data.

Keep the Spirit of the Existing API, but Make It Easier to Use

We want to keep the spirit of existing API ( we want our code to be nice and readable ), but look like Core Data code and be easy to use.

In Core Data, you have a very dynamic coupling between entity and classes. The entity is where you define your data module; the class ( you Swift class ) is where you have your custom logic. Usually you have a one-to-one mapping: one entity maps directly to one class. Because of the history of Core Data and Objective-C, the code may looks strange, if you want to insert a new object in Core Data ( see below ).

let city = NSEntityDescription   .insertNewObjectForEntityForName("City",     inManagedObjectContext: moc) as! City

There are three things about this that I do not like: 1) It is very long, 2) You have this “city” string, which the compiler cannot help us if I type something wrong, and 3) at the very end we have this typecast ( very ugly, we do not like these things in our Swift Code! ).

let city: City = moc.insertObject()

If we want to insert a city object, we just want to call insertObject() and that will do all the work for us. Here the Swift compiler can help us do the heavy lifting:

1: Create a protocol

First, we create a protocol, ManagedObjectType ; this protocol defines the entityName ( that was the thing that was the string before ).

protocol ManagedObjectType {   static var entityName: String { get } }

2: Make our class conform

We go back to our City class (ManagedObject class), and we want to make things work for the City class. We extend the City class to implement this protocol and we say, entityName is “City”.

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  extension City: ManagedObjectType {   static let entityName = "City" }

3: Add an extension to the context

We have linked the city entity with the city class ( see above ); now we can extend the context and add the method that we used before:

extension NSManagedObjectContext {   func insertObject<A: ManagedObject where A: ManagedObjectType>() -> A {     guard let obj = NSEntityDescription       .insertNewObjectForEntityForName(A.entityName,         inManagedObjectContext: self) as? A else {           fatalError("Entity /(A.entityName) does not correspond to /(A.self)")     }     return obj   }    ... }

( I am not going to go through all the details ) This is the old code that we had before, but now it is nicely wrapped. The City string ( that we had before ) we can extract it here, as well as the typecast. It is all nicely hidden away in one place.

4: Profit

Once we have that we can have this nice readable code, it is close to impossible to get wrong.

let city: City = moc.insertObject()

Key value coding is this old thing , particularly weird in Swift code. Key value coding is extremely dynamic and twelve years ago that was the hype. It is used by Core Data for key value observing, but it is very prone to typos and errors and it is not type-safe ( we do not like that! ). Let’s look at how it looks in Core Data:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }  func hasFaultForRelationshipNamed(name: String)

If we go back to our city class, we might want to use this Core Data method, hasFaultForRelationshipNamed, meaning that we have to parse in an argument, and that argument is a String. We might use:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }  func doSomething(city: City) {   if city.hasFaultForRelationshipNamed("mayor") {     // Do something...   } }

We have some method, “Do something”, and we have to parse in the string “mayor” that matches our attribute (our property mayor). Again, the compiler cannot help us; if we make a mistake, it will crash at runtime.

To show you how bad this situation is, Core Data has all these methods ( see video ) that all use key value coding… and we want things to be better ( see below ).

if city.hasFaultForRelationshipNamed(.mayor) {   // Do something... }

It is very easy to read ( similar to the previous one ), but we want something where the compiler can check that (.mayor) is a valid key and, not only a valid key, but it is a valid key for our City class, and Xcode can give us auto-completion. How do we do this?

1: Create a protocol

We start off with a protocol, KeyCodeable ; it only has this typealias Key:

protocol KeyCodable {   typealias Key: RawRepresentable }

2: Add key enum

We go back to our City class and we extend it to implement this protocol.

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person }

Now we have a nested type inside the City class, key belongs to City, and we have this two properties (name and mayor) defined as cases of this property, cases of this enum.

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person } extension City: KeyCodable {   public enum Key: String {     case name     case mayor   } }

3: Use a protocol extension

Then we use protocol extensions (as we did before) to create a simpler version. The hasFaultForRelationshipNamed method now takes a key instead of a string.

extension KeyCodable     where Self: ManagedObject, Key.RawValue == String {    func hasFaultForRelationshipNamed(key: Key) -> Bool {     hasFaultForRelationshipNamed(key.rawValue)   }    [...] }

Then we can implement the method below, which takes a key, the compiler will tell us whether this works, and Xcode can give use auto-completion. Now we have type-safe key value coding.

if city.hasFaultForRelationshipNamed(.mayor) {   // Do something... }

Better Fetch Requests by Adding Defaults

If we have our City class and if we want to fetch all cities, we would traditionally use:

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  let request = NSFetchRequest(entityName: "City") let sd = NSSortDescriptor(key: "population", ascending: true) request.sortDescriptors = [sd]

It does not look too bad (three lines)… but there are some things that are not that nice. We can do something that is better. :muscle:

What we want

final class City: ManagedObject {   @NSManaged public var name: String   @NSManaged public var mayor: Person   @NSManaged public var population: Int32 }  let request = City.sortedFetchRequest

We simply ask the City Class, give us a sortedFetchRequest. That has all the logic encapsulated inside; again, it is easier to read and more difficult to make mistakes. How would we implement this?

1: ManagedObjectType protocol

protocol ManagedObjectType {   static var defaultSortDescriptors: [NSSortDescriptor] { get } }

We use the same protocol and add defaultSortDescriptors , which will link the logical ordering for that class with the class.

2: Implement the protocol

For our City class we would simply say, while our city class implements us protocol and its natural sorting is to sort by the population.

extension City: ManagedObjectType {   static var defaultSortDescriptors: [NSSortDescriptor] {     return [NSSortDescriptor(key: City.Key.population.rawValue, ascending: true)]   } }

3: Protocol extension

Then we can implement this nice sorted FetchRequest method as a protocol extension (as in the other one), we can pull out the entityName ( because we already did that a few slides back ), we can get the defaultSortDescriptors and we return the fetch request.

extension ManagedObjectType {   static var sortedFetchRequest: NSFetchRequest {     let request = NSFetchRequest(entityName: entityName)     request.sortDescriptors = defaultSortDescriptors     return request   } }

4: Profit:moneybag::moneybag::moneybag:

With this we have one single line to create a FetchRequest for cities, let request = City.sortedFetchRequest . If we have a person class, let request = Person.sortedFetchRequest . It will work the same way, and it is nice and clean readable code. We keep model knowledge with the model, and we are making the code readable.

Bonus Points

Often in Core Data we use predicates when we want to Fetch; we can chain these two things together and create another method, sortedFetchRequestWithPredicateFormat:

let request = City.sortedFetchRequestWithPredicateFormat("%K >= %ld",   City.Key.population.rawValue, 1_000_000)    extension ManagedObjectType {   public static func sortedFetchRequestWithPredicateFormat(     format: String, args: CVarArgType...) -> NSFetchRequest {       request = sortedFetchRequest()       let predicate = withVaList(args) { NSPredicate(format: format, arguments: $0) }       request.predicate = defaultPredicate       return request     } }

We use the existing sortedFetchRequest method (that we just created), we create the predicate and set it on to return. We can use this to make our code more readable.

Turning Things into Other Things – NSValueTransformer

NSValueTransformer is part of the foundation API, it is used in Core Data, and for bindings (AppKits). It is also strange in the Swift world: you need to subclass to use it, and there is no type-safety.

Let’s look at an example: we have a UUID that we are using and we have a String representation, maybe while getting the string from our server, and we want to convert it in its UUID, between the string and the raw bytes, and back. Traditionally, you would do:

final class UUIDValueTransformer: NSValueTransformer {   override static func transformedValueClass() -> AnyClass {     return NSUUID.self   }   override class func allowsReverseTransformation() -> Bool {     return true   }   override func transformedValue(value: AnyObject?) -> AnyObject? {     return (value as? String).flatMap { NSUUID(UUIDString: $0) }   }   override func reverseTransformedValue(value: AnyObject?) -> AnyObject? {     return (value as? NSUUID).flatMap { $0.UUIDString }   } }  let transformer = UUIDValueTransformer()

You subclass, you implement these four methods, and then you instantiate it. It is not bad, but we can do better:

We want a ValueTransformer, the closure that converts from a string to NSUUID, and the closure that converts from a UUID back to string:

let transformer = ValueTransformer(transform: {     return NSUUID(UUIDString: $0)   }, reverseTransform: {     return $0.UUIDString })

Note that we do not tell which type we are converting in between; instead, the Swift compiler helps us. The first part looks like this:

class ValueTransformer<A: AnyObject, B: AnyObject>: NSValueTransformer {   typealias Transform = A? -> B?   typealias ReverseTransform = B? -> A?   private let transform: Transform   private let reverseTransform: ReverseTransform   init(transform: Transform, reverseTransform: ReverseTransform) {     self.transform = transform     self.reverseTransform = reverseTransform     super.init()   } }

It is a generic class between the two types, A and B, the one that we are converting in between. We have two closures, from A to B, and B back to A. We also implement those four methods.

We can then do this very elegant way of implementing and instantiating NSValueTransformer ( very modern and Swifty ):

let transformer = ValueTransformer(transform: {     return NSUUID(UUIDString: $0)   }, reverseTransform: {     return $0.UUIDString })

Wrapping Block API – Saving

Blocks are new in comparison to some of the APIs that we use. Core Data is twelve years old and Blocks are maybe 5 or 6 years old; they become fun to use once we have Swift.

The example I want to show is saving.

make changes make some more changes make even more changes  try moc.save()

In Core Data there is a method, to save your changes, simply called save. You would make some changes, and when you are done, you tell the context to save. It is very simple, but we can do something better.

moc.performChanges {   make changes   make some more changes   make even more changes }

We tell the context we want to do some changes and then we wrap all of our changes in a single block. It is easy to understand because we can see all of our changes wrapped together in a closure.

It is a very nice pattern, clear to read, less error prone, and the implementation is extremely trivial:

extension NSManagedObjectContext {   public func performChanges(block: () -> ()) {     performBlock {       block()       self.saveOrRollback()     }   } }

We have this method performChanges, it simply runs the block and then we do a save. The UI application API can become easier if you create these trivial block wrappers.

NSNotification – Observe Changes

Core Data uses NSManagedObjectContextObjectsDidChangeNotification , which let us to build code using a reactive approach. NSNotification is in our Core Data whenever an object changes() regardless of who changes it and why it changes), and we can tie up our code to keep our UI updated all the time.

Traditionally, we have all seen this and implemented this many times:

func observe() {   let center = NSNotificationCenter.defaultCenter()   center.addObserver(     self,     selector: "cityDidChange:",     name: NSManagedObjectContextObjectsDidChangeNotification,     object: city) }  @objc func cityDidChange(note: NSNotification) {   guard let city = note.object as? City else { return }   if city.deleted {     navigationController?.popViewControllerAnimated(true)   } else {     nameLabel.text = city.name   } }

You have the NSNotificationCenter.defaultCenter() , add an observer, set the selector, pass in the notification name (in this case, the city object that we want to observe). We have the a cityDidChange method, we pull the object out of the notification and check if it is a city. We have all done this, but we can do better .

observer = ManagedObjectObserver(object: city) { [unowned self] type in   switch type {   case .Delete:     self.navigationController?.popViewControllerAnimated(true)   case .Update:     self.nameLabel.text = city.name   } }

We create a ManagedObjectObserver, pass in the city, and then a closure that is supposed to run whenever that object changes. We check, “was it deleted?”. We pop the view controller; if the city changed, then we update the nameLabel with the new city.name. It is very easy to read and understand.

How would we implement this? I am going to cheat a bit, and just show the broad picture :

extension NSManagedObjectContext {   public func addObjectsDidChangeNotificationObserver(handler: ObjectsDidChangeNotification -> ())     -> NSObjectProtocol {       let nc = NSNotificationCenter.defaultCenter()       let name = NSManagedObjectContextObjectsDidChangeNotification       return nc.addObserverForName(name, object: self, queue: nil) {         handler(ObjectsDidChangeNotification(note: $0))       }   } }

We added this helper to observe the notification. We are getting the default notification center, have the notification name and adding the actual observer. In the last line we use a wrapper, which gives us type-safety. This wrapper is a simple Swift struct, and the only property that is has is the notification that it wraps, and this is the ObjectsDidChangeNotification.

To this struct, we add properties that are type-safe. This notification has stuff in its user info dictionary, and we extract those in a type-safe way. If you want the inserted object, now we have a type-safe inserted object method on this helper struct:

public struct ObjectsDidChangeNotification {   private let notification: NSNotification   init(note: NSNotification) {     assert(note.name == NSManagedObjectContextObjectsDidChangeNotification)     notification = note   }   public var insertedObjects: Set<ManagedObject> {     return objectsForKey(NSInsertedObjectsKey)   }   public var updatedObjects: Set<ManagedObject> {     return objectsForKey(NSUpdatedObjectsKey)   }   public var deletedObjects: Set<ManagedObject> {     return objectsForKey(NSDeletedObjectsKey)   }    [...] }

All of a sudden, our code is more Swift-like and nicer to use. Those were few of the examples from the helper that we have created to make things look more like Swift.

We have used protocol and protocol extensions to make our code more readable.

We have also used a few other tricks, but the main gist is that you can create small helpers in your code to make your life easier, and make life easier for other people that have to read your code. Using old APIs is great because they have been battle-tested; it is code that has been around for many years… and, for many years, bugs have been fixed. But we can make these APIs even better ( and make our lives easier ).

When we create these helpers, it is important to keep the spirit of the original implementation . We want to make it easy for other people who read the code, and that who may not know about these wrappers.

Q: I have seen some people use Core Data with structs, instead of using NSManagedObject, they get struct side and I have experimented with this myself and I liked it, but obviously I do not know that much about Core Data, is it a bad idea? It feels more Swift-like in a way, but it loses all of the Core Data stuff, what do you think about it? Should we do that, or not?

Daniel: That is a very good question, Chris. Usually in Core Data you have your attribute. Here in the city class we had a name and a mayor and we usually have that in a managed object subclass. What many people have been doing is that you copy that data into a Swift struct, which then is more Swift-like. Core Data does cashing with managed object and in the persistence store coordinator that gives Core Data some amazing performance benefits; once you copy into struct, you lose that. I could talk about this for 30 minutes, but the main difference is performance. If you have very few objects it is cool to copy it into another struct, but you have to be aware that once you have a bigger application it is probably a bad idea because your performance will be horrible. That was a very short answer.

Q: Could you go back to one of your first slides, when you had the existing API? You have a funny typo in the word existing (“exiting”), and I was wondering if you did it on purpose and if so, do you think that current API will exit, being replaced by some much more easier API in Swift?

Daniel: Yes, also a very good question. I definitely do not think so, and that is the other point I wanted to bring across. Core Data was written a long time ago, and it feels strange in a Swift world, but you have to remember that this code has been used, not just in thousands and thousands of iOS apps, but before that, even on the Mac in thousands and thousands of apps. Apple uses it heavily in their apps, and there is a team of people that have worked full-time on fixing bugs in this API for twelve years. If Apple says, “we are going to do something new.”, then you can see that in 2028, it will be as stable as Core Data is today. The reason I do not think it makes sense to replace Core Data, but of course if need to store things in your app, you need to evaluate if Core Data makes sense. There are other solutions, and you always have to pick the right tool for the right job. If Core Data is the right tool, I do not think Apple will write something new for it. It works extremely well for the stuff that it is intended for.

See the discussion on Hacker News .

Transcription below provided by Realm: a replacement for SQLite & Core Data with first-class support for Swift! Check out the Swift docs!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Modern Core Data

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址