Modeling form field
First step in building a form on iOS, which is going to be used inside a collection view is to make up your mind on the design. Second step, implement the design of the cell. Third step – create an appropriate model object for the given cell.
Let’s go back to example from previous intro post – regular one-line text field.
First, let’s build a simple FieldCell
subclass, which contains UILabel
for the title and UITextField
for the value.
final class TextFieldCell: FieldCell {
// UI
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var textField: UITextField!
}
Nothing unusual here: both controls are private for the cell, as they should be.
Now, let’s build a (view)model object to supply the required values. First, we need a unique ID for this field:
/// Model that corresponds to TextFieldCell instance.
class TextFieldModel: FieldModel {
/// unique identifier (across the containing form) for this field
let id: String
}
Next, we need few strings that corresponds to values shown in label and text field:
/// String to display in the title label
var title: String?
/// Value to show inside the textField
var value: String?
/// Placeholder value to show inside the textField
var placeholder: String?
Notice that value
is plain String
. Your form data source object, whatever that may be, is responsible to convert application’s model objects into TextFieldModel.
Now comes the fun part. Text field can be used to input unlimited variety of values: username, password, street name, email, city name, phone number, card number…anything, really. Some of these uses require custom configurations.
- For passwords, you need to setup the
UITextField
to be secure text field. - For the iOS’ password auto-fill feature, you need to set
textContentType
to.password
(similar for username). - If you are inputting phone numbers, you should set
keyboardType
to.phonePad
. - Similar for email fields. On and on.
There’s an ever increasing array of possible configuration options. Making a model property for each one is fool’s errand. Instead, let’s expose the UITextField
through custom closure:
/// Custom configuration for the textField.
///
/// Default implementation does nothing.
var customSetup: (UITextField) -> Void = {_ in}
This way, the model stays really clean and endlessly expandable while the field itself is still private and encapsulated inside the TextFieldCell
component. If you have custom config, supply the closure and it will be executed at the end of the cell.populate(_ model:TextFieldModel)
method.
Finally, we need a way to receive the final edited value back. For that, we use the similar closure approach:
/// Method called when editing is done.
///
/// Default implementation does nothing.
var valueChanged: (String?, TextFieldCell) -> Void = {_, _ in}
This is the minimal setup for the field. You can expand this to whatever you want, depending on the cell design. Let’s say that you want to show some glyph icon that suggest when a particular cell is mandatory + each field has an icon in-front of it:
class TextFieldModel: FieldModel {
...
var isMandatory: Bool = false
var icon: UImage
}
Now adjust the cell design to use these new properties and…done. There are really no design limits with this approach and there is nothing left unused; you adapt the Cell + Model for your intended design and then go on and just build your forms by creating instances of these model objects.
Ok, going back now to the cell. When it’s instantiated from its .xib file, first clean any contents (set in Interface Builder) and then process edited value when editing ends:
override func awakeFromNib() {
super.awakeFromNib()
cleanup()
textField.addTarget(self, action: #selector(editText), for: .editingDidEnd)
textField.addTarget(self, action: #selector(editText), for: .editingDidEndOnExit)
}
private @objc func editText(_ sender: UITextField) {
valueChanged(sender.text, self)
}
editText()
method has to call model’s valueChanged()
closure. In order to call it, we need to save it locally as the cell’s property. We do this in the same method that populates the cell:
func populate(with model: TextFieldModel) {
valueChanged = model.valueChanged
render(model)
}
private func render(_ model: TextFieldModel) {
titleLabel.text = model.title
textField.text = model.value
textField.placeholder = model.placeholder
model.customSetup( textField )
}
And that’s about it. The very same approach is applied to all other models, like ButtonModel, ToggleModel, TextViewModel etc. They are fairly self explanatory, I hope.
Not all fields are this straight-forward thus in next post I’ll look at the PickerCell
and its model.