GenericUI
GenericUI provides you with beautiful and generic UI elements for your iOS projects. For now it just focuses on inputs and forms but there's a lot more to come.
Contents
Requirements
- Swift 5.2
- iOS 9.0+
Components
Only a few components are available at this time. They're super customizable and should enable you to save a lot of time and code.
Inputs
UIInputField<OutputType>
This is a very simple generic extension to the UITextField that attempts to convert the input to the generic type.
UIActiveInput<OutputType>
This is a more sophisticated input that combines multiple views, based on the UIInputField<OutputType>. It is highly customizable
but by default comes with a nice design, a label, an animated indicator showing if the input is active and more. It exposes the raw
text field's API including accessibility features.
Forms
UIQuickForm<ModelType>
A simple component that allows you to build beautiful forms that can gather a generic type of model from the user for you. UIQuickForm takes
care of all the layout for you.
more...
a lot more to come...
Examples
Input for a String
The code below will make an input labeled "Firstname". It allows the user to enter text and the output will be a String.
let input = UIActiveInput<String>("Firstname")
addSubview(input)
let firstname: String = input.output // returns String?Input for a UInt
Below is an example of an input that gathers an unsigned integer from the user. The ouput is a UInt. The UIActiveInput is
smart and automatically sets the keyboard to a numeric pad with no decimal point.
let input = UIActiveInput<UInt>("Age")
addSubview(input)
let age: UInt = input.output // returns UInt?Input for a custom object T
You may gather your own type of model from a UIActiveInput, however it will need to conform with a couple protocols,
namely: StringTwoWayConvertible and BestKeyboardType. The purpose of these protocols is to ensure your object provides
a way to be converted to and from a String (since UIActiveInput is based on top of the native UITextField) and
specifies the best type of iOS keyboard to be used. The StringTwoWayConvertible is a way for you to provide a custom
String transform.
Using extensions, you may also enable existing or third party types to be returned by the UIActiveInput.
Example:
class MyCustomType : StringTwoWayConvertible, BestKeyboardType {
var number: Double
init?(_ text: String) {
// Write your own String -> Object transform
guard let n = Double(text) else {
return
}
number = n
}
var description: String {
// Write your own Object -> String transform
return "\(number)"
}
static var bestKeyboardType: UIKeyboardType {
return .decimalPad
}
}
let input = UIActiveInput<MyCustomType>()Customize the UIActiveInput
UIActiveInput is deeply customizable and is built right on top of the native UITextField. It is essentially a UIView that wraps
a UILabel, a UITextField and a UIView used to show whether the input is active or not.
Styles
Here are some of the properties you have access to that allow you to customize the style:
input.labelexposes theUILabelobject, which you may directly customize or even remove.input.inputColoris the background color of the text field.input.backgroundColoris the background color of the whole element.input.activeColoris the color of the little vertical indicator, blue by default.input.indicatorWidthis the width of the active indicator, 2.0 by default.input.forcedLabelWidthallows you to force the label to be a certain width. This is useful when you have multiple inputs and you want the left edge of all text fields to align.input.placeHoldersets the placeholder for the text input.input.fontsets the font for the text input.input.textColorsets the text color for the text input.input.layergives you access to the design layer of the whole element if you'd like to make deeper customizations.input.inputLayergives you access to the design layer of the text input.input.keyboardTypelets you set a keyboard type for the text input (note this is always set automatically, but you may override it by setting it manually).input.layoutMarginslets you customize the margins within the element (space between the label and text input from the edges of theUIActiveInput).
A lot more that I can't list here... UIActiveInput is a subclass of UITextField, which is a sublcass of UIView so all the properties and methods you
expect to find on those you will find on UIActiveInput.
Text Input Delegate
input.delegate lets you designate a delegate for the embedded text field.
Touch Events
By default, the text field will become firstResponder if the user taps anywhere on the UIActiveInput, including the label. You may also do so
programmatically by simply calling input.becomeFirstResponder() and input.resignFirstResponder().
Accessibility
The UIActiveInput provides you with all the standard accessbility functionality and relays all those calls to the embedded text input
(blind users should "see" the UIActiveInput the same as a regular UITextField).
A Simple Form
Form for CGSize
Here's an example of a very simple form with two inputs in it. The goal of this form is to collect a CGSize, one input for the width and one for the height.
class ViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//...
let form = UIQuickFormView<CGSize>()
view.addSubview(form)
// Form styling and layout
form.backgroundColor = .green
form.layer.cornerRadius = 3.0
form.translatesAutoresizingMaskIntoConstraints = false
form.topAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: 10.0).isActive = true
form.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 8.0).isActive = true
form.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: -8.0).isActive = true
let widthInput = UIActiveInput<CGFloat>(label: "WIDTH")
let heightInput = UIActiveInput<CGFloat>(label: "HEIGHT")
// Bind the inputs
let widthInputId = form.bind(input: widthInput) { (size: inout CGSize, input: UIActiveInput<CGFloat>) in
guard let width = input.output else {
// handle any errors here
return (false, nil)
}
size.width = width
return (true, nil)
}
let heightInputId = form.bind(input: heightInput) { (size: inout CGSize, input: UIActiveInput<CGFloat>) in
guard let height = input.output else {
// handle any errors here
return (false, nil)
}
size.height = height
return (true, nil)
}
// Lay out the inputs in the form (both inputs go on the same line here)
form.addRow([FormElement(widthInputId), FormElement(heightInputId)])
form.setRecommendedContentPriorities()
form.build() // sets up autolayout constraints for all the views within the form
// When you want to resolve the form to a CGSize:
var size = CGSize(width: 0, height: 0)
let success = form.resolve(model: &size) // returns a tuple (Bool, [Error])
}
}Handling Errors
Create your own error types for handling bad user input. Error handling is done at the binding. When the form is resolved you can obtain an array of all the errors.
enum PersonError : Error {
case tooOld, nilInput
}
...
let ageId = form.bind(input: ageInput) { (person, input) in
guard let age = input.output else {
return (false, PersonError.nilInput)
}
if age > 120 {
return (false, PersonError.tooOld)
}
person.age = age
return (true, nil)
}
...
let success = form.resolve(model: &person)
// success is a tuple (Bool, [Error])
if success.0 {
// everything looks good!
} else {
for error in success.1 {
...
}
}Adding a native input to a form
You may add any kind of input to the UIQuickFormView, including native and third party.
let textInput = UITextField(frame: .zero)
let inputIndentifier = form.bind(input: textInput) { (..., input: UITextField) in
guard let text = input.text else {
...
}
...
}
form.addRow([..., FormElement(inputIndentifier), ...])Adding a view to a form
You may add any kind of view to the UIQuickFormView.
let label = UILabel(frame: .zero)
let viewIndentifier = form.bind(view: label)
form.addRow([..., FormElement(viewIndentifier), ...])Customizing form element sizes
You may customize how much horizontal space every element takes within a line of the form.
The default size value is 1. If all the elements in a row are the same size number then they'll
each take an equal share of the horizontal space.
form.addRow([FormElement(input, size: 2), FormElement(view, size: 1), ...])Spacers
UIQuickFormView provides an easy way to create spacers.
form.addRow([FormElement.makeSpacer(size: 3), FormElement(view, size: 1), ...])Upcoming
- At this time it is not possible to know if there are input errors until form resolution. A feature that tells the calling code if there is an error as inputs are changed is coming soon. The form will expect a binding as well as an optional validator for every input.
- Automatic binding.
- Generic modals.
- Generic table views and table view controllers.





