Skip to content

Instantly share code, notes, and snippets.

@rinat-enikeev
Last active November 12, 2015 11:08
Show Gist options
  • Save rinat-enikeev/0e4440d480fa4db2e386 to your computer and use it in GitHub Desktop.
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.
// 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
}
// 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()
}
// 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()
}
}
// 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