Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a meaningful error message back to the view controller with Core Data validation?

Edit: I want to let everyone know that in the month since I posted this I've continued to work on the problem. There's now a repo on github that shows how to easily accomplish this and remain compliant with KVC. There's no reason to avoid Core Data validation on iOS. It may be different than on Mac OS X but it's not arduous or difficult.


I'm in a view controller editing the properties for a Person object. Person is an NSManagedObject subclass. I'm doing early (before save) validation with Core Data. I'm using the documented validateValue:forKey:error: method like this...

NSError *error;
BOOL isValid = [person validateValue:&firstNameString forKey:@"firstName" error:&error];
if (!isValid) {
    ...
}

And I've got min and max values set in Core Data's model editor in Xcode. When I validate firstName and it's too short I get an error like this...

Error Domain=NSCocoaErrorDomain Code=1670 "The operation couldn’t be completed. (Cocoa error 1670.)" UserInfo=0x8f44a90 {NSValidationErrorObject=<Event: 0xcb41a60> (entity: Event; id: 0xcb40d70 <x-coredata://ADB90708-BAD9-47D8-B722-E3B368598E94/Event/p1> ; data: {
firstName = B;
}), NSValidationErrorKey=firstName, NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1670.), NSValidationErrorValue=B}

But there's nothing there that I could display to the user. But the error code is there so I could do something like this...

    switch ([error code]) {

        case NSValidationStringTooShortError:
            errorMsg = @"First name must be at least two characters.";
            break;

        case NSValidationStringTooLongError:
            errorMsg = @"First name is too long.";
            break;

    // of course, for real, these would be localized strings, not just hardcoded like this
    }

This is good in concept but firstName, and other Person properties, is editable on other view controllers so that switch would have to be implemented again on whatever view controller edits firstName. There must be a better way.

Looking at the Core Data docs for Property-Level Validation reveals this...

If you want to implement logic in addition to the constraints 
you provide in the managed object model, you should not override 
validateValue:forKey:error:. Instead you should implement methods 
of the form validate<Key>:error:. 

So I implemented validateFirstName:error: in Person.m. And handily, it executes via the existing validateValue:forKey:error: method in the view controller, just as the docs say it will.

But inside validateFirstName:error:, error is still nil even when firstName is too short. When I continue and control returns to the view controller there is an error like at the top of this question but, again, that's too late. I was hoping that by the time control reached validateFirstName:error:, firstName would have been validated against the constraints specified in the model editor and the filled in error object would be passed in via the error parameter. But it's not.

I'm left with two ideas that might lead to a good home for the switch statement...

  1. In Person.m implement a custom method like firstNameValidationForValue:error:. The view controller would call that method. In firstNameValidationForValue:error: call validateValue:forKey:error:. When it returns with an error, construct a meaningful error message, using the switch, create a new NSError object and return that to the view controller for consumption. This works but it deviates from the standard KVC approach.

  2. Remove all the validation from the Core Data model editor in Xcode and perform all of the validation in the methods like validateFirstName:error:. Based on the results, construct a meaningful error message, using the switch, create a new NSError object and return that to the view controller for consumption. This has the advantage that the constraints and the error messages are in the same method. And, unlike the first idea, continues to follow the standard KVC approach.

What would you do? Is there another way?

Edit: Additional detail on the edit cycle...

The edit cycle begins with the user tapping Add. A new Person object is inserted into the MOC. The view displays a form for editing and Cancel and Done buttons appear on the nav bar. The user starts entering data in fieldA, finishes and taps fieldB. Assume fieldA must be valid before continuing. Before fieldB becomes the first responder the validation for fieldA runs. It fails. The view controller displays the error message returned from the validation and fieldA remains the first responder. The user fixes the problem, and taps on fieldB again. Again the validation runs and this time it passes. fieldB becomes the first responder. This "add data/tap another field or tap Next/validate/move to next field or not" process continues.

It's important to know that at any time the user can tap Cancel. In that case, in terms of the MOC, all I have to do is [myMOC rollback];.

@ImHuntingWabbits: If I invoke save instead of validateValue:forKey:error: there is a problem with that approach. Assume the user is entering data in fieldA. The user taps fieldB and the validation for fieldA runs. This validation uses the "save and then parse the error" method. But assume it passes and all the other fields also pass. So now the MOC has been saved. But the user has not tapped Done and could very well press Cancel. If the user taps Cancel then the save must be undone. That might be relatively easy if the model was very simple but could be really complex. In my particular case I would not want to take this approach.

Another Edit

Can we all reconvene here at github: github.com/murraysagal/CoreDataValidationWithiOS I've got a sample app there and perhaps a better description of the problem in the readme. The sample app shows that validation, in general, is not at all difficult on iOS. The app demonstrates how it's possible to get a meaningful error message back to the VC and remain fully KVC compliant.

And it discusses two possible enhancements to Core Data that I'd like some feedback on before putting them on radar.

like image 977
Murray Sagal Avatar asked Jan 16 '14 15:01

Murray Sagal


1 Answers

I would not use Core Data validation for the UI on iOS. Core Data validation was designed for the desktop with bindings and not for iOS.

You will have a far easier time doing the validation in your view controllers and using Core Data validation as a back up instead of trying to wire Core Data validation to the User Interface.

Update

Could you explain why you think it will be easier to implement validation on the View Controller level. While proper validation and error handling is never easy, I can't see a reason why the Core Data level validation should be more complex (besides the issue that validations are possibly performed more than one time, even when it is not required). You also don't answer the case where there is no VC, or when there are more than one VCs handling objects. Also, certain validations can't be performed on VC level (e.g. check for uniqueness of certain properties, which is a pain anyway).

Core Data validation was added for OS X back when there was a fairly tight coupling between Core Data and the user interface. That coupling was called bindings. With bindings, entry into a text field was immediately and "automatically" added to the Core Data entity associated with that field.

Further, when that data got updated, Core Data could "respond" with validation back to the text field so that the text field could reject the data entry and explain why it was rejected.

Those bindings do not exist on iOS. They need to be written, by hand, for every data entry point.

Since we are writing those check points anyway, there is no reason to try and hook into a generic system like Core Data's validation when we can write far more focused validation directly into the view controllers and save ourselves a level of abstraction.

In the case of data importing, we again have a controller handling the import. There is no direct wiring between the import controller and Core Data anyway so again there in reason to try and plug into a generic system like Core Data's validation when we can write focused code to solve the issue.

Generic systems leak edge cases. If someone has taken the time to cover most of those edge cases (as in the case of Core Data and bindings on OS X) then go ahead and use them. But if you must cover those edges or the connective code yourself anyway, there is very little value in integrating into a generic system. This is especially true of the case where the generic system was not designed to handle the use case (such as Core Data validation and iOS).

There are several parts of Core Data that pre-date iOS and do not fit into iOS very well. Validation is one of them.

like image 187
Marcus S. Zarra Avatar answered Sep 28 '22 10:09

Marcus S. Zarra