Eureka Tutorial – Start Building Easy iOS Forms

This Eureka tutorial will teach you how Eureka makes it easy to build forms into your iOS app with various commonly-used user interface elements. By Nicholas Sakaimbo.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.
Nicholas Sakaimbo

Contributors

Nicholas Sakaimbo

Author

Jeff Rames

Tech Editor

Chris Belanger

Editor

Andy Obusek

Final Pass Editor and Team Lead

Over 300 content creators. Join our team.