├── .github ├── ISSUE_TEMPLATE.md └── contributing.md ├── .gitignore ├── .swift-version ├── DBClient.podspec ├── DBClient ├── Core │ ├── DBClient.swift │ ├── DatabaseError.swift │ ├── FetchRequest.swift │ └── RequestObservable.swift ├── CoreData │ ├── CoreDataChange.swift │ ├── CoreDataDBClient.swift │ ├── CoreDataMigrationManager.swift │ ├── CoreDataObservable.swift │ ├── MigrationManager.swift │ └── NSManagedObjectContext+Extension.swift └── Realm │ ├── RealmDBClient.swift │ └── RealmObservable.swift ├── Example ├── DBClientTests │ ├── DBClientTest.swift │ ├── Info.plist │ ├── Interface │ │ ├── CoreData │ │ │ ├── CoreDataCreateTests.swift │ │ │ ├── CoreDataDeleteTests.swift │ │ │ ├── CoreDataExecuteTests.swift │ │ │ ├── CoreDataFetchTests.swift │ │ │ ├── CoreDataObservableTests.swift │ │ │ ├── CoreDataUpdateTests.swift │ │ │ ├── CoreDataUpsertTests.swift │ │ │ └── DBClientCoreDataTest.swift │ │ └── Realm │ │ │ ├── DBClientRealmTest.swift │ │ │ ├── RealmCreateTests.swift │ │ │ ├── RealmDeleteTests.swift │ │ │ ├── RealmExecuteTests.swift │ │ │ ├── RealmFetchTests.swift │ │ │ ├── RealmObservableTests.swift │ │ │ ├── RealmUpdateTests.swift │ │ │ └── RealmUpsertTests.swift │ ├── User+Comparable.swift │ └── User+Equtable.swift ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── DBClientInjector.swift │ ├── DetailViewController.swift │ ├── Info.plist │ ├── MasterViewController.swift │ └── Models │ │ ├── CoreData │ │ ├── ManagedUser+CoreDataClass.swift │ │ ├── ManagedUser+CoreDataProperties.swift │ │ ├── User+CoreData.swift │ │ └── Users.xcdatamodeld │ │ │ └── Users.xcdatamodel │ │ │ └── contents │ │ ├── Realm │ │ ├── ObjectUser.swift │ │ └── User+Realm.swift │ │ └── User.swift ├── Podfile └── Podfile.lock ├── LICENSE └── README.md /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Report 2 | 3 | > The more information you provide, the faster we can help you. 4 | 5 | ⚠️ Select what you want - **a feature request** or **report a bug**. Please remove the section you aren't interested in. 6 | 7 | ## A feature request 8 | 9 | ### What do you want to add? 10 | 11 | > Please describe what you want to add to the component. 12 | 13 | ### How should it look like? 14 | 15 | > Please add images. 16 | 17 | ## Report a bug 18 | 19 | ### What did you do? 20 | 21 | > Please replace this with what you did. 22 | 23 | ### What did you expect to happen? 24 | 25 | > Please replace this with what you expected to happen. 26 | 27 | ### What happened instead? 28 | 29 | > Please replace this with what happened instead. 30 | 31 | ### Your Environment 32 | 33 | - Version of the component: _insert here_ 34 | - Swift version: _insert here_ 35 | - iOS version: _insert here_ 36 | - Device: _insert here_ 37 | - Xcode version: _insert here_ 38 | - If you use Cocoapods: _run `pod env | pbcopy` and insert here_ 39 | - If you use Carthage: _run `carthage version | pbcopy` and insert here_ 40 | 41 | ### Project that demonstrates the bug 42 | 43 | > Please add a link to a project we can download that reproduces the bug. 44 | 45 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to DBClient 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching under [Issues](https://github.com/Yalantis/DBClient/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Yalantis/DBClient/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **example project** demonstrating the expected behavior that is not occurring. 8 | 9 | * Fill appropriate section in issue template and remove the section you aren't interested in. 10 | 11 | #### **Did you write a patch that fixes a bug?** 12 | 13 | * Open a new GitHub pull request with the patch. 14 | 15 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | * Ensure the PR doesn't extend the number of existing issues. 18 | 19 | #### **Did you fix whitespace, format code, or make a purely cosmetic patch?** 20 | 21 | * Changes that are **cosmetic** in nature and **do not add anything substantial** to the stability or functionality of DBClient will generally **not be accepted**. 22 | 23 | #### **Did you write patch that extends functionality?** 24 | 25 | * Ensure the functionality you trying to add needed not only for your case. 26 | 27 | #### Each issue will be labeled by it's `type`, `priority` and `status`. 28 | 29 | **Issue types:** 30 | * Bug 31 | * Enhancement 32 | 33 | **These are the available priority labels:** 34 | * Critical 35 | * High 36 | * Medium 37 | * Low 38 | 39 | **Status label will be assigned to your issue to keep the issue tracker easy to follow:** 40 | * Queued (will be reviewed soon) 41 | * Reviewed (assignee has read it) 42 | * Pending (will work on it soon) 43 | * Work in progress (is working on it now) 44 | * On hold 45 | * Invalid (if bug it's not reproducible) 46 | * Need feedback (signal to get people to read and comment or provide help) 47 | 48 | #### **Coding Style** 49 | 50 | * Most importantly, match the existing code style as much as possible. 51 | 52 | #### **Do you have a question?** 53 | 54 | For any usage questions that are not specific to the project itself, please ask on [Stack Overflow](https://stackoverflow.com/). By doing so, you'll be more likely to quickly solve your problem, and you'll allow anyone else with the same question to find the answer. This also allows maintainers to focus on improving the project for others. 55 | 56 | ## Thank you! 57 | 58 | #### [![Yalantis](https://raw.githubusercontent.com/Yalantis/PullToMakeSoup/master/PullToMakeSoupDemo/Resouces/badge_dark.png)](https://Yalantis.com/?utm_source=github) 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Build generated 3 | build/ 4 | DerivedData/ 5 | 6 | # Various settings 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata/ 16 | 17 | # Other 18 | *.moved-aside 19 | *.xcuserstate 20 | 21 | # Obj-C/Swift specific 22 | *.hmap 23 | *.ipa 24 | *.dSYM.zip 25 | *.dSYM 26 | 27 | # Playgrounds 28 | timeline.xctimeline 29 | playground.xcworkspace 30 | 31 | # CocoaPods 32 | Pods/ 33 | 34 | # Carthage 35 | Carthage/Build 36 | Carthage/Checkouts 37 | 38 | # fastlane 39 | fastlane/report.xml 40 | 41 | # development 42 | 43 | logs/* 44 | 45 | # AppCode 46 | 47 | .idea/ 48 | 49 | # fastlane 50 | # 51 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 52 | # screenshots whenever they are needed. 53 | # For more information about the recommended setup visit: 54 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 55 | 56 | fastlane/report.xml 57 | fastlane/README.md 58 | fastlane/Preview.html 59 | fastlane/screenshots/**/*.png 60 | fastlane/test_output 61 | 62 | ## GitLab CI 63 | .bundle/ 64 | vendor/ -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /DBClient.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DBClient' 3 | s.version = '1.4.2' 4 | s.requires_arc = true 5 | s.summary = 'CoreData & Realm wrapper written on Swift' 6 | s.homepage = '' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'Yalantis' => 'mail@yalantis.com' } 9 | s.source = { :git => 'https://github.com/Yalantis/DBClient.git', :tag => s.version } 10 | s.social_media_url = 'https://yalantis.com/' 11 | s.homepage = 'https://yalantis.com/' 12 | s.ios.deployment_target = '10.0' 13 | s.dependency 'YALResult', '1.4' 14 | s.default_subspec = 'Core' 15 | 16 | s.subspec 'Core' do |spec| 17 | spec.source_files = ['DBClient/Core/*.swift'] 18 | end 19 | 20 | s.subspec 'CoreData' do |spec| 21 | spec.dependency 'DBClient/Core' 22 | spec.source_files = ['DBClient/CoreData/*.swift'] 23 | spec.frameworks = ['CoreData'] 24 | end 25 | 26 | s.subspec 'Realm' do |spec| 27 | spec.dependency 'DBClient/Core' 28 | spec.source_files = ['DBClient/Realm/*.swift'] 29 | spec.dependency 'RealmSwift', '~> 3.15.0' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /DBClient/Core/DBClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBClient.swift 3 | // DBClient 4 | // 5 | // Created by Yury Grinenko on 03.11.16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import YALResult 11 | 12 | public typealias Result = YALResult 13 | 14 | public enum DBClientError: Error { 15 | 16 | case missingPrimaryKey, missingData 17 | } 18 | 19 | /// Protocol for transaction restrictions in `DBClient`. 20 | /// Used for transactions of all type. 21 | public protocol Stored { 22 | 23 | /// Primary key for an object. 24 | static var primaryKeyName: String? { get } 25 | 26 | /// Primary value for an instance 27 | var valueOfPrimaryKey: CVarArg? { get } 28 | } 29 | 30 | /// Describes abstract database transactions, common for all engines. 31 | public protocol DBClient { 32 | 33 | /// Executes given request and calls completion result wrapped in `Result`. 34 | /// 35 | /// - Parameters: 36 | /// - request: request to execute 37 | /// - completion: `Result` with array of objects or error in case of failude. 38 | func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) 39 | 40 | /// Creates observable request from given `FetchRequest`. 41 | /// 42 | /// - Parameter request: fetch request to be observed 43 | /// - Returns: observable of for given request. 44 | func observable(for request: FetchRequest) -> RequestObservable 45 | 46 | /// Inserts objects to database. 47 | /// 48 | /// - Parameters: 49 | /// - objects: list of objects to be inserted 50 | /// - completion: `Result` with inserted objects or appropriate error in case of failure. 51 | func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) 52 | 53 | /// Updates changed performed with objects to database. 54 | /// 55 | /// - Parameters: 56 | /// - objects: list of objects to be updated 57 | /// - completion: `Result` with updated objects or appropriate error in case of failure. 58 | func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) 59 | 60 | /// Deletes objects from database. 61 | /// 62 | /// - Parameters: 63 | /// - objects: list of objects to be deleted 64 | /// - completion: `Result` with appropriate error in case of failure. 65 | func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) 66 | 67 | /// Removes all object of a given type from database. 68 | func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) 69 | 70 | /// Iterates through given objects and updates existing in database instances or creates them 71 | /// 72 | /// - Parameters: 73 | /// - objects: objects to be worked with 74 | /// - completion: `Result` with inserted and updated instances. 75 | func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) 76 | 77 | /// Synchronously executes given request and calls completion result wrapped in `Result`. 78 | /// 79 | /// - Parameters: 80 | /// - request: request to execute 81 | /// - Returns: `Result` with array of objects or error in case of failude. 82 | func execute(_ request: FetchRequest) -> Result<[T]> 83 | 84 | /// Synchronously inserts objects to database. 85 | /// 86 | /// - Parameters: 87 | /// - objects: list of objects to be inserted 88 | /// - Returns: `Result` with inserted objects or appropriate error in case of failure. 89 | @discardableResult 90 | func insert(_ objects: [T]) -> Result<[T]> 91 | 92 | /// Synchronously updates changed performed with objects to database. 93 | /// 94 | /// - Parameters: 95 | /// - objects: list of objects to be updated 96 | /// - Returns: `Result` with updated objects or appropriate error in case of failure. 97 | @discardableResult 98 | func update(_ objects: [T]) -> Result<[T]> 99 | 100 | /// Synchronously deletes objects from database. 101 | /// 102 | /// - Parameters: 103 | /// - objects: list of objects to be deleted 104 | /// - Returns: `Result` with appropriate error in case of failure. 105 | @discardableResult 106 | func delete(_ objects: [T]) -> Result 107 | 108 | /// Synchronously iterates through given objects and updates existing in database instances or creates them 109 | /// 110 | /// - Parameters: 111 | /// - objects: objects to be worked with 112 | /// - Returns: `Result` with inserted and updated instances. 113 | @discardableResult 114 | func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> 115 | } 116 | 117 | public extension DBClient { 118 | 119 | /// Fetch all entities from database 120 | /// 121 | /// - Parameter completion: `Result` with array of objects 122 | func findAll(completion: @escaping (Result<[T]>) -> Void) { 123 | execute(FetchRequest(), completion: completion) 124 | } 125 | 126 | /// Synchronously fetch all entities from database 127 | /// 128 | /// - Returns: `Result` with array of objects 129 | func findAll() -> Result<[T]> { 130 | return execute(FetchRequest()) 131 | } 132 | 133 | /// Finds first element with given value as primary. 134 | /// If no primary key specified for given type, or object with such value doesn't exist returns nil. 135 | /// 136 | /// - Parameters: 137 | /// - type: type of object to search for 138 | /// - primaryValue: the value of primary key field to search for 139 | /// - predicate: predicate for request 140 | /// - completion: `Result` with found object or nil 141 | func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil, completion: @escaping (Result) -> Void) { 142 | guard let primaryKey = type.primaryKeyName else { 143 | completion(.failure(DBClientError.missingPrimaryKey)) 144 | return 145 | } 146 | 147 | let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) 148 | let fetchPredicate: NSPredicate 149 | if let predicate = predicate { 150 | fetchPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [primaryKeyPredicate, predicate]) 151 | } else { 152 | fetchPredicate = primaryKeyPredicate 153 | } 154 | let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) 155 | 156 | execute(request) { result in 157 | completion(result.map({ $0.first })) 158 | } 159 | } 160 | 161 | /// Synchronously finds first element with given value as primary. 162 | /// If no primary key specified for given type, or object with such value doesn't exist returns nil. 163 | /// 164 | /// - Parameters: 165 | /// - type: type of object to search for 166 | /// - primaryValue: the value of primary key field to search for 167 | /// - predicate: predicate for request 168 | /// - Returns: `Result` with found object or nil 169 | func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil) -> Result { 170 | guard let primaryKey = type.primaryKeyName else { 171 | return .failure(DBClientError.missingPrimaryKey) 172 | } 173 | 174 | let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) 175 | let fetchPredicate: NSPredicate 176 | if let predicate = predicate { 177 | fetchPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [primaryKeyPredicate, predicate]) 178 | } else { 179 | fetchPredicate = primaryKeyPredicate 180 | } 181 | let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) 182 | 183 | let result = execute(request) 184 | return result.map({ $0.first }) 185 | } 186 | 187 | /// Inserts object to database. 188 | /// 189 | /// - Parameters: 190 | /// - object: object to be inserted 191 | /// - completion: `Result` with inserted object or appropriate error in case of failure. 192 | func insert(_ object: T, completion: @escaping (Result) -> Void) { 193 | insert([object], completion: { completion($0.next(self.convertArrayTaskToSingleObject)) }) 194 | } 195 | 196 | @discardableResult 197 | func insert(_ object: T) -> Result { 198 | return insert([object]).next(convertArrayTaskToSingleObject) 199 | } 200 | 201 | /// Updates changed performed with object to database. 202 | /// 203 | /// - Parameters: 204 | /// - object: object to be updated 205 | /// - completion: `Result` with updated object or appropriate error in case of failure. 206 | func update(_ object: T, completion: @escaping (Result) -> Void) { 207 | update([object], completion: { completion($0.next(self.convertArrayTaskToSingleObject)) }) 208 | } 209 | 210 | @discardableResult 211 | func update(_ object: T) -> Result { 212 | return update([object]).next(convertArrayTaskToSingleObject) 213 | } 214 | 215 | /// Deletes object from database. 216 | /// 217 | /// - Parameters: 218 | /// - object: object to be deleted 219 | /// - completion: `Result` with appropriate error in case of failure. 220 | func delete(_ object: T, completion: @escaping (Result<()>) -> Void) { 221 | delete([object], completion: completion) 222 | } 223 | 224 | @discardableResult 225 | func delete(_ object: T) -> Result { 226 | return delete([object]) 227 | } 228 | 229 | /// Updates existing in database instances or creates them using upsert method defined in your model 230 | /// 231 | /// - Parameters: 232 | /// - object: object to be worked with 233 | /// - completion: `Result` with inserted or updated instance. 234 | func upsert(_ object: T, completion: @escaping (Result<(object: T, isUpdated: Bool)>) -> Void) { 235 | upsert([object]) { result in 236 | completion(result.next { (updated: [T], inserted: [T]) -> Result<(object: T, isUpdated: Bool)> in 237 | guard let object = updated.first ?? inserted.first else { 238 | return Result.failure(DBClientError.missingData) 239 | } 240 | return Result.success((object: object, isUpdated: !updated.isEmpty)) 241 | }) 242 | } 243 | } 244 | 245 | @discardableResult 246 | func upsert(_ object: T) -> Result<(object: T, isUpdated: Bool)> { 247 | return upsert([object]).next { (updated: [T], inserted: [T]) in 248 | guard let object = updated.first ?? inserted.first else { 249 | return Result.failure(DBClientError.missingData) 250 | } 251 | return Result.success((object: object, isUpdated: !updated.isEmpty)) 252 | } 253 | } 254 | 255 | private func convertArrayTaskToSingleObject(_ array: [T]) -> Result { 256 | guard let first = array.first else { 257 | return .failure(DBClientError.missingData) 258 | } 259 | return .success(first) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /DBClient/Core/DatabaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseError.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 19/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Transaction error type. 12 | /// 13 | /// - write: For write transactions. 14 | /// - read: For read transactions. 15 | public enum DatabaseError: Error { 16 | 17 | case write, read 18 | 19 | } 20 | -------------------------------------------------------------------------------- /DBClient/Core/FetchRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchRequest.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 15/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describes a fetch request to get objects from a database. 12 | public struct FetchRequest { 13 | 14 | public let sortDescriptors: [NSSortDescriptor]? 15 | public let predicate: NSPredicate? 16 | public let fetchOffset: Int 17 | public let fetchLimit: Int 18 | 19 | /// - Parameters: 20 | /// - predicate: Predicate for objects filtering; nil by default. 21 | /// - sortDescriptors: Sort descriptors; nil by default. 22 | /// - fetchOffset: Offset of data for request; 0 by default (no offset). 23 | /// - fetchLimit: Amount of objects to be fetched; no limit if zero given; 0 by default. 24 | public init(predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil, fetchOffset: Int = 0, fetchLimit: Int = 0) { 25 | self.predicate = predicate 26 | self.sortDescriptors = sortDescriptors 27 | self.fetchOffset = fetchOffset 28 | self.fetchLimit = fetchLimit 29 | } 30 | } 31 | 32 | // MARK: - Filtering 33 | 34 | public extension FetchRequest { 35 | 36 | /** 37 | Filters all objects with given predicate 38 | 39 | - Returns: New instance 40 | */ 41 | func filtered(with predicate: NSPredicate) -> FetchRequest { 42 | return request(withPredicate: predicate) 43 | } 44 | 45 | /** 46 | Filters all objects to match `key`=`value`. 47 | 48 | - Returns: New intance 49 | */ 50 | func filtered(with key: String, equalTo value: String) -> FetchRequest { 51 | return request(withPredicate: NSPredicate(format: "\(key) == %@", value)) 52 | } 53 | 54 | /** 55 | Removes any object with value of `key` property not from given array of values from request. 56 | 57 | - Returns: New instance 58 | */ 59 | func filtered(with key: String, in value: [String]) -> FetchRequest { 60 | return request(withPredicate: NSPredicate(format: "\(key) IN %@", value)) 61 | } 62 | 63 | /** 64 | Removes any object with value of `key` property from given array of values from request. 65 | 66 | - Returns: New instance 67 | */ 68 | func filtered(with key: String, notIn value: [String]) -> FetchRequest { 69 | return request(withPredicate: NSPredicate(format: "NOT (\(key) IN %@)", value)) 70 | } 71 | } 72 | 73 | // MARK: - Sorting 74 | 75 | public extension FetchRequest { 76 | 77 | func sorted(with sortDescriptor: NSSortDescriptor) -> FetchRequest { 78 | return request(withSortDescriptors: [sortDescriptor]) 79 | } 80 | 81 | func sorted(with sortDescriptors: [NSSortDescriptor]) -> FetchRequest { 82 | return request(withSortDescriptors: sortDescriptors) 83 | } 84 | 85 | func sorted(with key: String?, ascending: Bool, comparator cmptr: @escaping Comparator) -> FetchRequest { 86 | return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, comparator: cmptr)]) 87 | } 88 | 89 | func sorted(with key: String?, ascending: Bool) -> FetchRequest { 90 | return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending)]) 91 | } 92 | 93 | func sorted(with key: String?, ascending: Bool, selector: Selector) -> FetchRequest { 94 | return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, selector: selector)]) 95 | } 96 | } 97 | 98 | // MARK: - Private 99 | 100 | private extension FetchRequest { 101 | 102 | func request(withPredicate predicate: NSPredicate) -> FetchRequest { 103 | return FetchRequest(predicate: predicate, sortDescriptors: sortDescriptors, fetchOffset: fetchOffset, fetchLimit: fetchLimit) 104 | } 105 | 106 | func request(withSortDescriptors sortDescriptors: [NSSortDescriptor]) -> FetchRequest { 107 | return FetchRequest(predicate: predicate, sortDescriptors: sortDescriptors, fetchOffset: fetchOffset, fetchLimit: fetchLimit) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /DBClient/Core/RequestObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestObservable.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 15/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describes changes in database: 12 | /// 13 | /// - initial: initial storred entities; 14 | /// - update: 15 | /// -- objects: all objects in current version of the collection; 16 | /// -- deletions: the indices in the previous version of the collection which were removed from this one; 17 | /// -- insertions: the indices in the new collection and object which was added in this version; 18 | /// -- modifications: the indices of the objects in the new collection and objects inself which was modified in this version; 19 | /// - error: an error occurred during fetch. 20 | public enum ObservableChange { 21 | 22 | public typealias ModelChange = ( 23 | objects: [T], 24 | deletions: [Int], 25 | insertions: [(index: Int, element: T)], 26 | modifications: [(index: Int, element: T)] 27 | ) 28 | 29 | case initial([T]) 30 | case change(ModelChange) 31 | case error(Error) 32 | } 33 | 34 | public class RequestObservable { 35 | 36 | let request: FetchRequest 37 | 38 | init(request: FetchRequest) { 39 | self.request = request 40 | } 41 | 42 | /// Starts observing with a given fetch request. 43 | /// 44 | /// - Parameter closure: gets called once any changes in database are occurred. 45 | /// - Warning: You cannot call the method only if you don't observe it now. 46 | public func observe(_ closure: @escaping (ObservableChange) -> Void) { 47 | assertionFailure("The observe method must be overriden") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DBClient/CoreData/CoreDataChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataChange.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 19/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum CoreDataChange { 12 | 13 | case update(Int, T) 14 | case delete(Int, T) 15 | case insert(Int, T) 16 | 17 | func object() -> T { 18 | switch self { 19 | case .update(_, let object): return object 20 | case .delete(_, let object): return object 21 | case .insert(_, let object): return object 22 | } 23 | } 24 | 25 | func index() -> Int { 26 | switch self { 27 | case .update(let index, _): return index 28 | case .delete(let index, _): return index 29 | case .insert(let index, _): return index 30 | } 31 | } 32 | 33 | var isDeletion: Bool { 34 | switch self { 35 | case .delete(_): return true 36 | default: return false 37 | } 38 | } 39 | 40 | var isUpdate: Bool { 41 | switch self { 42 | case .update(_): return true 43 | default: return false 44 | } 45 | } 46 | 47 | var isInsertion: Bool { 48 | switch self { 49 | case .insert(_): return true 50 | default: return false 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /DBClient/CoreData/CoreDataDBClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataDBClient.swift 3 | // DBClient 4 | // 5 | // Created by Yury Grinenko on 03.11.16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | /// Describes type of model for CoreData database client. 12 | /// Model should conform to CoreDataModelConvertible protocol 13 | /// for ability to be fetched/saved/updated/deleted in CoreData 14 | public protocol CoreDataModelConvertible: Stored { 15 | 16 | /// Returns type of object for model. 17 | static func managedObjectClass() -> NSManagedObject.Type 18 | 19 | /// Executes mapping from `NSManagedObject` instance. 20 | /// 21 | /// - Parameter managedObject: object to be mapped from. 22 | /// - Returns: mapped object. 23 | static func from(_ managedObject: NSManagedObject) -> Stored 24 | 25 | /// Executes backward mapping to `NSManagedObject` from given context 26 | /// 27 | /// - Parameters: 28 | /// - context: context, where object should be created; 29 | /// - existedInstance: if instance was already created it will be passed. 30 | /// - Returns: created instance. 31 | func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject 32 | 33 | /// The name of the entity from ".xcdatamodeld" 34 | static var entityName: String { get } 35 | 36 | /// Decides whether primary value of object equal to given 37 | func isPrimaryValueEqualTo(value: Any) -> Bool 38 | } 39 | 40 | extension NSManagedObject: Stored { 41 | 42 | public static var primaryKeyName: String? { return nil } 43 | 44 | public var valueOfPrimaryKey: CVarArg? { return nil } 45 | } 46 | 47 | public enum MigrationType { 48 | 49 | // provide persistent store constructor with appropriate options 50 | case lightweight 51 | // in case of failure old model file will be removed 52 | case removeOnFailure 53 | // perform progressive migration with delegate 54 | case progressive(MigrationManagerDelegate?) 55 | 56 | public func isLightweight() -> Bool { 57 | switch self { 58 | case .lightweight: 59 | return true 60 | 61 | default: 62 | return false 63 | } 64 | } 65 | } 66 | 67 | /// Implementation of database client for CoreData storage type. 68 | public class CoreDataDBClient { 69 | 70 | private let modelName: String 71 | private let bundle: Bundle 72 | private let migrationType: MigrationType 73 | private let persistentStoreType = NSSQLiteStoreType 74 | 75 | /// Constructor for client 76 | /// 77 | /// - Parameters: 78 | /// - modelName: the name of the model 79 | /// - bundle: the bundle which contains the model; default is main 80 | /// - migrationType: migration type (in case it needed) for model; default is `MigrationType.lightweight` 81 | public init(forModel modelName: String, in bundle: Bundle = Bundle.main, migrationType: MigrationType = .lightweight) { 82 | self.modelName = modelName 83 | self.bundle = bundle 84 | self.migrationType = migrationType 85 | } 86 | 87 | // MARK: - CoreData stack 88 | 89 | private lazy var storeURL: URL = { 90 | let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 91 | let applicationDocumentsDirectory = urls[urls.count - 1] 92 | 93 | return applicationDocumentsDirectory.appendingPathComponent("\(self.modelName).sqlite") 94 | }() 95 | 96 | private lazy var managedObjectModel: NSManagedObjectModel = { 97 | guard let modelURL = self.bundle.url(forResource: self.modelName, withExtension: "momd"), 98 | let objectModel = NSManagedObjectModel(contentsOf: modelURL) else { 99 | fatalError("Can't find managedObjectModel named \(self.modelName) in \(self.bundle)") 100 | } 101 | 102 | return objectModel 103 | }() 104 | 105 | private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 106 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) 107 | 108 | if !self.isMigrationNeeded() { 109 | do { 110 | try coordinator.addPersistentStore( 111 | ofType: self.persistentStoreType, 112 | configurationName: nil, 113 | at: self.storeURL, 114 | options: nil 115 | ) 116 | 117 | return coordinator 118 | } catch let error { 119 | fatalError("\(error)") 120 | } 121 | } 122 | 123 | // need perform migration 124 | do { 125 | try self.performMigration(coordinator) 126 | } catch let error { 127 | fatalError("\(error)") 128 | } 129 | 130 | return coordinator 131 | }() 132 | 133 | private lazy var rootContext: NSManagedObjectContext = { 134 | let coordinator = self.persistentStoreCoordinator 135 | let parentContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 136 | parentContext.persistentStoreCoordinator = coordinator 137 | 138 | return parentContext 139 | }() 140 | 141 | fileprivate lazy var mainContext: NSManagedObjectContext = { 142 | let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 143 | mainContext.parent = self.rootContext 144 | 145 | return mainContext 146 | }() 147 | 148 | private lazy var readManagedContext: NSManagedObjectContext = { 149 | let fetchContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 150 | fetchContext.parent = self.mainContext 151 | 152 | return fetchContext 153 | }() 154 | 155 | private lazy var writeManagedContext: NSManagedObjectContext = { 156 | let fetchContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 157 | fetchContext.parent = self.mainContext 158 | 159 | return fetchContext 160 | }() 161 | 162 | // MARK: - Migration 163 | 164 | private func isMigrationNeeded() -> Bool { 165 | do { 166 | let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( 167 | ofType: persistentStoreType, 168 | at: storeURL, 169 | options: nil 170 | ) 171 | let model = self.managedObjectModel 172 | 173 | return !model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) 174 | } catch { 175 | return false 176 | } 177 | } 178 | 179 | private func performMigration(_ coordinator: NSPersistentStoreCoordinator) throws { 180 | var options: [AnyHashable: Any]? 181 | if self.migrationType.isLightweight() { 182 | // trying to make it automatically 183 | options = [ 184 | NSMigratePersistentStoresAutomaticallyOption: true, 185 | NSInferMappingModelAutomaticallyOption: true 186 | ] 187 | } 188 | do { 189 | try coordinator.addPersistentStore( 190 | ofType: self.persistentStoreType, 191 | configurationName: nil, 192 | at: self.storeURL, 193 | options: options 194 | ) 195 | } catch let error { 196 | switch self.migrationType { 197 | case .removeOnFailure: 198 | // remove store and retry 199 | try? FileManager.default.removeItem(at: self.storeURL) 200 | try coordinator.addPersistentStore( 201 | ofType: self.persistentStoreType, 202 | configurationName: nil, 203 | at: self.storeURL, 204 | options: nil 205 | ) 206 | 207 | case .progressive(let delegate): 208 | try self.performHeavyMigration(coordinator, delegate: delegate) 209 | 210 | default: 211 | throw error 212 | } 213 | } 214 | } 215 | 216 | private func performHeavyMigration(_ coordinator: NSPersistentStoreCoordinator, delegate: MigrationManagerDelegate?) throws { 217 | let manager = CoreDataMigrationManager() 218 | manager.delegate = delegate 219 | manager.bundle = self.bundle 220 | try manager.progressivelyMigrate( 221 | sourceStoreURL: self.storeURL, 222 | of: self.persistentStoreType, 223 | to: self.managedObjectModel 224 | ) 225 | 226 | let options: [AnyHashable: Any] = [ 227 | NSInferMappingModelAutomaticallyOption: true, 228 | NSSQLitePragmasOption: ["journal_mode": "DELETE"] 229 | ] 230 | try coordinator.addPersistentStore( 231 | ofType: self.persistentStoreType, 232 | configurationName: nil, 233 | at: self.storeURL, 234 | options: options 235 | ) 236 | } 237 | 238 | // MARK: - Read/write 239 | 240 | private func performWriteTask(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { 241 | let context = writeManagedContext 242 | context.perform { 243 | closure(context) { 244 | try context.save(includingParent: true) 245 | } 246 | } 247 | } 248 | 249 | private func performReadTask(closure: @escaping (NSManagedObjectContext) -> ()) { 250 | let context = readManagedContext 251 | context.perform { 252 | closure(context) 253 | } 254 | } 255 | 256 | private func performWriteTaskAndWait(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { 257 | let context = writeManagedContext 258 | context.performAndWait { 259 | closure(context) { 260 | try context.save(includingParent: true) 261 | } 262 | } 263 | } 264 | 265 | private func performReadTaskAndWait(closure: @escaping (NSManagedObjectContext) -> ()) { 266 | let context = readManagedContext 267 | context.performAndWait { 268 | closure(context) 269 | } 270 | } 271 | 272 | } 273 | 274 | // MARK: - DBClient methods 275 | 276 | extension CoreDataDBClient: DBClient { 277 | 278 | public func observable(for request: FetchRequest) -> RequestObservable { 279 | return CoreDataObservable(request: request, context: mainContext) 280 | } 281 | 282 | public func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) where T: Stored { 283 | let coreDataModelType = checkType(T.self) 284 | 285 | performReadTask { context in 286 | let fetchRequest = self.fetchRequest(for: coreDataModelType) 287 | fetchRequest.predicate = request.predicate 288 | fetchRequest.sortDescriptors = request.sortDescriptors 289 | fetchRequest.fetchLimit = request.fetchLimit 290 | fetchRequest.fetchOffset = request.fetchOffset 291 | do { 292 | let result = try context.fetch(fetchRequest) as! [NSManagedObject] 293 | let resultModels = result.compactMap { coreDataModelType.from($0) as? T } 294 | 295 | completion(.success(resultModels)) 296 | } catch let error { 297 | completion(.failure(error)) 298 | } 299 | } 300 | } 301 | 302 | /// Insert given objects into context and save it 303 | /// If appropriate object already exists in DB it will be ignored and nothing will be inserted 304 | public func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { 305 | checkType(T.self) 306 | 307 | performWriteTask { context, savingClosure in 308 | var insertedObjects = [T]() 309 | let foundObjects = self.find(objects: objects, in: context) 310 | for (object, storedObject) in foundObjects { 311 | if storedObject != nil { 312 | continue 313 | } 314 | 315 | _ = object.upsertManagedObject(in: context, existedInstance: nil) 316 | insertedObjects.append(object as! T) 317 | } 318 | 319 | do { 320 | try savingClosure() 321 | completion(.success(insertedObjects)) 322 | } catch let error { 323 | completion(.failure(error)) 324 | } 325 | } 326 | } 327 | 328 | /// Method to update existed in DB objects 329 | /// if there is no such object in db nothing will happened 330 | public func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { 331 | checkType(T.self) 332 | 333 | performWriteTask { context, savingClosure in 334 | var updatedObjects = [T]() 335 | 336 | let foundObjects = self.find(objects: objects, in: context) 337 | for (object, storedObject) in foundObjects { 338 | guard let storedObject = storedObject else { 339 | continue 340 | } 341 | 342 | _ = object.upsertManagedObject(in: context, existedInstance: storedObject) 343 | updatedObjects.append(object as! T) 344 | } 345 | 346 | do { 347 | try savingClosure() 348 | completion(.success(updatedObjects)) 349 | } catch let error { 350 | completion(.failure(error)) 351 | } 352 | } 353 | } 354 | 355 | /// Update object if it exists or insert new one otherwise 356 | public func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) where T: Stored { 357 | checkType(T.self) 358 | 359 | performWriteTask { context, savingClosure in 360 | var updatedObjects = [T]() 361 | var insertedObjects = [T]() 362 | let foundObjects = self.find(objects: objects, in: context) 363 | 364 | for (object, storedObject) in foundObjects { 365 | _ = object.upsertManagedObject(in: context, existedInstance: storedObject) 366 | if storedObject == nil { 367 | insertedObjects.append(object as! T) 368 | } else { 369 | updatedObjects.append(object as! T) 370 | } 371 | } 372 | 373 | do { 374 | try savingClosure() 375 | completion(.success((updated: updatedObjects, inserted: insertedObjects))) 376 | } catch let error { 377 | completion(.failure(error)) 378 | } 379 | } 380 | } 381 | 382 | /// For each element in collection: 383 | /// After all deletes try to save context 384 | public func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) where T: Stored { 385 | checkType(T.self) 386 | 387 | performWriteTask { context, savingClosure in 388 | let foundObjects = self.find(objects, in: context) 389 | foundObjects.forEach { context.delete($0) } 390 | 391 | do { 392 | try savingClosure() 393 | completion(.success(())) 394 | } catch let error { 395 | completion(.failure(error)) 396 | } 397 | } 398 | } 399 | 400 | public func execute(_ request: FetchRequest) -> Result<[T]> { 401 | let coreDataModelType = checkType(T.self) 402 | 403 | var executeResult: Result<[T]>! 404 | 405 | performReadTaskAndWait { context in 406 | let fetchRequest = self.fetchRequest(for: coreDataModelType) 407 | fetchRequest.predicate = request.predicate 408 | fetchRequest.sortDescriptors = request.sortDescriptors 409 | fetchRequest.fetchLimit = request.fetchLimit 410 | fetchRequest.fetchOffset = request.fetchOffset 411 | do { 412 | let result = try context.fetch(fetchRequest) as! [NSManagedObject] 413 | let resultModels = result.compactMap { coreDataModelType.from($0) as? T } 414 | 415 | executeResult = .success(resultModels) 416 | } catch let error { 417 | executeResult = .failure(error) 418 | } 419 | } 420 | 421 | return executeResult 422 | } 423 | 424 | @discardableResult 425 | public func insert(_ objects: [T]) -> Result<[T]> { 426 | checkType(T.self) 427 | 428 | var result: Result<[T]>! 429 | 430 | performWriteTaskAndWait { context, savingClosure in 431 | var insertedObjects = [T]() 432 | let foundObjects = self.find(objects: objects, in: context) 433 | for (object, storedObject) in foundObjects { 434 | if storedObject != nil { 435 | continue 436 | } 437 | 438 | _ = object.upsertManagedObject(in: context, existedInstance: nil) 439 | insertedObjects.append(object as! T) 440 | } 441 | 442 | do { 443 | try savingClosure() 444 | result = .success(insertedObjects) 445 | } catch let error { 446 | result = .failure(error) 447 | } 448 | } 449 | 450 | return result 451 | } 452 | 453 | @discardableResult 454 | public func update(_ objects: [T]) -> Result<[T]> { 455 | checkType(T.self) 456 | 457 | var result: Result<[T]>! 458 | 459 | performWriteTaskAndWait { context, savingClosure in 460 | var updatedObjects = [T]() 461 | 462 | let foundObjects = self.find(objects: objects, in: context) 463 | for (object, storedObject) in foundObjects { 464 | guard let storedObject = storedObject else { 465 | continue 466 | } 467 | 468 | _ = object.upsertManagedObject(in: context, existedInstance: storedObject) 469 | updatedObjects.append(object as! T) 470 | } 471 | 472 | do { 473 | try savingClosure() 474 | result = .success(updatedObjects) 475 | } catch let error { 476 | result = .failure(error) 477 | } 478 | } 479 | 480 | return result 481 | } 482 | 483 | @discardableResult 484 | public func delete(_ objects: [T]) -> Result<()> { 485 | checkType(T.self) 486 | 487 | var result: Result<()>! 488 | 489 | performWriteTaskAndWait { context, savingClosure in 490 | let foundObjects = self.find(objects, in: context) 491 | foundObjects.forEach { context.delete($0) } 492 | 493 | do { 494 | try savingClosure() 495 | result = .success(()) 496 | } catch let error { 497 | result = .failure(error) 498 | } 499 | } 500 | 501 | return result 502 | } 503 | 504 | public func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) where T : Stored { 505 | let type = checkType(T.self) 506 | 507 | let fetchRequest = NSFetchRequest(entityName: type.entityName) 508 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 509 | deleteRequest.resultType = .resultTypeObjectIDs 510 | performWriteTask { [weak mainContext] context, savingClosure in 511 | do { 512 | let result = try context.execute(deleteRequest) as? NSBatchDeleteResult 513 | if let objectIDs = result?.result as? [NSManagedObjectID] { 514 | for objectID in objectIDs { 515 | guard let object = mainContext?.object(with: objectID) else { continue } 516 | mainContext?.delete(object) 517 | } 518 | } 519 | try savingClosure() 520 | completion(.success(())) 521 | } catch { 522 | completion(.failure(error)) 523 | } 524 | } 525 | } 526 | 527 | @discardableResult 528 | public func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> { 529 | checkType(T.self) 530 | 531 | var result: Result<(updated: [T], inserted: [T])>! 532 | 533 | performWriteTaskAndWait { context, savingClosure in 534 | var updatedObjects = [T]() 535 | var insertedObjects = [T]() 536 | let foundObjects = self.find(objects: objects, in: context) 537 | 538 | for (object, storedObject) in foundObjects { 539 | _ = object.upsertManagedObject(in: context, existedInstance: storedObject) 540 | if storedObject == nil { 541 | insertedObjects.append(object as! T) 542 | } else { 543 | updatedObjects.append(object as! T) 544 | } 545 | } 546 | 547 | do { 548 | try savingClosure() 549 | result = .success((updated: updatedObjects, inserted: insertedObjects)) 550 | } catch let error { 551 | result = .failure(error) 552 | } 553 | } 554 | 555 | return result 556 | } 557 | } 558 | 559 | private extension CoreDataDBClient { 560 | 561 | func fetchRequest(for entity: CoreDataModelConvertible.Type) -> NSFetchRequest { 562 | return NSFetchRequest(entityName: entity.entityName) 563 | } 564 | 565 | @discardableResult 566 | func checkType(_ inputType: T) -> CoreDataModelConvertible.Type { 567 | switch inputType { 568 | case let type as CoreDataModelConvertible.Type: 569 | return type 570 | 571 | default: 572 | let modelType = String(describing: CoreDataDBClient.self) 573 | let protocolType = String(describing: CoreDataModelConvertible.self) 574 | let givenType = String(describing: inputType) 575 | fatalError("`\(modelType)` can manage only types which conform to `\(protocolType)`. `\(givenType)` given.") 576 | } 577 | } 578 | 579 | func find(_ objects: [T], in context: NSManagedObjectContext) -> [NSManagedObject] { 580 | let coreDataModelType = checkType(T.self) 581 | guard let primaryKeyName = T.primaryKeyName else { 582 | return [] 583 | } 584 | 585 | let ids = objects.compactMap { $0.valueOfPrimaryKey } 586 | let fetchRequest = self.fetchRequest(for: coreDataModelType) 587 | fetchRequest.predicate = NSPredicate(format: "\(primaryKeyName) IN %@", ids) 588 | guard let result = try? context.fetch(fetchRequest), let storedObjects = result as? [NSManagedObject] else { 589 | return [] 590 | } 591 | 592 | return storedObjects 593 | } 594 | 595 | func find(objects: [T], in context: NSManagedObjectContext) -> [(object: CoreDataModelConvertible, storedObject: NSManagedObject?)] { 596 | guard let primaryKeyName = T.primaryKeyName else { 597 | return [] 598 | } 599 | 600 | let storedObjects = find(objects, in: context) 601 | 602 | return convert(objects: objects).map { object -> (CoreDataModelConvertible, NSManagedObject?) in 603 | let managedObject = storedObjects.first(where: { (obj: NSManagedObject) -> Bool in 604 | if let value = obj.value(forKey: primaryKeyName) { 605 | return object.isPrimaryValueEqualTo(value: value) 606 | } 607 | 608 | return false 609 | }) 610 | 611 | return (object, managedObject) 612 | } 613 | } 614 | 615 | func convert(objects: [T]) -> [CoreDataModelConvertible] { 616 | checkType(T.self) 617 | 618 | return objects.compactMap { $0 as? CoreDataModelConvertible } 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /DBClient/CoreData/CoreDataMigrationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataMigrationManager.swift 3 | // DBClient 4 | // 5 | // Created by Roman Kyrylenko on 2/17/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | final class CoreDataMigrationManager: NSObject, MigrationManager { 13 | 14 | weak var delegate: MigrationManagerDelegate? = nil 15 | var bundle: Bundle = .main 16 | 17 | func progressivelyMigrate(sourceStoreURL: URL, of type: String, to finalModel: NSManagedObjectModel) throws { 18 | let sourceMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( 19 | ofType: type, 20 | at: sourceStoreURL, 21 | options: nil 22 | ) 23 | if finalModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) { 24 | return 25 | } 26 | guard let sourceModel = self.sourceModel(for: sourceMetadata) else { 27 | throw MigrationError.modelsNotFound 28 | } 29 | 30 | let data = try getDestinationModel(for: sourceModel) 31 | let destinationModel = data.0 32 | let mappingModel = data.1 33 | let modelName = data.2 34 | let mappingModels: [NSMappingModel] 35 | if let explicitMappingModels = delegate?.migrationManager(self, mappingModelsForSourceModel: sourceModel), 36 | !explicitMappingModels.isEmpty { 37 | mappingModels = explicitMappingModels 38 | } else { 39 | mappingModels = [mappingModel] 40 | } 41 | let destinationStoreURL = self.destinationStoreURL(with: sourceStoreURL, modelName: modelName) 42 | let manager = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel) 43 | manager.addObserver(self, forKeyPath: #keyPath(NSMigrationManager.migrationProgress), options: .new, context: nil) 44 | var migrated = false 45 | for mappingModel in mappingModels { 46 | do { 47 | try manager.migrateStore( 48 | from: sourceStoreURL, 49 | sourceType: type, 50 | options: nil, 51 | with: mappingModel, 52 | toDestinationURL: destinationStoreURL, 53 | destinationType: type, 54 | destinationOptions: nil 55 | ) 56 | migrated = true 57 | } catch { 58 | migrated = false 59 | } 60 | } 61 | manager.removeObserver(self, forKeyPath: #keyPath(NSMigrationManager.migrationProgress)) 62 | 63 | if !migrated { 64 | return 65 | } 66 | // Migration was successful, move the files around to preserve the source in case things go bad 67 | try backup(sourceStoreAtURL: sourceStoreURL, movingDestinationStoreAtURL: destinationStoreURL) 68 | // We may not be at the "current" model yet, so recurse 69 | try self.progressivelyMigrate(sourceStoreURL: sourceStoreURL, of: type, to: finalModel) 70 | } 71 | 72 | func modelPaths() -> [String] { 73 | // Find all of the mom and momd files in the Resources directory 74 | var modelPaths: [String] = [] 75 | let momdArray = bundle.paths(forResourcesOfType: "momd", inDirectory: nil) 76 | for path in momdArray { 77 | let resourceSubpath = (path as NSString).lastPathComponent 78 | let array = bundle.paths(forResourcesOfType: "mom", inDirectory: resourceSubpath) 79 | modelPaths.append(contentsOf: array) 80 | } 81 | let otherModels = bundle.paths(forResourcesOfType: "mom", inDirectory: nil) 82 | modelPaths.append(contentsOf: otherModels) 83 | 84 | return modelPaths 85 | } 86 | 87 | func sourceModel(for sourceMetadata: [String: Any]) -> NSManagedObjectModel? { 88 | return NSManagedObjectModel.mergedModel(from: [bundle], forStoreMetadata: sourceMetadata) 89 | } 90 | 91 | func getDestinationModel(for sourceModel: NSManagedObjectModel) throws -> (NSManagedObjectModel, NSMappingModel, String) { 92 | let modelPaths = self.modelPaths() 93 | if modelPaths.isEmpty { 94 | throw MigrationError.modelsNotFound 95 | } 96 | // See if we can find a matching destination model 97 | var model: NSManagedObjectModel? = nil 98 | var mapping: NSMappingModel? = nil 99 | var modelURL: URL? = nil 100 | for modelPath in modelPaths { 101 | let mURL = URL(fileURLWithPath: modelPath) 102 | modelURL = mURL 103 | model = NSManagedObjectModel(contentsOf: mURL) 104 | mapping = NSMappingModel(from: [bundle], forSourceModel: sourceModel, destinationModel: model) 105 | // If we found a mapping model then proceed 106 | if mapping != nil { 107 | break 108 | } 109 | } 110 | // We have tested every model, if nil here we failed 111 | if mapping == nil || mapping == nil || modelURL == nil { 112 | throw MigrationError.mappingModelNotFound 113 | } 114 | 115 | return (model!, mapping!, (modelURL!.lastPathComponent as NSString).deletingPathExtension) 116 | } 117 | 118 | func destinationStoreURL(with sourceStoreURL: URL, modelName: String) -> URL { 119 | // We have a mapping model, time to migrate 120 | let storeExtension = sourceStoreURL.pathExtension 121 | var storePath = sourceStoreURL.deletingPathExtension().path 122 | // Build a path to write the new store 123 | storePath = "\(storePath).\(modelName).\(storeExtension)" 124 | 125 | return URL(fileURLWithPath: storePath) 126 | } 127 | 128 | func backup(sourceStoreAtURL: URL, movingDestinationStoreAtURL: URL) throws { 129 | let guid = ProcessInfo.processInfo.globallyUniqueString 130 | let backupPath = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(guid) 131 | let fileManager = FileManager.default 132 | try fileManager.moveItem(at: sourceStoreAtURL, to: backupPath) 133 | // Move the destination to the source path 134 | do { 135 | try fileManager.moveItem(at: movingDestinationStoreAtURL, to: sourceStoreAtURL) 136 | } catch { 137 | // Try to back out the source move first, no point in checking it for errors 138 | try fileManager.moveItem(at: backupPath, to: sourceStoreAtURL) 139 | throw error 140 | } 141 | } 142 | 143 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 144 | if keyPath == "migrationProgress", let object = object as? NSMigrationManager { 145 | delegate?.migrationManager(self, updateMigrationProgress: object.migrationProgress) 146 | } else { 147 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /DBClient/CoreData/CoreDataObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataObservable.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 15/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | class CoreDataObservable: RequestObservable { 13 | 14 | var observer: ((ObservableChange) -> Void)? 15 | 16 | let fetchRequest: NSFetchRequest 17 | let fetchedResultsController: NSFetchedResultsController 18 | 19 | private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate 20 | 21 | init(request: FetchRequest, context: NSManagedObjectContext) { 22 | guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { 23 | fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") 24 | } 25 | 26 | fetchRequest = { 27 | let fetchRequest = NSFetchRequest(entityName: coreDataModelType.entityName) 28 | if let predicate = request.predicate { 29 | fetchRequest.predicate = predicate 30 | } 31 | if let sortDescriptors = request.sortDescriptors { 32 | fetchRequest.sortDescriptors = sortDescriptors 33 | } else { 34 | guard let primaryKeyName = coreDataModelType.primaryKeyName else { 35 | fatalError("Fetch request shoud have sortDescriptor or core data model need implement primaryKeyName") 36 | } 37 | let defaultSortDescriptor = NSSortDescriptor(key: primaryKeyName, ascending: true) 38 | fetchRequest.sortDescriptors = [defaultSortDescriptor] 39 | } 40 | fetchRequest.fetchLimit = request.fetchLimit 41 | fetchRequest.fetchOffset = request.fetchOffset 42 | 43 | return fetchRequest 44 | }() 45 | 46 | fetchedResultsControllerDelegate = FetchedResultsControllerDelegate() 47 | 48 | fetchedResultsController = NSFetchedResultsController( 49 | fetchRequest: fetchRequest, 50 | managedObjectContext: context, 51 | sectionNameKeyPath: nil, 52 | cacheName: nil 53 | ) 54 | fetchedResultsController.delegate = fetchedResultsControllerDelegate 55 | 56 | super.init(request: request) 57 | } 58 | 59 | override func observe(_ closure: @escaping (ObservableChange) -> Void) { 60 | assert(observer == nil, "Observable can be observed only once") 61 | 62 | guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { 63 | fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") 64 | } 65 | 66 | do { 67 | let initial = try fetchedResultsController.managedObjectContext.fetch(fetchRequest) 68 | let mapped = initial.map { coreDataModelType.from($0) as! T } 69 | closure(.initial(mapped)) 70 | observer = closure 71 | 72 | fetchedResultsControllerDelegate.observer = { [unowned self] (change: ObservableChange) in 73 | guard case .change(let change) = change else { return } 74 | let mappedInsertions = change.insertions.map { ($0, coreDataModelType.from($1) as! T) } 75 | let mappedModifications = change.modifications.map { ($0, coreDataModelType.from($1) as! T) } 76 | let mappedObjects = change.objects.map { coreDataModelType.from($0) as! T } 77 | let mappedChange: ObservableChange.ModelChange = ( 78 | objects: mappedObjects, 79 | deletions: change.deletions, 80 | insertions: mappedInsertions, 81 | modifications: mappedModifications 82 | ) 83 | self.observer?(.change(mappedChange)) 84 | } 85 | 86 | try fetchedResultsController.performFetch() 87 | } catch let error { 88 | closure(.error(error)) 89 | } 90 | } 91 | 92 | } 93 | 94 | /// A separate class to avoid inherintace from NSObject 95 | private class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate { 96 | 97 | var observer: ((ObservableChange) -> Void)? 98 | private var batchChanges: [CoreDataChange] = [] 99 | 100 | func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { 101 | guard let object = anObject as? T else { 102 | return 103 | } 104 | 105 | switch type { 106 | case .delete: 107 | batchChanges.append(.delete(indexPath!.row, object)) 108 | 109 | case .insert: 110 | batchChanges.append(.insert(newIndexPath!.row, object)) 111 | 112 | case .update, .move: 113 | batchChanges.append(.update(indexPath!.row, object)) 114 | 115 | @unknown 116 | default: 117 | assertionFailure("trying to handle unknown case \(type)") 118 | } 119 | } 120 | 121 | func controllerWillChangeContent(_ controller: NSFetchedResultsController) { 122 | batchChanges = [] 123 | } 124 | 125 | func controllerDidChangeContent(_ controller: NSFetchedResultsController) { 126 | let deleted: [Int] = batchChanges.filter { $0.isDeletion }.map { $0.index() } 127 | let inserted: [(index: Int, element: T)] = batchChanges.filter { $0.isInsertion }.map { (index: $0.index(), element: $0.object()) } 128 | let updated: [(index: Int, element: T)] = batchChanges.filter { $0.isUpdate }.map { (index: $0.index(), element: $0.object()) } 129 | let objects: [T] = controller.fetchedObjects as? [T] ?? [] 130 | let mappedChange: ObservableChange.ModelChange = ( 131 | objects: objects, 132 | deletions: deleted, 133 | insertions: inserted, 134 | modifications: updated 135 | ) 136 | if let observer = observer { 137 | observer(.change(mappedChange)) 138 | } 139 | batchChanges = [] 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /DBClient/CoreData/MigrationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigrationManager.swift 3 | // DBClient 4 | // 5 | // Created by Roman Kyrylenko on 2/17/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | public protocol MigrationManagerDelegate: class { 12 | 13 | func migrationManager(_ migrationManager: MigrationManager, updateMigrationProgress: Float) 14 | func migrationManager(_ migrationManager: MigrationManager, mappingModelsForSourceModel: NSManagedObjectModel) -> [NSMappingModel] 15 | 16 | } 17 | 18 | public extension MigrationManagerDelegate { 19 | 20 | func migrationManager(_ migrationManager: MigrationManager, updateMigrationProgress: Float) { 21 | } 22 | 23 | func migrationManager(_ migrationManager: MigrationManager, mappingModelsForSourceModel: NSManagedObjectModel) -> [NSMappingModel] { 24 | return [] 25 | } 26 | 27 | } 28 | 29 | public enum MigrationError: Error { 30 | case modelsNotFound 31 | case mappingModelNotFound 32 | } 33 | 34 | public protocol MigrationManager { 35 | 36 | var delegate: MigrationManagerDelegate? { get set } 37 | var bundle: Bundle { get set } 38 | 39 | func progressivelyMigrate(sourceStoreURL: URL, of type: String, to model: NSManagedObjectModel) throws 40 | 41 | } 42 | -------------------------------------------------------------------------------- /DBClient/CoreData/NSManagedObjectContext+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContext+Extension.swift 3 | // DBClient 4 | // 5 | // Copyright © 2016 Yalantis. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension NSManagedObjectContext { 12 | 13 | func save(includingParent: Bool) throws { 14 | guard hasChanges else { 15 | return 16 | } 17 | 18 | try save() 19 | 20 | if includingParent, let parent = parent { 21 | try parent.safePerformAndWait { 22 | try parent.save(includingParent: true) 23 | } 24 | } 25 | } 26 | 27 | func safePerformAndWait(_ block: @escaping () throws -> Void) throws { 28 | var outError: Error? 29 | 30 | performAndWait { 31 | do { 32 | try block() 33 | } catch { 34 | outError = error 35 | } 36 | } 37 | 38 | // fake rethrowing 39 | if let outError = outError { 40 | throw outError 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /DBClient/Realm/RealmDBClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmDBClient.swift 3 | // DBClient 4 | // 5 | // Created by Serhii Butenko on 19/12/16. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | /// Describes protocol to be implemented by model for `RealmDBClient` 13 | public protocol RealmModelConvertible: Stored { 14 | 15 | /// - Returns: type of object for model 16 | static func realmClass() -> Object.Type 17 | 18 | /// Executes mapping from `Realm.Object` instance 19 | /// 20 | /// - Parameter realmObject: Object to be mapped from 21 | /// - Returns: Fulfilled model instance 22 | static func from(_ realmObject: Object) -> Stored 23 | 24 | /// Executes backward mapping from `Realm.Object` 25 | func toRealmObject() -> Object 26 | } 27 | 28 | extension RealmModelConvertible { 29 | 30 | func realmClassForInstance() -> Object.Type { 31 | return Self.realmClass() 32 | } 33 | } 34 | 35 | /// Implementation of database client for Realm storage type. 36 | /// Model for this client must conform to `RealmModelConverible` protocol or error will be raised. 37 | public class RealmDBClient { 38 | 39 | let realm: Realm 40 | 41 | public init(realm: Realm) { 42 | self.realm = realm 43 | } 44 | } 45 | 46 | // MARK: DBClient 47 | 48 | extension RealmDBClient: DBClient { 49 | 50 | /// Executes given request. Fetches all entities and then applies all given restrictions 51 | public func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) { 52 | completion(execute(request)) 53 | } 54 | 55 | /// Inserts new objects to database. If object with such `primaryKeyValue` already exists Realm'll throw an error 56 | public func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T : Stored { 57 | completion(insert(objects)) 58 | } 59 | 60 | /// Updates objects which are already in db. 61 | public func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T : Stored { 62 | completion(update(objects)) 63 | } 64 | 65 | /// Removes objects by it `primaryKeyValue`s 66 | public func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) where T : Stored { 67 | completion(delete(objects)) 68 | } 69 | 70 | public func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) where T: Stored { 71 | let type = checkType(T.self) 72 | 73 | let realmType = type.realmClass() 74 | 75 | do { 76 | let realmObjects = realm.objects(realmType) 77 | realm.beginWrite() 78 | realm.delete(realmObjects) 79 | try realm.commitWrite() 80 | 81 | completion(.success(())) 82 | } catch { 83 | completion(.failure(error)) 84 | } 85 | } 86 | 87 | public func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) where T : Stored { 88 | completion(upsert(objects)) 89 | } 90 | 91 | public func observable(for request: FetchRequest) -> RequestObservable { 92 | checkType(T.self) 93 | 94 | return RealmObservable(request: request, realm: realm) 95 | } 96 | 97 | public func execute(_ request: FetchRequest) -> Result<[T]> { 98 | let modelType = checkType(T.self) 99 | let neededType = modelType.realmClass() 100 | let objects = request 101 | .applyTo(realmObjects: realm.objects(neededType)) 102 | .map { $0 } 103 | .slice(offset: request.fetchOffset, limit: request.fetchLimit) 104 | .compactMap { modelType.from($0) as? T } 105 | 106 | return .success(objects) 107 | } 108 | 109 | @discardableResult 110 | public func insert(_ objects: [T]) -> Result<[T]> { 111 | checkType(T.self) 112 | 113 | let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } 114 | 115 | do { 116 | realm.beginWrite() 117 | realm.add(realmObjects) 118 | try realm.commitWrite() 119 | return .success(objects) 120 | } catch { 121 | return .failure(error) 122 | } 123 | } 124 | 125 | @discardableResult 126 | public func update(_ objects: [T]) -> Result<[T]> { 127 | checkType(T.self) 128 | 129 | let realmObjects = separate(objects: objects) 130 | .present 131 | .compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } 132 | do { 133 | realm.beginWrite() 134 | realm.add(realmObjects, update: true) 135 | try realm.commitWrite() 136 | 137 | return .success(objects) 138 | } catch let error { 139 | return .failure(error) 140 | } 141 | } 142 | 143 | @discardableResult 144 | public func delete(_ objects: [T]) -> Result<()> { 145 | let type = checkType(T.self) 146 | 147 | let realmType = type.realmClass() 148 | 149 | do { 150 | let primaryValues = objects.compactMap { $0.valueOfPrimaryKey } 151 | let realmObjects = primaryValues.compactMap { realm.object(ofType: realmType, forPrimaryKey: $0) } 152 | realm.beginWrite() 153 | realm.delete(realmObjects) 154 | try realm.commitWrite() 155 | 156 | return .success(()) 157 | } catch { 158 | return .failure(error) 159 | } 160 | } 161 | 162 | @discardableResult 163 | public func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> { 164 | checkType(T.self) 165 | 166 | let separatedObjects = separate(objects: objects) 167 | let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } 168 | do { 169 | realm.beginWrite() 170 | realm.add(realmObjects, update: true) 171 | try realm.commitWrite() 172 | return .success((updated: separatedObjects.present, inserted: separatedObjects.new)) 173 | } catch { 174 | return .failure(error) 175 | } 176 | } 177 | 178 | } 179 | 180 | private extension RealmDBClient { 181 | 182 | @discardableResult 183 | func checkType(_ inputType: T) -> RealmModelConvertible.Type { 184 | switch inputType { 185 | case let type as RealmModelConvertible.Type: 186 | return type 187 | 188 | default: 189 | let model = String(describing: RealmDBClient.self) 190 | let prot = String(describing: RealmModelConvertible.self) 191 | let given = String(describing: inputType) 192 | fatalError("`\(model)` can manage only types which conform to `\(prot)`. `\(given)` given.") 193 | } 194 | } 195 | 196 | func separate(objects: [T]) -> (present: [T], new: [T]) { 197 | var presentObjects: [T] = [] 198 | var notPresentObjects: [T] = [] 199 | objects.forEach { object in 200 | guard let convertedObject = object as? RealmModelConvertible, 201 | let primaryValue = convertedObject.valueOfPrimaryKey else { 202 | return 203 | } 204 | 205 | let entry = self.realm.object(ofType: convertedObject.realmClassForInstance(), forPrimaryKey: primaryValue) 206 | if entry != nil { 207 | presentObjects.append(object) 208 | } else { 209 | notPresentObjects.append(object) 210 | } 211 | } 212 | 213 | return (present: presentObjects, new: notPresentObjects) 214 | } 215 | } 216 | 217 | internal extension FetchRequest { 218 | 219 | func applyTo(realmObjects: Results) -> Results { 220 | var objects: Results = realmObjects 221 | if let sortDescriptors = sortDescriptors?.compactMap(SortDescriptor.init), !sortDescriptors.isEmpty { 222 | objects = realmObjects.sorted(by: sortDescriptors) 223 | } 224 | if let predicate = predicate { 225 | objects = objects.filter(predicate) 226 | } 227 | 228 | return objects 229 | } 230 | } 231 | 232 | private extension SortDescriptor { 233 | 234 | init?(_ descriptor: NSSortDescriptor) { 235 | if let key = descriptor.key { 236 | self = SortDescriptor(keyPath: key, ascending: descriptor.ascending) 237 | } else { 238 | return nil 239 | } 240 | } 241 | 242 | } 243 | 244 | private extension Array { 245 | 246 | func slice(offset: Int, limit: Int) -> [T] { 247 | var lim = 0 248 | var off = 0 249 | let count = self.count 250 | 251 | if off <= offset && offset < count - 1 { 252 | off = offset 253 | } 254 | if limit > count || limit == 0 { 255 | lim = count 256 | } else { 257 | lim = offset + limit 258 | } 259 | 260 | return (off..: RequestObservable { 21 | 22 | internal let realm: Realm 23 | internal var notificationToken: NotificationToken? 24 | 25 | internal init(request: FetchRequest, realm: Realm) { 26 | self.realm = realm 27 | super.init(request: request) 28 | } 29 | 30 | open override func observe(_ closure: @escaping (ObservableChange) -> Void) { 31 | precondition(notificationToken == nil, "Observable can be observed only once") 32 | 33 | guard let realmModelType = T.self as? RealmModelConvertible.Type else { 34 | fatalError("RealmDBClient can manage only types which conform to RealmModelConvertible") 35 | } 36 | 37 | let realmObjects = request.applyTo(realmObjects: realm.objects(realmModelType.realmClass())) 38 | notificationToken = realmObjects.observe { changes in 39 | switch changes { 40 | case .initial(let initial): 41 | let mapped = initial.map { realmModelType.from($0) as! T } 42 | closure(.initial(Array(mapped))) 43 | 44 | case .update(let objects, let deletions, let insertions, let modifications): 45 | let mappedObjects = objects.map { realmModelType.from($0) as! T } 46 | let insertions = insertions.map { (index: $0, element: mappedObjects[$0]) } 47 | let modifications = modifications.map { (index: $0, element: mappedObjects[$0]) } 48 | let mappedChange: ObservableChange.ModelChange = ( 49 | objects: Array(mappedObjects), 50 | deletions: deletions, 51 | insertions: insertions, 52 | modifications: modifications 53 | ) 54 | closure(.change(mappedChange)) 55 | 56 | case .error(let error): 57 | closure(.error(error)) 58 | } 59 | } 60 | } 61 | 62 | public func stopObserving() { 63 | notificationToken = nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/DBClientTests/DBClientTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBClientTest.swift 3 | // DBClientTests 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | @testable import Example 12 | 13 | class DBClientTest: XCTestCase { 14 | 15 | var dbClient: DBClient! { return nil } 16 | 17 | override func setUp() { 18 | super.setUp() 19 | 20 | cleanUpDatabase() 21 | } 22 | 23 | override func tearDown() { 24 | cleanUpDatabase() 25 | 26 | super.tearDown() 27 | } 28 | 29 | // removes all objects from the database 30 | func cleanUpDatabase() { 31 | guard dbClient != nil else { return } 32 | let expectationDeleletion = expectation(description: "Deletion") 33 | var isDeleted = false 34 | 35 | dbClient.findAll { (result: Result<[User]>) in 36 | guard let objects = result.value else { 37 | expectationDeleletion.fulfill() 38 | return 39 | } 40 | self.dbClient.delete(objects) { _ in 41 | isDeleted = true 42 | expectationDeleletion.fulfill() 43 | } 44 | } 45 | 46 | waitForExpectations(timeout: 1) { _ in 47 | XCTAssert(isDeleted) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/DBClientTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataCreateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataCreateTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class CoreDataCreateTests: DBClientCoreDataTest { 13 | 14 | func test_SyncSingleInsertion_WhenSuccessful_ReturnsObject() { 15 | let randomUser = User.createRandom() 16 | let result = dbClient.insert(randomUser) 17 | switch result { 18 | case .failure(let error): XCTFail(error.localizedDescription) 19 | case .success(let user): XCTAssertEqual(randomUser, user) 20 | } 21 | } 22 | 23 | func test_SyncBulkInsertion_WhenSuccessful_ReturnsObjects() { 24 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 25 | 26 | let result = dbClient.insert(randomUsers) 27 | 28 | switch result { 29 | case .failure(let error): XCTFail(error.localizedDescription) 30 | case .success(let users): XCTAssertEqual(users.sorted(), randomUsers.sorted()) 31 | } 32 | } 33 | 34 | func test_SingleInsertion_WhenSuccessful_ReturnsObject() { 35 | let randomUser = User.createRandom() 36 | let expectationObject = expectation(description: "Object") 37 | var expectedObject: User? 38 | 39 | dbClient.insert(randomUser) { result in 40 | expectedObject = result.value 41 | expectationObject.fulfill() 42 | } 43 | 44 | waitForExpectations(timeout: 1) { _ in 45 | XCTAssertNotNil(expectedObject) 46 | } 47 | } 48 | 49 | func test_BulkInsertion_WhenSuccessful_ReturnsBulk() { 50 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 51 | 52 | let expectationObjects = expectation(description: "Objects") 53 | var expectedObjectsCount = 0 54 | 55 | dbClient.insert(randomUsers) { result in 56 | expectedObjectsCount = result.value?.count ?? 0 57 | expectationObjects.fulfill() 58 | } 59 | 60 | waitForExpectations(timeout: 1) { _ in 61 | XCTAssertEqual(expectedObjectsCount, randomUsers.count) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataDeleteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataDeleteTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class CoreDataDeleteTests: DBClientCoreDataTest { 13 | 14 | func test_SyncSingleDeletion_WhenSuccessful_ReturnsNil() { 15 | let randomUser = User.createRandom() 16 | 17 | let result = dbClient.insert(randomUser) 18 | let removalResult = dbClient.delete(result.require()) 19 | 20 | XCTAssertNotNil(removalResult.value) 21 | } 22 | 23 | func test_SyncBulkDeletion_WhenSuccessful_ReturnsNil() { 24 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 25 | 26 | let insertionResult = dbClient.insert(randomUsers) 27 | let removalResult = dbClient.delete(insertionResult.require()) 28 | 29 | XCTAssertNotNil(removalResult.value) 30 | } 31 | 32 | func test_SingleDeletion_WhenSuccessful_ReturnsNil() { 33 | let randomUser = User.createRandom() 34 | let expectationHit = expectation(description: "Object") 35 | var isDeleted = false 36 | 37 | dbClient.insert(randomUser) { result in 38 | if let object = result.value { 39 | self.dbClient.delete(object) { result in 40 | isDeleted = result.value != nil 41 | expectationHit.fulfill() 42 | } 43 | } 44 | } 45 | 46 | waitForExpectations(timeout: 1) { _ in 47 | XCTAssert(isDeleted) 48 | } 49 | } 50 | 51 | func test_BulkDeletion_WhenSuccessful_ReturnsNil() { 52 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 53 | let expectationHit = expectation(description: "Object") 54 | var isDeleted = false 55 | 56 | dbClient.insert(randomUsers) { result in 57 | if let objects = result.value { 58 | self.dbClient.delete(objects) { result in 59 | isDeleted = result.value != nil 60 | expectationHit.fulfill() 61 | } 62 | } 63 | } 64 | 65 | waitForExpectations(timeout: 1) { _ in 66 | XCTAssert(isDeleted) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataExecuteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataExecuteTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | @testable import Example 12 | 13 | final class CoreDataExecuteTests: DBClientCoreDataTest { 14 | 15 | func test_SingleSyncExecute_WhenSuccessful_ReturnsCount() { 16 | let randomUser = User.createRandom() 17 | 18 | dbClient.insert(randomUser) 19 | let request = FetchRequest() 20 | let executionResult = dbClient.execute(request) 21 | 22 | XCTAssertEqual(executionResult.require().first!, randomUser) 23 | } 24 | 25 | func test_SingleExecute_WhenSuccessful_ReturnsCount() { 26 | let randomUser = User.createRandom() 27 | let expectationObject = expectation(description: "Object") 28 | var expectedCount = 0 29 | 30 | self.dbClient.insert(randomUser) { result in 31 | if result.value != nil { 32 | let request = FetchRequest() 33 | self.dbClient.execute(request) { result in 34 | expectedCount = result.value?.count ?? 0 35 | expectationObject.fulfill() 36 | } 37 | } 38 | } 39 | 40 | waitForExpectations(timeout: 1) { _ in 41 | XCTAssertEqual(expectedCount, 1) 42 | } 43 | } 44 | 45 | func test_ExecuteWithOffset_WhenSuccessful_ReturnsCount() { 46 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 47 | let offset = 5 48 | let shiftedUsers = Array(randomUsers[offset..(fetchOffset: offset) 55 | self.dbClient.execute(request) { result in 56 | expectedCount = result.value?.count ?? 0 57 | expectationObjects.fulfill() 58 | } 59 | } 60 | } 61 | 62 | waitForExpectations(timeout: 1) { _ in 63 | XCTAssertEqual(expectedCount, shiftedUsers.count) 64 | } 65 | } 66 | 67 | func test_ExecuteWithLimit_WhenSuccessful_ReturnsCount() { 68 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 69 | let limit = 3 70 | 71 | let expectationObjects = expectation(description: "Object") 72 | var expectedCount = 0 73 | 74 | self.dbClient.insert(randomUsers) { result in 75 | if result.value != nil { 76 | let request = FetchRequest(fetchLimit: limit) 77 | self.dbClient.execute(request) { result in 78 | expectedCount = result.value?.count ?? 0 79 | expectationObjects.fulfill() 80 | } 81 | } 82 | } 83 | 84 | waitForExpectations(timeout: 1) { _ in 85 | XCTAssertEqual(expectedCount, limit) 86 | } 87 | } 88 | 89 | func test_ExecuteWithSortDescriptor_WhenSuccessful_ReturnsCount() { 90 | let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) 91 | let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending 92 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 93 | let sortedUsers = randomUsers.sorted { $0.name.compare($1.name) == order } 94 | let expectationObjects = expectation(description: "Object") 95 | var expectedUsers = [User]() 96 | 97 | self.dbClient.insert(randomUsers) { result in 98 | if result.value != nil { 99 | let request = FetchRequest(sortDescriptor: sortDescriptor) 100 | 101 | self.dbClient.execute(request) { result in 102 | expectedUsers = result.value ?? [] 103 | expectationObjects.fulfill() 104 | } 105 | } 106 | } 107 | 108 | waitForExpectations(timeout: 1) { _ in 109 | XCTAssertEqual(expectedUsers, sortedUsers) 110 | } 111 | } 112 | 113 | func test_ExecuteWithPredicate_WhenSuccessful_ReturnsCount() { 114 | let arg = "1" 115 | let predicate = NSPredicate(format: "SELF.id ENDSWITH %@", arg) 116 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 117 | let preicatedUsers = randomUsers.filter { $0.id.hasSuffix(arg) } 118 | let expectationObjects = expectation(description: "Object") 119 | var expectedUsers = [User]() 120 | 121 | self.dbClient.insert(randomUsers) { result in 122 | guard result.value != nil else { 123 | expectationObjects.fulfill() 124 | return 125 | } 126 | let request = FetchRequest(predicate: predicate) 127 | 128 | self.dbClient.execute(request) { result in 129 | expectedUsers = result.value ?? [] 130 | expectationObjects.fulfill() 131 | } 132 | } 133 | 134 | waitForExpectations(timeout: 1) { _ in 135 | XCTAssertEqual(expectedUsers.sorted(), preicatedUsers.sorted()) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataFetchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataFetchTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import DBClient 12 | @testable import Example 13 | 14 | class CoreDataFetchTests: DBClientCoreDataTest { 15 | 16 | func test_SyncFetch_WhenSuccessful_ReturnObject() { 17 | let randomUser = User.createRandom() 18 | let expectationObject = expectation(description: "Inserting object") 19 | 20 | self.dbClient.insert(randomUser) { _ in expectationObject.fulfill() } 21 | 22 | waitForExpectations(timeout: 1) { _ in 23 | let result = self.dbClient.execute(FetchRequest(predicate: NSPredicate(format: "id == %@", randomUser.id))) 24 | XCTAssertEqual(result.require().count, 1) 25 | let object = result.require().first 26 | XCTAssertEqual(object, randomUser) 27 | } 28 | } 29 | 30 | func test_SingleFetch_WhenSuccessful_ReturnsObject() { 31 | let randomUser = User.createRandom() 32 | let expectationObject = expectation(description: "Object") 33 | var expectedObject: User? 34 | 35 | self.dbClient.insert(randomUser) { result in 36 | self.dbClient.findFirst(User.self, primaryValue: randomUser.id) { result in 37 | expectedObject = result.require() 38 | expectationObject.fulfill() 39 | } 40 | } 41 | 42 | waitForExpectations(timeout: 5) { _ in 43 | XCTAssertNotNil(expectedObject) 44 | } 45 | } 46 | 47 | func test_BulkFetch_WhenSuccessful_ReturnsBulk() { 48 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 49 | 50 | let expectationObjects = expectation(description: "Objects") 51 | var expectedObjectsCount = 0 52 | 53 | self.dbClient.insert(randomUsers) { result in 54 | self.dbClient.findAll { (result: Result<[User]>) in 55 | expectedObjectsCount = result.value?.count ?? 0 56 | expectationObjects.fulfill() 57 | } 58 | } 59 | 60 | waitForExpectations(timeout: 1) { _ in 61 | XCTAssertEqual(expectedObjectsCount, randomUsers.count) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataObservableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataObservableTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/13/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | @testable import Example 12 | 13 | final class CoreDataObservableTests: DBClientCoreDataTest { 14 | 15 | func test_InsertionObservation_WhenSuccessful_InvokesChnages() { 16 | let request = FetchRequest() 17 | let observable = dbClient.observable(for: request) 18 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 19 | let expectationObject = expectation(description: "Object") 20 | var expectedInsertedObjects = [User]() 21 | 22 | observable.observe { (change: ObservableChange) in 23 | switch change { 24 | case .change(let change): 25 | expectedInsertedObjects = change.insertions.map { $0.element } 26 | expectationObject.fulfill() 27 | default: break 28 | } 29 | } 30 | 31 | dbClient.insert(objectsToCreate) { _ in } 32 | 33 | waitForExpectations(timeout: 1) { _ in 34 | XCTAssertEqual(expectedInsertedObjects.sorted(), objectsToCreate.sorted()) 35 | } 36 | } 37 | 38 | func test_UpdationObservation_WhenSuccessful_InvokesChnages() { 39 | let request = FetchRequest() 40 | let observable = dbClient.observable(for: request) 41 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 42 | let expectationObject = expectation(description: "Changes observe") 43 | var expectedUpdatedObjects = [User]() 44 | 45 | observable.observe { (change: ObservableChange) in 46 | switch change { 47 | case .change(let change): 48 | if !change.modifications.isEmpty { 49 | expectedUpdatedObjects = change.modifications.map { $0.element } 50 | expectationObject.fulfill() 51 | } 52 | default: break 53 | } 54 | } 55 | 56 | let updateExpectation = expectation(description: "Insert and update") 57 | dbClient.insert(objectsToCreate) { _ in 58 | objectsToCreate.forEach { $0.mutate() } 59 | self.dbClient.update(objectsToCreate) { _ in 60 | updateExpectation.fulfill() 61 | } 62 | } 63 | 64 | waitForExpectations(timeout: 1) { _ in 65 | XCTAssertEqual(expectedUpdatedObjects.sorted(), objectsToCreate.sorted()) 66 | } 67 | } 68 | 69 | func test_DeletionObservation_WhenSuccessful_InvokesChnages() { 70 | let request = FetchRequest() 71 | let observable = dbClient.observable(for: request) 72 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 73 | let expectationObject = expectation(description: "Object") 74 | var expectedDeletedObjectsCount = 0 75 | 76 | observable.observe { (change: ObservableChange) in 77 | switch change { 78 | case .change(let change): 79 | if !change.deletions.isEmpty { 80 | expectedDeletedObjectsCount = change.deletions.count 81 | expectationObject.fulfill() 82 | } 83 | default: break 84 | } 85 | } 86 | 87 | dbClient.insert(objectsToCreate) { _ in 88 | self.dbClient.delete(objectsToCreate) { _ in } 89 | } 90 | 91 | waitForExpectations(timeout: 1) { _ in 92 | XCTAssertEqual(expectedDeletedObjectsCount, objectsToCreate.count) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataUpdateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataUpdateTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class CoreDataUpdateTests: DBClientCoreDataTest { 13 | 14 | func test_SyncUpdateUserName_WhenSuccessful_SetsCorrectName() { 15 | let randomUser = User.createRandom() 16 | 17 | dbClient.insert(randomUser) 18 | randomUser.name = "Bob" 19 | let updationResult = dbClient.update(randomUser) 20 | 21 | XCTAssertEqual(randomUser, updationResult.value) 22 | } 23 | 24 | func test_UpdateUserName_WhenSuccessful_SetsCorrectName() { 25 | let randomUser = User.createRandom() 26 | let expectationObject = expectation(description: "Object") 27 | var expectedUser: User? 28 | 29 | self.dbClient.insert(randomUser) { result in 30 | randomUser.name = "Bob" 31 | self.dbClient.update(randomUser) { result in 32 | expectedUser = result.value 33 | expectationObject.fulfill() 34 | } 35 | } 36 | 37 | waitForExpectations(timeout: 1) { _ in 38 | XCTAssertEqual(expectedUser?.name, "Bob") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/CoreDataUpsertTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataUpsertTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/15/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class CoreDataUpsertTests: DBClientCoreDataTest { 13 | 14 | func test_SyncUpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { 15 | let newUsers: [User] = (0...5).map { _ in User.createRandom() } 16 | let savedUsers: [User] = (0...5).map { _ in User.createRandom() } 17 | let combinedUsers = savedUsers + newUsers 18 | 19 | dbClient.insert(savedUsers) 20 | let result = dbClient.upsert(combinedUsers) 21 | 22 | let expectedUsers = result.require().updated + result.require().inserted 23 | XCTAssertEqual(expectedUsers.sorted(), combinedUsers.sorted()) 24 | } 25 | 26 | func test_UpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { 27 | let newUsers: [User] = (0...5).map { _ in User.createRandom() } 28 | let savedUsers: [User] = (0...5).map { _ in User.createRandom() } 29 | let expectationObjects = expectation(description: "Object") 30 | var expectedUsers = [User]() 31 | let combinedUsers = savedUsers + newUsers 32 | 33 | self.dbClient.insert(savedUsers) { _ in 34 | self.dbClient.upsert(combinedUsers) { result in 35 | expectedUsers = result.require().updated + result.require().inserted 36 | expectationObjects.fulfill() 37 | } 38 | } 39 | 40 | waitForExpectations(timeout: 1) { _ in 41 | XCTAssertEqual(expectedUsers, combinedUsers) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/CoreData/DBClientCoreDataTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBClientCoreDataTest.swift 3 | // DBClientTests 4 | // 5 | // Created by Roman Kyrylenko on 10/23/18. 6 | // Copyright © 2018 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | 12 | class DBClientCoreDataTest: DBClientTest { 13 | 14 | private let client = CoreDataDBClient(forModel: "Users") 15 | override var dbClient: DBClient! { 16 | return client 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/DBClientRealmTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBClientRealmTest.swift 3 | // DBClientTests 4 | // 5 | // Created by Roman Kyrylenko on 10/23/18. 6 | // Copyright © 2018 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | import RealmSwift 12 | 13 | class DBClientRealmTest: DBClientTest { 14 | 15 | private let client = RealmDBClient(realm: try! Realm()) 16 | override var dbClient: DBClient! { 17 | return client 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmCreateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmCreateTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class RealmCreateTests: DBClientRealmTest { 13 | 14 | func test_SingleInsertion_WhenSuccessful_ReturnsObject() { 15 | let randomUser = User.createRandom() 16 | let expectationObject = expectation(description: "Object") 17 | var expectedObject: User? 18 | 19 | self.dbClient.insert(randomUser) { result in 20 | expectedObject = result.value 21 | expectationObject.fulfill() 22 | } 23 | 24 | waitForExpectations(timeout: 1) { _ in 25 | XCTAssertNotNil(expectedObject) 26 | } 27 | } 28 | 29 | func test_BulkInsertion_WhenSuccessful_ReturnsBulk() { 30 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 31 | 32 | let expectationObjects = expectation(description: "Objects") 33 | var expectedObjectsCount = 0 34 | 35 | self.dbClient.insert(randomUsers) { result in 36 | expectedObjectsCount = result.value?.count ?? 0 37 | expectationObjects.fulfill() 38 | } 39 | 40 | waitForExpectations(timeout: 1) { _ in 41 | XCTAssertEqual(expectedObjectsCount, randomUsers.count) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmDeleteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmDeleteTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class RealmDeleteTests: DBClientRealmTest { 13 | 14 | func test_SingleDeletion_WhenSuccessful_ReturnsNil() { 15 | let randomUser = User.createRandom() 16 | let expectationHit = expectation(description: "Object") 17 | var isDeleted = false 18 | 19 | self.dbClient.insert(randomUser) { result in 20 | if let object = result.value { 21 | self.dbClient.delete(object) { result in 22 | isDeleted = result.value != nil 23 | expectationHit.fulfill() 24 | } 25 | } 26 | } 27 | 28 | waitForExpectations(timeout: 1) { _ in 29 | XCTAssert(isDeleted) 30 | } 31 | } 32 | 33 | func test_BulkDeletion_WhenSuccessful_ReturnsNil() { 34 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 35 | let expectationHit = expectation(description: "Object") 36 | var isDeleted = false 37 | 38 | self.dbClient.insert(randomUsers) { result in 39 | if let objects = result.value { 40 | self.dbClient.delete(objects) { result in 41 | isDeleted = result.value != nil 42 | expectationHit.fulfill() 43 | } 44 | } 45 | } 46 | 47 | waitForExpectations(timeout: 1) { _ in 48 | XCTAssert(isDeleted) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmExecuteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmExecuteTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DBClient 11 | @testable import Example 12 | 13 | final class RealmExecuteTests: DBClientRealmTest { 14 | 15 | func test_SingleExecute_WhenSuccessful_ReturnsCount() { 16 | let randomUser = User.createRandom() 17 | let expectationObject = expectation(description: "Object") 18 | var expectedCount = 0 19 | 20 | self.dbClient.insert(randomUser) { result in 21 | if result.value != nil { 22 | let request = FetchRequest() 23 | self.dbClient.execute(request) { result in 24 | expectedCount = result.value?.count ?? 0 25 | expectationObject.fulfill() 26 | } 27 | } 28 | } 29 | 30 | waitForExpectations(timeout: 1) { _ in 31 | XCTAssertEqual(expectedCount, 1) 32 | } 33 | } 34 | 35 | func test_ExecuteWithOffset_WhenSuccessful_ReturnsCount() { 36 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 37 | let offset = 5 38 | let shiftedUsers = Array(randomUsers[offset..(fetchOffset: offset) 45 | self.dbClient.execute(request) { result in 46 | expectedCount = result.value?.count ?? 0 47 | expectationObjects.fulfill() 48 | } 49 | } 50 | } 51 | 52 | waitForExpectations(timeout: 1) { _ in 53 | XCTAssertEqual(expectedCount, shiftedUsers.count) 54 | } 55 | } 56 | 57 | func test_ExecuteWithLimit_WhenSuccessful_ReturnsCount() { 58 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 59 | let limit = 3 60 | 61 | let expectationObjects = expectation(description: "Object") 62 | var expectedCount = 0 63 | 64 | self.dbClient.insert(randomUsers) { result in 65 | if result.value != nil { 66 | let request = FetchRequest(fetchLimit: limit) 67 | self.dbClient.execute(request) { result in 68 | expectedCount = result.value?.count ?? 0 69 | expectationObjects.fulfill() 70 | } 71 | } 72 | } 73 | 74 | waitForExpectations(timeout: 1) { _ in 75 | XCTAssertEqual(expectedCount, limit) 76 | } 77 | } 78 | 79 | func test_ExecuteWithSortDescriptor_WhenSuccessful_ReturnsCount() { 80 | let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) 81 | let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending 82 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 83 | let sortedUsers = randomUsers.sorted { $0.name.compare($1.name) == order } 84 | let expectationObjects = expectation(description: "Object") 85 | var expectedUsers = [User]() 86 | 87 | self.dbClient.insert(randomUsers) { result in 88 | if result.value != nil { 89 | let request = FetchRequest(sortDescriptor: sortDescriptor) 90 | 91 | self.dbClient.execute(request) { result in 92 | expectedUsers = result.value ?? [] 93 | expectationObjects.fulfill() 94 | } 95 | } 96 | } 97 | 98 | waitForExpectations(timeout: 1) { _ in 99 | XCTAssertEqual(expectedUsers, sortedUsers) 100 | } 101 | } 102 | 103 | func test_ExecuteWithPredicate_WhenSuccessful_ReturnsCount() { 104 | let arg = "1" 105 | let predicate = NSPredicate(format: "SELF.id ENDSWITH %@", arg) 106 | let randomUsers: [User] = (0...10).map { _ in User.createRandom() } 107 | let preicatedUsers = randomUsers.filter { $0.id.hasSuffix(arg) } 108 | let expectationObjects = expectation(description: "Object") 109 | var expectedUsers = [User]() 110 | 111 | self.dbClient.insert(randomUsers) { result in 112 | guard result.value != nil else { 113 | expectationObjects.fulfill() 114 | return 115 | } 116 | let request = FetchRequest(predicate: predicate) 117 | 118 | self.dbClient.execute(request) { result in 119 | expectedUsers = result.value ?? [] 120 | expectationObjects.fulfill() 121 | } 122 | } 123 | 124 | waitForExpectations(timeout: 1) { _ in 125 | XCTAssertEqual(expectedUsers, preicatedUsers) 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmFetchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmFetchTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import DBClient 12 | @testable import Example 13 | 14 | class RealmFetchTests: DBClientRealmTest { 15 | 16 | func test_SyncFetch_WhenSuccessful_ReturnObject() { 17 | let randomUser = User.createRandom() 18 | let expectationObject = expectation(description: "Inserting object") 19 | 20 | self.dbClient.insert(randomUser) { _ in expectationObject.fulfill() } 21 | 22 | waitForExpectations(timeout: 1) { _ in 23 | let result = self.dbClient.execute(FetchRequest(predicate: NSPredicate(format: "id == %@", randomUser.id))) 24 | XCTAssertEqual(result.require().count, 1) 25 | let object = result.require().first 26 | XCTAssertEqual(object, randomUser) 27 | } 28 | } 29 | 30 | func test_SingleFetch_WhenSuccessful_ReturnsObject() { 31 | let randomUser = User.createRandom() 32 | let expectationObject = expectation(description: "Object") 33 | var expectedObject: User? 34 | 35 | self.dbClient.insert(randomUser) { result in 36 | self.dbClient.findFirst(User.self, primaryValue: randomUser.id) { result in 37 | expectedObject = result.require() 38 | expectationObject.fulfill() 39 | } 40 | } 41 | 42 | waitForExpectations(timeout: 5) { _ in 43 | XCTAssertNotNil(expectedObject) 44 | } 45 | } 46 | 47 | func test_BulkFetch_WhenSuccessful_ReturnsBulk() { 48 | let randomUsers: [User] = (0...100).map { _ in User.createRandom() } 49 | 50 | let expectationObjects = expectation(description: "Objects") 51 | var expectedObjectsCount = 0 52 | 53 | self.dbClient.insert(randomUsers) { result in 54 | self.dbClient.findAll { (result: Result<[User]>) in 55 | expectedObjectsCount = result.value?.count ?? 0 56 | expectationObjects.fulfill() 57 | } 58 | } 59 | 60 | waitForExpectations(timeout: 1) { _ in 61 | XCTAssertEqual(expectedObjectsCount, randomUsers.count) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmObservableTests.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// RealmObservableTests.swift 3 | //// DBClient-Example 4 | //// 5 | //// Created by Roman Kyrylenko on 2/13/17. 6 | //// Copyright © 2017 Yalantis. All rights reserved. 7 | //// 8 | 9 | import XCTest 10 | import DBClient 11 | @testable import Example 12 | 13 | final class RealmObservableTests: DBClientRealmTest { 14 | 15 | func test_InsertionObservation_WhenSuccessful_InvokesChnages() { 16 | let request = FetchRequest() 17 | let observable = dbClient.observable(for: request) 18 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 19 | let expectationObject = expectation(description: "Object") 20 | var expectedInsertedObjects = [User]() 21 | 22 | observable.observe { (change: ObservableChange) in 23 | switch change { 24 | case .change(let change): 25 | expectedInsertedObjects = change.insertions.map { $0.element } 26 | expectationObject.fulfill() 27 | default: break 28 | } 29 | } 30 | 31 | dbClient.insert(objectsToCreate) { _ in } 32 | 33 | waitForExpectations(timeout: 1) { _ in 34 | XCTAssertEqual(expectedInsertedObjects.sorted(), objectsToCreate.sorted()) 35 | } 36 | } 37 | 38 | func test_UpdationObservation_WhenSuccessful_InvokesChnages() { 39 | let request = FetchRequest() 40 | let observable = dbClient.observable(for: request) 41 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 42 | let expectationObject = expectation(description: "Changes observe") 43 | var expectedUpdatedObjects = [User]() 44 | 45 | observable.observe { (change: ObservableChange) in 46 | switch change { 47 | case .change(let change): 48 | if !change.modifications.isEmpty { 49 | expectedUpdatedObjects = change.modifications.map { $0.element } 50 | expectationObject.fulfill() 51 | } 52 | default: break 53 | } 54 | } 55 | 56 | let updateExpectation = expectation(description: "Insert and update") 57 | dbClient.insert(objectsToCreate) { _ in 58 | objectsToCreate.forEach { $0.mutate() } 59 | self.dbClient.update(objectsToCreate) { _ in 60 | updateExpectation.fulfill() 61 | } 62 | } 63 | 64 | waitForExpectations(timeout: 1) { _ in 65 | XCTAssertEqual(expectedUpdatedObjects.sorted(), objectsToCreate.sorted()) 66 | } 67 | } 68 | 69 | func test_DeletionObservation_WhenSuccessful_InvokesChnages() { 70 | let request = FetchRequest() 71 | let observable = dbClient.observable(for: request) 72 | let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } 73 | let expectationObject = expectation(description: "Object") 74 | var expectedDeletedObjectsCount = 0 75 | 76 | observable.observe { (change: ObservableChange) in 77 | switch change { 78 | case .change(let change): 79 | if !change.deletions.isEmpty { 80 | expectedDeletedObjectsCount = change.deletions.count 81 | expectationObject.fulfill() 82 | } 83 | default: break 84 | } 85 | } 86 | 87 | dbClient.insert(objectsToCreate) { _ in 88 | self.dbClient.delete(objectsToCreate) { _ in } 89 | } 90 | 91 | waitForExpectations(timeout: 1) { _ in 92 | XCTAssertEqual(expectedDeletedObjectsCount, objectsToCreate.count) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmUpdateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmUpdateTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class RealmUpdateTests: DBClientRealmTest { 13 | 14 | func test_UpdateUserName_WhenSuccessful_SetsCorrectName() { 15 | let randomUser = User.createRandom() 16 | let expectationObject = expectation(description: "Object") 17 | var expectedUser: User? 18 | 19 | self.dbClient.insert(randomUser) { result in 20 | randomUser.name = "Bob" 21 | self.dbClient.update(randomUser) { result in 22 | expectedUser = result.value 23 | expectationObject.fulfill() 24 | } 25 | } 26 | 27 | waitForExpectations(timeout: 1) { _ in 28 | XCTAssertEqual(expectedUser?.name, "Bob") 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Example/DBClientTests/Interface/Realm/RealmUpsertTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmUpsertTests.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/15/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | final class RealmUpsertTests: DBClientRealmTest { 13 | 14 | func test_UpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { 15 | let newUsers: [User] = (0...5).map { _ in User.createRandom() } 16 | let savedUsers: [User] = (0...5).map { _ in User.createRandom() } 17 | let expectationObjects = expectation(description: "Object") 18 | var expectedUsers = [User]() 19 | let combinedUsers = savedUsers + newUsers 20 | 21 | self.dbClient.insert(savedUsers) { _ in 22 | self.dbClient.upsert(combinedUsers) { result in 23 | expectedUsers = result.require().updated + result.require().inserted 24 | expectationObjects.fulfill() 25 | } 26 | } 27 | 28 | waitForExpectations(timeout: 1) { _ in 29 | XCTAssertEqual(expectedUsers, combinedUsers) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Example/DBClientTests/User+Comparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+Comparable.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/9/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | @testable import Example 10 | 11 | // allows us to use `.sorted()` on the array of `User objects 12 | extension User: Comparable { 13 | 14 | public static func < (lhs: User, rhs: User) -> Bool { 15 | return lhs.id < rhs.id 16 | } 17 | 18 | public static func <= (lhs: User, rhs: User) -> Bool { 19 | return lhs.id <= rhs.id 20 | } 21 | 22 | public static func >= (lhs: User, rhs: User) -> Bool { 23 | return lhs.id >= rhs.id 24 | } 25 | 26 | public static func > (lhs: User, rhs: User) -> Bool { 27 | return rhs.id > rhs.id 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Example/DBClientTests/User+Equtable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+Equtable.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 2/8/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | @testable import Example 10 | 11 | // allows us to use `XCAssertEqual` on `User` objects 12 | extension User: Equatable { 13 | 14 | public static func == (lhs: User, rhs: User) -> Bool { 15 | return lhs.id == rhs.id && lhs.name == rhs.name 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6544BEF6B0A326E74A0930F3 /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EE3B934F48958491C32E38F /* Pods_Example.framework */; }; 11 | 91146657A7C9C72AEB2CA0A0 /* Pods_DBClientTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */; }; 12 | B8275B001E4B6D2600232EE4 /* DBClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */; }; 13 | B8477EA11E4DC0EA00608B78 /* User+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */; }; 14 | B8477EA21E4DC0EA00608B78 /* User+Equtable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */; }; 15 | B8D2D430217F35200069CC57 /* CoreDataUpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */; }; 16 | B8D2D431217F35200069CC57 /* CoreDataExecuteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */; }; 17 | B8D2D432217F35200069CC57 /* CoreDataDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */; }; 18 | B8D2D433217F35200069CC57 /* CoreDataUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */; }; 19 | B8D2D434217F35200069CC57 /* CoreDataObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */; }; 20 | B8D2D435217F35200069CC57 /* CoreDataFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */; }; 21 | B8D2D436217F35200069CC57 /* CoreDataCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */; }; 22 | B8D2D43E217F35260069CC57 /* RealmUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */; }; 23 | B8D2D43F217F35260069CC57 /* RealmCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D438217F35260069CC57 /* RealmCreateTests.swift */; }; 24 | B8D2D440217F35260069CC57 /* RealmDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */; }; 25 | B8D2D441217F35260069CC57 /* RealmUpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */; }; 26 | B8D2D442217F35260069CC57 /* RealmExecuteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */; }; 27 | B8D2D443217F35260069CC57 /* RealmFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */; }; 28 | B8D2D444217F35260069CC57 /* RealmObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */; }; 29 | B8D2D446217F36B40069CC57 /* DBClientRealmTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */; }; 30 | B8D2D448217F36DC0069CC57 /* DBClientCoreDataTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */; }; 31 | C533725A1E26155D004ECBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372591E26155D004ECBCF /* AppDelegate.swift */; }; 32 | C533725C1E26155D004ECBCF /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C533725B1E26155D004ECBCF /* MasterViewController.swift */; }; 33 | C533725E1E26155D004ECBCF /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C533725D1E26155D004ECBCF /* DetailViewController.swift */; }; 34 | C53372611E26155D004ECBCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C533725F1E26155D004ECBCF /* Main.storyboard */; }; 35 | C53372631E26155D004ECBCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C53372621E26155D004ECBCF /* Assets.xcassets */; }; 36 | C53372661E26155D004ECBCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C53372641E26155D004ECBCF /* LaunchScreen.storyboard */; }; 37 | C53372791E261A30004ECBCF /* DBClientInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = C533726D1E261A30004ECBCF /* DBClientInjector.swift */; }; 38 | C533727A1E261A30004ECBCF /* ManagedUser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372701E261A30004ECBCF /* ManagedUser+CoreDataClass.swift */; }; 39 | C533727B1E261A30004ECBCF /* ManagedUser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372711E261A30004ECBCF /* ManagedUser+CoreDataProperties.swift */; }; 40 | C533727C1E261A30004ECBCF /* User+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372721E261A30004ECBCF /* User+CoreData.swift */; }; 41 | C533727D1E261A30004ECBCF /* Users.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C53372731E261A30004ECBCF /* Users.xcdatamodeld */; }; 42 | C533727E1E261A30004ECBCF /* ObjectUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372761E261A30004ECBCF /* ObjectUser.swift */; }; 43 | C533727F1E261A30004ECBCF /* User+Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372771E261A30004ECBCF /* User+Realm.swift */; }; 44 | C53372801E261A30004ECBCF /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372781E261A30004ECBCF /* User.swift */; }; 45 | /* End PBXBuildFile section */ 46 | 47 | /* Begin PBXContainerItemProxy section */ 48 | B8275B021E4B6D2600232EE4 /* PBXContainerItemProxy */ = { 49 | isa = PBXContainerItemProxy; 50 | containerPortal = C533724E1E26155D004ECBCF /* Project object */; 51 | proxyType = 1; 52 | remoteGlobalIDString = C53372551E26155D004ECBCF; 53 | remoteInfo = Example; 54 | }; 55 | /* End PBXContainerItemProxy section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DBClientTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DBClientTests/Pods-DBClientTests.release.xcconfig"; sourceTree = ""; }; 59 | 8EE3B934F48958491C32E38F /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 9A5965F4590A17BA658C0DF4 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = ""; }; 61 | 9D0BE2A63D58FAA10DB1082A /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = ""; }; 62 | A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DBClientTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DBClientTests/Pods-DBClientTests.debug.xcconfig"; sourceTree = ""; }; 63 | B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DBClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientTest.swift; sourceTree = ""; }; 65 | B8275B011E4B6D2600232EE4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Comparable.swift"; sourceTree = ""; }; 67 | B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Equtable.swift"; sourceTree = ""; }; 68 | B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataUpsertTests.swift; sourceTree = ""; }; 69 | B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataExecuteTests.swift; sourceTree = ""; }; 70 | B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataDeleteTests.swift; sourceTree = ""; }; 71 | B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataUpdateTests.swift; sourceTree = ""; }; 72 | B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObservableTests.swift; sourceTree = ""; }; 73 | B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataFetchTests.swift; sourceTree = ""; }; 74 | B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataCreateTests.swift; sourceTree = ""; }; 75 | B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmUpdateTests.swift; sourceTree = ""; }; 76 | B8D2D438217F35260069CC57 /* RealmCreateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmCreateTests.swift; sourceTree = ""; }; 77 | B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmDeleteTests.swift; sourceTree = ""; }; 78 | B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmUpsertTests.swift; sourceTree = ""; }; 79 | B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmExecuteTests.swift; sourceTree = ""; }; 80 | B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmFetchTests.swift; sourceTree = ""; }; 81 | B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmObservableTests.swift; sourceTree = ""; }; 82 | B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientRealmTest.swift; sourceTree = ""; }; 83 | B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientCoreDataTest.swift; sourceTree = ""; }; 84 | C53372561E26155D004ECBCF /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 85 | C53372591E26155D004ECBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 86 | C533725B1E26155D004ECBCF /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; 87 | C533725D1E26155D004ECBCF /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 88 | C53372601E26155D004ECBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 89 | C53372621E26155D004ECBCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 90 | C53372651E26155D004ECBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 91 | C53372671E26155D004ECBCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 92 | C533726D1E261A30004ECBCF /* DBClientInjector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DBClientInjector.swift; sourceTree = ""; }; 93 | C53372701E261A30004ECBCF /* ManagedUser+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ManagedUser+CoreDataClass.swift"; sourceTree = ""; }; 94 | C53372711E261A30004ECBCF /* ManagedUser+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ManagedUser+CoreDataProperties.swift"; sourceTree = ""; }; 95 | C53372721E261A30004ECBCF /* User+CoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+CoreData.swift"; sourceTree = ""; }; 96 | C53372741E261A30004ECBCF /* Users.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Users.xcdatamodel; sourceTree = ""; }; 97 | C53372761E261A30004ECBCF /* ObjectUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectUser.swift; sourceTree = ""; }; 98 | C53372771E261A30004ECBCF /* User+Realm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Realm.swift"; sourceTree = ""; }; 99 | C53372781E261A30004ECBCF /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 100 | F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DBClientTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | /* End PBXFileReference section */ 102 | 103 | /* Begin PBXFrameworksBuildPhase section */ 104 | B8275AFA1E4B6D2500232EE4 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 91146657A7C9C72AEB2CA0A0 /* Pods_DBClientTests.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | C53372531E26155D004ECBCF /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | 6544BEF6B0A326E74A0930F3 /* Pods_Example.framework in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXFrameworksBuildPhase section */ 121 | 122 | /* Begin PBXGroup section */ 123 | 508C283B1E64110854D66E0E /* Pods */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 9A5965F4590A17BA658C0DF4 /* Pods-Example.debug.xcconfig */, 127 | 9D0BE2A63D58FAA10DB1082A /* Pods-Example.release.xcconfig */, 128 | A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */, 129 | 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */, 130 | ); 131 | name = Pods; 132 | sourceTree = ""; 133 | }; 134 | 779BEA8364EDA4F88D3C7A7B /* Frameworks */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 8EE3B934F48958491C32E38F /* Pods_Example.framework */, 138 | F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */, 139 | ); 140 | name = Frameworks; 141 | sourceTree = ""; 142 | }; 143 | B8275AFE1E4B6D2600232EE4 /* DBClientTests */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */, 147 | B8275B011E4B6D2600232EE4 /* Info.plist */, 148 | B8477E941E4DC0EA00608B78 /* Interface */, 149 | B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */, 150 | B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */, 151 | ); 152 | path = DBClientTests; 153 | sourceTree = ""; 154 | }; 155 | B8477E941E4DC0EA00608B78 /* Interface */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | B8D2D427217F35010069CC57 /* CoreData */, 159 | B8D2D428217F350B0069CC57 /* Realm */, 160 | ); 161 | path = Interface; 162 | sourceTree = ""; 163 | }; 164 | B8D2D427217F35010069CC57 /* CoreData */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */, 168 | B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */, 169 | B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */, 170 | B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */, 171 | B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */, 172 | B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */, 173 | B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */, 174 | B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */, 175 | ); 176 | path = CoreData; 177 | sourceTree = ""; 178 | }; 179 | B8D2D428217F350B0069CC57 /* Realm */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */, 183 | B8D2D438217F35260069CC57 /* RealmCreateTests.swift */, 184 | B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */, 185 | B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */, 186 | B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */, 187 | B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */, 188 | B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */, 189 | B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */, 190 | ); 191 | path = Realm; 192 | sourceTree = ""; 193 | }; 194 | C533724D1E26155D004ECBCF = { 195 | isa = PBXGroup; 196 | children = ( 197 | B8275AFE1E4B6D2600232EE4 /* DBClientTests */, 198 | C53372581E26155D004ECBCF /* Example */, 199 | 779BEA8364EDA4F88D3C7A7B /* Frameworks */, 200 | 508C283B1E64110854D66E0E /* Pods */, 201 | C53372571E26155D004ECBCF /* Products */, 202 | ); 203 | sourceTree = ""; 204 | }; 205 | C53372571E26155D004ECBCF /* Products */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | C53372561E26155D004ECBCF /* Example.app */, 209 | B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */, 210 | ); 211 | name = Products; 212 | sourceTree = ""; 213 | }; 214 | C53372581E26155D004ECBCF /* Example */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | C53372591E26155D004ECBCF /* AppDelegate.swift */, 218 | C53372621E26155D004ECBCF /* Assets.xcassets */, 219 | C533726D1E261A30004ECBCF /* DBClientInjector.swift */, 220 | C533725D1E26155D004ECBCF /* DetailViewController.swift */, 221 | C53372671E26155D004ECBCF /* Info.plist */, 222 | C53372641E26155D004ECBCF /* LaunchScreen.storyboard */, 223 | C533725F1E26155D004ECBCF /* Main.storyboard */, 224 | C533725B1E26155D004ECBCF /* MasterViewController.swift */, 225 | C533726E1E261A30004ECBCF /* Models */, 226 | ); 227 | path = Example; 228 | sourceTree = ""; 229 | }; 230 | C533726E1E261A30004ECBCF /* Models */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | C533726F1E261A30004ECBCF /* CoreData */, 234 | C53372751E261A30004ECBCF /* Realm */, 235 | C53372781E261A30004ECBCF /* User.swift */, 236 | ); 237 | path = Models; 238 | sourceTree = ""; 239 | }; 240 | C533726F1E261A30004ECBCF /* CoreData */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | C53372701E261A30004ECBCF /* ManagedUser+CoreDataClass.swift */, 244 | C53372711E261A30004ECBCF /* ManagedUser+CoreDataProperties.swift */, 245 | C53372721E261A30004ECBCF /* User+CoreData.swift */, 246 | C53372731E261A30004ECBCF /* Users.xcdatamodeld */, 247 | ); 248 | path = CoreData; 249 | sourceTree = ""; 250 | }; 251 | C53372751E261A30004ECBCF /* Realm */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | C53372761E261A30004ECBCF /* ObjectUser.swift */, 255 | C53372771E261A30004ECBCF /* User+Realm.swift */, 256 | ); 257 | path = Realm; 258 | sourceTree = ""; 259 | }; 260 | /* End PBXGroup section */ 261 | 262 | /* Begin PBXNativeTarget section */ 263 | B8275AFC1E4B6D2500232EE4 /* DBClientTests */ = { 264 | isa = PBXNativeTarget; 265 | buildConfigurationList = B8275B061E4B6D2600232EE4 /* Build configuration list for PBXNativeTarget "DBClientTests" */; 266 | buildPhases = ( 267 | BF151D4AF0AD5B6D29F45416 /* [CP] Check Pods Manifest.lock */, 268 | B8275AF91E4B6D2500232EE4 /* Sources */, 269 | B8275AFA1E4B6D2500232EE4 /* Frameworks */, 270 | B8275AFB1E4B6D2500232EE4 /* Resources */, 271 | 3C04B97117E537708993B649 /* [CP] Embed Pods Frameworks */, 272 | ); 273 | buildRules = ( 274 | ); 275 | dependencies = ( 276 | B8275B031E4B6D2600232EE4 /* PBXTargetDependency */, 277 | ); 278 | name = DBClientTests; 279 | productName = DBClientTests; 280 | productReference = B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */; 281 | productType = "com.apple.product-type.bundle.unit-test"; 282 | }; 283 | C53372551E26155D004ECBCF /* Example */ = { 284 | isa = PBXNativeTarget; 285 | buildConfigurationList = C533726A1E26155D004ECBCF /* Build configuration list for PBXNativeTarget "Example" */; 286 | buildPhases = ( 287 | 7A7AA963D82D9DF854F879D4 /* [CP] Check Pods Manifest.lock */, 288 | C53372521E26155D004ECBCF /* Sources */, 289 | C53372531E26155D004ECBCF /* Frameworks */, 290 | C53372541E26155D004ECBCF /* Resources */, 291 | 5DD0265501157C9DFB18D487 /* [CP] Embed Pods Frameworks */, 292 | 8839DF70215C08D90021A85A /* ShellScript */, 293 | ); 294 | buildRules = ( 295 | ); 296 | dependencies = ( 297 | ); 298 | name = Example; 299 | productName = Example; 300 | productReference = C53372561E26155D004ECBCF /* Example.app */; 301 | productType = "com.apple.product-type.application"; 302 | }; 303 | /* End PBXNativeTarget section */ 304 | 305 | /* Begin PBXProject section */ 306 | C533724E1E26155D004ECBCF /* Project object */ = { 307 | isa = PBXProject; 308 | attributes = { 309 | LastSwiftUpdateCheck = 0820; 310 | LastUpgradeCheck = 1000; 311 | ORGANIZATIONNAME = Yalantis; 312 | TargetAttributes = { 313 | B8275AFC1E4B6D2500232EE4 = { 314 | CreatedOnToolsVersion = 8.2.1; 315 | LastSwiftMigration = 1000; 316 | ProvisioningStyle = Automatic; 317 | TestTargetID = C53372551E26155D004ECBCF; 318 | }; 319 | C53372551E26155D004ECBCF = { 320 | CreatedOnToolsVersion = 8.2.1; 321 | LastSwiftMigration = 1000; 322 | ProvisioningStyle = Automatic; 323 | }; 324 | }; 325 | }; 326 | buildConfigurationList = C53372511E26155D004ECBCF /* Build configuration list for PBXProject "Example" */; 327 | compatibilityVersion = "Xcode 3.2"; 328 | developmentRegion = English; 329 | hasScannedForEncodings = 0; 330 | knownRegions = ( 331 | English, 332 | en, 333 | Base, 334 | ); 335 | mainGroup = C533724D1E26155D004ECBCF; 336 | productRefGroup = C53372571E26155D004ECBCF /* Products */; 337 | projectDirPath = ""; 338 | projectRoot = ""; 339 | targets = ( 340 | C53372551E26155D004ECBCF /* Example */, 341 | B8275AFC1E4B6D2500232EE4 /* DBClientTests */, 342 | ); 343 | }; 344 | /* End PBXProject section */ 345 | 346 | /* Begin PBXResourcesBuildPhase section */ 347 | B8275AFB1E4B6D2500232EE4 /* Resources */ = { 348 | isa = PBXResourcesBuildPhase; 349 | buildActionMask = 2147483647; 350 | files = ( 351 | ); 352 | runOnlyForDeploymentPostprocessing = 0; 353 | }; 354 | C53372541E26155D004ECBCF /* Resources */ = { 355 | isa = PBXResourcesBuildPhase; 356 | buildActionMask = 2147483647; 357 | files = ( 358 | C53372661E26155D004ECBCF /* LaunchScreen.storyboard in Resources */, 359 | C53372631E26155D004ECBCF /* Assets.xcassets in Resources */, 360 | C53372611E26155D004ECBCF /* Main.storyboard in Resources */, 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | }; 364 | /* End PBXResourcesBuildPhase section */ 365 | 366 | /* Begin PBXShellScriptBuildPhase section */ 367 | 3C04B97117E537708993B649 /* [CP] Embed Pods Frameworks */ = { 368 | isa = PBXShellScriptBuildPhase; 369 | buildActionMask = 2147483647; 370 | files = ( 371 | ); 372 | inputPaths = ( 373 | "${PODS_ROOT}/Target Support Files/Pods-DBClientTests/Pods-DBClientTests-frameworks.sh", 374 | "${BUILT_PRODUCTS_DIR}/DBClient/DBClient.framework", 375 | "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", 376 | "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", 377 | "${BUILT_PRODUCTS_DIR}/YALResult/YALResult.framework", 378 | ); 379 | name = "[CP] Embed Pods Frameworks"; 380 | outputPaths = ( 381 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DBClient.framework", 382 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", 383 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", 384 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YALResult.framework", 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | shellPath = /bin/sh; 388 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DBClientTests/Pods-DBClientTests-frameworks.sh\"\n"; 389 | showEnvVarsInLog = 0; 390 | }; 391 | 5DD0265501157C9DFB18D487 /* [CP] Embed Pods Frameworks */ = { 392 | isa = PBXShellScriptBuildPhase; 393 | buildActionMask = 2147483647; 394 | files = ( 395 | ); 396 | inputPaths = ( 397 | "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh", 398 | "${BUILT_PRODUCTS_DIR}/DBClient/DBClient.framework", 399 | "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", 400 | "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", 401 | "${BUILT_PRODUCTS_DIR}/YALResult/YALResult.framework", 402 | ); 403 | name = "[CP] Embed Pods Frameworks"; 404 | outputPaths = ( 405 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DBClient.framework", 406 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", 407 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", 408 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YALResult.framework", 409 | ); 410 | runOnlyForDeploymentPostprocessing = 0; 411 | shellPath = /bin/sh; 412 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n"; 413 | showEnvVarsInLog = 0; 414 | }; 415 | 7A7AA963D82D9DF854F879D4 /* [CP] Check Pods Manifest.lock */ = { 416 | isa = PBXShellScriptBuildPhase; 417 | buildActionMask = 2147483647; 418 | files = ( 419 | ); 420 | inputPaths = ( 421 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 422 | "${PODS_ROOT}/Manifest.lock", 423 | ); 424 | name = "[CP] Check Pods Manifest.lock"; 425 | outputPaths = ( 426 | "$(DERIVED_FILE_DIR)/Pods-Example-checkManifestLockResult.txt", 427 | ); 428 | runOnlyForDeploymentPostprocessing = 0; 429 | shellPath = /bin/sh; 430 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 431 | showEnvVarsInLog = 0; 432 | }; 433 | 8839DF70215C08D90021A85A /* ShellScript */ = { 434 | isa = PBXShellScriptBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | ); 438 | inputFileListPaths = ( 439 | ); 440 | inputPaths = ( 441 | ); 442 | outputFileListPaths = ( 443 | ); 444 | outputPaths = ( 445 | ); 446 | runOnlyForDeploymentPostprocessing = 0; 447 | shellPath = /bin/sh; 448 | shellScript = "\n"; 449 | }; 450 | BF151D4AF0AD5B6D29F45416 /* [CP] Check Pods Manifest.lock */ = { 451 | isa = PBXShellScriptBuildPhase; 452 | buildActionMask = 2147483647; 453 | files = ( 454 | ); 455 | inputPaths = ( 456 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 457 | "${PODS_ROOT}/Manifest.lock", 458 | ); 459 | name = "[CP] Check Pods Manifest.lock"; 460 | outputPaths = ( 461 | "$(DERIVED_FILE_DIR)/Pods-DBClientTests-checkManifestLockResult.txt", 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | shellPath = /bin/sh; 465 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 466 | showEnvVarsInLog = 0; 467 | }; 468 | /* End PBXShellScriptBuildPhase section */ 469 | 470 | /* Begin PBXSourcesBuildPhase section */ 471 | B8275AF91E4B6D2500232EE4 /* Sources */ = { 472 | isa = PBXSourcesBuildPhase; 473 | buildActionMask = 2147483647; 474 | files = ( 475 | B8D2D434217F35200069CC57 /* CoreDataObservableTests.swift in Sources */, 476 | B8D2D442217F35260069CC57 /* RealmExecuteTests.swift in Sources */, 477 | B8D2D435217F35200069CC57 /* CoreDataFetchTests.swift in Sources */, 478 | B8D2D444217F35260069CC57 /* RealmObservableTests.swift in Sources */, 479 | B8D2D441217F35260069CC57 /* RealmUpsertTests.swift in Sources */, 480 | B8275B001E4B6D2600232EE4 /* DBClientTest.swift in Sources */, 481 | B8D2D43F217F35260069CC57 /* RealmCreateTests.swift in Sources */, 482 | B8D2D436217F35200069CC57 /* CoreDataCreateTests.swift in Sources */, 483 | B8D2D448217F36DC0069CC57 /* DBClientCoreDataTest.swift in Sources */, 484 | B8D2D431217F35200069CC57 /* CoreDataExecuteTests.swift in Sources */, 485 | B8D2D443217F35260069CC57 /* RealmFetchTests.swift in Sources */, 486 | B8D2D433217F35200069CC57 /* CoreDataUpdateTests.swift in Sources */, 487 | B8D2D440217F35260069CC57 /* RealmDeleteTests.swift in Sources */, 488 | B8D2D432217F35200069CC57 /* CoreDataDeleteTests.swift in Sources */, 489 | B8477EA21E4DC0EA00608B78 /* User+Equtable.swift in Sources */, 490 | B8D2D446217F36B40069CC57 /* DBClientRealmTest.swift in Sources */, 491 | B8D2D43E217F35260069CC57 /* RealmUpdateTests.swift in Sources */, 492 | B8D2D430217F35200069CC57 /* CoreDataUpsertTests.swift in Sources */, 493 | B8477EA11E4DC0EA00608B78 /* User+Comparable.swift in Sources */, 494 | ); 495 | runOnlyForDeploymentPostprocessing = 0; 496 | }; 497 | C53372521E26155D004ECBCF /* Sources */ = { 498 | isa = PBXSourcesBuildPhase; 499 | buildActionMask = 2147483647; 500 | files = ( 501 | C533727F1E261A30004ECBCF /* User+Realm.swift in Sources */, 502 | C533727C1E261A30004ECBCF /* User+CoreData.swift in Sources */, 503 | C533727E1E261A30004ECBCF /* ObjectUser.swift in Sources */, 504 | C53372791E261A30004ECBCF /* DBClientInjector.swift in Sources */, 505 | C533727A1E261A30004ECBCF /* ManagedUser+CoreDataClass.swift in Sources */, 506 | C533725E1E26155D004ECBCF /* DetailViewController.swift in Sources */, 507 | C533727D1E261A30004ECBCF /* Users.xcdatamodeld in Sources */, 508 | C533725C1E26155D004ECBCF /* MasterViewController.swift in Sources */, 509 | C533727B1E261A30004ECBCF /* ManagedUser+CoreDataProperties.swift in Sources */, 510 | C533725A1E26155D004ECBCF /* AppDelegate.swift in Sources */, 511 | C53372801E261A30004ECBCF /* User.swift in Sources */, 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | /* End PBXSourcesBuildPhase section */ 516 | 517 | /* Begin PBXTargetDependency section */ 518 | B8275B031E4B6D2600232EE4 /* PBXTargetDependency */ = { 519 | isa = PBXTargetDependency; 520 | target = C53372551E26155D004ECBCF /* Example */; 521 | targetProxy = B8275B021E4B6D2600232EE4 /* PBXContainerItemProxy */; 522 | }; 523 | /* End PBXTargetDependency section */ 524 | 525 | /* Begin PBXVariantGroup section */ 526 | C533725F1E26155D004ECBCF /* Main.storyboard */ = { 527 | isa = PBXVariantGroup; 528 | children = ( 529 | C53372601E26155D004ECBCF /* Base */, 530 | ); 531 | name = Main.storyboard; 532 | sourceTree = ""; 533 | }; 534 | C53372641E26155D004ECBCF /* LaunchScreen.storyboard */ = { 535 | isa = PBXVariantGroup; 536 | children = ( 537 | C53372651E26155D004ECBCF /* Base */, 538 | ); 539 | name = LaunchScreen.storyboard; 540 | sourceTree = ""; 541 | }; 542 | /* End PBXVariantGroup section */ 543 | 544 | /* Begin XCBuildConfiguration section */ 545 | B8275B041E4B6D2600232EE4 /* Debug */ = { 546 | isa = XCBuildConfiguration; 547 | baseConfigurationReference = A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */; 548 | buildSettings = { 549 | BUNDLE_LOADER = "$(TEST_HOST)"; 550 | INFOPLIST_FILE = DBClientTests/Info.plist; 551 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 552 | PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; 553 | PRODUCT_NAME = "$(TARGET_NAME)"; 554 | SWIFT_VERSION = 5.0; 555 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 556 | }; 557 | name = Debug; 558 | }; 559 | B8275B051E4B6D2600232EE4 /* Release */ = { 560 | isa = XCBuildConfiguration; 561 | baseConfigurationReference = 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */; 562 | buildSettings = { 563 | BUNDLE_LOADER = "$(TEST_HOST)"; 564 | INFOPLIST_FILE = DBClientTests/Info.plist; 565 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 566 | PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; 567 | PRODUCT_NAME = "$(TARGET_NAME)"; 568 | SWIFT_VERSION = 5.0; 569 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 570 | }; 571 | name = Release; 572 | }; 573 | C53372681E26155D004ECBCF /* Debug */ = { 574 | isa = XCBuildConfiguration; 575 | buildSettings = { 576 | ALWAYS_SEARCH_USER_PATHS = NO; 577 | CLANG_ANALYZER_NONNULL = YES; 578 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 579 | CLANG_CXX_LIBRARY = "libc++"; 580 | CLANG_ENABLE_MODULES = YES; 581 | CLANG_ENABLE_OBJC_ARC = YES; 582 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 583 | CLANG_WARN_BOOL_CONVERSION = YES; 584 | CLANG_WARN_COMMA = YES; 585 | CLANG_WARN_CONSTANT_CONVERSION = YES; 586 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 587 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 588 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 589 | CLANG_WARN_EMPTY_BODY = YES; 590 | CLANG_WARN_ENUM_CONVERSION = YES; 591 | CLANG_WARN_INFINITE_RECURSION = YES; 592 | CLANG_WARN_INT_CONVERSION = YES; 593 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 594 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 595 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 596 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 597 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 598 | CLANG_WARN_STRICT_PROTOTYPES = YES; 599 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 600 | CLANG_WARN_UNREACHABLE_CODE = YES; 601 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 602 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 603 | COPY_PHASE_STRIP = NO; 604 | DEBUG_INFORMATION_FORMAT = dwarf; 605 | ENABLE_STRICT_OBJC_MSGSEND = YES; 606 | ENABLE_TESTABILITY = YES; 607 | GCC_C_LANGUAGE_STANDARD = gnu99; 608 | GCC_DYNAMIC_NO_PIC = NO; 609 | GCC_NO_COMMON_BLOCKS = YES; 610 | GCC_OPTIMIZATION_LEVEL = 0; 611 | GCC_PREPROCESSOR_DEFINITIONS = ( 612 | "DEBUG=1", 613 | "$(inherited)", 614 | ); 615 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 616 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 617 | GCC_WARN_UNDECLARED_SELECTOR = YES; 618 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 619 | GCC_WARN_UNUSED_FUNCTION = YES; 620 | GCC_WARN_UNUSED_VARIABLE = YES; 621 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 622 | MTL_ENABLE_DEBUG_INFO = YES; 623 | ONLY_ACTIVE_ARCH = YES; 624 | SDKROOT = iphoneos; 625 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 626 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 627 | TARGETED_DEVICE_FAMILY = "1,2"; 628 | }; 629 | name = Debug; 630 | }; 631 | C53372691E26155D004ECBCF /* Release */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | ALWAYS_SEARCH_USER_PATHS = NO; 635 | CLANG_ANALYZER_NONNULL = YES; 636 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 637 | CLANG_CXX_LIBRARY = "libc++"; 638 | CLANG_ENABLE_MODULES = YES; 639 | CLANG_ENABLE_OBJC_ARC = YES; 640 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 641 | CLANG_WARN_BOOL_CONVERSION = YES; 642 | CLANG_WARN_COMMA = YES; 643 | CLANG_WARN_CONSTANT_CONVERSION = YES; 644 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 645 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 646 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 647 | CLANG_WARN_EMPTY_BODY = YES; 648 | CLANG_WARN_ENUM_CONVERSION = YES; 649 | CLANG_WARN_INFINITE_RECURSION = YES; 650 | CLANG_WARN_INT_CONVERSION = YES; 651 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 652 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 653 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 654 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 655 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 656 | CLANG_WARN_STRICT_PROTOTYPES = YES; 657 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 658 | CLANG_WARN_UNREACHABLE_CODE = YES; 659 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 660 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 661 | COPY_PHASE_STRIP = NO; 662 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 663 | ENABLE_NS_ASSERTIONS = NO; 664 | ENABLE_STRICT_OBJC_MSGSEND = YES; 665 | GCC_C_LANGUAGE_STANDARD = gnu99; 666 | GCC_NO_COMMON_BLOCKS = YES; 667 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 668 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 669 | GCC_WARN_UNDECLARED_SELECTOR = YES; 670 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 671 | GCC_WARN_UNUSED_FUNCTION = YES; 672 | GCC_WARN_UNUSED_VARIABLE = YES; 673 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 674 | MTL_ENABLE_DEBUG_INFO = NO; 675 | SDKROOT = iphoneos; 676 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 677 | TARGETED_DEVICE_FAMILY = "1,2"; 678 | VALIDATE_PRODUCT = YES; 679 | }; 680 | name = Release; 681 | }; 682 | C533726B1E26155D004ECBCF /* Debug */ = { 683 | isa = XCBuildConfiguration; 684 | baseConfigurationReference = 9A5965F4590A17BA658C0DF4 /* Pods-Example.debug.xcconfig */; 685 | buildSettings = { 686 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 687 | INFOPLIST_FILE = Example/Info.plist; 688 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 689 | PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.dbclient.Example; 690 | PRODUCT_NAME = "$(TARGET_NAME)"; 691 | SWIFT_VERSION = 5.0; 692 | }; 693 | name = Debug; 694 | }; 695 | C533726C1E26155D004ECBCF /* Release */ = { 696 | isa = XCBuildConfiguration; 697 | baseConfigurationReference = 9D0BE2A63D58FAA10DB1082A /* Pods-Example.release.xcconfig */; 698 | buildSettings = { 699 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 700 | INFOPLIST_FILE = Example/Info.plist; 701 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 702 | PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.dbclient.Example; 703 | PRODUCT_NAME = "$(TARGET_NAME)"; 704 | SWIFT_VERSION = 5.0; 705 | }; 706 | name = Release; 707 | }; 708 | /* End XCBuildConfiguration section */ 709 | 710 | /* Begin XCConfigurationList section */ 711 | B8275B061E4B6D2600232EE4 /* Build configuration list for PBXNativeTarget "DBClientTests" */ = { 712 | isa = XCConfigurationList; 713 | buildConfigurations = ( 714 | B8275B041E4B6D2600232EE4 /* Debug */, 715 | B8275B051E4B6D2600232EE4 /* Release */, 716 | ); 717 | defaultConfigurationIsVisible = 0; 718 | defaultConfigurationName = Release; 719 | }; 720 | C53372511E26155D004ECBCF /* Build configuration list for PBXProject "Example" */ = { 721 | isa = XCConfigurationList; 722 | buildConfigurations = ( 723 | C53372681E26155D004ECBCF /* Debug */, 724 | C53372691E26155D004ECBCF /* Release */, 725 | ); 726 | defaultConfigurationIsVisible = 0; 727 | defaultConfigurationName = Release; 728 | }; 729 | C533726A1E26155D004ECBCF /* Build configuration list for PBXNativeTarget "Example" */ = { 730 | isa = XCConfigurationList; 731 | buildConfigurations = ( 732 | C533726B1E26155D004ECBCF /* Debug */, 733 | C533726C1E26155D004ECBCF /* Release */, 734 | ); 735 | defaultConfigurationIsVisible = 0; 736 | defaultConfigurationName = Release; 737 | }; 738 | /* End XCConfigurationList section */ 739 | 740 | /* Begin XCVersionGroup section */ 741 | C53372731E261A30004ECBCF /* Users.xcdatamodeld */ = { 742 | isa = XCVersionGroup; 743 | children = ( 744 | C53372741E261A30004ECBCF /* Users.xcdatamodel */, 745 | ); 746 | currentVersion = C53372741E261A30004ECBCF /* Users.xcdatamodel */; 747 | path = Users.xcdatamodeld; 748 | sourceTree = ""; 749 | versionGroupType = wrapper.xcdatamodel; 750 | }; 751 | /* End XCVersionGroup section */ 752 | }; 753 | rootObject = C533724E1E26155D004ECBCF /* Project object */; 754 | } 755 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DBClient-Example 4 | // 5 | // Created by Serhii Butenko on 11/1/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 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 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Example/Example/DBClientInjector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBClientInjector.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 1/6/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DBClient 11 | import RealmSwift 12 | 13 | private struct DBClientInjector { 14 | 15 | static let coreDataClient: DBClient = CoreDataDBClient(forModel: "Users") 16 | static let realmClient: DBClient = RealmDBClient(realm: try! Realm()) 17 | } 18 | 19 | protocol DBClientInjectable {} 20 | 21 | extension DBClientInjectable { 22 | 23 | var dbClient: DBClient { 24 | // return DBClientInjector.coreDataClient 25 | return DBClientInjector.realmClient 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Example/Example/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // DBClient-Example 4 | // 5 | // Created by Serhii Butenko on 11/1/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DetailViewController: UIViewController, DBClientInjectable { 12 | 13 | var detailItem: User! 14 | 15 | @IBOutlet private weak var userNameTextField: UITextField! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | userNameTextField.text = detailItem.name 21 | title = detailItem.id 22 | } 23 | 24 | @IBAction private func saveButtonAction() { 25 | detailItem.name = userNameTextField.text ?? "" 26 | dbClient.update(detailItem) { [weak self] _ in 27 | self?.navigationController?.popViewController(animated: true) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Example/Example/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 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Example/Example/MasterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.swift 3 | // DBClient-Example 4 | // 5 | // Created by Serhii Butenko on 11/1/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DBClient 11 | 12 | final class MasterViewController: UITableViewController, DBClientInjectable { 13 | 14 | fileprivate var objects = [User]() 15 | 16 | private var userChangesObservable: RequestObservable? 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | let observable = dbClient.observable(for: FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])) 22 | userChangesObservable = observable 23 | observable.observe { [weak self] changeSet in 24 | self?.observeChanges(changeSet) 25 | } 26 | 27 | navigationItem.leftBarButtonItem = editButtonItem 28 | } 29 | 30 | // MARK: - Segues 31 | 32 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 33 | guard segue.identifier == "showDetail", 34 | let controller = segue.destination as? DetailViewController else { 35 | fatalError() 36 | } 37 | 38 | if let indexPath = tableView.indexPathForSelectedRow { 39 | let object = objects[indexPath.row] 40 | controller.detailItem = object 41 | } 42 | } 43 | 44 | // MARK: - Actions 45 | 46 | @IBAction private func addObject(_ sender: Any) { 47 | dbClient.insert(User.createRandom()) { _ in } 48 | } 49 | 50 | private func observeChanges(_ changeSet: ObservableChange) { 51 | switch changeSet { 52 | case .initial(let initial): 53 | objects.append(contentsOf: initial) 54 | tableView.reloadData() 55 | 56 | case .change(let change): 57 | self.objects = change.objects 58 | tableView.beginUpdates() 59 | 60 | let insertedIndexPaths = change.insertions.map { IndexPath(row: $0.index, section: 0) } 61 | tableView.insertRows(at: insertedIndexPaths, with: .automatic) 62 | 63 | let deletedIndexPaths = change.deletions.map { IndexPath(row: $0, section: 0) } 64 | tableView.deleteRows(at: deletedIndexPaths, with: .automatic) 65 | 66 | let updatedIndexPaths = change.modifications.map { IndexPath(row: $0.index, section: 0) } 67 | tableView.reloadRows(at: updatedIndexPaths, with: .automatic) 68 | 69 | tableView.endUpdates() 70 | 71 | case .error(let error): 72 | print("Got an error: \(error)") 73 | } 74 | } 75 | } 76 | 77 | // MARK: - Table View 78 | 79 | extension MasterViewController { 80 | 81 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 82 | return objects.count 83 | } 84 | 85 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 86 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 87 | 88 | let object = objects[indexPath.row] 89 | cell.textLabel!.text = object.name 90 | return cell 91 | } 92 | 93 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 94 | return true 95 | } 96 | 97 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 98 | guard editingStyle == .delete else { 99 | return 100 | } 101 | 102 | let user = objects[indexPath.row] 103 | dbClient.delete(user) { _ in } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedUser+CoreDataClass.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 01/06/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @objc(ManagedUser) 13 | public class ManagedUser: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedUser+CoreDataProperties.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 01/06/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | extension ManagedUser { 13 | 14 | @nonobjc 15 | class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: User.entityName) 17 | } 18 | 19 | @NSManaged var id: String? 20 | @NSManaged var name: String? 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Example/Example/Models/CoreData/User+CoreData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+CoreData.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 01/06/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DBClient 11 | import CoreData 12 | 13 | extension User: CoreDataModelConvertible { 14 | 15 | public static var entityName: String { 16 | return String(describing: self) 17 | } 18 | 19 | public static func managedObjectClass() -> NSManagedObject.Type { 20 | return ManagedUser.self 21 | } 22 | 23 | public func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject { 24 | var user: ManagedUser 25 | if let result = existedInstance as? ManagedUser { // fetch existing 26 | user = result 27 | } else { // or create new 28 | user = NSEntityDescription.insertNewObject( 29 | forEntityName: User.entityName, 30 | into: context 31 | ) as! ManagedUser 32 | } 33 | user.id = id 34 | user.name = name 35 | 36 | return user 37 | } 38 | 39 | public static func from(_ managedObject: NSManagedObject) -> Stored { 40 | guard let managedUser = managedObject as? ManagedUser else { 41 | fatalError("can't create User object from object \(managedObject)") 42 | } 43 | guard let id = managedUser.id, 44 | let name = managedUser.name else { 45 | fatalError("can't get required properties for user \(managedObject)") 46 | } 47 | 48 | return User(id: id, name: name) 49 | } 50 | 51 | func isPrimaryValueEqualTo(value: Any) -> Bool { 52 | if let value = value as? String { 53 | return value == id 54 | } 55 | 56 | return false 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Example/Example/Models/CoreData/Users.xcdatamodeld/Users.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Example/Example/Models/Realm/ObjectUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectUser.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 1/6/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class ObjectUser: Object { 13 | 14 | override class func primaryKey() -> String? { 15 | return #keyPath(ObjectUser.id) 16 | } 17 | 18 | @objc dynamic var id: String = "" 19 | @objc dynamic var name: String = "" 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Models/Realm/User+Realm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+Realm.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 01/06/17. 6 | // Copyright © 2016 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DBClient 11 | import RealmSwift 12 | 13 | extension User: RealmModelConvertible { 14 | 15 | static func from(_ realmObject: Object) -> Stored { 16 | guard let objectUser = realmObject as? ObjectUser else { 17 | fatalError("Can't create `User` from \(realmObject)") 18 | } 19 | 20 | return User(id: objectUser.id, name: objectUser.name) 21 | } 22 | 23 | static func realmClass() -> Object.Type { 24 | return ObjectUser.self 25 | } 26 | 27 | func toRealmObject() -> Object { 28 | let user = ObjectUser() 29 | user.id = id 30 | user.name = name 31 | 32 | return user 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Example/Example/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // DBClient-Example 4 | // 5 | // Created by Roman Kyrylenko on 1/6/17. 6 | // Copyright © 2017 Yalantis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DBClient 11 | 12 | class User { 13 | 14 | var name: String 15 | var id: String 16 | 17 | init(id: String, name: String) { 18 | self.id = id 19 | self.name = name 20 | } 21 | 22 | func mutate() { 23 | name = String(name.reversed()) 24 | } 25 | } 26 | 27 | extension User: Stored { 28 | 29 | public static var primaryKeyName: String? { 30 | return "id" 31 | } 32 | 33 | public var valueOfPrimaryKey: CVarArg? { 34 | return id 35 | } 36 | } 37 | 38 | extension User { 39 | 40 | static func createRandom() -> User { 41 | let id = arc4random() 42 | let user = User(id: "\(id)", name: "User #\(id)") 43 | 44 | return user 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | use_frameworks! 3 | 4 | target 'Example' do 5 | 6 | pod 'DBClient/Realm', :path => '../' 7 | pod 'DBClient/CoreData', :path => '../' 8 | end 9 | 10 | target 'DBClientTests' do 11 | 12 | pod 'DBClient/Realm', :path => '../' 13 | pod 'DBClient/CoreData', :path => '../' 14 | end 15 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DBClient/Core (1.4.2): 3 | - YALResult (= 1.4) 4 | - DBClient/CoreData (1.4.2): 5 | - DBClient/Core 6 | - YALResult (= 1.4) 7 | - DBClient/Realm (1.4.2): 8 | - DBClient/Core 9 | - RealmSwift (~> 3.15.0) 10 | - YALResult (= 1.4) 11 | - Realm (3.15.0): 12 | - Realm/Headers (= 3.15.0) 13 | - Realm/Headers (3.15.0) 14 | - RealmSwift (3.15.0): 15 | - Realm (= 3.15.0) 16 | - YALResult (1.4) 17 | 18 | DEPENDENCIES: 19 | - DBClient/CoreData (from `../`) 20 | - DBClient/Realm (from `../`) 21 | 22 | SPEC REPOS: 23 | https://github.com/cocoapods/specs.git: 24 | - Realm 25 | - RealmSwift 26 | - YALResult 27 | 28 | EXTERNAL SOURCES: 29 | DBClient: 30 | :path: "../" 31 | 32 | SPEC CHECKSUMS: 33 | DBClient: 6833b6f3abb9bbd90dc3289621179f3fad102c0c 34 | Realm: 9b834e1be6062f544805252c812348872dc5d4ed 35 | RealmSwift: 8a41886f8ab6efef9eb8df97de2f2bb911561a79 36 | YALResult: 26915691cdd19269936336d6f28e1a015c64175e 37 | 38 | PODFILE CHECKSUM: 01e2cf56f70348c45b9e7248a505591dd86d210b 39 | 40 | COCOAPODS: 1.6.1 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Yalantis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DBClient 2 | 3 | [![cocoapods](https://img.shields.io/cocoapods/v/DBClient.svg)](https://img.shields.io/cocoapods/v/DBClient.svg) ![swift](https://img.shields.io/badge/Swift-5.0-orange.svg) ![Platform](http://img.shields.io/badge/platform-iOS-blue.svg?style=flat) [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/Yalantis/DBClient/blob/master/LICENSE) 4 | 5 | ## Integration (Cocoapods) 6 | 7 | There're three podspecs: 8 | 9 | - `DBClient/Core` contains pure (CoreData/Realm-free) interface / types used to abstract from implementation. Use it only in case you're about to provide custom implementation of any available storage types. 10 | - `DBClient/CoreData` contains CoreData implementation. 11 | - `DBClient/Realm` contains Realm implementation. 12 | 13 | ## Usage 14 | 15 | Depending on DataBase type you need to create a client: 16 | `let client: DBClient = RealmDBClient(realm: realm)` 17 | or 18 | `let client: DBClient = CoreDataDBClient(forModel: "Users")` 19 | 20 | Base methods (`CRUD`, `observe`) are the same for each type and could be found documented in [`DBClient.swift`](https://github.com/Yalantis/DBClient/blob/master/DBClient/Core/DBClient.swift) 21 | 22 | Each model you create required to conform `Stored` protocol with two properties: 23 | ``` 24 | extension User: Stored { 25 | 26 | public static var primaryKeyName: String? { 27 | return "id" 28 | } 29 | 30 | public var valueOfPrimaryKey: CVarArg? { 31 | return id 32 | } 33 | } 34 | ``` 35 | 36 | For each model you create you need to define associated database model described below. 37 | 38 | ### Realm 39 | 40 | To adopt Realm, you need to provide `RealmModelConvertible` protocol implementation for each model you want to support. 41 | `extension User: RealmModelConvertible` 42 | 43 | The protocol contains three required methods. 44 | 45 | The first one provides a class (decendant of realm's `Object`) to be associated with your model: 46 | ``` 47 | static func realmClass() -> Object.Type { 48 | return ObjectUser.self 49 | } 50 | ``` 51 | 52 | The second one converts abstract realm's `Object` to your model: 53 | ``` 54 | static func from(_ realmObject: Object) -> Stored { 55 | guard let objectUser = realmObject as? ObjectUser else { 56 | fatalError("Can't create `User` from \(realmObject)") 57 | } 58 | 59 | return User(id: objectUser.id, name: objectUser.name) 60 | } 61 | ``` 62 | 63 | The last one converts your model to realm's object: 64 | ``` 65 | func toRealmObject() -> Object { 66 | let user = ObjectUser() 67 | user.id = id 68 | user.name = name 69 | 70 | return user 71 | } 72 | ``` 73 | 74 | ### CoreData 75 | 76 | To adopt CoreData, you need to create your model and provide appropriate file name to client's constructor (bundle could also be specified) and for each your model provide implementation of the `CoreDataModelConvertible` protocol. 77 | `extension User: CoreDataModelConvertible` 78 | 79 | The protocol requires four methods and one field to be implemented. Documentation for each method/field could be found in [`CoreDataDBClient.swift`](https://github.com/Yalantis/DBClient/blob/master/DBClient/CoreData/CoreDataDBClient.swift) 80 | 81 | In the field `entityName` you should provide entity name (equal to one specified in your model): 82 | ``` 83 | public static var entityName: String { 84 | return String(describing: self) 85 | } 86 | ``` 87 | 88 | The next method used to determine associated `NSManagedObject` ancestor to your model: 89 | ``` 90 | public static func managedObjectClass() -> NSManagedObject.Type { 91 | return ManagedUser.self 92 | } 93 | ``` 94 | 95 | The next method determines whether given object equal to current: 96 | ``` 97 | func isPrimaryValueEqualTo(value: Any) -> Bool { 98 | if let value = value as? String { 99 | return value == id 100 | } 101 | 102 | return false 103 | } 104 | ``` 105 | 106 | Next method used to convert `NSManagedObject` to your model. Feel free to fail with `fatalError` here since it's developer's issue. 107 | ``` 108 | public static func from(_ managedObject: NSManagedObject) -> Stored { 109 | guard let managedUser = managedObject as? ManagedUser else { 110 | fatalError("can't create User object from object \(managedObject)") 111 | } 112 | guard let id = managedUser.id, 113 | let name = managedUser.name else { 114 | fatalError("can't get required properties for user \(managedObject)") 115 | } 116 | 117 | return User(id: id, name: name) 118 | } 119 | ``` 120 | 121 | The last method used to create/update `NSManagedObject` in given context based on your model: 122 | ``` 123 | public func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject { 124 | var user: ManagedUser 125 | if let result = existedInstance as? ManagedUser { // fetch existing 126 | user = result 127 | } else { // or create new 128 | user = NSEntityDescription.insertNewObject( 129 | forEntityName: User.entityName, 130 | into: context 131 | ) as! ManagedUser 132 | } 133 | user.id = id 134 | user.name = name 135 | 136 | return user 137 | } 138 | 139 | ``` 140 | 141 | ## Version history 142 | 143 | 144 | | Version | Swift | Dependencies | iOS | 145 | |----------|-------|----------------------------------------|------| 146 | | `1.4.2` | 5 | RealmSwift 3.15.0, YALResult 1.4 | 10 | 147 | | `1.3` | 4.2 | RealmSwift 3.11.1, YALResult 1.1 | 10 | 148 | | `1.0` | 4.2 | RealmSwift 2.10.1, YALResult 1.0 | 10 | 149 | | `0.7` | 4.0 | RealmSwift 2.10.1, BoltsSwift 1.4 | 9 | 150 | | `0.6` | 4 | RealmSwift 2.10.1, BoltsSwift 1.3 | 9 | 151 | | `0.4.2` | 3.2 | RealmSwift 2.1.1, BoltsSwift 1.3 | 9 | 152 | --------------------------------------------------------------------------------