Taming Core Data by Arek Holko, Macoscope

103
Taming Core Data CocoaHeads Berlin

Transcript of Taming Core Data by Arek Holko, Macoscope

TamingCoreDataCocoaHeads Berlin

Arkadiusz Holko

@arekholkoholko.pl

Core DataAnyone?

What isCoreData?

Core Data is a framework that you use to manage the model layer objects in

your application.

CanWe DoBetter?

A framework that you use to build the persistence layer in your application.

Example

Core DataPrimer

NSManagedObject

NSManagedObjectContextSimilar to:

» sqlite3 *

» RLMRealm */Realm

TweetListViewController

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

}}

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)]

}}

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 } }}

data propertyclass TweetListViewController: UITableViewController { ... var data: [ManagedTweet] = [] { didSet { tableView.reloadData() } }}

cellForRowAtIndexPath:override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell

}

cellForRowAtIndexPath:override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! TweetTableViewCell

let tweet = data[indexPath.row]

}

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}

9 Steps

Step #1–

Setup

The app delegate works alongside the app object to ensure your app interacts

properly with the system and with other apps.

— Apple

PersistentStackclass PersistentStack { let managedObjectContext: NSManagedObjectContext

init(storeType: String) { ... }}

AppDelegate nowclass AppDelegate: UIResponder, UIApplicationDelegate {

let persistentStack = PersistentStack(NSSQLiteStoreType)

...}

Step #2–

AccessingNSManagedObjectContext

The Easy Wayclass TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext

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

Singletonclass TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext

init() { managedObjectContext = PersistentStack.sharedInstance().managedObjectContext super.init(nibName: nil, bundle: nil) }}

Dependency Injectionclass TweetListViewController: UITableViewController { let managedObjectContext: NSManagedObjectContext

init(managedObjectContext: NSManagedObjectContext) { self.managedObjectContext = managedObjectContext super.init(nibName: nil, bundle: nil) }}

Step #3–

NSManagedObject subclass

NSManagedObject with KVClet tweet: NSManagedObject = data[indexPath.row]if let text = tweet.valueForKey("text") as? String { cell.multilineTextLabel?.text = text}

Subclassed NSManagedObjectlet tweet: ManagedTweet = data[indexPath.row] cell.multilineTextLabel.text = tweet.text

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

» mogenerator

class _ManagedTweet: NSManagedObject { ... // MARK: - Properties @NSManaged var identifier: NSNumber

@NSManaged var text: String

// MARK: - Relationships @NSManaged var homeTimeline: ManagedHomeTimeline?

@NSManaged var user: ManagedUser}

enum ManagedTweetAttributes: String { case identifier = "identifier" case text = "text"}

enum ManagedTweetRelationships: String { case homeTimeline = "homeTimeline" case user = "user"}

Step #4–

Who should create NSFetchRequest?

Data Manager?class DataManager {

}

Repositoryclass TweetListRepository { let managedObjectContext: NSManagedObjectContext

init(managedObjectContext: NSManagedObjectContext) { self.managedObjectContext = managedObjectContext }

...}

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) } }}

Impact on the view controlleroverride func viewDidLoad() { super.viewDidLoad()

setupTableView()

repository.performFetch { [weak self] managedTweets -> Void in self?.data = managedTweets }}

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) }

...}

Data repositories' flowclass ProfileRepository {

let managedObjectContext: NSManagedObjectContext let profileIdentifier: Int

init(managedObjectContext: NSManagedObjectContext, profileIdentifier: Int) { self.managedObjectContext = managedObjectContext self.profileIdentifier = profileIdentifier }}

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)

Step #5–

Networking

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

extension Request {

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

}

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)) } }}

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}

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

class TweetListImporter {

let managedObjectContext: NSManagedObjectContext

init(managedObjectContext: NSManagedObjectContext) { self.managedObjectContext = managedObjectContext }

func importTweets(tweets: [Tweet], completion: (Result<Bool, NSError> -> Void)) { ... }}

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

Step #6–

NSFetchedResultsController!

Problem withNSFetchedResultsControllerDelegateoptional public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)

Problem withNSFetchedResultsControllerDelegate

didChangeObject anObject: AnyObject,

TweetListRepository APIclass TweetListRepository: Repository {

weak var delegate: NSFetchedResultsControllerDelegate?

var objects: [ManagedTweet] // computed property}

TweetListRepository APIclass TweetListRepository: Repository {

weak var delegate: RepositoryDelegate?

var objects: [ManagedTweet] // computed property}

Our own delegateprotocol 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)}

From NSFRCDelegate to RepositoryDelegateclass 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!) } }}

Reacting to changesextension 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) }}

NSFetchedResultsControllerNot only for

table/collection views

ProfileRepositoryprotocol SingleElementRepository { func performFetch()}

protocol SingleElementRepositoryDelegate: class { func singleElementRepositoryDidUpdate(repository: SingleElementRepository)}

class ProfileRepository: SingleElementRepository {

weak var delegate: SingleElementRepositoryDelegate? var user: User? // computed property

...}

!

Issues» mutability

» faulting

» lack of thread safety

Step #7–

Protocolization

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 }}

class TweetListRepository: Repository {

weak var delegate: RepositoryDelegate?

var objects: [TweetType] // computed property}

Issues» mutability

» faulting

» lack of thread safety

Step #8–

Immutable Models

Immutable Core Data ?!

Those Structsstruct 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}

class TweetListRepository: Repository {

weak var delegate: RepositoryDelegate?

var objects: [TweetType] { // non-optimized version let fetchedObjects = fetchedResultsController.fetchedObjects return structsFromManagedObjects(fetchedObjects) }}

Mutationfunc favoriteTweet(tweet: TweetType) { // modifies corresponding managed object under the hood}

Issues» mutability

» faulting

» lack of thread safety

New Issues» data duplication in memory

» CPU time for conversions (can be in the background)

» possible lack of synchronization

Step #9–

Modularization

.framework

Package.swift

Summary

Lessons learned» small classes/structs

» testability

» separation of concerns

Can WeTake This

Even Further?

Thank You!

Questions?

@arekholko

Slideshttps://speakerdeck.com/fastred/taming-core-data-cocoaheads

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/

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