I'm building a dummy iOS project in order to understand how to implement validation in Core Data with Swift. The Core Data model of the project has one entity called Person that contains two attributes: firstName and lastName. The project is based on Swift but, in order to start it, I'm using Objective-C to define the NSManagedObject subclass:
Person.h
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString *firstName;
#property (nonatomic, retain) NSString *lastName;
#end
Person.m
#implementation Person
#dynamic firstName;
#dynamic lastName;
-(BOOL)validateFirstName:(id *)ioValue error:(NSError **)outError {
if (*ioValue == nil || [*ioValue isEqualToString: #""]) {
if (outError != NULL) {
NSString *errorStr = NSLocalizedStringFromTable(#"First name can't be empty", #"Person", #"validation: first name error");
NSDictionary *userInfoDict = #{ NSLocalizedDescriptionKey : errorStr };
NSError *error = [[NSError alloc] initWithDomain:#"Domain" code: 101 userInfo: userInfoDict];
*outError = error;
}
return NO;
}
return YES;
}
#end
Person-Bridging-Header.h
#import "Person.h"
In the Core Data Model Editor, I've set the entity class inside the Data Model Inspector as indicated:
class: Person
The first time I launch the project, I create an instance of Person in the AppDelegate application:didFinishLaunchingWithOptions: method with the following code:
if !NSUserDefaults.standardUserDefaults().boolForKey("isNotInitialLoad") {
let person = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: managedObjectContext!) as Person
person.firstName = "John"
person.lastName = "Doe"
var error: NSError?
if !managedObjectContext!.save(&error) {
println("Unresolved error \(error), \(error!.userInfo)")
abort()
}
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "isNotInitialLoad")
NSUserDefaults.standardUserDefaults().synchronize()
}
The project has one UIViewController with the following code:
class ViewController: UIViewController {
var managedObjectContext: NSManagedObjectContext!
var person: Person!
override func viewDidLoad() {
super.viewDidLoad()
//Fetch the Person object
var error: NSError?
let fetchRequest = NSFetchRequest(entityName: "Person")
let array = managedObjectContext.executeFetchRequest(fetchRequest, error:&error)
if array == nil {
println("Unresolved error \(error), \(error!.userInfo)")
abort()
}
person = array![0] as Person
}
#IBAction func changeFirstName(sender: AnyObject) {
//Generate a random firstName
let array = ["John", "Jimmy", "James", "Johnny", ""]
person.firstName = array[Int(arc4random_uniform(UInt32(5)))]
var error: NSError?
if !managedObjectContext.save(&error) {
println("Unresolved error \(error), \(error!.userInfo)")
return
}
//If success, display the new person's name
println("\(person.firstName)" + " " + "\(person.lastName)")
}
}
changeFirstName: is linked to a UIButton. Therefore, whenever I click on this button, a new String is randomly generated and assigned to person.firstName. If this new String is empty, validateFirstName:error: generates a NSError and the save operation fails.
This works great but, in order to have a pure Swift project, I've decided to delete Person.h, Person.m and Person-Bridging-Header.h and to replace them with a single Swift file:
class Person: NSManagedObject {
#NSManaged var firstName: String
#NSManaged var lastName: String
func validateFirstName(ioValue: AnyObject, error: NSErrorPointer) -> Bool {
if ioValue as? String == "" {
if error != nil {
let myBundle = NSBundle(forClass: self.dynamicType)
let errorString = myBundle.localizedStringForKey("First name can't be empty", value: "validation: first name error", table: "Person")
let userInfo = NSMutableDictionary()
userInfo[NSLocalizedFailureReasonErrorKey] = errorString
userInfo[NSValidationObjectErrorKey] = self
var validationError = NSError(domain: "Domain", code: NSManagedObjectValidationError, userInfo: userInfo)
error.memory = validationError
}
return false
}
return true
}
}
In the Core Data Model Editor, I've also changed the entity class inside the Data Model Inspector as indicated:
class: Person.Person //<Project name>.Person
The problem now is that the project crashes whenever I call changeFirstName:. The weirdest thing is that if I put a breakpoint inside validateFirstName:, I can see that this method is never called.
What am I doing wrong?
I am a little bit guessing here, but the (id *)ioValue parameter is mapped to Swift as
ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>
therefore the Swift variant should probably look like
func validateFirstName(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, error: NSErrorPointer) -> Bool {
if let firstName = ioValue.memory as? String {
if firstName == "" {
// firstName is empty string
// ...
}
} else {
// firstName is nil (or not a String)
// ...
}
return true
}
Update for Swift 2:
func validateFirstName(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
guard let firstName = ioValue.memory as? String where firstName != "" else {
// firstName is nil, empty, or not a String
let errorString = "First name can't be empty"
let userDict = [ NSLocalizedDescriptionKey: errorString ]
throw NSError(domain: "domain", code: NSManagedObjectValidationError, userInfo: userDict)
}
// firstName is a non-empty string
}
As #SantaClaus correctly noticed, the validation function must now
throw an error if the validation fails.
Apple's Core Data Programming Guide is now updated for Swift 3. Here's the example code from the Managing Object Life Cycle > Object Validation page (memory has been renamed to pointee):
func validateAge(value: AutoreleasingUnsafeMutablePointer<AnyObject?>!) throws {
if value == nil {
return
}
let valueNumber = value!.pointee as! NSNumber
if valueNumber.floatValue > 0.0 {
return
}
let errorStr = NSLocalizedString("Age must be greater than zero", tableName: "Employee", comment: "validation: zero age error")
let userInfoDict = [NSLocalizedDescriptionKey: errorStr]
let error = NSError(domain: "EMPLOYEE_ERROR_DOMAIN", code: 1123, userInfo: userInfoDict)
throw error
}
EDIT: The example is not quite right. To get it to work, I've changed AutoreleasingUnsafeMutablePointer<AnyObject?> to an unwrapped optional and value?.pointee to value.pointee.
Related
I have a coreData NSManagedObject as follows:
public class Records: NSManagedObject {
#NSManaged public var uid: String
#NSManaged public var datetime: Date
}
In addition, I have a helper to retrieve the record by UID:
func getRecordByUid(uid: String) -> Records!{
do {
let fetchRequest : NSFetchRequest<Records> = Records.createFetchRequest()
fetchRequest.predicate = NSPredicate(format: "uid = %#", uid)
let result: [Records] = try container.viewContext.fetch(fetchRequest)
return result.first
} catch {
print(error.localizedDescription)
return nil
}
}
Now, in my view controller I used a core-data object as non-optional (for adding new record or editing existing record purpose) as described below:
class AddRecordViewController: UIViewController {
var container: NSPersistentContainer!
var record: Records!
var currentUid = ""
#IBOutlet weak var dateTextField: PickerBasedTextField!
override func viewDidLoad() {
super.viewDidLoad()
// initialise core data
container = NSPersistentContainer(name: "MyModel")
container.loadPersistentStores { (storeDescription, error) in
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
if let error = error {
print("Unsolved error \(error.localizedDescription)")
}
}
if let existingRecord = Facade.share.model.getRecordByUid(uid: currentUid) {
record = existingRecord
} else {
record = Records(context: self.container.viewContext)
}
// datePicker
let formatter = DateFormatter()
formatter.dateStyle = .medium
dateTextField.text = formatter.string(from: record.datetime)
...
}
}
The problem is that it cause an error in dateTextField.text = ... line, because it thinks the record is optional, however it isn't a case:
(lldb) po record
▿ Optional<Records>
Fatal error: Unexpectedly found nil while unwrapping an Optional value
What should I do?
I think your code would behave much better if you wrapped the fetch with the create into one method that always returns an object.
Something like
func getOrCreateRecord(uid: String) -> Records{
var record: Records?
do {
let fetchRequest : NSFetchRequest<Records> = Records.createFetchRequest()
fetchRequest.predicate = NSPredicate(format: "uid = %#", uid)
let result: [Records] = try container.viewContext.fetch(fetchRequest)
record = result.first
} catch {
print(error.localizedDescription)
}
return record ?? Records(context: container.viewContext)
}
There still might be an issue with the text field but I still think it makes sense to create a wrapper method for this logic.
I think that dateTextField is probably nil, and the fatal error is related to it. Either that, or Records(context: self.container.viewContext) is a failable initializer that returns a nil object in some cases.
I am trying to convert my below fetch request code from core data to generic type.
let request = NSPredicate(format: "name == %# AND password == %# AND type == %#", "admin", "admin", "admin")
let fetchReq : NSFetchRequest = UserRegistration.fetchRequest()
fetchReq.predicate = request
let adminDetail :[UserRegistration] = DatabaseEngine.fetch(fetchRequest: fetchReq)!
Converted so far:
extension UIViewController{
class func getData<T: NSManagedObject>(req: NSPredicate) -> T{
let fetchReq : NSFetchRequest = T.fetchRequest()
fetchReq.predicate = req
return DatabaseEngine.fetch(fetchRequest: fetchReq as! NSFetchRequest<NSManagedObject>)! as! T
}
}
DatabaseEngine.fetch function.
static func fetch (fetchRequest: NSFetchRequest<T> = NSFetchRequest(), context:NSManagedObjectContext = kApplicationDelegate.managedObjectContext) -> [T]? {
let entity = NSEntityDescription.entity(forEntityName: typeName(some:T.self)
, in:context)
// Configure Fetch Request
fetchRequest.entity = entity
do {
return try context.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as? [T]
} catch {
//let fetchError = error as NSError
// return nil
}
return nil
}
But no results any more. Anybody help me to convert this code with few explaining lines. Ans will be appreciated sure.
According to my comment I recommend to use a protocol with extension for example
protocol Fetchable
{
associatedtype FetchableType: NSManagedObject = Self
static var entityName : String { get }
static var managedObjectContext : NSManagedObjectContext { get }
static func objects(for predicate: NSPredicate?) throws -> [FetchableType]
}
extension Fetchable where Self : NSManagedObject
{
static var entityName : String {
return String(describing: self)
}
static var managedObjectContext : NSManagedObjectContext {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
}
static func objects(for predicate: NSPredicate?) throws -> [FetchableType]
{
let request = NSFetchRequest<FetchableType>(entityName: entityName)
request.predicate = predicate
return try managedObjectContext.fetch(request)
}
}
Change (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext to the reference to your managed object context.
Make all NSManagedObject subclasses adopt Fetchable. There is no extra code needed in the subclasses.
Now you can get the data with
do {
let predicate = NSPredicate(format: ...
let objects = try MyEntity.objects(for: predicate)
} catch {
print(error)
}
That's all, objects are [MyEntity] without any type casting and always non-optional on success.
The protocol is easily extendable by default sorting descriptors, sorting directions etc.
I have this line of code that successfully runs on first got:
notificationCenter.postNotificationName(NotificationContactCreated, object: nil)
But later on when this same line in same function is called I get the fatal error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Yet if I set through the code both notificationCenter and NotificationContactCreated are both set.
I'm going round in circles and cannot find the cause of this...
Any help ...please???
Thanks
here is the notification center:
//
// notification center to be used across phone app
//
lazy var notificationCenter: NSNotificationCenter = {
return NSNotificationCenter.defaultCenter()
}()
here is the func where it gets called:
func updateAccount(firstName: String, lastName: String, emailAddress: String, companyName: String, isSelf: Bool) -> Void {
// we are going to update the Contacts entity
let fetchRequest = NSFetchRequest(entityName: entityNameContacts)
// filter on first name + last name + email address
fetchRequest.predicate = NSPredicate(format: "firstName == %# && lastName == %# && emailAddress == %#", firstName, lastName, emailAddress)
do {
// try to get an array of accounts based on filter, should only be one returned
let contactsArray = try context.executeFetchRequest(fetchRequest) as? [Contacts]
// if there is a record we just want to update it
if contactsArray?.count > 0 {
// grab the reading object
let contact = contactsArray![0]
// and update its data
contact.firstName = firstName
contact.lastName = lastName
contact.emailAddress = emailAddress
contact.companyName = companyName
}
else {
// otherwise we need to insert a new record
let newContact: Contacts = NSEntityDescription.insertNewObjectForEntityForName(entityNameContacts, inManagedObjectContext: context) as! Contacts
// and populate with new data
newContact.firstName = firstName
newContact.lastName = lastName
newContact.emailAddress = emailAddress
newContact.companyName = companyName
newContact.isSelf = isSelf
}
// write amends
saveContext()
self.notificationCenter.postNotificationName(NotificationContactCreated, object: nil)
} catch {
print("func updateAccount(firstName: String, lastName: String, emailAddress: String, companyName: String, isSelf: Bool) -> Void !!!ERROR")
}
This is my code, which builds in XCode 7.2.1.
When I try to build the project in XCode 7.3 beta 2, I got the error "Value of type 'Self.ManageableType' has no member 'uid'"
protocol Manageable {
typealias ManageableType : NSManagedObject
var uid: String { get set }
}
extension Manageable {
static func className() -> String {
return String(self)
}
static func fetchObjects(predicate: NSPredicate?,
completion:(fetchedObjects: [String: ManageableType]) -> ()) {
let entityDescription = NSEntityDescription.entityForName(className(),
inManagedObjectContext: CoreDataStack.sharedInstance.context)
let fetchRequest = NSFetchRequest()
fetchRequest.entity = entityDescription
if let p = predicate {
fetchRequest.predicate = p
}
var fetchedObjectsDict: [String: ManageableType] = [:]
do {
let result = try CoreDataStack.sharedInstance.context.executeFetchRequest(fetchRequest) as! [ManageableType]
if result.count > 0 {
for object in result {
fetchedObjectsDict[object.uid] = object
}
}
} catch {
print("ERROR FETCH MANAGEABLE OBJECTS: \(error)")
}
completion(fetchedObjects: fetchedObjectsDict)
}
}
When I try to change loop code block into:
for object in result {
let uid = object.valueForKey("uid") as! String
fetchedObjectsDict[uid] = object
}
I got the error "Ambiguous use of 'valueForKey'"
Why these errors happen here in new XCode version, help please?
Your protocol extension needs a type constraint
extension Manageable where Self : NSManagedObject, ManageableType == Self { ... }
I have successfully set up a RestKit to CoreData mapping implementation in a new Swift-based app (currently XCode 6 beta 3). I know the import mappings are working from my RestKit JSON call because I can inspect the sqlite database and see my data. I am unable, however, to get the data back out of the data store in a NSFetchRequest. What am I doing wrong?
I'll spare all the RestKit setup and teardown because it's pretty standard and appears to be working great. So here is my AppDelegate query code that doesn't seem to be working:
var currentUser: User? {
if !_currentUser {
var error: NSError? = nil
let request = NSFetchRequest(entityName: "User")
let recordCount = self.managedObjectContext.countForFetchRequest(request, error:&error)
NSLog("user records found: \(recordCount)")
var result = self.managedObjectContext.executeFetchRequest(request, error:&error)
for resultItem : AnyObject in result {
_currentUser = resultItem as? User
if _currentUser {
NSLog("Fetched User for \(_currentUser!.firstname) \(_currentUser!.lastname)")
}
}
}
return _currentUser;
}
self.managedObjectContext refers to this from my AppDelegate to get the context from the RestKit shareObject:
var managedObjectContext: NSManagedObjectContext {
return RKObjectManager.sharedManager().managedObjectStore.mainQueueManagedObjectContext
}
It appears that the fetch request is successful because a breakpoint in the for/in loop occurs. When I inspect the resultItem or _currentUser objects, however, they appear empty and the "if _currentUser" NSLog never fires.
Any ideas? Am I making incorrect assumptions about getting data back out in Swift?
EDIT 2:
The problem stems from my attempt to case the resultItem into an Optional. If declare _currentUser without the optional and remove the as? optional cast the query returns a proper User object:
for resultItem : AnyObject in result {
_currentUser = resultItem as User
NSLog("Fetched User for \(_currentUser.firstname) \(_currentUser.lastname)")
}
EDIT:
I added a record count before the main fetch request and it properly shows 1 record. So something is wrong with how I'm trying to map the fetch result into my user object. Here is my user class:
import Foundation
import CoreData
class User: NSManagedObject {
#NSManaged
var id: Int32
#NSManaged
var createdAt: NSDate
#NSManaged
var udpatedAt: NSDate
#NSManaged
var username: String
#NSManaged
var email: String
#NSManaged
var firstname: String
#NSManaged
var lastname: String
#NSManaged
var organization: String
#NSManaged
var tokens: NSArray
}
The answer is that apparently Swift does not like casting the fetch result as an optional. I have to put the result into a local variable and then set the optional:
var currentUser: User? {
if !_currentUser {
var error: NSError? = nil
let request = NSFetchRequest(entityName: "User")
let recordCount = self.managedObjectContext.countForFetchRequest(request, error:&error)
NSLog("user records found: \(recordCount)")
var result = self.managedObjectContext.executeFetchRequest(request, error:&error)
for resultItem : AnyObject in result {
var currentUserItem = resultItem as User
NSLog("Fetched User for \(currentUserItem.firstname) \(currentUserItem.lastname)")
_currentUser = currentUserItem
}
}
return _currentUser;
}
Here is my setup and teardown of RestKit in Swift in case anyone (like niiamon) finds it helpful:
From my RestApi.swift:
var objectStore: RKManagedObjectStore = RKManagedObjectStore()
init() {
configureRestKit()
}
func configureRestKit() {
let objectManager = RKObjectManager(baseURL: NSURL.URLWithString(baseUrl))
//objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
RKObjectManager.setSharedManager(objectManager)
objectStore = RKManagedObjectStore(managedObjectModel: managedObjectModel())
let dataPath = "\(RKApplicationDataDirectory())/MyApp.sqlite"
NSLog("Setting up store at \(dataPath)")
objectStore.addSQLitePersistentStoreAtPath(dataPath, fromSeedDatabaseAtPath: nil, withConfiguration: nil, options: optionsForSqliteStore(), error: nil)
objectStore.createManagedObjectContexts()
objectStore.managedObjectCache = RKInMemoryManagedObjectCache(managedObjectContext: objectStore.persistentStoreManagedObjectContext)
objectManager.managedObjectStore = objectStore
// -- Declare routes -- //
// Login Route
objectManager.addResponseDescriptor(userLoginResponseDescriptor())
objectManager.addResponseDescriptor(eventLoginResponseDescriptor())
objectManager.router.routeSet.addRoute(RKRoute(name:kUserLoginRouteName, pathPattern: "/login", method: RKRequestMethod.POST))
}
func tearDownRestKit() {
// Cancel any network operations and clear the cache
RKObjectManager.sharedManager().operationQueue.cancelAllOperations()
NSURLCache.sharedURLCache().removeAllCachedResponses()
// Cancel any object mapping in the response mapping queue
RKObjectRequestOperation.responseMappingQueue().cancelAllOperations()
// Ensure the existing defaultStore is shut down
NSNotificationCenter.defaultCenter().removeObserver(RKManagedObjectStore.defaultStore())
RKObjectManager.setSharedManager(nil)
RKManagedObjectStore.setDefaultStore(nil)
}
func userMapping() -> RKEntityMapping {
let userMapping = RKEntityMapping(forEntityForName: "User", inManagedObjectStore: objectStore)
var userDictionary = ["id": "id", "created_at": "createdAt", "updated_at": "updatedAt", "username": "username", "email": "email", "firstname": "firstname", "lastname": "lastname", "organization": "organization"]
userMapping.addAttributeMappingsFromDictionary(userDictionary)
let tokenMapping = RKEntityMapping(forEntityForName: "ApiToken", inManagedObjectStore: objectStore)
tokenMapping.addAttributeMappingsFromArray(["token", "expiration"])
userMapping.addRelationshipMappingWithSourceKeyPath("tokens", mapping:tokenMapping)
return userMapping
}
func userLoginResponseDescriptor() -> RKResponseDescriptor {
let userResponseDescriptor = RKResponseDescriptor(mapping: userMapping(), method: RKRequestMethod.POST, pathPattern: "/login", keyPath: "user", statusCodes: NSIndexSet(index: 200))
return userResponseDescriptor
}
func managedObjectModel() -> NSManagedObjectModel {
return NSManagedObjectModel.mergedModelFromBundles(nil)
}
func optionsForSqliteStore() -> NSDictionary {
return [
NSInferMappingModelAutomaticallyOption: true,
NSMigratePersistentStoresAutomaticallyOption: true
];
}