Neoform 1.3.0

Neoform 1.3.0

TestsTested
LangLanguage SwiftSwift
License Apache 2
ReleasedLast Release Mar 2019
SPMSupports SPM

Maintained by Edison Santiago.



Neoform 1.3.0

  • By
  • Neomode

Neoform

Neoform is a simple framework to make form validation a little less painful without intefering with your UI.

Features

  • Field validation and masks
  • Pre-defined and custom formats for fields.
  • Form hierarchy with automatically parameterization (Dictionary)
  • Form "navigation" using return buttons
  • Support for date pickers, regular pickers, searchable pickers, checkboxes and radios
  • Storyboard support
  • No UI changes needed

Installation

CocoaPods

Neoform is available through CocoaPods. Just add pod 'Neoform' to your Podfile and run pod install

Getting Started

Creating a form

Creating a form is as easy as creating a Neoform object giving it a name for it and an array of objects conforming to NeoformElement.

We have some defaults NeoformElement that you can use on a form and are explained in more detail below (NeoformTextField, NeoformTextView, NeoformCheckElement/NeoformCheckElementCollection and NeoformTextFieldPasswordCollection).

The Neoform itself (which is an alias for NeoformElementCollection) conforms to NeoformElement, as explained on the Nesting Forms section below.

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField
    ]
)

Setting the format and validating

Every NeoformTextField must have a name and a format or a picker set, which would be used to validate the form and generate a JSON-Like Dictionary<String:Any> for you. The complete code for a simple form like the one above would be something like:

self.nameField.name = "name"
self.nameField.format = NeoformTextField.Formats.lettersOnly

self.emailField.name = "email"
self.emailField.format = NeoformTextField.Formats.email

self.birthDateField.name = "birthDate"
self.birthDateField.datePickerMaximumDate = Date()
self.birthDateField.dateFormatShow = "dd/MM/yyyy"

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField
    ]
)

To validate the form you just call form.validate() throws:

//do/catch ommited here for simplicity
//As explained below, you SHOULD use a try/catch here to get the errors thrown on form validation
let params = try! form.validate()
print(params)

/*
print(params) will print this:
[
    "name": "Your Name",
    "email": "[email protected]",
    "birthDate": "05/12/2017"
]
*/

Setting values from storyboard

To simplify the code the name and format of a NeoformTextField can also be set through Storyboard using the fields Nameand FieldFormatIB on Xcode Attributes Inspector. Since @IBInspectable currently has no support for enums you must provide a String with the name of the format. This works only with the default formats and a list of all the support formats is available on the NeoformTextFieldFormats section of this document.

Nesting forms

If your API asks for a more "structured" JSON you can also nest forms to create an hierarchy:

//Field configuration omitted here for simplicity
let addressForm = Neoform(
    name: "address",
    elements: [
        self.cityField,
        self.countryField
    ]
)

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField,
        addressForm
    ]
)

let params = try! form.validate()
print(params)

/*
print(params) will print this:
[
    "name": "Your Name",
    "email": "[email protected]",
    "birthDate": "05/12/2017",
    "address": [
        "city": "Curitiba",
        "country": "Brazil""
    ]
]
*/

Validating forms

Form validations are hard (which is the main reason for creating this) and there are no 'right' way of doing it. That's why we provide three different ways to validate a form:

1) form.validate() throws:

This method uses the do/catch structure and will throw an error when a field value is invalid. The field are looped on the order set on the elements array and the validation will stop at the first invalid field.

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField
    ]
)

do {
    let params = try form.validate()
}
catch {
    let formError = error as! NeoformError
    print(formError)
}

2) Error button

On this validation an icon will be shown on the right side of the field. When tapped, a popover will appear with an error message. Please note that this only works with NeoformTextField by now.

To activate this behavior, you must set the flag showErrorButton on the form to true and the UIViewController containing the fields must conform to UIPopoverControllerDelegate.
The form.validate() throws behaves the same way as described above, with the error being thrown.

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField
    ]
)

form.showErrorButton = true

do {
    let params = try form.validate()
}
catch {
    let formError = error as! NeoformError
    print(formError)
}

The error button is configurable on each NeoformTextField using the properties described below. All of them have default values set, so it works out of the box while allowing for further customization. Also, all the properties are also available through Interface Builder's Attribute Inspector.

  • errorButtonIconImage: The image shown on the button. The button has an fixed size, so the size should not be a large problem here. Defaults to an info symbol.

  • errorButtonIconTintColor: The tintColor applied on the image. It may be helpful while distinguishing a "large" error from a simple warning. Defaults to UIColor.red.

  • errorButtonIconSize: The CGSize of the button to be shown. Defaults to 25x25.

  • errorMessage: The message shown on the popover. This goes through NSLocalizedString before being shown, so you can provide a localized macro. Defaults to a really generic message.

  • centerErrorButtonWithSuperview: In some form designs we have the NeoformTextField and the UILabel wrapped inside a view because it has a border or an effect around the entire field, which would make centering the button relative to the field looks weird. When this flag is set the button will be vertically aligned in reference to the NeoformTextField superview. Defaults to false, with the error button being vertically centered in reference to the NeoformTextField.

3) form.validateAll(onError: ((NeoformError) -> Void)):

The onError block will be called for every error found on the form, which can be useful on a very large form where you want to warn your user only once about the mistaked they made.

The Dictionary<String,Any> returned by this method will contain only the valid info get from the form.

NeoformTextField

The NeoformTextField is the main element and the most configurable element of the form. A NeoformTextField can have a Format OR a Picker, and while the code today allows you to set both on the same field this is not supported and will cause issues with the input and the validation.

As you may have inferred, we use the UITextFieldDelegate to control a lot of the behaviors of NeoformTextField. You can set a custom UITextFieldDelegate for the field using the same .delegate property if you need, but it is not recommended to use them to change the values of the field yourself since it can cause conflits with the framework. Please also note that the method textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool of the delegate will never be called on a field with a format set, because with this happened there was no way to guarantee that the masks would work.

Because of the masks, typing at the middle or the begin of the text and pastes are not allowed at this time.

Setting a initial value to the TextField

Sometimes the TextField must have a default value. If you use a picker or a custom format you must set a value to the field by setting a string to NeoformTextField.formattedText. If the NeoformTextField has a mask, the value set will be formatted accordingly before being displayed to the user. If the field has a datePicker or a pickerView the value set here will also be selected on the picker.

//The phoneField has the mask `phoneBr` set
self.phoneField.formattedText = "4133333333"

//The value displayed on the phoneField will be "(41) 3333-3333".


self.birthDateField.dateFormatShow = "dd/MM/yyyy"
self.birthDateField.dateFormatSave = "yyyy-MM-dd"

self.birthDateField.formattedText = "2017-12-06"

//The value displayed on the birthDateField will be "06/12/2017".
//When the user taps on the field to edit the value this date will already be set on the datePicker.

NeoformTextFieldFormats

The format of a NeoformTextField controls not only the possible valid values of the field but also the valid characters that an user can type, the keyboard of the field and the masks of it. We provide many default formats for many uses (which you can also set through Attributes Inspector by typing the name manually at the FieldFormatIB property) but you can also create your own as explained below.

The currently default formats for the fields are the following:

  • optional: Almost any character, allows an empty value
  • nonEmpty: Almost any character, but at least one character is required
  • email: Validated by NSDataDetector, no regexes involved. If more than one email is typed on the field only the last one will be used.
  • optionalNumbersOnly: Changes the keyboard to numberPad and allows an empty value.
  • numbersOnly: Changes the keyboard to numberPad and at least one character is required.
  • pin4: Only numbers, changes the keyboard to numberPad, exactly 4 characters required.
  • pin6: Only numbers, changes the keyboard to numberPad, exactly 6 characters required.
  • lettersOnly: Whitespaces are also allowed.
  • currency: Always have a valid value and always shows the fraction. Begans with 0.00. As of today the currency symbol will always appear on the left on the number.
  • cpf: Provides the mask for the brazilian identification number. Does not validate the verification digits.
  • cep: Provides the mask for the brazilian postal code.
  • phoneBr: Provides the mask for brazilian phone numbers. Numbers with 8 and 9 digits are supported.
  • creditCard: Provides the mask for a pretty credit card number input, splitting the number in groups of four digits.
  • cvv: Credit card cvv, only numbers and exactly 3 characters required.
  • password: Almost any character allowed, minimum of 6 characters.

Custom TextField Format

You can also create your own format by creating a struct that adopts the NeoformTextFieldFormat protocol. Please note that as of today custom formats can only be set through code.

Simple formats

Any object that adopts the NeoformTextFieldFormat must implement those four properties:

  • minCharactersAllowed: The minimum valid size of the input. If the field canbe empty please set this as 0.
  • maxCharactersAllowed: The maximum valid size of the input. Can be set to Int.max.
  • allowedCharactersSet: The characters that can be typed by an user on the field. Other characters can be set to the field by code (i.e., in a phone mask you can set this to CharacterSet.decimalDigits and add characters like (, ), - in the mask.
  • keyboardType: Any keyboard allowed by iOS.

Please note that if you use custom formatting/masks minCharactersAllowed and maxCharactersAllowed must reflect the desired size of the input after the masking (i.e., in a phone mask any characters that you insert like (, ), - must be included on those constants).

Advanced formatting

The NeoformTextFieldFormat has three methods with default implementations that you can override to have more control of the formatting. If the four properties mentioned above are enough to control the input there is no need to override any of this methods.

  • format(_ string: String) -> String?: Received a string of any size with any characters on it and must return the string that will be displayed by the field. This method will be called when setting a initial value to the field and after the user types or delete any character from the field, so "cleaning" the string before formatting it might be a good idea. Default: removes any character in the string that is not a part of the specified CharacterSet.
  • textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool: The good old UITextFieldDelegate method, called after every change to the field value. Default: Denies the change if an invalid character is found or if we are out of bounds. Calls format(_ string: String) -> String? with the new string (using the range) and set the string returned by it as the new value of the TextField.
  • getValidValue(from value: String) throws -> String: The method called by the form validator. Any error found on the current value must be thrown here. The returned value is the one that would be parameterized, so "cleaning" the string if a mask was applied might be a good idea.

Pickers

Pickers are essential to create a good form and NeoformTextField has built-in support for three different types of pickers. Please note that a field with a picker must not have a NeoformTextFieldFormat set and that only one picker is supported by a field. If you set more than one picker to the same field only the last one will be shown.

DatePicker

The default iOS UIDatePicker set to UTC and the current locale is shown on the place of the keyboard, with a bar on top of it containing a button to move to the next element or finish the input.

Setting a value to any of the properties below will activate the UIDatePicker for the field.

Required properties:

  • dateFormatShow: The format used by the DateFormatter to show the date to the user.
    Optional properties:
  • dateFormatSave: If the API uses a different date format than the one shown to the user you can set an specific format to be used by the DateFormatter to format the initial value of the field and to parameterize the selected date. The date shown to the user will always follow the format set on dateFormatShow.
  • datePickerMinimumDate: Set the .minimumDate property of the UIDatePicker.
  • datePickerMaximumDate: Set the .maximumDate property of the UIDatePicker.

PickerView

The default iOS UIPickerView is shown on the place of the keyboard, with a bar on top of it containing a button to move to the next element or finish the input.
The objects used by the pickerView must adopt the NeoformSelectableItemProtocol (that requires only two String properties, an id and a name), so you can use your existing models with the pickerView.
Please note that when a UITextField with a pickerView becomes the first responder for the first time the first row will be selected on all the components of the pickerView.

Required properties:

  • pickerViewItems: A two-dimension array of NeoformSelectableItemProtocol, containing the components and the rows of the pickerView. Setting this property will enable the UIPickerView on the field.
    Optional properties:
  • pickerSelectedItemToJson: ((NeoformSelectableItemProtocol) -> String)?: By default when the form is parameterized we use the id as the default value for the selected picker item. If you want to use another property as the parameterized value you can provide this closure and the value returned by it will be used instead.
  • onPickerViewSelectedItem: (() -> Void)?: The closure set here will be called every time an item is selected on the pickerView by the user. This can be useful on "chained" forms, where the value of a picker depends on another (i.e., a state/city selection)
  • pickerViewComponentsFormat: If the pickerView has more than one component this MUST be set in order to create the string to be displayed to the user from the selected values (i.e., in a credit card expiration date picker with two components we might set this format to "%@/%@ so the selected value can be shown to the user as 09/19).

When needed, the current selected value from the UIPickerView of a NeoformTextField can be retrieved using the optional pickerViewSelectedItem property.

PickerTableView

The Picker Table View is a UITableViewController displayed modally over the form with a list of all the selectable options and a search bar on top. This is the recommended picker to be used for large sets of data or for data loaded remotely.
The objects used by the PickerTableView must adopt the PickerTableViewElementProtocol (that requires a id: String an a computed property displayableName: String).
Please note that an user can close a PickerTableView without selecting any element and that the modal controller will be automatically dismissed when an element is selected by the user.

Required properties:

  • loadPickerTableViewItemsHandler: ((@escaping ([PickerTableViewElementProtocol]?) -> Void) -> Void): This closure is used to fetch the available selectable elements. A network request can made here, since the array of the selected values must be passed to the closure receive as a parameter. While the list is not received a loading is displayed on the UITableViewController. If the received closure is not called the selectable items will never be displayed.
    Optional properties:
  • viewToPresentPickerTableView: The view where to present the modal controller. Default: The viewController that contains the NeoformTextField.
  • pickerTableViewTitle: The title to be displayed on the navigation bar on top of the UITableViewController. Default: The title of the field will be displayed.
  • pickerTableViewSelectedItemToJson: ((PickerTableViewElementProtocol) -> String)?: By default when the form is parameterized we use the id as the default value for the selected pickerTableViewItem. If you want to use another property as the parameterized value you can provide this closure and the value returned by it will be used instead.
  • onPickerTableViewSelectedItem: ((PickerTableViewElementProtocol) -> Void)?: The closure set here will be called when a element is selected by the user.

When needed, the selected value of a PickerTableView can be retrieved using the optional pickerTableViewSelectedItem property.

NeoformTextView

A basic UITextView with an automatic created placeholder that will work just like the native one from UITextField.
The placeholder will disappear on the user inserts text on the TextView and reappear if the textview is left if an empty value.

As the other elements, it has a mandatory name property that will be used on the parameterized dictionary.

  • isOptional: Set if a empty value is valid value. Default: true
  • placeholderText: The text to be displayed on the textView placeholder.
  • placeholderTextColor: The color of the placeholder text.

NeoformCheckElement

Checkboxes are also supported by Neoform. A CheckElement can be added to any form and, like the NeoformTextField, has a mandatory name property.

On validation, the value returned will be a boolean indicating if the box was selected or not. For more options on selection, please refer to NeoformCheckElementCollection below.

NeoformCheckElement also has Storyboard Support, so you can see the final look of it before compiling.

Customization

There are some properties available on NeoformCheckElement to customize the look of the Checkbox. All of them are also available through Attributes Inspector.

  • startSelected: The initial state of the checkbox. Default: false.
  • defaultMode: The NeoformCheckElement has two default modes, CheckLeft and CheckRight, that defines the position of the label relative to the checkbox. You can customize it as stated below, but for an advanced customization please refer to the Further Customization section.
  • selectedIcon: The icon to be displayed when the checkbox is selected.
  • selectedIconTintColor: The color to be applied to the displayed selected icon. Default: UIColor.Black.
  • unselectedIcon: The icon to be displayed when the checkbox is unselected.
  • unselectedIconTintColor: The color to be applied to the displayed selected icon. Default: UIColor.Black.

The following properties are only used when the defaultMode is used:

  • iconSize: The CGSize of the selected/unselected icon.
  • labelLocalizedText: The text displayed on the label.
  • labelTextColor: The color of the text displayed on the label.
  • labelFontSize: The size of the font displayed on the label.

Further Customization

If the default modes are not enough for your needs you can drag one UIImageView and one UILabel inside a NeoformCheckElement and they will be automatically threated as the "icon" and "text" of the checkbox.

Even using a fully customized checkbox you still get the selected/unselectedIcon behavior, along with the full form validation!

NeoformCheckElementCollection

If your form has more than one checkbox tied together it is a good idea to use a NeoformCheckElementCollection. Since it's a specialized NeoformElementCollection you can create it like a form and add to an existing form.

let notificationForm = NeoformCheckElementCollection(
    name: "notification",
    elements: [
        self.emailCheckbox
        self.pushCheckbox
    ]
)

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        self.birthDateField,
        notificationForm
    ]
)

NeoformCheckElementCollection has some attributes to help managing your collection of checkboxes:

  • isSelectionOptional: Set if we can have no checkboxes selected. Default:: false.
  • isRadio: Set if the checkboxes should work like a radio (only one can be selected at a time). Default:: false.
  • onElementSelected: ((NeoformCheckElement) -> Void)?: A closure to be called when an element is selected.
  • saveUnselectedValues: Set if the unselected values should be add to the parameterized Dictionary. Default:: false. Only the selected checkboxes will be added to the parameterized Dictionary.
let notificationForm = NeoformCheckElementCollection(
    name: "notification",
    elements: [
        self.emailCheckbox
        self.pushCheckbox
    ]
)

//Let's assume the emailCheckbox is selected and pushCheckbox is not selected

notificationForm.saveUnselectedValues = false
let params = try! notificationForm.validate()
print(params)

/*
print(params) will print
[
    "email": true
]
*/

notificationForm.saveUnselectedValues = true

let params = try! notificationForm.validate()
print(params)

/*
print(params) will print
[
    "email": true,
    "push": false
]
*/

NeoformTextFieldPasswordCollection

A simple element to check if the Password and Confirm Password matches. The password fields can have a custom format set which will be validated as well. If both fields are valid the adittional validation of the matching values will be made.

let passwordCollection = NeoformTextFieldPasswordCollection(
    name: "password",
    passwordTextField: self.passwordField,
    confirmPasswordTextField: self.confPasswordField
)

let form = Neoform(
    name: "form",
    elements: [
        self.nameField,
        self.emailField,
        passwordCollection
    ]
)

let params = try! notificationForm.validate()
print(params)

/*
print(params) will print
[
    "name": "Name",
    "email": "Email"
    "password": "Password"
]
*/

If a field value is invalid according to the format a NeoformError.invalidValue(textField: NeoformTextField) will be thrown.

If the passwords provided on the fields does not match a NeoformError.passwordNotMatch(passwordCollection: NeoformTextFieldPasswordCollection) will be thrown and the validation will not go through.

License

Neorequest is licensed under the Apache v2 License. See the LICENSE file for more info.