TestsTested | ✗ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | May 2015 |
Maintained by Bruno Berisso.
TLFormView is yet another form view truly universal. This means that the same component support both iPhone and iPad using a mechanism around the Auto Layout Visual Format to adjust the layout to the running device.
Because it doesn't extend UITableView
you are completely free to create anything to use as a form field as long as it extends the base TLFormField
class. It also has some nice features like: conditional visibility using NSPredicate
, in-place help for each field with UIPopoverControler
and on-the-fly edit/read-only modes switch among other things.
There is two basic components: TLFormView
and TLFormField
. TLFormView
inherit from UIScrollView and add another data source and delegate to create and handle events form the form. You need to implement three methods of the TLFormViewDataSource
protocol to get the form functional, these are:
- (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form;
- (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName;
- (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form;
fieldNamesToShowInFormView:
return an array of strings containing the field names (or ids) used to identify the fields in the form. formView:fieldForName:
creates a field for every field name in the form. A field is any subclass of TLFormField
(that is a sub class of UIView
), there is a set of default fields that can be used to fit the 80% of the use case. These are:
TLFormFieldImage
: for display images form an url or a raw imageTLFormFieldList
: for display a list of thingsTLFormFieldMultiLine
: to show long textTLFormFieldSingleLine
: to show short text, numbers and bool valuesTLFormFieldTitle
: to show a short text formatted as a titleThe project has a category over TLFormField
with basic methods for customising the aspect of the fields. It has some methods to configure the title and give access to the UIAppearance
proxy of the internal components.
Once you give a TLFormField
for each field name to the form you need to define how to layout those fields. To do so you need to implement the third method of the data source constraintsFormatForFieldsInForm:
and return an array of strings containing the the rules in Auto Layout Visual Format to place the fields in the screen. When writing the rules you need to reference the fields using the field names you return in the fieldNamesToShowInFormView:
implementation. Here you have the chance to check for the capabilities of the device and adapt your layout changing the rules as you need.
Here is an example implementation of the three required methods in TLFormViewDataSource
:
- (NSArray *)fieldNamesToShowInFormView:(TLFormView *)form {
return @[
@"user_name",
@"avatar",
@"age",
];
}
- (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName {
Class fieldClass;
NSString *title;
id value;
if ([fieldName isEqualToString:@"user_name"]) {
fieldClass = [TLFormFieldSingleLine class];
title = @"User Name";
value = userModel.name;
}
else if ([fieldName isEqualToString:@"avatar"]) {
fieldClass = [TLFormFieldImage class];
title = @"Avatar";
value = userModel.avatarUrl;
}
else {
fieldClass = [TLFormFieldSingleLine class];
title = @"Age";
value = userModel.age;
}
return [fieldClass formFieldWithName:fieldName title:title andDefaultValue:value];
}
- (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form {
//For iPhone we want a vertical layout like we get on a UITableView
if (isIPhone) {
return @[
//Place the avatar on the top
@"V:|-[avatar(==230)]-",
@"H:|-[avatar(==420)]-|",
//Now place all the fields to the bottom
@"V:[avatar]-[user_name(>=44)]-",
@"H:|-[user_name]-|",
@"V:[age(==user_name)]-|",
@"H:|-[age]-|"
];
//For anything else we will place the image on the top left and the rest of the fields to the right
} else {
return @[
//Place the avatar on the top left
@"V:|-[avatar(==230)]",
@"H:|-[avatar]",
//Now place all the fields to the right
@"V:|-[user_name(>=44)]",
@"H:|-[avatar]-[user_name]-|",
@"V:[user_name]-[age(==user_name)]-|",
@"H:|-[avatar(==420)]-[age]-|"
];
}
}
This example produce this on iPhone
and this on iPad
There are cases when you need to show a particular field only when other fields has a special value. For this propose TLFormField
has a property visibilityPredicate
that you can set to an NSPredicate
at the time the field is created. The predicate can access the values of all the fields in the form through the NSPredicate variable substitution. This means that in you can access any field value in the form with the syntax $[name of the field].value
.
Let suppose that we want to show the "User Name" field only if the "Age" is greater than 12 years old. We can write a predicate that evaluate to true if the "age" field is greater than 12 and assign it as the visibilityPredicate
for the "user_name" field. This is the code:
- (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName {
...
//Asume that 'field' is an instance of TLFormField created before somehow
if ([fieldName isEqualToString:@"user_name"])
field.visibilityPredicate = [NSPredicate predicateWithFormat:@"$age.value > 12"];
...
}
Suppose that we want to explain the strange behaviour of our previous example in a concise way. To explain this kind of little special behaviours we can use the in-place support provided by TLFormField
. The only thing we need to do is set the help text string in the helpText
property in the field. When there is a value for that property the field show a question mark icon next to the field title that when is taped show the help text in a popup window. Here is the code:
- (TLFormField *)formView:(TLFormView *)form fieldForName:(NSString *)fieldName {
...
if ([fieldName isEqualToString:@"age"])
field.helpText = @"This is the age of the user. Users with less than 12 years are not allowed to enter his name.";
...
}
And this is how it looks when the icon is taped:
To help you with the setup of the form there is a class TLFormModel
that do what we just did automatically inferring the implementation of the TLFormViewDataSource
form his own taxonomy. You only need to extend it and add one property for each field you want to show in the form. The types of the properties must be one of the types declared in the file TLFormModel.h. Each type corresponds to one of the standard TLFormField
provided.
To read the values from the form just access the properties like you wold do on any object. To write the values you will need to wrap the values in one of the value constructors provide for each type. Once the values in the model are updated you need to manually perform a reload to show the new values with the reloadValues
method of TLFormView
(this is a cheep update, no view destruction is involved).
These are the declarations for the types supported by TLFormModel
:
TLFormSeparator
: extend NSObject. Rendered as a separator in the form, allow to group fields in sections. Has no value or title.TLFormText
: extend NSString.TLFormLongText
: extend NSString.TLFormTitle
: extend NSString.TLFormNumber
: extend NSNumber.TLFormBoolean
: extend NSNumber.TLFormList
: extend NSArray.TLFormEnumerated
: extend NSDictionary.TLFormImage
: extend NSObject.These classes doesn't have any logic other than the one inherited form his superclasses, they act almost as an annotation over a property. The intended way to construct values of this types is with plain C functions declared in the TLFormModel.h that check the type of the parameter and copy the value given as parameter.
Using the type and value of his properties TLFormModel
infer what kind of form field should use for each property. For the field title the property name is used. If the property name use snake-case naming convention each "_" is translated to a space and all words are capitalised, so the property "user_name" will have the title "User Name". The order of the fields is the one used to declare the properties. For ex:
@interface UserModel : TLFormModel
@property (nonatomic, strong) TLFormImage *avatar;
@property (nonatomic, strong) TLFormText *user_name;
@end
Will present the avatar field at the top and the user name below.
Our previous example could be written with TLFormModel
like this:
@interface UserModel : TLFormModel
@property (nonatomic, strong) TLFormImage *avatar;
@property (nonatomic, strong) TLFormText *user_name;
@property (nonatomic, strong) TLFormNumber *age;
@end
@implementation UserModel @end
That's it. This will produce a vertical layout like the one we get on iPhone on all the platforms. Now to connect the TLFormModel
to the form we need to use the setFormModel:
method on the TLFormView
we are using.
...
//At some point in some place...
FormUserModel *formUserModel = [FormUserModel new];
//This copy the values of our user model to the form model constructing the correct types using plain C functions
formUserModel.avatar = TLFormImage(userModel.avatar);
formUserModel.user_name = TLFormText(userModel.userName);
formUserModel.age = TLFormNumber(userModel.age);
TLFormView *form = ...
[form setFormModel:formUserModel];
To adapt the layout for other device families we need to override the implementation of constraintsFormatForFieldsInForm:
provided by TLFormModel
like this:
@implementation UserModel
- (NSArray *)constraintsFormatForFieldsInForm:(TLFormView *)form {
//For iPhone we want the default implementation provided so just return the 'super' version
if (isIPhone)
return [super constraintsFormatForFieldsInForm:form];
//For anything else use our custom layout
else {
return @[
//Place the avatar on the top left
@"V:|-[avatar(==230)]",
@"H:|-[avatar]",
//Now place all the fields to the right
@"V:|-[user_name(>=44)]",
@"H:|-[avatar]-[user_name]-|",
@"V:[user_name]-[age(==user_name)]-|",
@"H:|-[avatar(==420)]-[age]-|"
];
}
}
@end
Now suppose that we want to edit this user info and get the result back. We need to toggle the edit state on the form and read the updated values from the TLFormModel
instance we are using. This is how we could do it:
- (IBAction)toggleEditionAction:(id)sender {
//Set the form on edit mode
self.form.editing = !self.form.editing;
//update the fields to reflect it
[self.form setupFields];
}
- (IBAction)saveUserAction:(id)sender {
//Read the values entered by the user and save it to disc.
NSDictionary values = @{
@"avatar": formUserModel.avatar,
@"user_name": formUserModel.name,
@"age": formUserModel.age
}
[values writeToURL:[self saveUrl] atomically:YES];
}
About "editing" the images. The default TLFormFieldImage
don't provide any way to pick an image at this point. You will need to handle the field selection and show some kind of image picker.
TLFormView
report two events through TLFormViewDelegate
out of the box: didSelectField:
and didChangeValueForField:
. The events are fired by a TLFormField
implementation that notify a form about it and the form then propagate the event out. Depending on how the field is implemented it might have more events to report. For example the TLFormFieldList
use a UITableView
to present the list of values so it has it's own delegate that allows to customise if the rows can be rearranged, or the selection of one row.
iOS >= 8.0
Or clone the repo and check the code under Pod/Classes
.
There are many things that need improvement, here are some:
TLFormField+UIAppearance.h
If you want to contribute to the project please consider pick one of this items.
BrunoBerisso, [email protected]
TLFormView is available under the MIT license. See the LICENSE file for more info.