Taming Core Data

Taming Core Data

789ad08862abcdfbca8beec84e4998d9?s=128

Arkadiusz Holko

November 23, 2015
Tweet

Transcript

  1. Taming Core Data

  2. Arkadiusz Holko @arekholko holko.pl Macoscope

  3. Core Data Anyone?

  4. None
  5. What is Core Data?

  6. Core Data is a framework that you use to manage

    the model layer objects in your application.
  7. None
  8. Can We Do Better?

  9. None
  10. A framework that you use to build the persistence layer

    in your application.
  11. None
  12. Example

  13. None
  14. Core Data Primer

  15. NSManagedObject

  16. NSManagedObjectContext Similar to: » sqlite3 * » RLMRealm */Realm

  17. TweetList ViewController

  18. Fetch class TweetListViewController: UITableViewController { ... func fetchTweets() { let

    application = UIApplication.sharedApplication() guard let appDelegate = application.delegate as? AppDelegate else { return } let managedObjectContext = appDelegate.managedObjectContext } }
  19. Fetch class TweetListViewController: UITableViewController { ... func fetchTweets() { let

    application = UIApplication.sharedApplication() guard let appDelegate = application.delegate as? AppDelegate else { return } let managedObjectContext = appDelegate.managedObjectContext let fetchRequest = NSFetchRequest(entityName: "ManagedTweet") fetchRequest.predicate = NSPredicate(format: "homeTimeline != NULL") fetchRequest.sortDescriptors = [NSSortDescriptor(key: "identifier", ascending: false)] } }
  20. Fetch class TweetListViewController: UITableViewController { ... func fetchTweets() { let

    application = UIApplication.sharedApplication() guard let appDelegate = application.delegate as? AppDelegate else { return } let managedObjectContext = appDelegate.managedObjectContext let fetchRequest = NSFetchRequest(entityName: "ManagedTweet") fetchRequest.predicate = NSPredicate(format: "homeTimeline != NULL") fetchRequest.sortDescriptors = [NSSortDescriptor(key: "identifier", ascending: false)] let results = try? managedObjectContext.executeFetchRequest(fetchRequest) if let managedObjects = results as? [NSManagedObject] { data = managedObjects } } }
  21. data property class TweetListViewController: UITableViewController { ... var data: [ManagedTweet]

    = [] { didSet { tableView.reloadData() } } }
  22. cellForRowAtIndexPath: override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell

    { let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell }
  23. cellForRowAtIndexPath: override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell

    { let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell let tweet = data[indexPath.row] }
  24. cellForRowAtIndexPath: override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell

    { let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell let tweet = data[indexPath.row] if let text = tweet.valueForKey("text") as? String { cell.multilineTextLabel?.text = text } // set up other views return cell }
  25. 9 Steps

  26. Step #1 – Setup

  27. None
  28. None
  29. None
  30. The app delegate works alongside the app object to ensure

    your app interacts properly with the system and with other apps. — Apple
  31. PersistentStack class PersistentStack { let managedObjectContext: NSManagedObjectContext init(storeType: String) {

    ... } }
  32. AppDelegate now class AppDelegate: UIResponder, UIApplicationDelegate { let persistentStack =

    PersistentStack(NSSQLiteStoreType) ... }
  33. Step #2 – Accessing NSManagedObjectContext

  34. The Easy Way class TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext

    init() { if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate { managedObjectContext = appDelegate.persistentStack.managedObjectContext } super.init(nibName: nil, bundle: nil) } }
  35. Singleton class TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext init() {

    managedObjectContext = PersistentStack.sharedInstance().managedObjectContext super.init(nibName: nil, bundle: nil) } }
  36. None
  37. Dependency Injection class TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext init(managedObjectContext:

    NSManagedObjectContext) { self.managedObjectContext = managedObjectContext super.init(nibName: nil, bundle: nil) } }
  38. Step #3 – NSManagedObject subclass

  39. NSManagedObject with KVC let tweet: NSManagedObject = data[indexPath.row] if let

    text = tweet.valueForKey("text") as? String { cell.multilineTextLabel?.text = text }
  40. Subclassed NSManagedObject let tweet: ManagedTweet = data[indexPath.row] cell.multilineTextLabel.text = tweet.text

  41. Generators » Xcode: Editor → Create NSManagedObject subclass... » mogenerator

  42. class _ManagedTweet: NSManagedObject { ... // MARK: - Properties @NSManaged

    var identifier: NSNumber @NSManaged var text: String // MARK: - Relationships @NSManaged var homeTimeline: ManagedHomeTimeline? @NSManaged var user: ManagedUser }
  43. enum ManagedTweetAttributes: String { case identifier = "identifier" case text

    = "text" } enum ManagedTweetRelationships: String { case homeTimeline = "homeTimeline" case user = "user" }
  44. Step #4 – Who should create NSFetchRequest?

  45. Data Manager? class DataManager { }

  46. None
  47. Repository class TweetListRepository { let managedObjectContext: NSManagedObjectContext init(managedObjectContext: NSManagedObjectContext) {

    self.managedObjectContext = managedObjectContext } ... }
  48. class TweetListRepository { ... func performFetch(completion: [ManagedTweet] -> Void) {

    let fetchRequest = NSFetchRequest(entityName: ManagedTweet.entityName()) let homeTimelineKey = ManagedTweetRelationships.homeTimeline.rawValue fetchRequest.predicate = NSPredicate(format: "%@ != NULL", homeTimelineKey); let identifierKey = ManagedTweetAttributes.identifier.rawValue fetchRequest.sortDescriptors = [NSSortDescriptor(key: identifierKey, ascending: false)] let results = try? managedObjectContext.executeFetchRequest(fetchRequest) if let managedTweets = results as? [ManagedTweet] { completion(managedTweets) } } }
  49. Impact on the view controller override func viewDidLoad() { super.viewDidLoad()

    setupTableView() repository.performFetch { [weak self] managedTweets -> Void in self?.data = managedTweets } }
  50. NSManagedObjectContext gets hidden from the view controller class TweetListViewController: UITableViewController

    { let repository: TweetListRepository init(repository: TweetListRepository) { self.repository = repository super.init(nibName: nil, bundle: nil) } ... }
  51. Data repositories' flow class ProfileRepository { let managedObjectContext: NSManagedObjectContext let

    profileIdentifier: Int64 init(managedObjectContext: NSManagedObjectContext, profileIdentifier: Int64) { self.managedObjectContext = managedObjectContext self.profileIdentifier = profileIdentifier } }
  52. extension ProfileRepository { convenience init(tweetListRepository: TweetListRepository, profileIdentifier: Int64) { self.init(managedObjectContext:

    tweetListRepository.managedObjectContext, profileIdentifier: profileIdentifier) } }
  53. Step #5 – Networking

  54. protocol Request { typealias ResultType var method: Method { get

    } var path: String { get } var parameters: Dictionary<String, String> { get } func send(completion: (Result<ResultType, NSError> -> Void)) func resultFromJSON(JSON: AnyObject) throws -> ResultType } Result: github.com/antitypical/Result
  55. extension Request { var method: Method { return .GET }

    var path: String { return "" } var parameters: Dictionary<String, String> { return Dictionary() } }
  56. extension Request { var method: Method { return .GET }

    var path: String { return "" } var parameters: Dictionary<String, String> { return Dictionary() } func send(completion: (Result<ResultType, NSError> -> Void)) { let response = RequestSender().send(self) do { let result = try self.resultFromJSON(response) completion(Result(result)) } catch let nserror as NSError { completion(Result(error: nserror)) } } }
  57. struct Tweet { let identifier: Int let text: String let

    user: User } struct User { let identifier: Int let name: String let profileImageUrl: String? let screenName: String }
  58. class TweetListRequest: Request { typealias ResultType = [Tweet] var path:

    String { return "statuses/home_timeline" } func resultFromJSON(JSON: AnyObject) throws -> [Tweet] { return try [Tweet].decode(JSONString) } } decode: github.com/Anviking/Decodable
  59. class TweetListImporter { let managedObjectContext: NSManagedObjectContext init(managedObjectContext: NSManagedObjectContext) { self.managedObjectContext

    = managedObjectContext } func importTweets(tweets: [Tweet], completion: (Result<Bool, NSError> -> Void)) { ... } }
  60. Value Type → NSManagedObject » Objective-C » Mantle with MTLManagedObjectAdapter

    » Swift » CoreValue, but too powerful » own solution similar to Argo/Decodable
  61. None
  62. Step #6 – NSFetchedResultsController

  63. Problem with NSFetchedResultsControllerDelegate optional public func controller(controller: NSFetchedResultsController, didChangeObject anObject:

    AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
  64. Problem with NSFetchedResultsControllerDelegate didChangeObject anObject: AnyObject,

  65. TweetListRepository API class TweetListRepository: Repository { weak var delegate: NSFetchedResultsControllerDelegate?

    var objects: [ManagedTweet] // computed property }
  66. TweetListRepository API class TweetListRepository: Repository { weak var delegate: RepositoryDelegate?

    var objects: [ManagedTweet] // computed property }
  67. Our own delegate protocol Repository { func performFetch() } protocol

    RepositoryDelegate: class { func repository(repository: Repository, didFinishInitialRequestWithResult result: Result<Bool, NSError>) func repositoryWillChangeContent(repository: Repository) func repositoryDidChangeContent(repository: Repository) func repository(repository: Repository, didInsertRowAtIndexPath indexPath: NSIndexPath) func repository(repository: Repository, didDeleteRowAtIndexPath indexPath: NSIndexPath) func repository(repository: Repository, didUpdateRowAtIndexPath indexPath: NSIndexPath) }
  68. From NSFRCDelegate to RepositoryDelegate class TweetListRepository: Repository { // other

    delegate methods omitted for clarity func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch (type) { case .Insert: delegate?.repository(self, didInsertRowAtIndexPath: newIndexPath!) case .Delete: delegate?.repository(self, didDeleteRowAtIndexPath: indexPath!) case .Update: delegate?.repository(self, didUpdateRowAtIndexPath: indexPath!) case .Move: // consider adding separate update callback delegate?.repository(self, didDeleteRowAtIndexPath: indexPath!) delegate?.repository(self, didInsertRowAtIndexPath: newIndexPath!) } } }
  69. Reacting to changes class TableViewUpdater: RepositoryDelegate { let tableView: UITableView

    init(tableView: UITableView) { self.tableView = tableView } // some methods omitted func repository(repository: Repository, didInsertRowAtIndexPath indexPath: NSIndexPath) { tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } func repository(repository: Repository, didDeleteRowAtIndexPath indexPath: NSIndexPath) { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } func repository(repository: Repository, didUpdateRowAtIndexPath indexPath: NSIndexPath) { tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } }
  70. NSFetchedResultsController Not only for table/collection views

  71. ProfileRepository protocol SingleElementRepository { func performFetch() } protocol SingleElementRepositoryDelegate: class

    { func singleElementRepositoryDidUpdate(repository: SingleElementRepository) } class ProfileRepository: SingleElementRepository { weak var delegate: SingleElementRepositoryDelegate? var user: User? // computed property ... }
  72. Quick Stop

  73. None
  74. Issues » mutability » faulting » lack of thread safety

  75. None
  76. Step #7 – Protocolization

  77. None
  78. protocol TweetType { var identifier: Int { get } var

    text: String { get } var user: UserType { get } } protocol UserType { var identifier: Int { get } var name: String { get } var profileImageUrl: String? { get } var screenName: String { get } }
  79. class TweetListRepository: Repository { weak var delegate: RepositoryDelegate? var objects:

    [TweetType] // computed property }
  80. Issues » mutability » faulting » lack of thread safety

  81. Step #8 – Value Types

  82. Reminder struct Tweet: TweetType { let identifier: Int let text:

    String let user: User } struct User: UserType { let identifier: Int let name: String let profileImageUrl: String? let screenName: String }
  83. class TweetListRepository: Repository { weak var delegate: RepositoryDelegate? var objects:

    [TweetType] { let fetchedObjects = fetchedResultsController.fetchedObjects return structsFromManagedObjects(fetchedObjects) } }
  84. Issues » mutability » faulting » lack of thread safety

    » data duplication in memory » CPU time for conversions (can be in the background) » possible lack of synchronization
  85. Step #9 – Modularization

  86. .framework

  87. Summary

  88. Lessons learned » small classes » testability » separation of

    concerns
  89. Can We Take This Even Further?

  90. None
  91. Thank You!

  92. Questions?

  93. Slides https://speakerdeck.com/fastred/taming-core-data Sample Code Coming soon!

  94. Links (1/2) » https://www.youtube.com/watch? v=WpkDN78P884 » https://www.destroyallsoftware.com/talks/ boundaries » http://objectsonrails.com/

    » http://blog.8thlight.com/uncle-bob/ 2012/08/13/the-clean-architecture.html » https://realm.io/news/andy-matuschak- controlling-complexity/
  95. Links (2/2) » http://khanlou.com/2015/06/protocol- oriented-networking/ » https://twitter.com/andy_matuschak/status/ 560857237640343552 » https://github.com/rentzsch/mogenerator

    » https://www.objc.io/issues/13-architecture/ viper/ » https://developers.facebooklive.com/videos/ 525/facebook-on-ios-inside-the-big-blue-app