Quantcast
Viewing all articles
Browse latest Browse all 4399

Eureka Tutorial – Start Building Easy iOS Forms

Image may be NSFW.
Clik here to view.
Many apps let the user enter information via a form interface. Forms can vary in complexity, from simple username and password screens, to more involved interfaces like those for a contact book or calendar.

Eureka is a powerful library that allows developers to rapidly create form interfaces for user input.

This Eureka tutorial will teach you how to use Eureka’s essential building blocks by crafting the interface of a simple to-do app called EurekaToDo. You’ll see how Eureka makes it easy to set up various commonly-used user interface elements such as date pickers, text fields and segmented controls with no boilerplate UIKit code!

In addition:

  • Eureka’s components are very flexible and extensible out-of-the-box and cover the majority of use cases. Eureka makes it easy to implement your custom components if your needs get more specific. Examples of community-generated plugins include a GooglePlacesRow and an ImageRow, which you’ll get to use in this Eureka tutorial.
  • Eureka is a well-documented library with plenty of helpful tips and examples available through its official Github portal.

Getting Started

Download the starter project for EurekaToDo, the to-do list app you’ll be working with for this Eureka tutorial.

In addition to basic view controller transitions, the starter project includes the app’s model and view model layers. Open EurekaToDo.xcworkspace and take a few minutes to browse the project. Here’s a quick overview of important classes:

  • ToDoListViewController: Manages the list of to-do items presented to the user.
  • ToDoListViewModel: The presentation logic for ToDoListViewController.
  • EditToDoItemViewController: Enables the user to add and edit to-do items — it currently doesn’t do much. All the work for this Eureka tutorial will be in this file.
  • EditToDoItemViewModel: The presentation logic supporting EditToDoItemViewController.

You’ll notice the project doesn’t use a storyboard. While Eureka can leverage nib files for custom views, you’ll be amazed by how easy it is to programmatically create and customize common controls.

Note: For this Eureka tutorial, you will be passing values to and from a view model and not the model directly. Although Eureka does not require the use of the Model-View-ViewModel (MVVM) paradigm, MVVM encourages a cleaner, more testable app architecture. For this Eureka tutorial, the view model may be thought of as directly substituting for the app’s model. See the Further Reading section for more on this topic.

Build and run the application. You’ll see a to-do list pre-populated with a single item. Tapping the item takes you to a blank screen (controlled by EditToDoItemViewController) with Back and Save navigation items. Tap the Back button in the top left to return to the to-do list.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

The edit screen currently leaves a lot to be desired. You probably recall EditToDoItemViewController didn’t have much in it. This is where you come in! By the time you’re done, the final project’s interface will look like the picture below:

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Adding Eureka to our View Controller

Open EditToDoItemViewController.swift and replace the current import and class declaration with the following:

import Eureka
import UIKit

class EditToDoItemViewController: FormViewController {

Note: You may need to build the project if the Cocoapod module is not immediately visible to your project.

You’ve imported the Eureka framework and changed the superclass to be FormViewController.

FormViewController is a UIViewController subclass provided with Eureka. It includes a form property, which is an instance of Eureka’s Form class. The Form class is an abstraction of the UITableView object into which you’ll be adding various user interface elements.

A Form instance may contain one or more Section objects. Each section, in turn, may contain one or more Row objects. As you may have guessed from their names, these properties correspond to the sections and rows of the UITableView. Eureka’s Form, Section and Row abstractions provide some very powerful and flexible functionality.

Adding a Section and a Row

In order to add rows to a form, you will first need a Section object to contain them.

You’ll use Eureka’s custom +++ operator to add a Section to the form, and the <<< operator to add rows to a section. Add the following to viewDidLoad(), just beneath the call to super:

//1
form
  +++ Section()	//2
  <<< TextRow() { // 3
    $0.title = "Description" //4
    $0.placeholder = "e.g. Pick up my laundry"
    $0.value = viewModel.title //5
    $0.onChange { [unowned self] row in //6
      self.viewModel.title = row.value
    }
}

Here's a look at what this code does:

  1. Acts on the form object provided by FormViewControler.
  2. Instantiates and adds a Section to the form using Eureka's +++ operator.
  3. Adds a TextRow to the section. As you'd expect, this is a row that will contain some text. The initializer accepts a closure used to customize the row's appearance and events.
  4. Adds a title and placeholder text to the textfield. The title is a left-justified label and the placeholder appears on the right until a value is added.
  5. This sets the initial value of the row to show the to-do item's title.
  6. Eureka's Row superclass comes with a host of callbacks that correspond to various interaction and view lifecycle events. The onChange(_ :) closure is triggered when the row's value property changes. When a change happens, this updates the viewModel's title property to the row's current value.

Build and run the application. When you tap the lone to-do item in the list, the EditToDoItemViewController screen should now look like the picture below. On the edit screen, tap the item, update the text and then Save. The model object updates with your form input!

Image may be NSFW.
Clik here to view.
Eureka Tutorial

In only 10 lines of code, you displayed a model-driven textfield in a tableview.

Now that you have an idea of how Eureka works, time to add some other elements!

Setting the Due Date with a Date Picker

Every to-do list needs to have due dates. Fortunately, Eureka has a row type that displays a date picker when tapped. Add the following to the bottom of viewDidLoad():

+++ Section()
  <<< DateTimeRow() {
    $0.dateFormatter = type(of: self).dateFormatter //1
    $0.title = "Due date" //2
    $0.value = viewModel.dueDate //3
    $0.minimumDate = Date() //4
    $0.onChange { [unowned self] row in //5
      if let date = row.value {
        self.viewModel.dueDate = date
      }
    }
  }

You've added another Section, this time with a DateTimeRow to display the picker. Here's a deeper look at how it's configured:

  1. To format the presentation of the date, set the row's dateFormatter to the static dateFormatter
  2. provided in the starter project.

  3. Most Eureka Row subclasses allow you to set their title property to make the purpose of the row clear to the user.
  4. When the row is initially configured, set its value to the view model's due date.
  5. Use today's date as the minimum date that can be accepted as user input.
  6. Set the newly-selected date to the view model when onChange is triggered.

Build and run the project to confirm the new row is in its own section right below the item title. Tap the row, and a date picker will appear at the bottom of the screen. Note that you can't select a date prior to the present day.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Selecting the Repeat Frequency

Any worthwhile to-do item interface should let the user specify whether a task is recurring, and at what interval. You will make use of Eureka's PushRow class for this. PushRow accepts an array of options of a given type. Eureka will then take care of generating the supporting interface and navigation to enable the user to make a selection.

Add a PushRow right below the date picker, in the same section:

<<< PushRow<String>() { //1
          $0.title = "Repeats" //2
          $0.value = viewModel.repeatFrequency //3
          $0.options = viewModel.repeatOptions //4
          $0.onChange { [unowned self] row in //5
          if let value = row.value {
            self.viewModel.repeatFrequency = value
          }
        }
     }

By now, some of the above steps should look a little familiar:

  1. Add a new PushRow to the most-recently instantiated section. PushRow is a generic class, so you need to specify that you're using it with type String in angle brackets.
  2. Again, to make the purpose of this selector clear to the user, set its title to "Repeats".
  3. Initialize the row's value with the view model's repeatFrequency property to show the current selection.
  4. As you might have guessed, the options of a PushRow represent the list of possible values the user can select. Set this to viewModel.repeatOptions, an array of strings that have been declared in the starter project. If you Command+Click repeatOptions, you'll see the repeat options are: never, daily, weekly, monthly and annually.
  5. Whenever the row's value changes, update viewModel with the newly-selected value.

Build and run. You'll see that a new row titled Repeats is added to the form.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Tapping this row transports you to a view where you can select from the provided options. Upon selection, you're popped back to the root task edit view with your selection reflected.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Adding a Priority Selector

A user should be able to specify how important an item is. To do that, you'll use a SegmentedRow which embeds a UISegmented​Control into a UITableViewCell.

Below the code you just added for the PushRow, add the following:

+++ Section()
      <<< SegmentedRow<String>() {
        $0.title = "Priority"
        $0.value = viewModel.priority
        $0.options = viewModel.priorityOptions
        $0.onChange { [unowned self] row in
          if let value = row.value {
            self.viewModel.priority = value
          }
        }
      }

The code above adds a SegmentedRow with a String type parameter to the form. By now, the rest of the steps outlined should look familiar. Like the row setup you've seen so far, you're setting the title, value, options and onChange(_:) properties using the viewModel.

Build and run. You now have a fully-functioning segmented control to set the item's priority, where "!", "!!" and "!!!" correspond to low-, medium- and high-importance, respectively.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Setting a Reminder with an Alert Row

The interface requires a way for the user to select when they will be reminded of an upcoming item, such as "30 minutes before," "1 hour before", or "1 day before". You could use a PushRow, as in the repeat frequency example. However, to explore the variety of Eureka's components, you will use an alert controller instead. And guess what? Eureka has an AlertRow for that!

Add the following just below the SegmentedRow:

<<< AlertRow<String>() {
        $0.title = "Reminder"
        $0.selectorTitle = "Remind me"
        $0.value = viewModel.reminder
        $0.options = viewModel.reminderOptions
        $0.onChange { [unowned self] row in
          if let value = row.value {
            self.viewModel.reminder = value
          }
        }
      }

The setup of this row is identical to the rows you have added until this point. However, you also set the additional selectorTitle property, which is the title of the UIAlertController presenting the list of options.

Note: Eureka can also display alert controllers with the ActionSheet style using ActionSheetRow in a similar fashion.

Build and run. When you tap the row titled Reminder, an alert controller is presented, allowing you to select the desired reminder time. You didn't even have to write any UIAlertController code!

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Validation

Still on the task edit view, tap-to-edit the description text field. Delete all characters until you can see the placeholder text, then hit Save. Your to-do item no longer has a title!

Image may be NSFW.
Clik here to view.
Eureka Tutorial

It would be a good idea to make sure the user cannot leave the description blank. Back in viewDidLoad(), find the code where you added a TextRow. Add the following just before the closing bracket of the TextRow closure (and after the onChange closure):

$0.add(rule: RuleRequired()) //1
$0.validationOptions = .validatesOnChange //2
$0.cellUpdate { (cell, row) in //3
  if !row.isValid {
    cell.titleLabel?.textColor = .red
  }
}
  1. Initialize and add a RuleRequired to the TextRow object. This is one of the validation rules provided with Eureka to handle required input in a form. It indicates that a value must be provided in the field to pass validation.
  2. Set the row's validationOptions to .validatesOnChange, meaning the validation rule will be evaluated as the row's value changes.
  3. If the value of the row is not valid, set the row's title color to red to red to alert the user.

Note: You can also add custom rules to handle use cases more specific to our needs, as described in Eureka's documentation.

To make sure the user can't leave the editing screen with an invalid entry, replace the contents of the saveButtonPressed(_:) method with the following:

if form.validate().isEmpty {
  _ = navigationController?.popViewController(animated: true)
}

Eureka has a validate() method that returns an array of any validation errors from all rows with validation rules. If this array is empty, your form has no errors and you can pop the view controller from the navigation stack.

Build and run, and delete the contents of the Description field again. This time, the field label turns red, and the Save button won't allow you to leave until the issue is resolved.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Adding More Pizazz with Eureka Plugins

A plugin is a custom Row component just like any of the rows already included with the Eureka library. You can browse the plugins created by the community at the Eureka Community Portal.

Let's say you wanted the user to be able to attach an image to a to-do item as a visual aid. Sounds like a job for the ImageRow plugin!

The plugin has already been included in the starter project using the CocoaPods installation instructions found in the plugin readme. To integrate it, start by adding the following import statement to the top of EditToDoItemViewController.swift:

import ImageRow

Note: This plugin requires the addition of the NSCameraUsageDescription and NSPhotoLibraryUsageDescription keys in the project's info.plist file. This has already been done for you in the starter project.

In viewDidLoad(), add a new section with an ImageRow to the form:

  +++ Section("Picture Attachment")
  <<< ImageRow() {
    $0.title = "Attachment"
    $0.sourceTypes = [.PhotoLibrary, .SavedPhotosAlbum, .Camera] //1
    $0.value = viewModel.image //2
    $0.clearAction = .yes(style: .destructive) //3
    $0.onChange { [unowned self] row in //4
      self.viewModel.image = row.value
    }
}

Taking it comment-by-comment:

  1. In the initialization closure, allow the user to select images from their Photo Library, Saved Photos album, or camera if available.
  2. If an image is already attached to this to-do item, use it to initialize the row's value.
  3. Present the "Clear Photo" option with the "destructive" style to indicate that image data may be permanently destroyed when a photo attachment is cleared (when using the camera roll, for example).
  4. As with the previous examples, update the viewModel.image when a new value is set.

Build and run. Tap the row titled Attachment, pick Photo Library from the action sheet, then select an image attachment. The results will be shown in a preview on the Attachment cell.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Creating a Eureka Plugin

Open EditToDoItemViewModel.swift and check out the categoryOptions array. You can see that the starter project includes possible to-do item categories of Home, Work, Personal, Play and Health. You will create a custom component to allow the user to assign one of these categories to a to-do item.

You will use a Row subclass that provides the default functionality of a PushRow but whose layout is more tailored to your needs. Admittedly, this example is a little contrived, but it will help you understand the essentials of crafting your own custom components.

In Xcode's File Navigator, control click the Views group and create a new file named ToDoCategoryRow.swift. Import Eureka at the top of this file:

import Eureka

Until now, you have been dealing almost exclusively with subclasses of Eureka's Row class. Behind the scenes, the Row class works together with the Cell class. The Cell class is the actual UITableViewCell presented on screen. Both a Row and Cell must be defined for the same value type.

Adding a Custom Cell Subclass

You'll start by creating the cell. At the top of ToDoCategoryRow.swift, insert the following:

//1
class ToDoCategoryCell: PushSelectorCell<String> {

  //2
  lazy var categoryLabel: UILabel = {
    let lbl = UILabel()
    lbl.textAlignment = .center
    return lbl
  }()

  //3
  override func setup() {
    height = { 60 }
    row.title = nil
    super.setup()
    selectionStyle = .none

    //4
    contentView.addSubview(categoryLabel)
    categoryLabel.translatesAutoresizingMaskIntoConstraints = false
    let margin: CGFloat = 10.0
    categoryLabel.heightAnchor.constraint(equalTo: contentView.heightAnchor, constant: -(margin * 2)).isActive = true
    categoryLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -(margin * 2)).isActive = true
    categoryLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
    categoryLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
  }

  //5
  override func update() {
    row.title = nil
    accessoryType = .disclosureIndicator
    editingAccessoryType = accessoryType
    selectionStyle = row.isDisabled ? .none : .default
    categoryLabel.text = row.value
  }
}

You've created a custom PushSelectorCell, which derives from UITableViewCell and is managed by PushRow. The cell will display a centered label. Here are some details on how this works:

  1. You'll be displaying string values in this cell, so you provide String as the optional type.
  2. Instantiate the UILabel that will be added to the cell.
  3. setup() is called when the cell is initialized. You'll use it to lay out the cell - starting with setting the height (provided by a closure), title and selectionStyle.
  4. Add the categoryLabel and the constraints necessary to center it within the cell's contentView.
  5. Override the cell's update() method, which is called every time the cell is reloaded. This is where you tell the cell how to present the Row's value. Note that you're not calling the super implementation here, because you don't want to configure the textLabel included with the base class.

Adding a Custom Row Subclass

Below the ToDoCategoryCell class, add ToDoCategoryRow:

final class ToDoCategoryRow: _PushRow<ToDoCategoryCell>, RowType { }

Because Row subclasses are required to be final, PushRow cannot be subclassed directly. Instead, subclass the generic _PushRow provided by Eureka. In the angle brackets, associate the ToDoCategoryRow with the ToDoCategoryCell you just created. Finally, every row must adhere to the RowType protocol.

Now your custom row is all set up and ready to use!

Adding a Dynamic Section Footer

The custom row will be embedded in a "Category" section which will be initially hidden from the user. This section will be unhidden when the user taps a custom table view footer. Open EditToDoItemViewController.swift, and right below the declaration of the dateFormatter constant, add the following:

let categorySectionTag: String = "add category section"
let categoryRowTag: String = "add category row"

The tag property is used by the Form to obtain references to a specific Eureka Row or Section. You'll use this constant to tag and later retrieve the section and row used to manage an item's category.

Next, add the following lines at the end of viewDidLoad():

  //1
  +++ Section("Category") {
    $0.tag = categorySectionTag
  //2
    $0.hidden = (self.viewModel.category != nil) ? false : true
  }
  //3
  <<< ToDoCategoryRow() { [unowned self] row in
    row.tag = self.categoryRowTag
    //4
    row.value = self.viewModel.category
    //5
    row.options = self.viewModel.categoryOptions
    //6
    row.onChange { [unowned self] row in
      self.viewModel.category = row.value
    }
}

This adds a new section that includes your custom ToDoCategoryRow, which is initially hidden. Here are some details:

  1. Add a section to the form, assigning the categorySectionTag constant.
  2. Set the section's hidden property to true if the category property on the view model is nil. The plain nil-coalescing operator cannot be used here as the hidden property requires a Boolean literal value instead.
  3. Add an instance of ToDoCategoryRow to the section tagged with categoryRowTag.
  4. Set the row's value to viewModel.category.
  5. Because this row inherits from PushRow, you must set the row's options property to the options you want displayed.
  6. As you've seen in prior examples, use the row's onChange(_:) callback to update the view model's category property whenever the row's value changes.

Near the top of EditToDoItemViewController, right below the categorySectionTag definition, add the following:

lazy var footerTapped: EditToDoTableFooter.TappedClosure = { [weak self] footer in //1

   //2
   guard let form = self?.form,
     let tag = self?.categorySectionTag,
     let section = form.sectionBy(tag: tag) else {
     return
   }

   //3
   footer.removeFromSuperview()

   //4
   section.hidden = false
   section.evaluateHidden()

   //5
   if let rowTag = self?.categoryRowTag,
     let row = form.rowBy(tag: rowTag) as? ToDoCategoryRow {
     //6
     let category = self?.viewModel.categoryOptions[0]
     self?.viewModel.category = category
     row.value = category
     row.cell.update()
   }
}

EditToDoTableFooter is a view class included in the starter that contains a button with the title Add Category. It also includes TappedClosure, a typealias for an action to execute when tapped. The code you added defines a closure of this type that takes a footer, removes it from the view and displays the category section.

Here is a more detailed look:

  1. To avoid retain cycles, pass [weak self] to the closure.
  2. Safely unwrap references to the view controller and its form and categorySectionTag properties. You obtain a reference to the Section instance you defined with the categorySectionTag.
  3. When the footer is tapped, remove it from the view since the user shouldn't be allowed to tap it again.
  4. Unhide the section by setting hidden to false then calling evaluateHidden(). evaluateHidden() updates the form based on the hidden flag.
  5. Safely unwrap the reference to the ToDoCategoryRow we added to the form.
  6. Ensure the view model's category property and the cell's row value property are defaulted to the first item in the array of options. Call the cell's update() method so its label is refreshed to show the row's value.

The Home Stretch

You're almost at the finish line! At the bottom of viewDidLoad(), insert the following:

//1
let footer = EditToDoTableFooter(frame: .zero)
//2
footer.action = footerTapped
//3
if let tableView = tableView, viewModel.category == nil {
  tableView.tableFooterView = footer
  tableView.tableFooterView?.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 50.0)
}
  1. Declare an instance of EditToDoTableFooter. You pass a zero frame, because the size will be handled by constraints tied to the cell layout.
  2. footer.action is triggered when the footer button is pressed, and this ensures it fires the code you defined in the footerTapped closure.
  3. If the view model's category is nil, set the table view's tableFooterView property to our newly-instantiated footer. Next, set the footer's frame to the desired dimensions.

Build and run the project. Tap the large, white button with the words Add Category at the bottom of the table view. Voila! The button is replaced by the custom PushRow subclass you created.

Tap this row to choose from a selection of "emoji-fied" categories. Eureka!

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Finishing Touches

The app's users should have the ability to add and delete items. Luckily, the starter project has been set up to do this, and all you have have to do is wire it up.

Open ToDoListViewController.swift and uncomment the following lines of code in addButtonPressed(_:):

  // Uncomment these lines
  //1
  let addViewModel = viewModel.addViewModel()
  //2
  let addVC = EditToDoItemViewController(viewModel: addViewModel)
  navigationController?.pushViewController(addVC, animated: true)
  1. addViewModel() instantiates the view model necessary to add a new to-do item.
  2. EditToDoItemViewController is instantiated with the addViewModel just created, then pushed onto the navigation stack.

Build and run. This time tap the + to generate a blank to-do item. Fill in the details, then save it. It’s about time you picked up your laundry!

Image may be NSFW.
Clik here to view.
Eureka Tutorial

If you were being adventurous, you might have noticed that tapping Back instead of Save had the same effect: the item was added. This is because the model is created as soon as you tap +.

Next, you're going to work on deletion for this case as well as to delete older items.

In EditToDoItemViewController, find deleteButtonPressed(_:) and uncomment the following lines:

//1
let alert = UIAlertController(title: "Delete this item?", message: nil, preferredStyle: .alert)
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
let delete = UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
  //2
  self?.viewModel.delete()
    _ = self?.navigationController?.popViewController(animated: true)
    }
//3
alert.addAction(delete)
alert.addAction(cancel)
navigationController?.present(alert, animated: true, completion: nil)

The above code will be executed when the Delete button in the navigation bar is pressed.

  1. Create a UIAlertController with a title, cancel and delete actions.
  2. In the completion handler of the delete action, tell the view model to delete the to-do item currently being edited. Then pop the current view controller off the navigation stack.
  3. Add the cancel and delete actions to the alert controller, and present the alert controller on the navigation stack.

Next, delete the following lines of code from the bottom of deleteButtonPressed(_:):

 // Delete this line
 _ = self.navigationController?.popViewController(animated: true)

This is no longer necessary as you're now handling the pop after deleting the model.

And finally, go the initialize() method and find this line of code:

let deleteButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: .deleteButtonPressed)

Change the title of the bar button item from "Back" to "Delete" so it reads as follows:

let deleteButton = UIBarButtonItem(title: "Delete", style: .plain, target: self, action: .deleteButtonPressed)

Build and run. Whether you're adding a new item or editing an existing one, tapping Save will take you back to the to-do list only if there are no validation errors (i.e., if the item title is not blank). Tapping Delete will remove the item and take you back to the to-do list.

Image may be NSFW.
Clik here to view.
Eureka Tutorial

Where To Go From Here?

The finished project can be downloaded here.

I hope this Eureka tutorial helped you gain a broad understanding of the advantages and possibilities of using Eureka. I encourage you to check out Eureka's own excellent documentation and in-depth tutorials to continue your journey:

You can learn more about the Model-View-ViewModel (MVVM) architecture with the following resources:

Please share any questions or comments in the discussion below!

The post Eureka Tutorial – Start Building Easy iOS Forms appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4399

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>