├── .swift-version ├── Other └── logo.jpeg ├── Sources └── Reloaded │ ├── Exports.swift │ ├── Libs │ ├── QueryField.swift │ ├── Query.swift │ ├── Sorting │ │ ├── QuerySort.swift │ │ └── Sorting.swift │ └── Filter │ │ ├── QueryFilterValue.swift │ │ ├── QueryFilterGroup.swift │ │ ├── QueryFilter.swift │ │ ├── QueryFilterType.swift │ │ └── Filters.swift │ ├── Extensions │ └── Array+NSPredicate.swift │ ├── Protocols │ ├── QueryDataRepresentable.swift │ ├── QueryExecutable.swift │ └── Entity.swift │ └── CoreData.swift ├── Package.swift ├── LICENSE ├── .gitignore ├── Tests └── ReloadedTests │ ├── Setup.swift │ └── ReloadedTests.swift └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /Other/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveUI/Reloaded/HEAD/Other/logo.jpeg -------------------------------------------------------------------------------- /Sources/Reloaded/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import Foundation 2 | @_exported import CoreData 3 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Reloaded", 6 | platforms: [ 7 | .macOS(.v10_12), 8 | .iOS(.v10), 9 | .watchOS(.v2) 10 | ], 11 | products: [ 12 | .library(name: "Reloaded", targets: ["Reloaded"]) 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target(name: "Reloaded", dependencies: []), 17 | // .testTarget( 18 | // name: "ReloadedTests", 19 | // dependencies: [ 20 | // "Reloaded" 21 | // ] 22 | // ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/QueryField.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// Represents a field and its optional entity in a query. 5 | /// This is used mostly for query filters. 6 | public struct QueryField: Hashable { 7 | 8 | /// See `Hashable.hashValue` 9 | // public var hashValue: Int { 10 | // return name.hashValue 11 | // } 12 | 13 | /// See `Equatable.==` 14 | public static func ==(lhs: QueryField, rhs: QueryField) -> Bool { 15 | return lhs.name == rhs.name 16 | } 17 | 18 | /// The name of the field. 19 | public var name: String 20 | 21 | /// Create a new query field. 22 | public init(name: String) { 23 | self.name = name 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Query.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// Root query available for each NSManagedObject conforming to Entity 5 | public class Query: QueryExecutable where QueryEntityType: Entity { 6 | 7 | /// Name of the entity this query will be executed on 8 | public var entity: QueryEntityType.Type 9 | 10 | /// Query filters separated in logical groups 11 | public var filters: [QueryFilterGroup] = [] 12 | 13 | /// Sorting results of the query 14 | public var sorts: [QuerySort] = [] 15 | 16 | /// Limit the number of items 17 | public var limit: Int? 18 | 19 | /// Initialization 20 | public required init(_ entityType: QueryEntityType.Type) { 21 | self.entity = entityType 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Reloaded/Extensions/Array+NSPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+NSPredicate.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Array where Element: NSPredicate { 13 | 14 | /// Group an array of predicates with AND logical type 15 | public func andGroup() -> NSPredicate { 16 | return group(type: .and) 17 | } 18 | 19 | /// Group an array of predicates with OR logical type 20 | public func orGroup() -> NSPredicate { 21 | return group(type: .or) 22 | } 23 | 24 | /// Group an array of predicates with NOT logical type 25 | public func notGroup() -> NSPredicate { 26 | return group(type: .not) 27 | } 28 | 29 | /// Group an array of predicates 30 | public func group(type: NSCompoundPredicate.LogicalType) -> NSPredicate { 31 | let predicate = NSCompoundPredicate(type: type, subpredicates: self) 32 | return predicate 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LiveUI 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. 22 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Sorting/QuerySort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuerySort.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 30/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Sorts results based on a field and direction. 13 | public struct QuerySort { 14 | /// The field to sort. 15 | public let field: QueryField 16 | 17 | /// The direction to sort by. 18 | public let direction: ComparisonResult 19 | 20 | /// Create a new sort 21 | public init( 22 | field: QueryField, 23 | direction: ComparisonResult 24 | ) { 25 | self.field = field 26 | self.direction = direction 27 | } 28 | } 29 | 30 | extension QuerySort { 31 | 32 | public func asSortDescriptor() -> NSSortDescriptor { 33 | return NSSortDescriptor(key: field.name, ascending: direction == .orderedAscending) 34 | } 35 | 36 | } 37 | 38 | 39 | extension Array where Element == QuerySort { 40 | 41 | public func asSortDescriptors() -> [NSSortDescriptor] { 42 | return map({ $0.asSortDescriptor() }) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Filter/QueryFilterValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// Describes the values a subset can have. 5 | public struct QueryFilterValue { 6 | 7 | enum QueryFilterValueStorage { 8 | case field(QueryField) 9 | case array([QueryDataRepresentable]) 10 | case none 11 | } 12 | 13 | /// Internal storage. 14 | let storage: QueryFilterValueStorage 15 | 16 | /// Returns the `QueryField` value if it exists. 17 | public func field() -> QueryField? { 18 | switch storage { 19 | case .field(let field): return field 20 | default: return nil 21 | } 22 | } 23 | 24 | /// Query field. 25 | public static func field(_ field: QueryField) -> QueryFilterValue { 26 | return .init(storage: .field(field)) 27 | } 28 | 29 | /// An array of supported values 30 | public static func array(_ array: [T]) throws -> QueryFilterValue where T: QueryDataRepresentable { 31 | return .init(storage: .array(array)) 32 | } 33 | 34 | /// No value. 35 | public static func none() -> QueryFilterValue { 36 | return .init(storage: .none) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Filter/QueryFilterGroup.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public struct QueryFilterGroup { 5 | 6 | public enum Operator { 7 | case and 8 | case or 9 | } 10 | 11 | let `operator`: Operator 12 | 13 | let filters: [QueryFilter] 14 | 15 | public static func group(_ operator: Operator = .and, _ filters: QueryFilter ...) -> QueryFilterGroup { 16 | return QueryFilterGroup(operator: `operator`, filters: filters) 17 | } 18 | 19 | public static func group(_ operator: Operator = .and, _ filters: [QueryFilter]) -> QueryFilterGroup { 20 | return QueryFilterGroup(operator: `operator`, filters: filters) 21 | } 22 | 23 | } 24 | 25 | extension QueryFilterGroup { 26 | 27 | public func asPredicate() -> NSPredicate { 28 | switch `operator` { 29 | case .and: 30 | return NSCompoundPredicate(andPredicateWithSubpredicates: filters.map({ 31 | $0.asPredicate() 32 | })) 33 | case .or: 34 | return NSCompoundPredicate(orPredicateWithSubpredicates: filters.map({ 35 | $0.asPredicate() 36 | })) 37 | } 38 | } 39 | 40 | } 41 | 42 | extension Array where Element == QueryFilterGroup { 43 | 44 | public func asPredicate() -> NSPredicate? { 45 | if isEmpty { 46 | return nil 47 | } 48 | return NSCompoundPredicate(andPredicateWithSubpredicates: map({ 49 | $0.asPredicate() 50 | })) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Reloaded/Protocols/QueryDataRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryDataRepresentable.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public protocol QueryDataRepresentable { 13 | var isNull: Bool { get } 14 | } 15 | 16 | 17 | public protocol NumericQueryDataRepresentable: QueryDataRepresentable { } 18 | public protocol StringQueryDataRepresentable: QueryDataRepresentable { } 19 | public protocol ExactQueryDataRepresentable: QueryDataRepresentable { 20 | var value: String { get } 21 | } 22 | 23 | extension QueryDataRepresentable { 24 | 25 | public var isNull: Bool { 26 | return false 27 | } 28 | 29 | } 30 | 31 | /// Internal substitute for 32 | struct NULL: ExactQueryDataRepresentable { 33 | 34 | var value: String { 35 | return "NULL" 36 | } 37 | 38 | var isNull: Bool { 39 | return true 40 | } 41 | 42 | } 43 | 44 | extension Bool: ExactQueryDataRepresentable { 45 | 46 | public var value: String { 47 | return self ? "true" : "false" 48 | } 49 | 50 | } 51 | 52 | extension Date: QueryDataRepresentable { } 53 | extension String: StringQueryDataRepresentable { } 54 | extension Decimal: NumericQueryDataRepresentable { } 55 | extension Double: NumericQueryDataRepresentable { } 56 | extension Float: NumericQueryDataRepresentable { } 57 | extension Int: NumericQueryDataRepresentable { } 58 | extension Int16: NumericQueryDataRepresentable { } 59 | extension Int32: NumericQueryDataRepresentable { } 60 | extension Int64: NumericQueryDataRepresentable { } 61 | extension Bool: NumericQueryDataRepresentable { } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | .DS_Store 69 | .swiftpm 70 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Sorting/Sorting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sorting.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Query { 13 | 14 | /// Sort by 15 | @discardableResult public func sort(sort: QuerySort) -> Self { 16 | sorts.append(sort) 17 | return self 18 | } 19 | 20 | /// Sort by an array of QuerySort 21 | @discardableResult public func sort(sort: [QuerySort]) -> Self { 22 | sorts.append(contentsOf: sort) 23 | return self 24 | } 25 | 26 | /// Sort by an array of QuerySort 27 | @discardableResult public func sort(sort: QuerySort ...) -> Self { 28 | sorts.append(contentsOf: sort) 29 | return self 30 | } 31 | 32 | // @discardableResult public func sort(by keyPath: ReferenceWritableKeyPath , direction: QuerySortDirection = .ascending) -> Self where E: AnyEntity { 33 | // return self 34 | // } 35 | 36 | /// Sort by a field and direction 37 | @discardableResult public func sort(by field: String , direction: ComparisonResult = .orderedAscending) -> Self { 38 | guard direction != .orderedSame else { 39 | return self 40 | } 41 | self.sort(sort: QuerySort(field: QueryField(name: field), direction: direction)) 42 | return self 43 | } 44 | 45 | /// Sort by an array of fields and directions 46 | @discardableResult public func sort(by sort: (field: String , direction: ComparisonResult) ...) -> Self { 47 | self.sort(sort: sort.map({ 48 | QuerySort(field: QueryField(name: $0.field), direction: $0.direction) 49 | })) 50 | return self 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Tests/ReloadedTests/Setup.swift: -------------------------------------------------------------------------------- 1 | @testable import Reloaded 2 | 3 | 4 | class Setup { 5 | 6 | static func `do`() { 7 | CoreData.default.mock(forBundleWithClass: AppDelegate.self) 8 | 9 | let colors = ["green", "red", "yellow", "pink", "blue", "gray", "cyan", "orange", "black", "rainbow"] 10 | 11 | var hasChimney = true 12 | for color in colors { 13 | for _ in 1...5 { 14 | let snowman = try! Locomotive.new() 15 | snowman.color = color 16 | snowman.hasChimney = hasChimney 17 | hasChimney = !hasChimney 18 | } 19 | } 20 | } 21 | 22 | static func clean() { 23 | try! Locomotive.deleteAll() 24 | } 25 | 26 | } 27 | 28 | 29 | 30 | extension CoreData { 31 | 32 | public func mock(forBundleWithClass bundleClass: AnyClass) { 33 | let bundle = Bundle(for: bundleClass) 34 | let managedObjectModel = NSManagedObjectModel.mergedModel(from: [bundle] )! 35 | 36 | let container = NSPersistentContainer(name: containerName, managedObjectModel: managedObjectModel) 37 | let description = NSPersistentStoreDescription() 38 | description.type = NSInMemoryStoreType 39 | description.shouldAddStoreAsynchronously = false // Make it simpler in test env 40 | 41 | container.persistentStoreDescriptions = [description] 42 | container.loadPersistentStores { (description, error) in 43 | // Check if the data store is in memory 44 | precondition(description.type == NSInMemoryStoreType) 45 | 46 | // Check if creating container wrong 47 | if let error = error { 48 | fatalError("Create an in-memory coordinator failed \(error)") 49 | } 50 | } 51 | persistentContainer = container 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Filter/QueryFilter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// Defines a `Filter` that can be added on fetch operations to limit the set of data affected. 5 | public struct QueryFilter { 6 | 7 | /// The field to filer. 8 | public var field: QueryField 9 | 10 | /// The filter type. 11 | public var type: QueryFilterType 12 | 13 | /// The filter value, possible another field. 14 | public var value: QueryDataRepresentable 15 | 16 | /// Case sensitivity in text searches 17 | public var caseSensitive: Bool 18 | 19 | /// Create a new filter. 20 | public init(field: QueryField, type: QueryFilterType, value: QueryDataRepresentable, caseSensitive: Bool = true) { 21 | self.field = field 22 | self.type = type 23 | self.value = value 24 | self.caseSensitive = caseSensitive 25 | } 26 | 27 | } 28 | 29 | 30 | extension QueryFilter { 31 | 32 | /// Convert filter to predicate 33 | public func asPredicate() -> NSPredicate { 34 | if let value = value as? String { 35 | let predicate = NSPredicate(format: "\(field.name) \(type.interpretation)\(caseSensitive ? "" : "[c]") %@", value) 36 | return predicate 37 | } else if let value = value as? Bool { 38 | let predicate = NSPredicate(format: "\(field.name) \(type.interpretation) %@", NSNumber(booleanLiteral: value)) 39 | return predicate 40 | } else if let value = value as? LosslessStringConvertible { 41 | let predicate = NSPredicate(format: "\(field.name) \(type.interpretation) \(value)") 42 | return predicate 43 | } else if let value = value as? Date { 44 | let predicate = NSPredicate(format: "\(field.name) \(type.interpretation) %@", value as CVarArg) 45 | return predicate 46 | } else if let _ = value as? NULL { 47 | let predicate = NSPredicate(format: "\(field.name) \(type.interpretation) nil") 48 | return predicate 49 | } 50 | fatalError("Not implemented") 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Filter/QueryFilterType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public struct QueryFilterType: Equatable { 5 | 6 | enum Storage: Equatable { 7 | case equals 8 | case equalEquals 9 | case contains 10 | case beginsWith 11 | case endsWith 12 | case notEquals 13 | case greaterThan 14 | case lessThan 15 | case greaterThanOrEquals 16 | case lessThanOrEquals 17 | case custom(String) 18 | } 19 | 20 | public var interpretation: String { 21 | switch storage { 22 | case .equals: 23 | return "=" 24 | case .equalEquals: 25 | return "==" 26 | case .contains: 27 | return "CONTAINS" 28 | case .beginsWith: 29 | return "BEGINSWITH" 30 | case .endsWith: 31 | return "ENDSWITH" 32 | case .notEquals: 33 | return "!=" 34 | case .greaterThan: 35 | return ">" 36 | case .lessThan: 37 | return "<" 38 | case .greaterThanOrEquals: 39 | return ">=" 40 | case .lessThanOrEquals: 41 | return "<=" 42 | case .custom(let custom): 43 | return custom 44 | } 45 | } 46 | 47 | /// Internal storage. 48 | let storage: Storage 49 | 50 | /// = 51 | public static var equals: QueryFilterType { return .init(storage: .equals) } 52 | /// == 53 | public static var equalEquals: QueryFilterType { return .init(storage: .equalEquals) } 54 | /// CONTAINS 55 | public static var contains: QueryFilterType { return .init(storage: .contains) } 56 | /// BEGINSWITH 57 | public static var beginsWith: QueryFilterType { return .init(storage: .beginsWith) } 58 | /// ENDSWITH 59 | public static var endsWith: QueryFilterType { return .init(storage: .endsWith) } 60 | /// != 61 | public static var notEquals: QueryFilterType { return .init(storage: .notEquals) } 62 | /// > 63 | public static var greaterThan: QueryFilterType { return .init(storage: .greaterThan) } 64 | /// < 65 | public static var lessThan: QueryFilterType { return .init(storage: .lessThan) } 66 | /// >= 67 | public static var greaterThanOrEquals: QueryFilterType { return .init(storage: .greaterThanOrEquals) } 68 | /// <= 69 | public static var lessThanOrEquals: QueryFilterType { return .init(storage: .lessThanOrEquals) } 70 | 71 | /// Custom filter 72 | public static func custom(_ filter: String) -> QueryFilterType { 73 | return .init(storage: .custom(filter)) 74 | } 75 | 76 | } 77 | 78 | 79 | -------------------------------------------------------------------------------- /Sources/Reloaded/Protocols/QueryExecutable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryExecutable.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | public protocol QueryExecutable { 14 | 15 | associatedtype EntityType: Entity 16 | 17 | var entity: EntityType.Type { get } 18 | 19 | var filters: [QueryFilterGroup] { get set } 20 | var sorts: [QuerySort] { get set } 21 | var limit: Int? { get set } 22 | 23 | init(_ entityType: EntityType.Type) 24 | // func fetchRequest() -> Entity.Request 25 | 26 | } 27 | 28 | extension QueryExecutable { 29 | 30 | /// Get compiled fetch request 31 | public func fetchRequest() -> Entity.Request { 32 | let fetch = Entity.Request(entityName: entity.entityName) 33 | fetch.predicate = filters.asPredicate() 34 | if !sorts.isEmpty { 35 | fetch.sortDescriptors = sorts.asSortDescriptors() 36 | } 37 | if let limit = limit { 38 | fetch.fetchLimit = limit 39 | } 40 | return fetch 41 | } 42 | 43 | } 44 | 45 | extension QueryExecutable where EntityType: NSManagedObject { 46 | 47 | public func all(on context: NSManagedObjectContext = CoreData.managedContext) throws -> [EntityType] { 48 | guard let data = try context.fetch(fetchRequest()) as? [EntityType] else { 49 | return [] 50 | } 51 | return data 52 | } 53 | 54 | public func delete(on context: NSManagedObjectContext = CoreData.managedContext) throws { 55 | for object in try all(on: context) { 56 | try object.delete(on: context) 57 | } 58 | try context.save() 59 | 60 | // let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest()) 61 | // do { 62 | // let batchDeleteResult = try context.execute(deleteRequest) as! NSBatchDeleteResult 63 | // print("The batch delete request has deleted \(batchDeleteResult.result!) records.") 64 | // } catch { 65 | // let updateError = error as NSError 66 | // print("\(updateError), \(updateError.userInfo)") 67 | // } 68 | } 69 | 70 | public func count(on context: NSManagedObjectContext = CoreData.managedContext) throws -> Int { 71 | return try context.count(for: fetchRequest()) 72 | } 73 | 74 | public func first(on context: NSManagedObjectContext = CoreData.managedContext) throws -> EntityType? { 75 | return try context.fetch(fetchRequest()).first as? EntityType 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Reloaded: CoreData made easy!](https://github.com/LiveUI/Reloaded/raw/master/Other/logo.jpeg) 2 | 3 | ## 4 | 5 | A brand new Swift abstraction layer for CoreData's original interface 6 | 7 | 8 | [![Jenkins](https://ci.liveui.io/job/LiveUI/job/Boost/job/master/badge/icon)](https://ci.liveui.io/job/LiveUI/job/Reloaded/) 9 | [![Platforms](https://img.shields.io/badge/platforms-iOS%20|%20macOS|%20tvOS|%20watchOS-ff0000.svg?style=flat)](http://cocoapods.org/pods/Reloaded) 10 | [![Swift 4](https://img.shields.io/badge/swift-4.1-orange.svg?style=flat)](http://swift.org) 11 | [![Version](https://img.shields.io/cocoapods/v/Reloaded.svg?style=flat)](http://cocoapods.org/pods/Reloaded) 12 | [![License](https://img.shields.io/cocoapods/l/Reloaded.svg?style=flat)](http://cocoapods.org/pods/Reloaded) 13 | 14 | 15 | ## Setup data model 16 | 17 | Well, this is exactly the same process as you would do when setting up core data the old way. If you haven't created your app from a template, just: 18 | * create a new **Data Model** file 19 | * give it the same name as your bundle has (Reloaded is trying to use `kCFBundleNameKey` to generate the expected name) 20 | * Create your entities as you would 21 | 22 | > You can also override the default container name for use in multi-target apps using `CoreData.fallbackContainerName` 23 | 24 | ```swift 25 | /// Fallback container name, overrides bundle name globally 26 | /// Use in multitarget apps with shared model 27 | public static var fallbackContainerName: String? 28 | ``` 29 | 30 | ## Basic usage 31 | 32 | > If you don't have your data model (.xcdatamodeld) file yet, jump to the [Setup data model](#setup-data-model) section and then come back. 33 | 34 | Using Reloaded is super simple, in the basic configuration you don't have to write a single line of setup you would probably otherwise have in your `AppDelegate` but you can obviously still leverage your apps delegate methods as you would otherwise. 35 | 36 | ```swift 37 | import Reloaded 38 | 39 | // Conform your NSManagedObject to Reloaded own protocol `Entity` 40 | extension Locomotive: Entity { } 41 | 42 | // Creating a new object 43 | let loco = try! Locomotive.new() 44 | loco.color = "black" 45 | loco.hasChimney = true 46 | 47 | // Save an object 48 | try! loco.save() 49 | 50 | // Fetching all black locomotives with a chimney sorted by color 51 | let all = try! Locomotive.query.filter("color" == "red", "hasChimney" == true).sort(by: "color", direction: .orderedDescending).all() 52 | print(all) 53 | ``` 54 | 55 | ### Cocoapods & Carthage 56 | 57 | Sorry folks but we are ditching support for these two systems in favor of Swift Package Manager 58 | > Carthage might work, don't know but we won't be making sure it does anymore ... 59 | 60 | ## Author 61 | 62 | Ondrej Rafaj (@rafiki270 on [Github](https://github.com/rafiki270), [Twitter](https://twitter.com/rafiki270) and [LiveUI Slack](http://bit.ly/2B0dEyt)) 63 | -------------------------------------------------------------------------------- /Sources/Reloaded/Protocols/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // Reloaded 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2018. 6 | // Copyright © 2018 LiveUI. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | /// Type erased Entity 13 | public protocol AnyEntity: class { 14 | /// Name of the entity 15 | static var entityName: String { get } 16 | } 17 | 18 | /// Entity 19 | public protocol Entity: AnyEntity { } 20 | 21 | /// Entity extension for NSManagedObject 22 | extension Entity where Self: NSManagedObject { 23 | 24 | /// Typealias for NSFetchRequest 25 | public typealias Request = NSFetchRequest 26 | 27 | /// Name of the entity, converted from the name of the class by default 28 | public static var entityName: String { 29 | let name = String(describing: self) 30 | return name 31 | } 32 | 33 | /// Create new query 34 | public static var query: Query { 35 | return Query(self) 36 | } 37 | 38 | /// Basic fetch request 39 | public static var fetchRequest: Request { 40 | let fetch = Request(entityName: entityName) 41 | return fetch 42 | } 43 | 44 | /// Get all items based on optional criteria and sorting 45 | public static func all(filter: NSPredicate? = nil, sort: [NSSortDescriptor] = [], limit: Int = 0) throws -> [Self] { 46 | let fetch = fetchRequest 47 | fetch.predicate = filter 48 | if !sort.isEmpty { 49 | fetch.sortDescriptors = sort 50 | } 51 | if limit > 0 { 52 | fetch.fetchLimit = limit 53 | } 54 | guard let all = try CoreData.managedContext.fetch(fetch) as? [Self] else { 55 | throw CoreData.Problem.badData 56 | } 57 | return all 58 | } 59 | 60 | /// Delete all data for this entity 61 | public static func deleteAll(on context: NSManagedObjectContext = CoreData.managedContext) throws { 62 | try query.delete(on: context) 63 | } 64 | 65 | /// Delete this object 66 | @discardableResult public func delete(on context: NSManagedObjectContext = CoreData.managedContext) throws -> Bool { 67 | context.delete(self) 68 | try save(on: context) 69 | return isDeleted 70 | } 71 | 72 | /// Count all items 73 | public static func count() throws -> Int { 74 | let count = try CoreData.managedContext.count(for: fetchRequest) 75 | return count 76 | } 77 | 78 | /// Save context 79 | public func save(on context: NSManagedObjectContext = CoreData.managedContext) throws { 80 | try context.save() 81 | } 82 | 83 | /// Create new entity 84 | public static func new(on context: NSManagedObjectContext = CoreData.managedContext) throws -> Self { 85 | let object = try CoreData.new(Self.self, on: context) 86 | return object 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Reloaded/CoreData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | 5 | public class CoreData { 6 | 7 | /// Custom CoreData errors 8 | public enum Problem: Error { 9 | case unableToCreateEntity 10 | case badData 11 | case invalidPersistentStoreCoordinator 12 | } 13 | 14 | /// Default implementation, should be sufficient in most cases 15 | public static let `default` = CoreData() 16 | 17 | /// Fallback container name, overrides bundle name globally 18 | /// Use in multitarget apps with shared model 19 | public static var fallbackContainerName: String? 20 | 21 | /// Container name 22 | let containerName: String 23 | 24 | // MARK: Initialization 25 | 26 | /// Initialize CoreData with an optional NSPersistentContainer name. 27 | /// App name will be used as default if nil 28 | public init(containerName: String? = nil) { 29 | guard let containerName = containerName ?? CoreData.fallbackContainerName ?? Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String else { 30 | fatalError("CoreData container name is not set and can not be inferred") 31 | } 32 | self.containerName = containerName 33 | } 34 | 35 | // MARK: Basic methods 36 | 37 | /// Managed context for this instance 38 | public var managedContext: NSManagedObjectContext { 39 | return persistentContainer.viewContext 40 | } 41 | 42 | /// Default managed context 43 | public static var managedContext: NSManagedObjectContext { 44 | return CoreData.default.persistentContainer.viewContext 45 | } 46 | 47 | /// Create new entry on the default context 48 | public static func new(_ entityClass: T.Type, on context: NSManagedObjectContext = CoreData.managedContext) throws -> T where T: Entity { 49 | let o = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: context) 50 | guard let object = o as? T else { 51 | throw Problem.unableToCreateEntity 52 | } 53 | return object 54 | } 55 | 56 | // MARK: Default CoreData stuff 57 | 58 | /// Persistent connector for this instance 59 | public lazy var persistentContainer: NSPersistentContainer = { 60 | let container = NSPersistentContainer(name: containerName) 61 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 62 | if let error = error as NSError? { 63 | fatalError("Unresolved error \(error), \(error.userInfo)") 64 | } 65 | }) 66 | return container 67 | }() 68 | 69 | // MARK: CoreData save support 70 | 71 | /// Save managed context if it has changes 72 | public func saveContext() throws { 73 | try CoreData.save(context: managedContext) 74 | } 75 | 76 | /// Save default managed context if it has changes 77 | public static func saveContext() throws { 78 | try CoreData.save(context: managedContext) 79 | } 80 | 81 | // MARK: Private interface 82 | 83 | /// Save context, private helper 84 | private static func save(context: NSManagedObjectContext) throws { 85 | if context.hasChanges { 86 | try context.save() 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Reloaded/Libs/Filter/Filters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension Query { 5 | 6 | /// Basic filter 7 | @discardableResult public func filter(_ filters: QueryFilter ...) -> Self { 8 | self.filters.append(.group(.and, filters)) 9 | return self 10 | } 11 | 12 | /// Basic filter 13 | @discardableResult public func filter(_ operator: QueryFilterGroup.Operator, _ filters: QueryFilter ...) -> Self { 14 | self.filters.append(.group(`operator`, filters)) 15 | return self 16 | } 17 | 18 | } 19 | 20 | /// field == value 21 | public func == (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 22 | return _compare(lhs, .equals, rhs) 23 | } 24 | public func == (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 25 | return _compare(lhs, .equals, rhs) 26 | } 27 | 28 | /// field == value, case insensitive 29 | infix operator ==* 30 | public func ==* (lhs: String, rhs: StringQueryDataRepresentable) -> QueryFilter { 31 | return _compare(lhs, .equals, rhs, false) 32 | } 33 | public func ==* (lhs: String, rhs: StringQueryDataRepresentable?) -> QueryFilter { 34 | return _compare(lhs, .equals, rhs, false) 35 | } 36 | 37 | /// field CONTAINS value 38 | infix operator ~~ 39 | public func ~~ (lhs: String, rhs: StringQueryDataRepresentable) -> QueryFilter { 40 | return _compare(lhs, .contains, rhs) 41 | } 42 | 43 | /// field CONTAINS value, case insensitive 44 | infix operator ~~* 45 | public func ~~* (lhs: String, rhs: StringQueryDataRepresentable?) -> QueryFilter { 46 | return _compare(lhs, .contains, rhs, false) 47 | } 48 | 49 | /// field != value 50 | public func != (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 51 | return _compare(lhs, .notEquals, rhs) 52 | } 53 | public func != (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 54 | return _compare(lhs, .notEquals, rhs) 55 | } 56 | 57 | /// field != value, case insensitive 58 | infix operator !=* 59 | public func !=* (lhs: String, rhs: StringQueryDataRepresentable) -> QueryFilter { 60 | return _compare(lhs, .notEquals, rhs, false) 61 | } 62 | public func !=* (lhs: String, rhs: StringQueryDataRepresentable?) -> QueryFilter { 63 | return _compare(lhs, .notEquals, rhs, false) 64 | } 65 | 66 | /// field > value 67 | public func > (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 68 | return _compare(lhs, .greaterThan, rhs) 69 | } 70 | public func > (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 71 | return _compare(lhs, .greaterThan, rhs) 72 | } 73 | 74 | /// field < value 75 | public func < (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 76 | return _compare(lhs, .lessThan, rhs) 77 | } 78 | public func < (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 79 | return _compare(lhs, .lessThan, rhs) 80 | } 81 | 82 | /// field >= value 83 | public func >= (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 84 | return _compare(lhs, .greaterThanOrEquals, rhs) 85 | } 86 | public func >= (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 87 | return _compare(lhs, .greaterThanOrEquals, rhs) 88 | } 89 | 90 | /// field <= value 91 | public func <= (lhs: String, rhs: QueryDataRepresentable) -> QueryFilter { 92 | return _compare(lhs, .lessThanOrEquals, rhs) 93 | } 94 | public func <= (lhs: String, rhs: QueryDataRepresentable?) -> QueryFilter { 95 | return _compare(lhs, .lessThanOrEquals, rhs) 96 | } 97 | 98 | extension QueryFilter { 99 | 100 | public static func custom(_ lhs: String, _ comparison: QueryFilterType, _ rhs: QueryDataRepresentable?, caseSensitive: Bool = true) -> QueryFilter { 101 | return _compare(lhs, comparison, rhs, caseSensitive) 102 | } 103 | 104 | } 105 | 106 | private func _compare(_ lhs: String, _ comparison: QueryFilterType, _ rhs: QueryDataRepresentable?, _ caseSensitive: Bool = true) -> QueryFilter { 107 | return QueryFilter(field: QueryField(name: lhs), type: comparison, value: rhs ?? NULL(), caseSensitive: caseSensitive) 108 | } 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Tests/ReloadedTests/ReloadedTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Reloaded 3 | 4 | 5 | class ReloadedTests: XCTestCase { 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | Setup.do() 11 | } 12 | 13 | override func tearDown() { 14 | Setup.clean() 15 | 16 | super.tearDown() 17 | } 18 | 19 | func testCreate() { 20 | let loco = try! Locomotive.new() 21 | loco.color = "brown" 22 | try! loco.save() 23 | 24 | var count = try! Locomotive.count() 25 | XCTAssertEqual(count, 51, "There has to be 50 beautiful locomotives") 26 | 27 | count = try! Locomotive.query.filter("color" == "brown").count() 28 | XCTAssertEqual(count, 1, "There has to be 1 beautiful brown locomotive") 29 | } 30 | 31 | func testFirst() { 32 | var loco = try! Locomotive.new() 33 | loco.color = "brown" 34 | loco.hasChimney = true 35 | try! loco.save() 36 | 37 | loco = try! Locomotive.new() 38 | loco.color = "brown" 39 | loco.hasChimney = false 40 | try! loco.save() 41 | 42 | let object = try! Locomotive.query.filter("color" == "brown").sort(by: "hasChimney").first()! 43 | XCTAssertEqual(object.hasChimney, false, "Wrong locomotive :(") 44 | } 45 | 46 | func testCount() { 47 | var count = try! Locomotive.count() 48 | XCTAssertEqual(count, 50, "There has to be 50 beautiful locomotives") 49 | 50 | count = try! Locomotive.query.filter("hasChimney" == true).count() 51 | XCTAssertEqual(count, 25, "There has to be 25 beautiful locomotives") 52 | } 53 | 54 | func testAll() { 55 | var objects = try! Locomotive.all() 56 | XCTAssertEqual(objects.count, 50, "There has to be 50 beautiful locomotives") 57 | 58 | objects = try! Locomotive.query.filter("hasChimney" == true).all() 59 | XCTAssertEqual(objects.count, 25, "There has to be 25 beautiful locomotives") 60 | } 61 | 62 | func testDeleteAll() { 63 | try! Locomotive.deleteAll() 64 | let objects = try! Locomotive.all() 65 | XCTAssertEqual(objects.count, 0, "There has to be no beautiful locomotive") 66 | } 67 | 68 | func testOperators() { 69 | // Bool 70 | var objects = try! Locomotive.query.filter("hasChimney" == true).all() 71 | XCTAssertEqual(objects.count, 25, "Wrong number of beautiful locomotives in the result") 72 | 73 | objects = try! Locomotive.query.filter("hasChimney" != true).all() 74 | XCTAssertEqual(objects.count, 25, "Wrong number of beautiful locomotives in the result") 75 | 76 | objects = try! Locomotive.query.filter("hasChimney" == false).all() 77 | XCTAssertEqual(objects.count, 25, "Wrong number of beautiful locomotives in the result") 78 | 79 | objects = try! Locomotive.query.filter("hasChimney" != false).all() 80 | XCTAssertEqual(objects.count, 25, "Wrong number of beautiful locomotives in the result") 81 | 82 | // Strings 83 | objects = try! Locomotive.query.filter("color" == "green").all() 84 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 85 | 86 | objects = try! Locomotive.query.filter("color" == "GREEN").all() 87 | XCTAssertEqual(objects.count, 0, "Wrong number of beautiful locomotives in the result") 88 | 89 | objects = try! Locomotive.query.filter("color" ==* "green").all() 90 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 91 | 92 | objects = try! Locomotive.query.filter("color" ==* "GREEN").all() 93 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 94 | 95 | objects = try! Locomotive.query.filter("color" != "green").all() 96 | XCTAssertEqual(objects.count, 45, "Wrong number of beautiful locomotives in the result") 97 | 98 | objects = try! Locomotive.query.filter("color" != "GREEN").all() 99 | XCTAssertEqual(objects.count, 50, "Wrong number of beautiful locomotives in the result") 100 | 101 | objects = try! Locomotive.query.filter("color" !=* "green").all() 102 | XCTAssertEqual(objects.count, 45, "Wrong number of beautiful locomotives in the result") 103 | 104 | objects = try! Locomotive.query.filter("color" !=* "GREEN").all() 105 | XCTAssertEqual(objects.count, 45, "Wrong number of beautiful locomotives in the result") 106 | 107 | // Contains 108 | 109 | objects = try! Locomotive.query.filter("color" ~~ "gre").all() 110 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 111 | 112 | objects = try! Locomotive.query.filter("color" ~~ "GRE").all() 113 | XCTAssertEqual(objects.count, 0, "Wrong number of beautiful locomotives in the result") 114 | 115 | objects = try! Locomotive.query.filter("color" ~~* "gre").all() 116 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 117 | 118 | objects = try! Locomotive.query.filter("color" ~~* "GRE").all() 119 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 120 | 121 | objects = try! Locomotive.query.filter("color" ~~ "ree").all() 122 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 123 | 124 | objects = try! Locomotive.query.filter("color" ~~ "REE").all() 125 | XCTAssertEqual(objects.count, 0, "Wrong number of beautiful locomotives in the result") 126 | 127 | objects = try! Locomotive.query.filter("color" ~~* "ree").all() 128 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 129 | 130 | objects = try! Locomotive.query.filter("color" ~~* "REE").all() 131 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 132 | 133 | objects = try! Locomotive.query.filter("color" ~~ "reen").all() 134 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 135 | 136 | objects = try! Locomotive.query.filter("color" ~~ "REEn").all() 137 | XCTAssertEqual(objects.count, 0, "Wrong number of beautiful locomotives in the result") 138 | 139 | objects = try! Locomotive.query.filter("color" ~~* "reen").all() 140 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 141 | 142 | objects = try! Locomotive.query.filter("color" ~~* "REEn").all() 143 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 144 | 145 | // Bigger and smaller then 146 | 147 | objects = try! Locomotive.query.filter("hasChimney" > 0, "color" == "red").all() 148 | XCTAssertEqual(objects.count, 2, "Wrong number of beautiful locomotives in the result") 149 | 150 | objects = try! Locomotive.query.filter("hasChimney" >= 0, "color" == "red").all() 151 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 152 | 153 | objects = try! Locomotive.query.filter("hasChimney" < 1, "color" == "red").all() 154 | XCTAssertEqual(objects.count, 3, "Wrong number of beautiful locomotives in the result") 155 | 156 | objects = try! Locomotive.query.filter("hasChimney" <= 1, "color" == "red").all() 157 | XCTAssertEqual(objects.count, 5, "Wrong number of beautiful locomotives in the result") 158 | } 159 | 160 | } 161 | --------------------------------------------------------------------------------