TestsTested | ✗ |
LangLanguage | SwiftSwift |
License | Apache 2 |
ReleasedLast Release | Mar 2019 |
SPMSupports SPM | ✗ |
Maintained by Edison Santiago.
Neoform is a simple framework to make form validation a little less painful without intefering with your UI.
Neoform
is available through CocoaPods. Just add pod 'Neoform'
to your Podfile and run pod install
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
]
)
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"
]
*/
To simplify the code the name
and format
of a NeoformTextField can also be set through Storyboard using the fields Name
and 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.
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""
]
]
*/
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:
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)
}
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
.
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.
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.
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.
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:
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.
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).
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 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.
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.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
.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.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.
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.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.
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: trueplaceholderText
: The text to be displayed on the textView placeholder.placeholderTextColor
: The color of the placeholder text.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.
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.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!
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
]
*/
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.
Neorequest is licensed under the Apache v2 License. See the LICENSE file for more info.