From fb5f15ed5e6b51ce7f42e14e012816766ba84886 Mon Sep 17 00:00:00 2001 From: Jesse Squires Date: Mon, 27 Feb 2017 11:17:01 -0800 Subject: [PATCH] Core Data guide Summary: Issue answered: #407 #460 #461 I did not run any tool to generate documentation - [x] All tests pass. Demo project builds and runs. - [x] I added tests, an experiment, or detailed why my change isn't tested. - [x] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) Closes https://github.com/Instagram/IGListKit/pull/515 Differential Revision: D4621212 Pulled By: jessesquires fbshipit-source-id: 110e3d37d08e7c763b6a6cde70bc83280f7a2bb3 --- Guides/Best Practices and FAQ.md | 2 +- Guides/Getting Started.md | 4 + Guides/Working with Core Data.md | 166 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 Guides/Working with Core Data.md diff --git a/Guides/Best Practices and FAQ.md b/Guides/Best Practices and FAQ.md index c0e9db3c..a2c5161d 100644 --- a/Guides/Best Practices and FAQ.md +++ b/Guides/Best Practices and FAQ.md @@ -45,7 +45,7 @@ This feature is on the `master` branch only and hasn't been officially tagged an **Does `IGListKit` work with...?** -- Core Data ([#460](https://github.com/Instagram/IGListKit/issues/460), [#461](https://github.com/Instagram/IGListKit/issues/461)) +- Core Data ([Working with Core Data](https://instagram.github.io/IGListKit/working-with-core-data.html) Guide) - AsyncDisplayKit ([AsyncDisplayKit/#2942](https://github.com/facebook/AsyncDisplayKit/pull/2942)) - ComponentKit ([ocrickard/IGListKit-ComponentKit](https://github.com/ocrickard/IGListKit-ComponentKit)) - React Native diff --git a/Guides/Getting Started.md b/Guides/Getting Started.md index 6e470351..efd6c774 100644 --- a/Guides/Getting Started.md +++ b/Guides/Getting Started.md @@ -58,6 +58,10 @@ func emptyView(for listAdapter: IGListAdapter) -> UIView? { You can return an array of _any_ type of data, as long as it conforms to `IGListDiffable`. +### Immutability + +The data should be immutable. If you return mutable objects that you will be editing later, `IGListKit` will not be able to diff the models accurately. This is because the instances have already been changed. Thus, the updates to the objects would be lost. Instead, always return a newly instantiated, immutable object and implement `IGListDiffable`. + ## Diffing `IGListKit` uses an algorithm adapted from a paper titled [A technique for isolating differences between files](http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL) by Paul Heckel. This algorithm uses a technique known as the *longest common subsequence* to find a minimal diff between collections in linear time `O(n)`. It finds all **inserts**, **deletes**, **updates**, and **moves** between arrays of data. diff --git a/Guides/Working with Core Data.md b/Guides/Working with Core Data.md new file mode 100644 index 00000000..e9bbfa14 --- /dev/null +++ b/Guides/Working with Core Data.md @@ -0,0 +1,166 @@ +# Working with Core Data + +This guide provides details on how to work with [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/index.html) and `IGListKit`. + +## Background + +The main difference in the setup and architecture of a Core Data and `IGListKit` application is the configuration of the model layer. Core Data operates with a mutable model layer, where objects are always passed by reference and the same instance is modified when an object is edited. + +`IGListKit` requires an immutable model in order to correctly calculate the diffing between model snapshots and to correctly animate the `UICollectionView`. + +In order to satisfy these prerequisites, Core Data `NSManagedObject`s should not be used directly as `IGListDiffable` objects. Instead, a view model (or some sort of token object) should be used to mimic (or act as a placeholder for) the data that will be displayed in the collection view. + +## Further discussion + +There are further discussions on this topic at [#460](https://github.com/Instagram/IGListKit/issues/460), [#461](https://github.com/Instagram/IGListKit/issues/461), [#407](https://github.com/Instagram/IGListKit/issues/407). + +## Basic Setup + +The basic setup for Core Data and `IGListKit` is the same as the normal setup that is found in the [Getting Started Guide][https://instagram.github.io/IGListKit/getting-started.html]. The main difference will be in the setup of the model in the datasource. + +## Working with view model + +### Creating a view model + +Suppose the Core Data model consist of: + +```swift +extension User { + @NSManaged var firstName: String + @NSManaged var lastName: String + @NSManaged var address: String + @NSManaged var someVariableNotNeededInUI: String +} +``` + +A `ViewModel` object will contain only the necessary information needed to build UI. The properties of the `ViewModel` will be immutable: + +```swift +class UserViewModel: NSObject { + let firstName: String + let lastName: String + let address: String +} +``` + +We recommend writing a helper method to translate Core Data objects into `ViewModel` objects: + +```swift +extension UserViewModel { + static func fromCoreData(user: User) -> UserViewModel { + // - Note: For avoiding Core Data threading violation, the following code should be wrapped in a + // user.managedObjectContext?.performAndWait {} + return UserViewModel(firstName: user.firstName, lastName: user.lastName, address: user.lastName) + } +} +``` + +The `IGListDiffable` protocol is implemented on the `ViewModel` layer: + +```swift +extension UserViewModel: IGListDiffable { + + public func diffIdentifier() -> NSObjectProtocol { + return NSString(string: firstName + lastName) + } + + public func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { + guard let toObject = object as? UserViewModel else { return false } + + return self.firstName == toObject.firstName + && self.lastName == toObject.lastName + && self.address == toObject.address + } +} +``` + +## Setting up the view model in the adapter data source + +Steps to configure the `UICollectionView` with the `ViewModel`: + +- Retrieve Core Data objects +- Transform Core Data objects into ViewModel objects and return them +- Track changes to Core Data objects and update the datasource with them + +### Retrieve Core Data objects + +The way objects are retrieved from Core Data is depends on the project. + +Example: Suppose there is a delegate `Provider` class with the role of fetching Core Data objects and checking for updates. It can use an `NSFetchedResultsController` to leverage on the Core Data framework and rely on automatic notifications for updates. + +```swift +final class UserProvider: NSObject { + + private lazy var userFetchResultController: NSFetchedResultsController = { + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "User") + + // sort descriptors and predicates + // ... + + let fetchResultController = NSFetchedResultsController( + fetchRequest: tripsFetchRequest, + managedObjectContext: self.coreDataStack.mainQueueManagedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil) + + // Set delegate to track CoreData changes + fetchResultController.delegate = self + + return fetchResultController + }() + + init(coreDataStack: CoreDataStack) { + self.coreDataStack = coreDataStack + super.init() + do { + try userFetchResultController.performFetch() + } + catch { + fatalError("Cannot Fetch! \(error)") + } + } +} +``` + +### Transform Core Data objects into view models + +```swift +func getUsers() -> [UserViewModel]? { + guard let users = self.userFetchResultController.fetchedObjects else { return nil } + // Here we transform and return ViewModel objects! + return users.flatMap { UserViewModel.fromCoreData(user: $0) } +} +``` + +### Track changes to Core Data + +The `Provider` will track changes to the Core Data model by listening to the `NSFetchedResultsController` methods and inform the application about this changes via KVO, notifications, delegation, etc. + +```swift +extension UserProvider: NSFetchedResultsControllerDelegate { + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + self.delegate?.performUpdatesForCoreDataChange(animated: true) + } +} +``` + +### Configure the datasource + +The data source retrieves ViewModels and configures the `IGListSectionController` with them: + +```swift +func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] { + return self.userProvider.getUsers() +} +``` + +### Reacting to Core Data changes in UI + +The `UIViewController` containing the `UICollectionView`, will react to the `NSFetchedResultController` messages by updating the UI: + +```swift +func performUpdatesForCoreDataChange(animated: Bool) { + // Updating contents of collection view + self.adapter.performUpdates(animated: animated) +} +```