This guide covers how to handle basic form input in iOS.
Unlike HTML forms, in iOS there is no standard form submission action. This means that it is up to you to define when a form's contents should be processed. It also means that you are responsible for how to translate a form's state into meaningful information for the the application's models, and vice versa.
We'll focus on the very common use case of embedding form inputs in a table view. This will allow us to highlight some points that require extra care when working with inputs in iOS. For example, if the table view uses reusable cells, a cell containing a form input may be removed from memory as soon as it disappears from the screen when the user scrolls the table. This means that you cannot necessarily rely on the state of the form itself to store information the user has input. We'll cover how to overcome this and other problems below.
Here is a typical example of form inputs in a table view:
One way to avoid some complexity when working with form input in a table view is to use static cells. However, this is only applicable when using Interface Builder and when your table view is backed by a UITableViewController. Additionally you should only use static cells when the structure of your forms will never change.
To create a table with static cells you'll need to drag a Table View
Controller
into the scene from the Object Library and set Content
to
Static Cells
in the Attributes Inspector.
You can now control the number of sections in your table and the the
number of cells by selecting either the "Table View" or "Section" in the
Outline view and using the Attributes Inspector. Using Interface
Builder you can now design each individual cell as it would appear in
your app. You can also connect the controls in your cells to
@IBOutlets
and @IBActions
in your UITableViewController's
corresponding subclass. Since there is no danger of static cells being
removed from memory when they scroll off screen, you can choose whenever
you want to process the information from any form inputs into useful
information for the rest of your app.
Here is how we might implement a simple preferences page for an app. To
demonstrate the fact that the preferences will be used in the rest of
the app we have a main ViewController
that initializes and displays
the preferences. It has an "Edit Preferences" button that will open up
our PreferencesTableViewController
.
NB: In practice, you might instead opt to use the iOS Settings Bundle feature when creating a preferences page.
The preferences table has three static cells each containing a single
switch corresponding to an on/off preference. The swiches
are connected to @IBOutlets
in our PreferencesTableViewController
.
The preferences table view controller also has "Save" and "Cancel"
buttons connected to unwind segues in our main ViewController
.
The Preferences
class provides us with a model to store the
preferences.
class Preferences {
var autoRefresh = true, playSounds = true, showPhotos = true
}
In our preferences table view controller we initialize the state of the switches to match the current preferences—which will be set by our main view controller.
Notice that when a switch is toggled, we do not mutate the current
preferences since we might still cancel our changes. Instead we provide
a way to create a new Preferences
object from the current state of the
switches in the UI with the preferencesFromTableData
method. We'll use this
method in the unwind segue associated with the "Save" button.
We can wait to convert the state of the switches (as they appear in the
UI) to a Preferences
object until we are ready to save because we are
using static cells. This means that the cells do not get allocated
and removed from memory dynamically and that we can obtain @IBOutlets
to controls inside each specific cell.
import UIKit
class PreferencesTableViewController: UITableViewController {
@IBOutlet weak var autoRefreshSwitch: UISwitch!
@IBOutlet weak var soundsSwitch: UISwitch!
@IBOutlet weak var showPhotosSwitch: UISwitch!
// should be set by the class that instantiates this view controller
var currentPrefs: Preferences!
override func viewDidLoad() {
super.viewDidLoad()
currentPrefs = currentPrefs ?? Preferences()
initSwitches()
}
private func initSwitches() {
autoRefreshSwitch?.on = currentPrefs.autoRefresh
soundsSwitch?.on = currentPrefs.playSounds
showPhotosSwitch?.on = currentPrefs.showPhotos
}
func preferencesFromTableData() -> Preferences {
var newPrefs = Preferences()
newPrefs.autoRefresh = autoRefreshSwitch.on
newPrefs.playSounds = soundsSwitch.on
newPrefs.showPhotos = showPhotosSwitch.on
return newPrefs
}
}
In our main ViewController
we set up the
PreferencesTableViewController
with the current preferences before our
segue. When coming back from the segue via the "Save" button we read
off the new preferences from the switches and update our preferences
object. We don't take any action if the edit was "Canceled" because the
PreferencesTableViewController
does not modify the preferences
object we pass in.
class ViewController: UIViewController {
@IBOutlet weak var autoRefreshLabel: UILabel!
@IBOutlet weak var playSoundsLabel: UILabel!
@IBOutlet weak var showPhotosLabel: UILabel!
var preferences: Preferences = Preferences() {
didSet {
updateLabels()
}
}
override func viewDidLoad() {
super.viewDidLoad()
updateLabels()
}
private func updateLabels() {
autoRefreshLabel.text = preferences.autoRefresh ? "Yes" : "No"
playSoundsLabel.text = preferences.playSounds ? "Yes" : "No"
showPhotosLabel.text = preferences.showPhotos ? "Yes" : "No"
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showPreferencesSegue" {
// we wrapped our PreferencesTableViewController inside a UINavigationController
let navController = segue.destinationViewController as UINavigationController
let prefsVC = navController.topViewController as PreferencesTableViewController
prefsVC.currentPrefs = self.preferences
}
}
@IBAction func didSavePreferences(segue: UIStoryboardSegue) {
if let prefsVC = segue.sourceViewController as? PreferencesTableViewController {
self.preferences = prefsVC.preferencesFromTableData()
}
}
@IBAction func didCancelPreferences(segue: UIStoryboardSegue) {
// do nothing
}
}
Static cells are quite limited because single UITableView
has to
contain either all static cells or have all of its content be
provided dynamically. For example it would not be possible to use
static cells if the number of rows in any section of your table was
based on data loaded at runtime.
Furthermore, you can only use static cells in Interface Builder and when
your table view is backed by UITableViewController
.
There are a few additional challenges to overcome when working with form inputs in a table with dynamic content:
Since the cells are dynamically allocated, inside your view
controller there is no way to obtain @IBOutlets
to an form input
inside a specific cell. You must arrange a way for the cells to read
the state of its own form inputs and for the view controller to get this
information from the cell.
Likewise, if your view controller needs to respond to events from an
individual cell, you must arrange a way for the cell to propagate the
event to the view controller. You cannot simply associate an
@IBAction
inside the view controller with an element inside a specific
cell.
Generally you'll be getting reusable cells to configure by calling
dequeueReusableCellWithIdentifier
. This means you cannot rely on
the information contained in the form inputs in an individual cell to
persist when it scrolls off screen. You will have to maintain all the
information necessary to populate the state of all form inputs in each
cell somewhere else.
You'll also have to update this information immediately if the user interacts with the cell because, again, the form input itself cannot be used to to store information once the cell is no longer on the screen.
If your form is used for editing data and provides a "Cancel" option, you must maintain an additional copy of the data seperate from the initial data and seperate from the current value inside the form inputs.
To see how these factors come into play, we'll reimplement our basic preferences page example from above to use dynamically created cells based on prototype cells instead of static cells.
As before we have a main ViewController
displays the current
preferences and has an "Edit Preferences" button that does a modal segue
to our PreferencesViewController
. The ViewController
class also
contains @IBActions
for unwind segues that happen when the user either
saves or cancels editing preferences.
We renamed our PreferencesTableViewController
to
PreferencesViewController
. It no longer has to be a subclass of
UITableViewController
, and instead it only implements the
UITableViewDataSource
protocol. Other than replacing this type name,
the code in our ViewController
class remains the same as above. The
code for our Preferences
model has not changed from above at all.
The bulk of our changes are in PreferencesViewController
. We need to
maintain the temporary state of our table as we are editing our
preferences outside of the cells themselves. We introduce two
properties tableStructure
and prefValues
to keep track of this
state.
We write logic to convert a Preferences
object into our prefValues
.
This allow us to update the prefValues
to store the state of table as
we are editing without modifying the original Preferences
object that
is passed in. Likewise we have a procedure to obtain a new Preferences
object from the prefValues
. This allows the rest of the application
to obtain an updated Preferences
once the user hits "Save".
enum PrefRowIdentifier : String {
case AutoRefresh = "Auto Refresh"
case PlaySounds = "Play Sounds"
case ShowPhotos = "Show Photos"
}
class PreferencesViewController: UIViewController, UITableViewDataSource, PreferenceSwitchCellDelegate {
@IBOutlet weak var tableView: UITableView!
let tableStructure: [[PrefRowIdentifier]] = [[.AutoRefresh], [.PlaySounds, .ShowPhotos]]
var prefValues: [PrefRowIdentifier: Bool] = [:]
// should be set by the class that instantiates this view controller
var currentPrefs: Preferences! {
didSet {
prefValues[.AutoRefresh] = currentPrefs.autoRefresh
prefValues[.PlaySounds] = currentPrefs.playSounds
prefValues[.ShowPhotos] = currentPrefs.showPhotos
tableView?.reloadData()
}
}
func preferencesFromTableData() -> Preferences {
let ret = Preferences()
ret.autoRefresh = prefValues[.AutoRefresh] ?? ret.autoRefresh
ret.playSounds = prefValues[.PlaySounds] ?? ret.playSounds
ret.showPhotos = prefValues[.ShowPhotos] ?? ret.showPhotos
return ret
}
override func viewDidLoad() {
super.viewDidLoad()
currentPrefs = currentPrefs ?? Preferences()
tableView.dataSource = self
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tableStructure.count
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return " "
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableStructure[section].count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceSwitchCell") as PreferenceSwitchCell
let prefIdentifier = tableStructure[indexPath.section][indexPath.row]
cell.prefRowIdentifier = prefIdentifier
cell.onOffSwitch.on = prefValues[prefIdentifier]!
cell.delegate = self
return cell
}
func preferenceSwitchCellDidToggle(cell: PreferenceSwitchCell, newValue: Bool) {
prefValues[cell.prefRowIdentifier] = newValue
}
}
Finally we need a way for our custom cell containing a single switch to
know which preference it represents and to notify our view controller
immediately once its switch is toggled. We do this by implementing a
custom PreferenceSwitchCell
class with a corresponding delegate
PreferenceSwitchCellDelegate
that is implemented by our
PreferencesViewController
.
The action of the switch in our protoype cell is connected to our code
in PreferenceSwitchCell
by an @IBAction
. We notify our view
controller once this event is triggered by propagating the change
through the delegate.
protocol PreferenceSwitchCellDelegate: class {
func preferenceSwitchCellDidToggle(cell: PreferenceSwitchCell, newValue:Bool)
}
class PreferenceSwitchCell: UITableViewCell {
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var onOffSwitch: UISwitch!
weak var delegate: PreferenceSwitchCellDelegate?
var prefRowIdentifier: PrefRowIdentifier! {
didSet {
descriptionLabel?.text = prefRowIdentifier?.rawValue
}
}
@IBAction func didToggleSwitch(sender: AnyObject) {
delegate?.preferenceSwitchCellDidToggle(self, newValue: onOffSwitch.on)
}
}