Last active
November 12, 2015 11:08
-
-
Save rinat-enikeev/0e4440d480fa4db2e386 to your computer and use it in GitHub Desktop.
Separate Persistence and Controllers layer in iOS with Swift. SEE LAST (USAGE) FILE. Write small, easy to read, fully featured and model change responsive UITableViewControllers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2015 Rinat Enikeev | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// CoreData specific data storage protocol. | |
// REFRCTableViewController.swift is CoreData specific abstract UITableViewController, so show MOCs. | |
import Foundation | |
@objc(RECoreDataBase) | |
protocol RECoreDataBase : REDatabase { | |
func mocMain() -> NSManagedObjectContext | |
func mocBg() -> NSManagedObjectContext // I have used background moc from RestKit | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2015 Rinat Enikeev | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// The purpose of the protocol is to hide data storage code from services and controllers. | |
// EntityName, NSPredicate, NSSortDescriptors may be used everywhere. | |
import Foundation | |
@objc(REDatabase) | |
protocol REDatabase { | |
func insert(entityName: String) -> AnyObject | |
func fetchObject(entityName : String, predicate: NSPredicate?) -> AnyObject? | |
func fetchObject(entityName : String, predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> AnyObject? | |
func fetchObjects(entityName : String, predicate: NSPredicate?) -> [AnyObject]? | |
func fetchObjects(entityName : String, predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> [AnyObject]? | |
func countOfObjects(entityName : String) -> Int | |
func countOfObjects(entityName : String, predicate: NSPredicate?) -> Int | |
func persist() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2015 Rinat Enikeev | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// Class is responsible for handling all data changes stuff in CoreData, | |
// updateing UITableView with NSFetchResultsController. | |
// Child class does not know if it is CoreData of FMDB (f.e.). | |
// Child class overrides vars and provides ONLY what it have to know: | |
// EntityName, NSPredicate, SortDescriptions and some other params. | |
// Purpose is to hide unnecessary info from UIViewController in order | |
// to be able to change data provider in the entire project with ONE file change. | |
import UIKit | |
import CoreData | |
class REFRCTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { | |
var db : RECoreDataBase! // TODO: inject with Typhoon in every child class | |
// You should override some of the vars in child UIVC classes | |
var cellReuseIdentifier : String! { fatalError("It is an abstract class") } | |
var sectionHeaderViewReuseIdentifier : String? { return nil } // you MUST register or provide nib if override | |
var sectionHeaderNib : UINib? { return nil } | |
var sectionFooterViewReuseIdentifier : String? { return nil } // you MUST register class or provide nib if override | |
var sectionFooterNib : UINib? { return nil } | |
var entityName : String! { fatalError("It is an abstract class") } | |
var sortDescriptors : [NSSortDescriptor]? { return [NSSortDescriptor]() } | |
var sectionName : String? { return nil } | |
var limit : Int? { return nil } // ios buggy - see methods didChangeObject and numberOfRows | |
var relationshipKeyPathsForPrefetching : [String]? { return nil } | |
var predicate : NSPredicate? { | |
get { | |
return effectivePredicate | |
} | |
set { | |
self.effectivePredicate = newValue | |
self.frc.fetchRequest.predicate = effectivePredicate | |
} | |
} | |
// private | |
private lazy var frc: NSFetchedResultsController = { | |
let fetchRequest = NSFetchRequest() | |
let moc = self.db.mocMain() | |
let entityName = self.entityName | |
fetchRequest.entity = NSEntityDescription.entityForName(entityName, inManagedObjectContext: moc) | |
fetchRequest.sortDescriptors = self.sortDescriptors | |
fetchRequest.predicate = self.predicate | |
fetchRequest.relationshipKeyPathsForPrefetching = self.relationshipKeyPathsForPrefetching | |
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: self.sectionName, cacheName: nil) | |
aFetchedResultsController.delegate = self | |
if self.limit != nil { aFetchedResultsController.fetchRequest.fetchLimit = self.limit! } | |
return aFetchedResultsController | |
}() | |
private lazy var effectivePredicate : NSPredicate? = { | |
return NSPredicate(value: true) | |
}() | |
// Template Method. TODO: Override or use project wide tableview cell configuration here | |
func configureCell(cell : UITableViewCell?, object: AnyObject?) { | |
if let wordCell = cell as? WordCell { | |
if let word = object as? Word { | |
wordCell.label.text = word.text | |
} | |
} | |
} | |
//MARK: Data provider | |
func countOfObjects() -> Int { | |
if let objects = frc.fetchedObjects { | |
return objects.count | |
} else { | |
return 0 | |
} | |
} | |
func allObjects() -> [AnyObject]? { | |
return frc.fetchedObjects | |
} | |
func objectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? { | |
return frc.objectAtIndexPath(indexPath) | |
} | |
func indexPathForObject(object: AnyObject) -> NSIndexPath? { | |
return frc.indexPathForObject(object) | |
} | |
func fetchData() { | |
do { | |
try frc.performFetch() | |
} catch let error as NSError { | |
NSLog("Fetch failed with error: " + error.localizedDescription) | |
} | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
if sectionHeaderNib != nil && sectionHeaderViewReuseIdentifier != nil { | |
tableView.registerNib(sectionHeaderNib!, forHeaderFooterViewReuseIdentifier: sectionHeaderViewReuseIdentifier!) | |
} | |
if sectionFooterNib != nil && sectionFooterViewReuseIdentifier != nil { | |
tableView.registerNib(sectionFooterNib!, forHeaderFooterViewReuseIdentifier: sectionFooterViewReuseIdentifier!) | |
} | |
} | |
} | |
//MARK: UITableViewDataSource | |
extension REFRCTableViewController { | |
//MARK: Section | |
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { | |
if let sectionCount = frc.sections?.count { | |
return sectionCount | |
} else { | |
return 1 | |
} | |
} | |
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
if sectionHeaderViewReuseIdentifier == nil { | |
return frc.sections?[section].name | |
} | |
return nil | |
} | |
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { | |
if sectionHeaderViewReuseIdentifier != nil { | |
return self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(self.sectionHeaderViewReuseIdentifier!) | |
} else { | |
return nil | |
} | |
} | |
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { | |
if sectionFooterViewReuseIdentifier != nil { | |
return self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(sectionFooterViewReuseIdentifier!) | |
} else { | |
return nil | |
} | |
} | |
//MARK: Number | |
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
var frcSectionNumOfObjects = 0 | |
if let s = frc.sections { | |
if s.count > section { | |
frcSectionNumOfObjects = s[section].numberOfObjects | |
} | |
} | |
// ios bug (frc does not respect fetchrequest limit) | |
// see http://stackoverflow.com/questions/4858228/nsfetchedresultscontroller-ignores-fetchlimit/9264778#9264778 | |
if self.limit != nil && self.limit! < frcSectionNumOfObjects { | |
return self.limit! | |
} else { | |
return frcSectionNumOfObjects | |
} | |
} | |
//MARK: Cell | |
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { | |
let cell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier)! // fatal crash if not provided | |
configureCell(cell, object: frc.objectAtIndexPath(indexPath)) | |
return cell | |
} | |
} | |
//MARK: NSFetchResultsControllerDelegate | |
extension REFRCTableViewController { | |
func presentedTableView() -> UITableView? { | |
return self.tableView | |
} | |
func controllerWillChangeContent(controller: NSFetchedResultsController) { | |
presentedTableView()?.beginUpdates() | |
} | |
func controller(controller: NSFetchedResultsController, | |
didChangeObject anObject: AnyObject, | |
atIndexPath indexPath: NSIndexPath?, | |
forChangeType type: NSFetchedResultsChangeType, | |
newIndexPath: NSIndexPath?) | |
{ | |
// ios bug | |
// see http://stackoverflow.com/questions/4858228/nsfetchedresultscontroller-ignores-fetchlimit/9264778#9264778 | |
if self.limit != nil && (newIndexPath?.row >= self.limit! || indexPath?.row >= self.limit!) { | |
return | |
} | |
switch(type) { | |
case .Insert: | |
if let newIndexPath = newIndexPath { | |
presentedTableView()?.insertRowsAtIndexPaths([newIndexPath], | |
withRowAnimation:UITableViewRowAnimation.Fade) | |
} | |
case .Delete: | |
if let indexPath = indexPath { | |
presentedTableView()?.deleteRowsAtIndexPaths([indexPath], | |
withRowAnimation: UITableViewRowAnimation.Fade) | |
} | |
case .Update: | |
if let indexPath = indexPath { | |
presentedTableView()?.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade) | |
} | |
case .Move: | |
if let indexPath = indexPath { | |
if let newIndexPath = newIndexPath { | |
presentedTableView()?.deleteRowsAtIndexPaths([indexPath], | |
withRowAnimation: UITableViewRowAnimation.Fade) | |
presentedTableView()?.insertRowsAtIndexPaths([newIndexPath], | |
withRowAnimation: UITableViewRowAnimation.Fade) | |
} | |
} | |
} | |
} | |
func controller(controller: NSFetchedResultsController, | |
didChangeSection sectionInfo: NSFetchedResultsSectionInfo, | |
atIndex sectionIndex: Int, | |
forChangeType type: NSFetchedResultsChangeType) | |
{ | |
switch(type) { | |
case .Insert: | |
presentedTableView()?.insertSections(NSIndexSet(index: sectionIndex), | |
withRowAnimation: UITableViewRowAnimation.Fade) | |
case .Delete: | |
presentedTableView()?.deleteSections(NSIndexSet(index: sectionIndex), | |
withRowAnimation: UITableViewRowAnimation.Fade) | |
default: | |
break | |
} | |
} | |
func controllerDidChangeContent(controller: NSFetchedResultsController) { | |
presentedTableView()?.endUpdates() | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Purpose: separate Persistence and Controllers layers with NSPredicate, NSSortDescriptors and so on. | |
// Less code in ViewControllers - only MUST know info. | |
// | |
// And yes, prefer composition over inheritance. But. | |
// For the UIKit specific purposes this approach MAY be used as easy and pretty small classes code solution. | |
// Model class (for clarity) | |
@objc(Word) | |
class Word: NSManagedObject { | |
@NSManaged var text: String | |
} | |
// USAGE EXAMPLE: TableViewController. All supporting code listed above. | |
// Result: you may change CoreData to FMDB without changing Controllers layer (above). | |
// Less code in ViewControllers - only MUST know info. Readable. | |
class REWordsTableViewController: REFRCTableViewController { | |
override var cellReuseIdentifier : String! { return "WordCellReuseIdentifier" } | |
override var entityName : String! { return NSStringFromClass(Word) } | |
override var sortDescriptors : [NSSortDescriptor]? { return [NSSortDescriptor(key: "word.text", ascending: true)]} | |
var substring : String! { | |
didSet { | |
self.predicate = NSPredicate(format: "text CONTAINS[cd] %@", substring) | |
self.fetchData() | |
self.tableView.reloadData() | |
} | |
} | |
override func viewWillAppear(animated: Bool) { | |
fetchData() | |
super.viewWillAppear(animated) | |
} | |
// Template Method. You MAY override if you want VC specific cell configuration (project wide may be done in parent class) | |
func configureCell(cell : UITableViewCell?, object: AnyObject?) { | |
if let wordCell = cell as? WordCell { | |
if let word = object as? Word { | |
wordCell.label.text = word.text | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment