Category

NSTextField validation

Swift Language

Swift

You want to sanitize user input before it gets into your model.

Input validation can be brute force – you validate and set each field, or you can use Cocoa Bindings which will do both “automatically”.

Introduction

Table of Contents

I’ll start by setting up a simple UI. Nothing fancy; just a UIStackView with a few NSTextFields and an NSButton .

The submit button will get the values of each text field and assign them to model properties. For simplicity, the “model” will just be instance fields on the ViewController .

displayAlert  will show an NSAlert  with the given texts. So, for age, there is some rudimentary validation.

Using Bindings

Table of Contents

If you’re an iOS developer you may not know about Cocoa Bindings. It’s a bit confusing, but still powerful.

Let’s try setting a binding on an NSTextField . In IB, select the given name text field, then open the Bindings inspector. The first item in the panel is Value. Expand it to see the possible customizations. We want to bind the value of the text field to our model, so that whenever the text field changes, so does the model. Check the Bind to checkbox and select your ViewController . The you need to set the “model key path” to the ViewContoller's  model field self.givenName. Finally, in that sea of checkboxes, enable the last one that says “Validates Immediately”. Remember the submitAction ? We grabbed the values of each text field and shoved them into the model fields. Binding will do that automatically now.

It should look like this:

Run it and you get an error.

And since the contentViewController  is hosed, you see this:

Did I mention that Cocoa Bindings uses Key Value Observing (KVO)? You may have seen this in iOS when using Core Data, or maybe you’ve used observeValue(...)  when using AVFoundation or other library.

The tl;dr is that KVO uses the Objective-C runtime to work its magic.
What you need to do is mark each model field with the keyword dynamic . Swift 4 will also require you mark it with @objc .

Or, if you’re using Swift 4, you can enable this on each field by using the @objcMembers  annotation on the ViewController

One problem though. Swift’s optionals (e.g. Int?) cannot be represented in Objective-C, so you need to use Int. But using an optional works for String. Go figure.

Run it now and you’ll see your window in all it’s glory.
But, where’s the validation?

You want a function to be called when the user navigates away from your text field. That callback is:

The inKey  parameter will contain the name of the model field to which the binding was made. Since you have multiple fields, a switch statement would be appropriate. To get the value you need to access the pointer’s pointee and cast to the model field’s type. Or simply cast to String  and validate on that. I check the age field to be just digits with the String  and a CharacterSet  (see github project). Once you have the value, you can perform validation on it. If it fails, you throw an Error.
For now, the error will be a simple custom enum.

Enter fewer than 3 characters into the text field, navigate out of the text field (tab or mouse), and Cocoa will automatically display an alert or sheet. You get a sheet by default (along with layout constraint warnings). If you want an alert, in the Bindings Inspector, enable the checkbox “Always Presents Application Alerts”. Unfortunately, the message is crummy. It shows the type and rawValue of your Error . It would be nicer to actually display a message of your choosing.

Error protocols

Table of Contents

The Alert displays an NSError's  userInfo[NSLocalizedDescriptionKey] . So, you can create an NSError  instead.

But damn, we want to use Swift’s Error  type and it has no userInfo.
Luckily, there is a Protocol named LocalizedError  (and protocol extension providing a default implementation) we can use to set the error description. We set the errorDescription  field to our message. If there are multiple cases in our Error  enum, use a switch statement. Or use a switch even if there aren’t multiple cases to make it easy to add them later.
Here we set the errorDescription  field to the textFieldValidation’s message that is set when it error is thrown.

There other fields are shown here, but aren’t used in the alert.

Since I’m on an Error  rant, you can also customize other NSError -like fields by adopting the CustomNSError  protocol. If you use this instead of LocalizedError , you will see the domain and error code on the alert. If you adopt both, the LocalizedError  takes precedence.

You can override the default alert to show a help button that will use your helpAnchor set in the Error , along with the Error's  recoverySuggestion  by creating an NSAlert  with your Error .

Look nice. However, we’re trying to validate data before it gets to the model. For bad data, we need to throw an Error  which then displays the default alert/sheet. If we pop up our alert and throw, the user sees two alerts. If we don’t throw, the user sees just our alert, but the bad data gets into the model. So what can we do? Live with the default alert/sheet. Sorry. But you can use this alert approach in other contexts.

Final thoughts

Table of Contents

Wait a minute!

So, that’s an introduction for how this works. But is it appropriate for the ViewController to be doing this? In general, you don’t want huge controllers. And in a real program, you have actual domain objects. The appropriate place for validation is inside these domain classes. Why would any other class/struct need to know the internal details/rules of a domain class?

So, make a domain class that is KVO compatible and move the vaildateValue  function to it.

Then, in the ViewController, have an instance variable of type Person . This will probably be injected in real life, but go ahead an instantiate it in viewDidLoad() for now.

Finally, in the Bindings Inspector for each text field, update the Model Key Path to use the person field.

Groovy, huh?

 

Table of Contents

Summary

Table of Contents

You can use Cocoa Bindings to perform validation on user input before it is set on your model data.

There are several useful Error  protocols that you can adopt to make the user experience better.

Resources

Table of Contents

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.