Taming Core Data - CocoaHeads

Taming Core Data - CocoaHeads

789ad08862abcdfbca8beec84e4998d9?s=128

Arkadiusz Holko

January 20, 2016
Tweet

Transcript

  1. Taming Core Data CocoaHeads Berlin

  2. Arkadiusz Holko @arekholko holko.pl

  3. None
  4. Core Data Anyone?

  5. None
  6. What is Core Data?

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

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

  11. None
  12. A framework that you use to build the persistence layer

    in your application.
  13. None
  14. Example

  15. None
  16. Core Data Primer

  17. NSManagedObject

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

  19. TweetList ViewController

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

    application = UIApplication.sharedApplication() guard let appDelegate = application.delegate as? AppDelegate else { return } let managedObjectContext = appDelegate.managedObjectContext } }
  21. 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)] } }
  22. 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 } } }
  23. data property class TweetListViewController: UITableViewController { ... var data: [ManagedTweet]

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

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

    { let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell let tweet = data[indexPath.row] }
  26. 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 }
  27. None
  28. 9 Steps

  29. Step #1 – Setup

  30. None
  31. None
  32. None
  33. The app delegate works alongside the app object to ensure

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

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

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

  37. 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) } }
  38. Singleton class TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext init() {

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

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

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

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

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

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

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

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

  48. Data Manager? class DataManager { }

  49. None
  50. Repository class TweetListRepository { let managedObjectContext: NSManagedObjectContext init(managedObjectContext: NSManagedObjectContext) {

    self.managedObjectContext = managedObjectContext } ... }
  51. 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) } } }
  52. Impact on the view controller override func viewDidLoad() { super.viewDidLoad()

    setupTableView() repository.performFetch { [weak self] managedTweets -> Void in self?.data = managedTweets } }
  53. 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) } ... }
  54. None
  55. None
  56. Data repositories' flow class ProfileRepository { let managedObjectContext: NSManagedObjectContext let

    profileIdentifier: Int init(managedObjectContext: NSManagedObjectContext, profileIdentifier: Int) { self.managedObjectContext = managedObjectContext self.profileIdentifier = profileIdentifier } }
  57. protocol Repository { var managedObjectContext: NSManagedObjectContext { get } }

    extension ProfileRepository { convenience init(repository: Repository, profileIdentifier: Int64) { self.init(managedObjectContext: repository.managedObjectContext, profileIdentifier: profileIdentifier) } } let profileRepository = ProfileRepository(repository: repository, profileIdentifier: profileIdentifier) let viewController = ProfileViewController(repository: profileRepository)
  58. Step #5 – Networking

  59. 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
  60. extension Request { var method: Method { return .GET }

    var path: String { return "" } var parameters: Dictionary<String, String> { return Dictionary() } }
  61. 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)) } } }
  62. 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 }
  63. 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
  64. class TweetListImporter { let managedObjectContext: NSManagedObjectContext init(managedObjectContext: NSManagedObjectContext) { self.managedObjectContext

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

    » Swift » CoreValue, but too powerful » own solution similar to JSON parsing libraries, such as Argo or Decodable
  66. None
  67. Step #6 – NSFetchedResultsController !

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

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

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

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

    var objects: [ManagedTweet] // computed property }
  72. 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) }
  73. 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!) } } }
  74. Reacting to changes extension TweetListViewController: RepositoryDelegate { // 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) } }
  75. NSFetchedResultsController Not only for table/collection views

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

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

  78. None
  79. Issues » mutability » faulting » lack of thread safety

  80. None
  81. Step #7 – Protocolization

  82. 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 } }
  83. class TweetListRepository: Repository { weak var delegate: RepositoryDelegate? var objects:

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

  85. None
  86. Step #8 – Immutable Models

  87. Immutable Core Data ? !

  88. Those Structs 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 }
  89. class TweetListRepository: Repository { weak var delegate: RepositoryDelegate? var objects:

    [TweetType] { // non-optimized version let fetchedObjects = fetchedResultsController.fetchedObjects return structsFromManagedObjects(fetchedObjects) } }
  90. Mutation func favoriteTweet(tweet: TweetType) { // modifies corresponding managed object

    under the hood }
  91. Issues » mutability » faulting » lack of thread safety

  92. New Issues » data duplication in memory » CPU time

    for conversions (can be in the background) » possible lack of synchronization
  93. Step #9 – Modularization

  94. .framework Package.swift

  95. Summary

  96. Lessons learned » small classes/structs » testability » separation of

    concerns
  97. Can We Take This Even Further?

  98. None
  99. Thank You!

  100. Questions? @arekholko

  101. Slides https://speakerdeck.com/fastred/taming-core-data-cocoaheads

  102. 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/
  103. 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