├── Notes.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Notes ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Notes.xcdatamodeld │ └── Notes.xcdatamodel │ │ └── contents ├── AddNoteViewContorller.swift ├── NoteViewController.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── AppDelegate.swift ├── CoreDataManager.swift └── ViewController.swift ├── .gitignore └── README.md /Notes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Notes/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Notes/Notes.xcdatamodeld/Notes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Notes/AddNoteViewContorller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddNoteViewContorller.swift 3 | // Notes 4 | // 5 | // Created by Bart Jacobs on 08/03/2017. 6 | // Copyright © 2017 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AddNoteViewControllerDelegate { 12 | func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String) 13 | } 14 | 15 | class AddNoteViewController: UIViewController { 16 | 17 | @IBOutlet var titleTextField: UITextField! 18 | 19 | var delegate: AddNoteViewControllerDelegate? 20 | 21 | // MARK: - View Life Cycle 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | } 26 | 27 | // MARK: - Actions 28 | 29 | @IBAction func save(_ sender: Any) { 30 | guard let title = titleTextField.text else { return } 31 | guard let delegate = delegate else { return } 32 | 33 | // Notify Delegate 34 | delegate.controller(self, didAddNoteWithTitle: title) 35 | 36 | // Dismiss View Controller 37 | dismiss(animated: true, completion: nil) 38 | } 39 | 40 | @IBAction func cancel(_ sender: Any) { 41 | dismiss(animated: true, completion: nil) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Notes/NoteViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteViewController.swift 3 | // Notes 4 | // 5 | // Created by Bart Jacobs on 08/03/2017. 6 | // Copyright © 2017 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NoteViewController: UIViewController { 12 | 13 | // MARK: - Properties 14 | 15 | @IBOutlet var titleTextField: UITextField! 16 | @IBOutlet var contentTextView: UITextView! 17 | 18 | // MARK: - 19 | 20 | var note: Note? 21 | 22 | // MARK: - View Life Cycle 23 | 24 | override func viewWillAppear(_ animated: Bool) { 25 | super.viewWillAppear(animated) 26 | 27 | // Update User Interface 28 | titleTextField.text = note?.title 29 | contentTextView.text = note?.content 30 | } 31 | 32 | // MARK: - Actions 33 | 34 | @IBAction func save(_ sender: Any) { 35 | guard let title = titleTextField.text else { return } 36 | guard let content = contentTextView.text else { return } 37 | 38 | // Update Note 39 | note?.title = title 40 | note?.content = content 41 | note?.updatedAt = NSDate() 42 | 43 | // Pop View Controller From Navigation Stack 44 | _ = navigationController?.popViewController(animated: true) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Notes/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Notes/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Notes/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notes 4 | // 5 | // Created by Bart Jacobs on 08/03/2017. 6 | // Copyright © 2017 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode4 / OS X Source projects 3 | # 4 | # NB: if you are storing "built" products, this WILL NOT WORK, 5 | # and you should use a different .gitignore (or none at all) 6 | # This file is for SOURCE projects, where there are many extra 7 | # files that we want to exclude 8 | # 9 | # For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 10 | ######################### 11 | 12 | ##### 13 | # OS X temporary files that should never be committed 14 | 15 | .DS_Store 16 | *.swp 17 | *.lock 18 | profile 19 | 20 | 21 | #### 22 | # Xcode temporary files that should never be committed 23 | # 24 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 25 | 26 | *~.nib 27 | 28 | 29 | #### 30 | # Xcode build files - 31 | # 32 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 33 | 34 | DerivedData/ 35 | 36 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 37 | 38 | build/ 39 | 40 | 41 | ##### 42 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 43 | # 44 | # This is complicated: 45 | # 46 | # SOMETIMES you need to put this file in version control. 47 | # Apple designed it poorly - if you use "custom executables", they are 48 | # saved in this file. 49 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 50 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 51 | 52 | *.pbxuser 53 | *.mode1v3 54 | *.mode2v3 55 | *.perspectivev3 56 | # NB: also, whitelist the default ones, some projects need to use these 57 | !default.pbxuser 58 | !default.mode1v3 59 | !default.mode2v3 60 | !default.perspectivev3 61 | 62 | 63 | #### 64 | # Xcode 4 - semi-personal settings, often included in workspaces 65 | # 66 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them 67 | # 68 | 69 | xcuserdata 70 | 71 | #### 72 | # XCode 4 workspaces - more detailed 73 | # 74 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 75 | # 76 | # Workspace layout is quite spammy. For reference: 77 | # 78 | # (root)/ 79 | # (project-name).xcodeproj/ 80 | # project.pbxproj 81 | # project.xcworkspace/ 82 | # contents.xcworkspacedata 83 | # xcuserdata/ 84 | # (your name)/xcuserdatad/ 85 | # xcuserdata/ 86 | # (your name)/xcuserdatad/ 87 | # 88 | # 89 | # 90 | # Xcode 4 workspaces - SHARED 91 | # 92 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results 93 | # But if you're going to kill personal workspaces, at least keep the shared ones... 94 | # 95 | # 96 | !xcshareddata 97 | 98 | #### 99 | # XCode 4 build-schemes 100 | # 101 | # PRIVATE ones are stored inside xcuserdata 102 | !xcschemes 103 | 104 | #### 105 | # Xcode 4 - Deprecated classes 106 | # 107 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 108 | # 109 | # We're using source-control, so this is a "feature" that we do not want! 110 | 111 | *.moved-aside 112 | 113 | # CocoaPods 114 | /Pods 115 | -------------------------------------------------------------------------------- /Notes/CoreDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataManager.swift 3 | // Lists 4 | // 5 | // Created by Bart Jacobs on 07/03/2017. 6 | // Copyright © 2017 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | final class CoreDataManager { 13 | 14 | // MARK: - Properties 15 | 16 | private let modelName: String 17 | 18 | // MARK: - Initialization 19 | 20 | init(modelName: String) { 21 | self.modelName = modelName 22 | 23 | // Setup Notification Handling 24 | setupNotificationHandling() 25 | } 26 | 27 | // MARK: - Core Data Stack 28 | 29 | private(set) lazy var managedObjectContext: NSManagedObjectContext = { 30 | let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 31 | 32 | managedObjectContext.parent = self.privateManagedObjectContext 33 | 34 | return managedObjectContext 35 | }() 36 | 37 | private lazy var privateManagedObjectContext: NSManagedObjectContext = { 38 | // Initialize Managed Object Context 39 | var managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 40 | 41 | // Configure Managed Object Context 42 | managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator 43 | 44 | return managedObjectContext 45 | }() 46 | 47 | private lazy var managedObjectModel: NSManagedObjectModel = { 48 | guard let modelURL = Bundle.main.url(forResource: self.modelName, withExtension: "momd") else { 49 | fatalError("Unable to Find Data Model") 50 | } 51 | 52 | guard let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else { 53 | fatalError("Unable to Load Data Model") 54 | } 55 | 56 | return managedObjectModel 57 | }() 58 | 59 | private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 60 | let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) 61 | 62 | let fileManager = FileManager.default 63 | let storeName = "\(self.modelName).sqlite" 64 | 65 | let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] 66 | 67 | let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName) 68 | 69 | do { 70 | let options = [ NSInferMappingModelAutomaticallyOption : true, 71 | NSMigratePersistentStoresAutomaticallyOption : true] 72 | 73 | try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, 74 | configurationName: nil, 75 | at: persistentStoreURL, 76 | options: options) 77 | } catch { 78 | fatalError("Unable to Load Persistent Store") 79 | } 80 | 81 | return persistentStoreCoordinator 82 | }() 83 | 84 | // MARK: - Notification Handling 85 | 86 | @objc func saveChanges(_ notification: NSNotification) { 87 | managedObjectContext.perform { 88 | do { 89 | if self.managedObjectContext.hasChanges { 90 | try self.managedObjectContext.save() 91 | } 92 | } catch { 93 | let saveError = error as NSError 94 | print("Unable to Save Changes of Managed Object Context") 95 | print("\(saveError), \(saveError.localizedDescription)") 96 | } 97 | 98 | self.privateManagedObjectContext.perform { 99 | do { 100 | if self.privateManagedObjectContext.hasChanges { 101 | try self.privateManagedObjectContext.save() 102 | } 103 | } catch { 104 | let saveError = error as NSError 105 | print("Unable to Save Changes of Private Managed Object Context") 106 | print("\(saveError), \(saveError.localizedDescription)") 107 | } 108 | } 109 | 110 | } 111 | } 112 | 113 | // MARK: - Helper Methods 114 | 115 | private func setupNotificationHandling() { 116 | let notificationCenter = NotificationCenter.default 117 | notificationCenter.addObserver(self, selector: #selector(CoreDataManager.saveChanges(_:)), name: Notification.Name.UIApplicationWillTerminate, object: nil) 118 | notificationCenter.addObserver(self, selector: #selector(CoreDataManager.saveChanges(_:)), name: Notification.Name.UIApplicationDidEnterBackground, object: nil) 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [Core Data and Concurrency](https://cocoacasts.com/core-data-and-concurrency/) 2 | 3 | #### Author: Bart Jacobs 4 | 5 | Up to now, we've used a single managed object context, which we created in the `CoreDataManager` class. This works fine, but there will be times when one managed object context won't suffice. 6 | 7 | What happens if you access the same managed object context from different threads? What do you expect happens? What happens if you pass a managed object from a background thread to the main thread? Let's start with the basics. 8 | 9 | ## Concurrency Basics 10 | 11 | Before we explore solutions for using Core Data in multithreaded applications, we need to know how Core Data behaves on multiple threads. The documentation is very clear about this. Core Data expects to be run on a single thread. Even though that thread doesn't have to be the main thread, Core Data was not designed to be accessed from different threads. 12 | 13 | > Core Data expects to be run on a single thread. 14 | 15 | The Core Data team at Apple is not naive, though. It knows that a persistence framework needs to be accessible from multiple threads. A single thread, the main thread, may be fine for many applications. More complex applications need a robust, multithreaded persistence framework. 16 | 17 | Before I show you how Core Data can be used across multiple threads, I lay out the basic rules for accessing Core Data in a multithreaded application. 18 | 19 | ### Managed Objects 20 | 21 | `NSManagedObject` instances should never be passed from one thread to another. If you need to pass a managed object from one thread to another, you use a managed object's `objectID` property. 22 | 23 | The `objectID` property is of type `NSManagedObjectID` and uniquely identifies a record in the persistent store. A managed object context knows what to do when you hand it an `NSManagedObjectID` instance. There are three methods you need to know about: 24 | 25 | - `object(with:)` 26 | - `existingObject(with:)` 27 | - `registeredObject(for:)` 28 | 29 | The first method, `object(with:)`, returns a managed object that corresponds to the `NSManagedObjectID` instance. If the managed object context doesn't have a managed object for that object identifier, it asks the persistent store coordinator. This method always returns a managed object. 30 | 31 | Know that `object(with:)` throws an exception if no record can be found for the object identifier it receives. For example, if the application deleted the record corresponding with the object identifier, Core Data is unable to hand your application the corresponding record. The result is an exception. 32 | 33 | The `existingObject(with:)` method behaves in a similar fashion. The main difference is that the method throws an error if it cannot fetch the managed object corresponding to the object identifier. 34 | 35 | The third method, `registeredObject(for:)`, only returns a managed object if the record you're asking for is already registered with the managed object context. In other words, the return value is of type `NSManagedObject?`. The managed object context doesn't fetch the corresponding record from the persistent store if it cannot find it. 36 | 37 | The object identifier of a record is similar, but not identical, to the primary key of a database record. It uniquely identifies the record and enables your application to fetch a particular record regardless of what thread the operation is performed on. 38 | 39 | ```swift 40 | let objectID = managedObject.objectID 41 | 42 | DispatchQueue.main.async { 43 | let managedObject = managedObjectContext?.object(with: objectID) 44 | ... 45 | } 46 | ``` 47 | 48 | In the example, we ask a managed object context for the managed object that corresponds with `objectID`, an `NSManagedObjectID` instance. The managed object context first looks if a managed object with a corresponding object identifier is registered in the managed object context. If there isn't, the managed object is fetched or returned as a fault. 49 | 50 | It's important to understand that a managed object context always expects to find a record if you give it an `NSManagedObjectID` instance. That is why `object(with:)` returns an object of type `NSManagedObject`, not `NSManagedObject?`. 51 | 52 | ### Managed Object Context 53 | 54 | Creating an `NSManagedObjectContext` instance is a cheap operation. You should never share managed object contexts between threads. This is a hard rule you shouldn't break. The `NSManagedObjectContext` class isn't thread safe. Plain and simple. 55 | 56 | > You should never share managed object contexts between threads. This is a hard rule you shouldn't break. 57 | 58 | ### Persistent Store Coordinator 59 | 60 | Even though the `NSPersistentStoreCoordinator` class isn't thread safe either, the class knows how to lock itself if multiple managed object contexts request access, even if these managed object contexts live and operate on different threads. 61 | 62 | It's fine to use one persistent store coordinator, which is accessed by multiple managed object contexts from different threads. This makes Core Data concurrency a little bit easier ... a little bit. 63 | 64 | **Read this article on [Cocoacasts](https://cocoacasts.com/core-data-and-concurrency/)**. 65 | -------------------------------------------------------------------------------- /Notes/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Notes 4 | // 5 | // Created by Bart Jacobs on 08/03/2017. 6 | // Copyright © 2017 Cocoacasts. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | class ViewController: UIViewController { 13 | 14 | // MARK: - Properties 15 | 16 | @IBOutlet var tableView: UITableView! 17 | 18 | // MARK: - 19 | 20 | fileprivate let coreDataManager = CoreDataManager(modelName: "Notes") 21 | 22 | // MARK: - 23 | 24 | fileprivate lazy var fetchedResultsController: NSFetchedResultsController = { 25 | // Initialize Fetch Request 26 | let fetchRequest: NSFetchRequest = Note.fetchRequest() 27 | 28 | // Add Sort Descriptors 29 | let sortDescriptor = NSSortDescriptor(key: "updatedAt", ascending: false) 30 | fetchRequest.sortDescriptors = [sortDescriptor] 31 | 32 | // Initialize Fetched Results Controller 33 | let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil) 34 | 35 | // Configure Fetched Results Controller 36 | fetchedResultsController.delegate = self 37 | 38 | return fetchedResultsController 39 | }() 40 | 41 | // MARK: - View Life Cycle 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | do { 47 | try fetchedResultsController.performFetch() 48 | } catch { 49 | let fetchError = error as NSError 50 | print("Unable to Save Note") 51 | print("\(fetchError), \(fetchError.localizedDescription)") 52 | } 53 | } 54 | 55 | // MARK: - Navigation 56 | 57 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 58 | guard let identifier = segue.identifier else { return } 59 | 60 | switch identifier { 61 | case "SegueAddNoteViewController": 62 | guard let navigationController = segue.destination as? UINavigationController else { return } 63 | guard let viewController = navigationController.viewControllers.first as? AddNoteViewController else { return } 64 | 65 | // Configure View Controller 66 | viewController.delegate = self 67 | case "SegueNoteViewController": 68 | guard let indexPath = tableView.indexPathForSelectedRow else { return } 69 | guard let viewController = segue.destination as? NoteViewController else { return } 70 | 71 | // Fetch Note 72 | let note = fetchedResultsController.object(at: indexPath) 73 | 74 | // Configure View Controller 75 | viewController.note = note 76 | default: 77 | print("Unknown Segue") 78 | } 79 | } 80 | 81 | } 82 | 83 | extension ViewController: NSFetchedResultsControllerDelegate { 84 | 85 | func controllerWillChangeContent(_ controller: NSFetchedResultsController) { 86 | tableView.beginUpdates() 87 | } 88 | 89 | func controllerDidChangeContent(_ controller: NSFetchedResultsController) { 90 | tableView.endUpdates() 91 | } 92 | 93 | func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { 94 | switch (type) { 95 | case .insert: 96 | if let indexPath = newIndexPath { 97 | tableView.insertRows(at: [indexPath], with: .fade) 98 | } 99 | break; 100 | case .delete: 101 | if let indexPath = indexPath { 102 | tableView.deleteRows(at: [indexPath], with: .fade) 103 | } 104 | break; 105 | case .update: 106 | if let indexPath = indexPath, let cell = tableView.cellForRow(at: indexPath) { 107 | configureCell(cell, at: indexPath) 108 | } 109 | break; 110 | case .move: 111 | if let indexPath = indexPath { 112 | tableView.deleteRows(at: [indexPath], with: .fade) 113 | } 114 | 115 | if let newIndexPath = newIndexPath { 116 | tableView.insertRows(at: [newIndexPath], with: .fade) 117 | } 118 | break; 119 | } 120 | } 121 | 122 | } 123 | 124 | extension ViewController: UITableViewDataSource { 125 | 126 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 127 | guard let sections = fetchedResultsController.sections else { 128 | return 0 129 | } 130 | 131 | let sectionInfo = sections[section] 132 | return sectionInfo.numberOfObjects 133 | } 134 | 135 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 136 | let cell = tableView.dequeueReusableCell(withIdentifier: "NoteCell", for: indexPath) 137 | 138 | // Configure Cell 139 | configureCell(cell, at: indexPath) 140 | 141 | return cell 142 | } 143 | 144 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 145 | guard editingStyle == .delete else { return } 146 | 147 | // Fetch Note 148 | let note = fetchedResultsController.object(at: indexPath) 149 | 150 | // Delete Note 151 | fetchedResultsController.managedObjectContext.delete(note) 152 | } 153 | 154 | func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) { 155 | // Fetch Note 156 | let note = fetchedResultsController.object(at: indexPath) 157 | 158 | // Configure Cell 159 | cell.textLabel?.text = note.title 160 | cell.detailTextLabel?.text = note.content 161 | } 162 | 163 | } 164 | 165 | extension ViewController: UITableViewDelegate { 166 | 167 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 168 | tableView.deselectRow(at: indexPath, animated: true) 169 | } 170 | 171 | } 172 | 173 | extension ViewController: AddNoteViewControllerDelegate { 174 | 175 | func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String) { 176 | // Create Note 177 | let note = Note(context: coreDataManager.managedObjectContext) 178 | 179 | // Populate Note 180 | note.content = "" 181 | note.title = title 182 | note.updatedAt = NSDate() 183 | note.createdAt = NSDate() 184 | 185 | do { 186 | try note.managedObjectContext?.save() 187 | } catch { 188 | let saveError = error as NSError 189 | print("Unable to Save Note") 190 | print("\(saveError), \(saveError.localizedDescription)") 191 | } 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /Notes.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CC1046EF1E6FD8780062BF32 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1046EE1E6FD8780062BF32 /* AppDelegate.swift */; }; 11 | CC1046F11E6FD8780062BF32 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1046F01E6FD8780062BF32 /* ViewController.swift */; }; 12 | CC1046F41E6FD8790062BF32 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC1046F21E6FD8790062BF32 /* Main.storyboard */; }; 13 | CC1046F61E6FD8790062BF32 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC1046F51E6FD8790062BF32 /* Assets.xcassets */; }; 14 | CC1046F91E6FD8790062BF32 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC1046F71E6FD8790062BF32 /* LaunchScreen.storyboard */; }; 15 | CC1047021E6FD9120062BF32 /* Notes.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CC1047001E6FD9120062BF32 /* Notes.xcdatamodeld */; }; 16 | CC1047041E6FDA670062BF32 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1047031E6FDA670062BF32 /* CoreDataManager.swift */; }; 17 | CC1047061E6FDAFD0062BF32 /* AddNoteViewContorller.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1047051E6FDAFD0062BF32 /* AddNoteViewContorller.swift */; }; 18 | CCE3AB3A1E6FF7FD003CC1B8 /* NoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE3AB391E6FF7FD003CC1B8 /* NoteViewController.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | CC1046EB1E6FD8780062BF32 /* Notes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | CC1046EE1E6FD8780062BF32 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | CC1046F01E6FD8780062BF32 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | CC1046F31E6FD8790062BF32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | CC1046F51E6FD8790062BF32 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | CC1046F81E6FD8790062BF32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | CC1046FA1E6FD8790062BF32 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | CC1047011E6FD9120062BF32 /* Notes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Notes.xcdatamodel; sourceTree = ""; }; 30 | CC1047031E6FDA670062BF32 /* CoreDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; 31 | CC1047051E6FDAFD0062BF32 /* AddNoteViewContorller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddNoteViewContorller.swift; sourceTree = ""; }; 32 | CCE3AB391E6FF7FD003CC1B8 /* NoteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteViewController.swift; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | CC1046E81E6FD8780062BF32 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | CC1046E21E6FD8780062BF32 = { 47 | isa = PBXGroup; 48 | children = ( 49 | CC1046ED1E6FD8780062BF32 /* Notes */, 50 | CC1046EC1E6FD8780062BF32 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | CC1046EC1E6FD8780062BF32 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | CC1046EB1E6FD8780062BF32 /* Notes.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | CC1046ED1E6FD8780062BF32 /* Notes */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | CC1046EE1E6FD8780062BF32 /* AppDelegate.swift */, 66 | CC1046F01E6FD8780062BF32 /* ViewController.swift */, 67 | CCE3AB391E6FF7FD003CC1B8 /* NoteViewController.swift */, 68 | CC1047051E6FDAFD0062BF32 /* AddNoteViewContorller.swift */, 69 | CC1047031E6FDA670062BF32 /* CoreDataManager.swift */, 70 | CC1046F21E6FD8790062BF32 /* Main.storyboard */, 71 | CC1046F51E6FD8790062BF32 /* Assets.xcassets */, 72 | CC1046F71E6FD8790062BF32 /* LaunchScreen.storyboard */, 73 | CC1046FA1E6FD8790062BF32 /* Info.plist */, 74 | CC1047001E6FD9120062BF32 /* Notes.xcdatamodeld */, 75 | ); 76 | path = Notes; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | CC1046EA1E6FD8780062BF32 /* Notes */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = CC1046FD1E6FD8790062BF32 /* Build configuration list for PBXNativeTarget "Notes" */; 85 | buildPhases = ( 86 | CC1046E71E6FD8780062BF32 /* Sources */, 87 | CC1046E81E6FD8780062BF32 /* Frameworks */, 88 | CC1046E91E6FD8780062BF32 /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = Notes; 95 | productName = Notes; 96 | productReference = CC1046EB1E6FD8780062BF32 /* Notes.app */; 97 | productType = "com.apple.product-type.application"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | CC1046E31E6FD8780062BF32 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastSwiftUpdateCheck = 0820; 106 | LastUpgradeCheck = 0820; 107 | ORGANIZATIONNAME = Cocoacasts; 108 | TargetAttributes = { 109 | CC1046EA1E6FD8780062BF32 = { 110 | CreatedOnToolsVersion = 8.2.1; 111 | DevelopmentTeam = 2493UGBPKJ; 112 | ProvisioningStyle = Automatic; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = CC1046E61E6FD8780062BF32 /* Build configuration list for PBXProject "Notes" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = English; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = CC1046E21E6FD8780062BF32; 125 | productRefGroup = CC1046EC1E6FD8780062BF32 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | CC1046EA1E6FD8780062BF32 /* Notes */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | CC1046E91E6FD8780062BF32 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | CC1046F91E6FD8790062BF32 /* LaunchScreen.storyboard in Resources */, 140 | CC1046F61E6FD8790062BF32 /* Assets.xcassets in Resources */, 141 | CC1046F41E6FD8790062BF32 /* Main.storyboard in Resources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXResourcesBuildPhase section */ 146 | 147 | /* Begin PBXSourcesBuildPhase section */ 148 | CC1046E71E6FD8780062BF32 /* Sources */ = { 149 | isa = PBXSourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | CC1047061E6FDAFD0062BF32 /* AddNoteViewContorller.swift in Sources */, 153 | CC1046F11E6FD8780062BF32 /* ViewController.swift in Sources */, 154 | CC1047021E6FD9120062BF32 /* Notes.xcdatamodeld in Sources */, 155 | CC1047041E6FDA670062BF32 /* CoreDataManager.swift in Sources */, 156 | CCE3AB3A1E6FF7FD003CC1B8 /* NoteViewController.swift in Sources */, 157 | CC1046EF1E6FD8780062BF32 /* AppDelegate.swift in Sources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXSourcesBuildPhase section */ 162 | 163 | /* Begin PBXVariantGroup section */ 164 | CC1046F21E6FD8790062BF32 /* Main.storyboard */ = { 165 | isa = PBXVariantGroup; 166 | children = ( 167 | CC1046F31E6FD8790062BF32 /* Base */, 168 | ); 169 | name = Main.storyboard; 170 | sourceTree = ""; 171 | }; 172 | CC1046F71E6FD8790062BF32 /* LaunchScreen.storyboard */ = { 173 | isa = PBXVariantGroup; 174 | children = ( 175 | CC1046F81E6FD8790062BF32 /* Base */, 176 | ); 177 | name = LaunchScreen.storyboard; 178 | sourceTree = ""; 179 | }; 180 | /* End PBXVariantGroup section */ 181 | 182 | /* Begin XCBuildConfiguration section */ 183 | CC1046FB1E6FD8790062BF32 /* Debug */ = { 184 | isa = XCBuildConfiguration; 185 | buildSettings = { 186 | ALWAYS_SEARCH_USER_PATHS = NO; 187 | CLANG_ANALYZER_NONNULL = YES; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_CONSTANT_CONVERSION = YES; 194 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 195 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 196 | CLANG_WARN_EMPTY_BODY = YES; 197 | CLANG_WARN_ENUM_CONVERSION = YES; 198 | CLANG_WARN_INFINITE_RECURSION = YES; 199 | CLANG_WARN_INT_CONVERSION = YES; 200 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 201 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 205 | COPY_PHASE_STRIP = NO; 206 | DEBUG_INFORMATION_FORMAT = dwarf; 207 | ENABLE_STRICT_OBJC_MSGSEND = YES; 208 | ENABLE_TESTABILITY = YES; 209 | GCC_C_LANGUAGE_STANDARD = gnu99; 210 | GCC_DYNAMIC_NO_PIC = NO; 211 | GCC_NO_COMMON_BLOCKS = YES; 212 | GCC_OPTIMIZATION_LEVEL = 0; 213 | GCC_PREPROCESSOR_DEFINITIONS = ( 214 | "DEBUG=1", 215 | "$(inherited)", 216 | ); 217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 219 | GCC_WARN_UNDECLARED_SELECTOR = YES; 220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 221 | GCC_WARN_UNUSED_FUNCTION = YES; 222 | GCC_WARN_UNUSED_VARIABLE = YES; 223 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 224 | MTL_ENABLE_DEBUG_INFO = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = iphoneos; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | CC1046FC1E6FD8790062BF32 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 238 | CLANG_CXX_LIBRARY = "libc++"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_WARN_BOOL_CONVERSION = YES; 242 | CLANG_WARN_CONSTANT_CONVERSION = YES; 243 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 244 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 245 | CLANG_WARN_EMPTY_BODY = YES; 246 | CLANG_WARN_ENUM_CONVERSION = YES; 247 | CLANG_WARN_INFINITE_RECURSION = YES; 248 | CLANG_WARN_INT_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNREACHABLE_CODE = YES; 252 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 253 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 254 | COPY_PHASE_STRIP = NO; 255 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 256 | ENABLE_NS_ASSERTIONS = NO; 257 | ENABLE_STRICT_OBJC_MSGSEND = YES; 258 | GCC_C_LANGUAGE_STANDARD = gnu99; 259 | GCC_NO_COMMON_BLOCKS = YES; 260 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 261 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 262 | GCC_WARN_UNDECLARED_SELECTOR = YES; 263 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 264 | GCC_WARN_UNUSED_FUNCTION = YES; 265 | GCC_WARN_UNUSED_VARIABLE = YES; 266 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 267 | MTL_ENABLE_DEBUG_INFO = NO; 268 | SDKROOT = iphoneos; 269 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 270 | VALIDATE_PRODUCT = YES; 271 | }; 272 | name = Release; 273 | }; 274 | CC1046FE1E6FD8790062BF32 /* Debug */ = { 275 | isa = XCBuildConfiguration; 276 | buildSettings = { 277 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 278 | DEVELOPMENT_TEAM = 2493UGBPKJ; 279 | INFOPLIST_FILE = Notes/Info.plist; 280 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 281 | PRODUCT_BUNDLE_IDENTIFIER = com.cocoacasts.Notes; 282 | PRODUCT_NAME = "$(TARGET_NAME)"; 283 | SWIFT_VERSION = 3.0; 284 | }; 285 | name = Debug; 286 | }; 287 | CC1046FF1E6FD8790062BF32 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | DEVELOPMENT_TEAM = 2493UGBPKJ; 292 | INFOPLIST_FILE = Notes/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 294 | PRODUCT_BUNDLE_IDENTIFIER = com.cocoacasts.Notes; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_VERSION = 3.0; 297 | }; 298 | name = Release; 299 | }; 300 | /* End XCBuildConfiguration section */ 301 | 302 | /* Begin XCConfigurationList section */ 303 | CC1046E61E6FD8780062BF32 /* Build configuration list for PBXProject "Notes" */ = { 304 | isa = XCConfigurationList; 305 | buildConfigurations = ( 306 | CC1046FB1E6FD8790062BF32 /* Debug */, 307 | CC1046FC1E6FD8790062BF32 /* Release */, 308 | ); 309 | defaultConfigurationIsVisible = 0; 310 | defaultConfigurationName = Release; 311 | }; 312 | CC1046FD1E6FD8790062BF32 /* Build configuration list for PBXNativeTarget "Notes" */ = { 313 | isa = XCConfigurationList; 314 | buildConfigurations = ( 315 | CC1046FE1E6FD8790062BF32 /* Debug */, 316 | CC1046FF1E6FD8790062BF32 /* Release */, 317 | ); 318 | defaultConfigurationIsVisible = 0; 319 | defaultConfigurationName = Release; 320 | }; 321 | /* End XCConfigurationList section */ 322 | 323 | /* Begin XCVersionGroup section */ 324 | CC1047001E6FD9120062BF32 /* Notes.xcdatamodeld */ = { 325 | isa = XCVersionGroup; 326 | children = ( 327 | CC1047011E6FD9120062BF32 /* Notes.xcdatamodel */, 328 | ); 329 | currentVersion = CC1047011E6FD9120062BF32 /* Notes.xcdatamodel */; 330 | path = Notes.xcdatamodeld; 331 | sourceTree = ""; 332 | versionGroupType = wrapper.xcdatamodel; 333 | }; 334 | /* End XCVersionGroup section */ 335 | }; 336 | rootObject = CC1046E31E6FD8780062BF32 /* Project object */; 337 | } 338 | -------------------------------------------------------------------------------- /Notes/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 42 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | --------------------------------------------------------------------------------