thumbnail

Back to Blog

Using shinobiforms to add data to your table view

Posted on 20 May 2015 Written by Alison Clarke

shinobisuite 2.8 for iOS introduced our brand new control shinobiforms. It’s a fantastic control allowing for easy data entry, and this tutorial will show you how to use it to add data to a table view.

The source code for this tutorial is available on GitHub – you’ll probably want to download or clone it to follow along.

You’ll need a copy of shinobiforms to run the code. If you don’t already have it, you can download a free trial from the shinobicontrols website. If you’re using a trial version of shinobiforms you’ll need to add your trial licence key before running the code. To do so, open up ViewController.m and add the following line to the top of viewDidLoad::

[ShinobiForms setLicenseKey:@"<your-license-key>"];

The project consists of a simple demo address book in a table, with a detail view below, which enables you to add new entries using a shinobiform. When you run the project you should see something like this:

screenshot0.png

Tap the “Add new” button and a popover will present a shinobiform to you:

Screenshot

You can input form data and hit submit. The form will be validated, and if the input is valid, a new entry will be added to the table.

The storyboard

We’ll start by taking a look at the project’s storyboard. Open up Main.storyboard:

Screenshot

You’ll see that the main view controller contains a UITableView, with its dataSource and delegate set to the ViewController class, plus another view which displays the details. It also contains an “Add new” button, with an attached segue to present a popover in the “Form View Controller”. This second view controller uses the FormViewController class, which is where we make use of shinobiforms.

We’ll take a look at how the ViewController class works first, then move on to looking at the form in FormViewController – feel free to skip ahead if you’re anxious to find out about shinobiforms!

The Main View Controller

Let’s take a look at ViewController.m. Its viewDidLoad method simply creates an array of Person objects, which will be used later to populate the table view:

- (void)viewDidLoad {
  [super viewDidLoad];

  // Create an initial array of Person objects
  self.people = [@[[[Person alloc] initWithForename:@"John"
                                           surname:@"Smith"
                                      emailAddress:@"john.smith@shinobicontrols.com"
                                             avatar:[Avatar person2]],
                   [[Person alloc] initWithForename:@"Jane"
                                            surname:@"Brown"
                                       emailAddress:@"jane.brown@shinobicontrols.com"
                                             avatar:[Avatar person4]]] mutableCopy];
}

Person is a simple data object class, storing first and last names, an email address, and an Avatar object, and providing a single method to return the full name. Avatar is a similarly simple class whose instances store a small and large image. It provides an array of predefined Avatar objects.

ViewController also provides a public method to add a Person to the table, which is fairly straightforward:

- (void)addPerson:(Person *)person {
  // Add the person to our array, and reload the table
  [self.people addObject:person];
  [self.tableView reloadData];
}

Next we implement the standard UITableViewDataSource methods to populate the table from our array of Person objects:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  return self.people.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  // Retrieve or create a cell, using the subtitle cell style to display an icon and a subtitle
  static NSString *tableCellIdentifier = @"PersonCell";
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:tableCellIdentifier];

  if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                  reuseIdentifier:tableCellIdentifier];
  }

  // Populate the cell from the relevant Person object
  Person *person = self.people[indexPath.row];
  cell.textLabel.text = person.fullName;
  cell.detailTextLabel.text = person.emailAddress;
  cell.imageView.image = person.avatar.smallImage;
  return cell;
}

We create the table cell in the usual way, using the UITableViewCellStyleSubtitle style which will display an icon and a subtitle, then populate its labels and image with the data from the relevant Person object.

The final method in this class is an implementation of the UITableViewDelegate method which deals with row selection:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  Person *person = self.people[indexPath.row];
  self.detailNameLabel.text = person.fullName;
  self.detailEmailLabel.text = person.emailAddress;
  self.detailImageView.image = person.avatar.largeImage;
}

This simply populates the detail view with information from the selected Person object.

Creating the form

Now let’s take a look at FormViewController.m, which is displayed as a popover when the user taps the “Add new” button. This is where we make use of shinobiforms. The form is set up in viewDidLoad so we’ll take a look at that step-by-step.

First, we create some text fields:

// Create the form field models
SFormTextField *forenameField = [[SFormTextField alloc] initWithTitle:@"Forename"];
forenameField.required = YES;

SFormTextField *surnameField = [[SFormTextField alloc] initWithTitle:@"Surname"];
surnameField.required = YES;

SFormTextField *emailField = [[SFormEmailField alloc] initWithTitle:@"Email"];
emailField.required = YES;

The first two fields are simple text fields; the third is an email field, which automatically validates the input as an email address, and uses the email keyboard (with prominent ‘.’ and ‘@’ characters) when the user is entering data. We set all 3 of these fields to be required, which means they will fail validation if empty. You can see the results of the validation when you hit “Submit” with invalid data:

Screenshot

The final field in our form is an SFormChoiceField, which provides a multiple choice option to the user. The choices can be strings or images; in this case we’re providing an array of images from which the user can select an avatar:

// Create an array of small images to use to pick an avatar
NSArray *avatarImages = [Avatar.allAvatars valueForKey:@"smallImage"];
SFormChoiceField *avatarField = [[SFormChoiceField alloc] initWithTitle:@"Avatar"
                                                                choices:avatarImages];
avatarField.required = YES;

Here we first create an array of small UIImages from the array provided by the Avatar class, then simply supply the array when creating the field.

Now we have our form fields, we can create a section and a form model:

// Create a section model
SFormSection *section = [[SFormSection alloc] initWithFields:@[forenameField,
                                                               surnameField,
                                                               emailField,
                                                               avatarField]];

// Create a form model
self.form = [ShinobiForm new];
self.form.delegate = self;
self.form.sections = @[section];

Our form only has one section, so we add all our fields to it. We then create a ShinobiForm instance, set its delegate to be ourselves (more on that later), and add the section to it.

We’ve now created the model on which our form is based. The next step is to generate a view for the form. shinobiforms provides us with a handy view builder which will turn our model into an SFormView:

// Build the views
self.formView = [[SFormFormViewBuilder new] buildViewFromModel:self.form];
self.formView.submitButton = [SFormSubmitButton new];

We’ve also added a submit button to the view.

The default layout for a form created in this way is to display the field label above the input field, and to size the input to match the width of the label. It’s easy to change the layout though. shinobiforms provides three standard layouts, SFormFieldLayoutLabelOnTopOfInput (the default), SFormFieldLayoutLabelLeftOfInput and SFormFieldLayoutNoLabel, but it’s also possible to create your own custom layouts. In our example we want something along the lines of SFormFieldLayoutLabelOnTopOfInput but we’d like all the input fields to have the same width, so we’ve created a custom layout class FixedWidthFormFieldLayout. We’ll take a closer look at the layout class shortly, but first let’s finish setting up the form:

// Create a fixed width layout
FixedWidthFormFieldLayout *layout = [[FixedWidthFormFieldLayout alloc] init];

// Set the width of the layout to match that of our last field (the choice field)
NSArray *fieldViews = ((SFormSectionView *)self.formView.sectionViews[0]).fieldViews;
layout.width = ((SFormFieldView *) [fieldViews lastObject]).inputElement.frame.size.width;

// Apply the layout to every field
for (SFormFieldView *fieldView in fieldViews) {
  fieldView.layout = layout;
}

[self.formView sizeToFit];
[self.view addSubview:self.formView];

self.preferredContentSize = self.formView.frame.size;

Here we create an instance of our layout class, and set its fixed width to match that of the avatar field (as we know that’s the widest field). We then iterate through the SFormFieldViews in our form view, and apply the layout to each.

The last steps in creating our form are sizing its frame to fit the contents, and adding it to the view. Finally we set the preferred content size of our view controller to the size of the form.

Let’s look at our custom layout class now. We subclass SFormFieldLayoutLabelOnTopOfInput as that does most of what we want from our layout. (Alternatively you could subclass SFormFieldLayoutBase if you wanted more control over the layout.) FixedWidthFormFieldLayout.h looks like this:

#import <ShinobiForms/ShinobiForms.h>

@interface FixedWidthFormFieldLayout : SFormFieldLayoutLabelOnTopOfInput

@property (assign, nonatomic) CGFloat width;

@end

We’ve added a single public property, width, to set the fixed width.

In FixedWidthFormFieldLayout.m all we need to do is to override the -(void)layout:(SFormFieldView *)fieldView method (from the SFormFieldLayout protocol, to which all the layout classes conform).

- (void)layout:(SFormFieldView *)fieldView {
  [super layout:fieldView];

  CGRect inputFrame = fieldView.inputElement.frame;
  inputFrame.size.width = self.width;
  fieldView.inputElement.frame = inputFrame;

  CGRect fieldFrame = fieldView.frame;
  fieldFrame.size.width = self.width;
  fieldView.frame = fieldFrame;
}

Here we just call the parent class’s implementation to do the bulk of the layout work, then set the width of both the input element and the field view itself.

Responding to form submissions

Now we’ve laid out our form, we need to work out what to do when the user submits the data. We respond to submit events by implementing methods from the SFormDelegate protocol. (Remember that earlier on we set the form’s delegate to self.) The first method is called when the form was submitted with valid data:

- (void)formDidSubmit:(ShinobiForm *)form {
  // Form was submitted with valid inputs so create a person, dismiss the popover, and add
  // the Person to the parent VC
  Person *person = [self getPersonFromForm:form];
  ViewController *parentVC = (ViewController *)self.presentingViewController;

  [self dismissViewControllerAnimated:YES completion:^{
    [parentVC addPerson:person];
  }];
}

All we do here is create a Person object based on the form (we’ll look at the getPersonFromForm method shortly), then dismiss the view controller (to close the popover), calling the parent’s addPerson method on completion. We saw earlier that addPerson will put a new row in the table.

The next method is called when the user submits invalid data:

- (void)form:(ShinobiForm *)form didNotSubmitWithInvalidFields:(NSArray *)invalidFields {
  // Form was submitted with invalid values.
  // If all the fields are empty then we'll assume the user wanted to cancel the action,
  // so we'll dismiss the popover.
  // Otherwise we'll do nothing - the form will highlight the invalid fields for us.
  Person *person = [self getPersonFromForm:form];

  if (!person) {
    [self dismissViewControllerAnimated:YES completion:nil];
  }
}

We don’t need to worry about highlighting invalid fields, as that’s all handled for us by the form. But what we’d like to do is to close the popover if the user hasn’t entered anything at all, as in that case we can assume they wanted to cancel the action. We use our getPersonFromForm method to see if the form contained any valid data: if it doesn’t the method will return nil, so in that case we’ll simply dismiss the popover.

Finally let’s take a look at getPersonFromForm to see how we extract data from the form:

// Returns a Person based on the form fields, unless all fields are empty, in which case
// returns nil.
- (Person *)getPersonFromForm:(ShinobiForm *)form {
  SFormSection *section = form.sections[0];
  SFormField *forenameField = section.fields[0];
  SFormField *surnameField = section.fields[1];
  SFormField *emailField = section.fields[2];
  SFormField *avatarField = section.fields[3];

  if ([forenameField.value length] == 0 && [surnameField.value length] == 0 &&
      [emailField.value length] == 0 && avatarField.value == nil) {
    return nil;
  } else {
    // Create a person based on the field values
    NSArray *avatars = [Avatar allAvatars];
    NSInteger index = [avatarField.value integerValue];
    return [[Person alloc] initWithForename:forenameField.value
                                    surname:surnameField.value
                               emailAddress:emailField.value
                                     avatar:avatars[index]];
  }
}

We retrieve each field from the form’s only section, then check whether all the values are nil. Otherwise we create a Person from the field’s values. Note that the choice field for the avatar returns the index of the selected value, so we have to go back to the list of all choices to get the Avatar object.

The end

And that’s all there is to it! Hopefully you’ll see from this example that it’s pretty easy to create and customise a form with shinobiforms. You could extend this example to add another form to edit the entries, but we’ll leave that for another time.

Don’t forget that the source code is all available on github – feel free to fork it and see what you can do with shinobiforms!

Back to Blog