Skip to content

Instantly share code, notes, and snippets.

@galiak11
Last active September 1, 2016 17:23
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save galiak11/438c3f2883c5d7c0b0d8 to your computer and use it in GitHub Desktop.
Save galiak11/438c3f2883c5d7c0b0d8 to your computer and use it in GitHub Desktop.
Swift CoreData Query API
import Foundation
import CoreData
/**
Query: this is a Swift Query API for CodeData.
Usage example:
// fetch multiple rows
let people = Query("Person").whereEqual( "lastName", lastName ).sort( "name" ).fetch()
// find a person by its ID
if let person = Query("Person").whereEqual( "personId", personId ).fetchFirst() as Person? {
...
}
// create a new Person recrod
let newPerson : Person? = Query("Person").create() as? Person
This is a basic CoreData Query API, which may be used directly from anywhere in the app, and can also be used by ManagedObjects,
which in turn can expose more app-specific API.
See ManagedObjectTemplate.swift and SingletonManagedObjectTempalate.swift (below) for 2 ManagedObject examples that use the Query API.
Note: Currently only a single sort key and a single condition are supported.
However this API may be easily extended:
- To support multiple sort keys, change the private sort property to an array.
To support multiple conditions, change the condition property to an array.
( Then (1) have the sort() and where() functions add to the array instead of override it, and (2) have fetch() loop through the array to build the query's sort/predicate )
- To have non-equality conditions, like "less than", add more "where" methods like: whereLessThen(key,value).
- and many more options!
Dependency: Context.swift (see below)
*/
class Query {
private var entityName : String
private var sort : ( key: String, isAscending : Bool )? = nil // only a single sort key is supported at this point
private var condition : ( key : String, op : String, val : String )? = nil // only a single condition is supported at this point
/**
Query: a simplified Query object - that is all we may need for this project
- Example Usage:
Query("Player").fetch() as [Player]? // fetch all players
Query("Player").sort("name".fetch() as [Player]? // fetch all players sorted by name
Query("Player").whereEqual("name", "guest").fetch() as [Player]? // fetch the guest player
- note: must call "fetch()" on a Query to actually fetch data.
- note: must convert the result from [AnyObject]? to the Core Data Class
- note: each query must use a new Query() instance.
*/
init( _ entityName : String ) {
self.entityName = entityName
}
/**
adding a sort to a Query object.
- Example: whereEqual(<core data key>, <value to search>)
*/
func sort( key : String, isAscending : Bool = true ) -> Query {
self.sort = ( key : key, isAscending : isAscending )
return self
}
/**
adding a condition to a Query object.
- Example: whereEqual(<core data key>, <value to search>)
*/
func whereEqual( key : String, _ value : String ) -> Query {
self.condition = ( key : key, op : "==", val : value )
return self
}
/**
fetching data according to the query's criteria.
Note that it always returns an array (or nil if none found)
*/
func fetch() -> [AnyObject]? {
let fetchRequest = NSFetchRequest(entityName: self.entityName)
if let sort = self.sort {
// Create a sort descriptor object that sorts on the "title"
// property of the Core Data object
let sortDescriptor = NSSortDescriptor(key: sort.key, ascending: sort.isAscending)
// Set the list of sort descriptors in the fetch request,
// so it includes the sort descriptor
fetchRequest.sortDescriptors = [sortDescriptor]
}
if let condition = self.condition {
let predicate = NSPredicate(format: "\(condition.key) \(condition.op) %@", condition.val)
fetchRequest.predicate = predicate
}
return Context.instance.managedContext?.executeFetchRequest(fetchRequest, error: nil)
}
/**
returns the first match after applying the query's criteria.
Note that it always returns a single object (or nil if not found)
*/
func fetchFirst() -> AnyObject? {
if let rows = self.fetch() {
return rows.first
}
return nil
}
func create() -> AnyObject? {
if let managedContext = Context.instance.managedContext {
return NSEntityDescription.insertNewObjectForEntityForName(self.entityName, inManagedObjectContext: managedContext)
} else {
return nil
}
}
}
import UIKit
import CoreData
/*
Used by the Query API to get the appDelegate's managedObject.
Get the context:
Context.instance.managedContext
Example:
Context.instance.managedContext?.executeFetchRequest( ... )
*/
struct Context {
static var instance = Context()
// CoreData - default Managed Object Context
lazy var managedContext : NSManagedObjectContext? = {
return (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
}()
// save to contect
static func save() {
var error : NSError?
let success : Bool? = Context.instance.managedContext?.save(&error)
if (success == nil) || !success! {
Color.printError(error?.localizedDescription)
}
}
}
import Foundation
import CoreData
//
// Example of a Managed Object using the Query API.
// Since this is meant to be used by a "pure/100%" swift app, we wrap some of the
// objective C specific datatypes and expose them as swift types.
// The most notable example is Enum - see "orderType" variable below as an example.
//
// Usage Example:
//
// let newOrder = Order.create( name : "my order", orderNumber : 1 )
// let anotherNewOrder = Order.create( name : "new order", orderNumber : 2, orderType : .Urgent )
// let order = Order.fetch( name : "my order" )
//
// enum used in this example:
// enum OrderType : Int { case Limited, Urgent }
//
// note: make sure to give the enum a type matching its wrapper method type
//
@objc(Order)
class Order: NSManagedObject {
class var EntityName : String { return "Order" }
@NSManaged var name: String
@NSManaged var orderNumber_number: NSNumber
@NSManaged var orderType_number: NSNumber
@NSManaged var createdOn: NSDate
@NSManaged var updatedOn: NSDate
/** Int wrapper example for NSNumber properties ( orderNumber_number ) */
var orderNumber : Int {
get { return self.orderNumber_number.integerValue }
set(newNumber) { self.orderNumber_number = newNumber }
}
/** Enum wrapper example for NSNumber properties ( orderType_number ) */
var orderType : OrderType {
get { return OrderType(rawValue: self.orderType_number.integerValue)! }
set(orderType) { self.orderType_number = orderType.rawValue }
}
// fetch order by name
class func fetch( #name : String ) -> [Order]? {
if let orders = Query(self.EntityName).whereEqual("name", name).fetch() as [Order]? {
return orders
} else {
return nil
}
}
// create a new order
class func create( name : String, orderNumber : Int, orderType : OrderType = .Limited ) -> Order? {
if let newOrder : Order = Query(self.EntityName).create() as? Order {
newOrder.name = name
newOrder.orderNumber = orderNumber
newOrder.orderType = orderType
newOrder.createdOn = NSDate()
newOrder.updatedOn = NSDate()
return newOrder
} else {
// if we got here, something went wrong...
println("Error")
return nil
}
}
}
//
// singlton Managed Object example
//
// Usage Example:
// // this always returns a row
// let state = State.fetchOrCreate()
//
@objc(State)
class State: NSManagedObject {
class var EntityName : String { return "State" }
@NSManaged var name: String
@NSManaged var defaultOrder: Order
// Example access methods
/** fetchOrCreate always returns a single object. If there's an existing object, it returns it, otherwise, it creates a new one */
class func fetchOrCreate() -> State? {
// if we already have a State row - return it, otherwise, create it
if let state = Query(self.EntityName).fetchFirst() as State? {
return state
// a state object was not found - create one
} else {
if let state = Query(self.EntityName).create() as? State {
// initialize
state.stateName = "default state"
if let order = Order.fetch( name : "default" ) {
state.defaultOrder = order
}
return state
} else {
// if we got here, something went wrong...
println("Error")
return nil
}
}
}
}
import Foundation
import CoreData
/**
This files creates a ManagedObjectModel for Testing. It uses an in-memoy NSManagedObjectContext, which
replaces the default Context of the main app target.
Importat: This file goes in the Tests target only!
Note: The Context class in this file replaces the main target's Context class (defined in Context.swift - above).
Therefore the default target's Context.swift file must not be shared with the Tests target.
Note: This works well in the Tests target, however occasionally, I am not able to add new rows
when using: NSEntityDescription.insertNewObjectForEntityForName(...)
- which returns nil (this issue occurs only in the Test target).
Still investigationg this. I will post a fix once I find one.
*/
@objc(Contect)
class Context {
struct Instance {
static var instance = Context()
}
// returning a single static context instance for all cases. Alternatively, use Context() directly to create a new Context instance ( and a new in-memory NSManagedObjectContext) for test cases that require isolation.
class var instance : Context { return Context.Instance.instance }
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
let managedObjectModel = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle()])!
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
if (coordinator == nil) {
return nil
}
var error: NSError? = nil
if coordinator!.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil, error: &error) == nil {
return nil
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// CoreData - app default Managed Object Context
lazy var managedContext : NSManagedObjectContext? = {
return self.managedObjectContext
}()
class func save() {
var error : NSError?
let success : Bool? = Context.instance.managedContext?.save(&error)
if (success == nil) || !success! {
Color.printError(error?.localizedDescription)
}
}
}
@galiak11
Copy link
Author

-- The main API is in Query.swift (which depends on Context.swift).
-- ManagedObjectTempalate.swift and SingletonManagedObjectTempalate.swift are sample ManagedObjects using the Query API.
-- TestHelper.swift is for testing the Query API in Tests targets.

@mingsai
Copy link

mingsai commented Apr 8, 2015

Very nice work. I think you could transform this a bit to leverage more introspection rather than strings. Add multiple condition support via an array and this could mature into a highly flexible tool. Thanks for sharing. 💭

@carloslema
Copy link

Hi gk11, hope all is well. Thanks for sharing! Have any upates? Got a simple example?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment