Core Data validation: from Objective-C to Swift

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

  • how to display image in ios push notification?
  • NSDateFormatter, am I doing something wrong or is this a bug?
  • Dynamically insert more UITextFields
  • How can I delete object from core data in swift 3
  • How to customize numeric input for a UITextField?
  • How do you load a .dae file into an SCNNode in iOS SceneKit?
  • @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?

    2 Solutions Collect From Internet About “Core Data validation: from Objective-C to Swift”

    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.