Basic data validation: from Objective-C to Swift
I am creating a dummy iOS project to understand how to implement validation in Core Data using Swift. The project's Core Data model has one object named Person
, which contains two attributes: firstName
and lastName
. The project is based on Swift, but to run it I use Objective-C to define the subclass NSManagedObject
:
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
Human-Bridging-header.h
#import "Person.h"
In the master data model editor, I have set the entity class inside the data model inspector as stated:
class: Person
When I first run the project, I create an instance Person
in a method AppDelegate
application:didFinishLaunchingWithOptions:
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 name
println("\(person.firstName)" + " " + "\(person.lastName)")
}
}
changeFirstName:
linked to a UIButton
. Therefore, whenever I click on this button, a new one is String
generated randomly and assigned person.firstName
. If this new is String
empty, validateFirstName:error:
creates NSError
, and the save operation fails.
This works fine, but in order to have a clean design Swift, I decided to remove Person.h
, Person.m
and Person-Bridging-Header.h
and replace them with a single file Swift:
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 master data model editor, I also changed the entity class inside the data model inspector as stated:
class: Person.Person //<Project name>.Person
Now the problem is that the project crashes when I call changeFirstName:
. The strangest thing is that if I put a breakpoint inside validateFirstName:
, I see that this method is never called.
What am I doing wrong?
source to share
I'm guessing a bit here, but the parameter (id *)ioValue
shows up in Swift as
ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>
so 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 pointed out, now the validation function will throw an error if validation fails.
source to share
Apple 's Master Data Programming Guide has now been updated for Swift 3. Here's a sample code on the Object Lifecycle> Object Validation page (Object Validation page) has memory
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
}
The EDIT: . The example is not entirely correct. To make it work, I changed AutoreleasingUnsafeMutablePointer<AnyObject?>
to expanded optional and value?.pointee
to value.pointee
.
source to share