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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Eureka Tutorial – Start Building Easy iOS Forms
30 mins
- Getting Started
- Adding Eureka to our View Controller
- Adding a Section and a Row
- Setting the Due Date with a Date Picker
- Selecting the Repeat Frequency
- Adding a Priority Selector
- Setting a Reminder with an Alert Row
- Validation
- Adding More Pizazz with Eureka Plugins
- Creating a Eureka Plugin
- Adding a Custom Cell Subclass
- Adding a Custom Row Subclass
- Adding a Dynamic Section Footer
- The Home Stretch
- Finishing Touches
- Where To Go From Here?
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:
- You'll be displaying string values in this cell, so you provide
Stringas the optional type. - Instantiate the
UILabelthat will be added to the cell. -
setup()is called when the cell is initialized. You'll use it to lay out the cell - starting with setting theheight(provided by a closure),titleandselectionStyle. - Add the
categoryLabeland the constraints necessary to center it within the cell'scontentView. - 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 theRow'svalue. Note that you're not calling the super implementation here, because you don't want to configure thetextLabelincluded 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:
- Add a section to the form, assigning the
categorySectionTagconstant. - Set the section's
hiddenproperty totrueif thecategoryproperty 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. - Add an instance of
ToDoCategoryRowto the section tagged withcategoryRowTag. - Set the row's
valuetoviewModel.category. - Because this row inherits from
PushRow, you must set the row'soptionsproperty to the options you want displayed. - As you've seen in prior examples, use the row's
onChange(_:)callback to update the view model'scategoryproperty whenever the row'svaluechanges.
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:
- To avoid retain cycles, pass
[weak self]to the closure. - Safely unwrap references to the view controller and its
formandcategorySectionTagproperties. You obtain a reference to theSectioninstance you defined with thecategorySectionTag. - When the footer is tapped, remove it from the view since the user shouldn't be allowed to tap it again.
- Unhide the section by setting
hiddentofalsethen callingevaluateHidden().evaluateHidden()updates the form based on thehiddenflag. - Safely unwrap the reference to the
ToDoCategoryRowwe added to the form. - Ensure the view model's
categoryproperty and the cell's rowvalueproperty are defaulted to the first item in the array of options. Call the cell'supdate()method so its label is refreshed to show the row's value.