├── 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 |
--------------------------------------------------------------------------------