iOS Storyboards: Segues and More

In this tutorial, you’ll learn how to connect view controllers to a storyboard, programmatically trigger segues, control visual components and respond to the user interactions. By Ehab Amer.

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

Selecting the Game

Tapping any of the games won’t do anything except give the cell a gray background. Definitely, that’s not the desired behavior. What it should do is:

  1. Select the new game.
  2. Return back to the form.
  3. Provide the form the selected game

You’ll do the second step first since its easier. Open PlayerDetailsViewController.swift and add the following code at the end of the file:

extension PlayerDetailsViewController {
  @IBAction func unwindWithSelectedGame(segue: UIStoryboardSegue) {
  }
}

This is a simple Unwind segue to return back to the Add Player Scene.

Return to Main.storyboard and Control-drag from the GameCell to the Exit icon above the scene. Select unwindWithSelectedGameWithSegue: under Selection Segue.

Connectiong Segue

Build and run. Reach the games list and select any of the games.

Game Selection Running

The games list will automatically exit once you select any item, and will return to the previous scene. Wouldn’t it be cool if the list can show what has already been selected?

Passing Parameters With Segues

Chess is already set as the default game, so why is it not showing a checkmark beside it when the games list appears? To implement this, the Add Player Scene needs to pass the game name to Choose Game Scene whenever it opens. But the storyboard is already handling opening the scene itself, and there is no code for this at all. But there’s a hook.

When a segue is triggered, the view controller that triggered it will call the method prepare(for:sender:) and will provide the segue that it’s handling. There are two important properties in the segue you’ll frequently use: identifier and destination. The identifier is just a string, just like the cell identifier you already used. It’s also empty by default. The destination is the view controller instance that the segue will go to.

Once you know which segue is happening from its identifier, you can cast the destination object to the view controller class you know. Let’s try that.

Open Main.storyboard and select the segue named Show segue to “Choose Game”. Then from the Attributes inspector change its Identifier to PickGame.

Setting Segue Identifier

Then, in PlayerDetailsViewController.swift, add the following inside the class after viewDidLoad():

override func prepare(for segue: UIStoryboardSegue, sender: Any?)  {
  if segue.identifier == "PickGame",
     let gamePickerViewController = segue.destination as? GamePickerViewController {
    gamePickerViewController.gamesDataSource.selectedGame = game
  }
}

If the segue’s identifier is PickGame, cast the destination view controller as GamePickerViewController so you can access its properties. Set the current value of game as the selected game.

Finally, in GamePickerViewController.swift, add the following in tableView(_:cellForRowAt:) right before the return:

if indexPath.row == gamesDataSource.selectedGameIndex {
  cell.accessoryType = .checkmark
} else {
  cell.accessoryType = .none
}

Build and run. Open the games list screen and you’ll see that Chess is already marked.

Simulator Screenshot

To finalize the game selection, in GamePickerViewController.swift, add the following at the end of the file:

extension GamePickerViewController {
  override func tableView(
    _ tableView: UITableView, 
    didSelectRowAt indexPath: IndexPath
  ) {
    // 1
    tableView.deselectRow(at: indexPath, animated: true)
    // 2
    if let index = gamesDataSource.selectedGameIndex {
      let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0))
      cell?.accessoryType = .none
    }
    // 3
    gamesDataSource.selectGame(at: indexPath)
    // 4
    let cell = tableView.cellForRow(at: indexPath)
    cell?.accessoryType = .checkmark
  }
}

Whenever a cell in a table view is selected, tableView(_:didSelectRowAt:) will execute the following steps:

  1. Remove the gray selection background that appears by default.
  2. Get the previously selected game and remove the checkmark from its cell.
  3. Select the new game in the data source.
  4. Mark the new cell with the checkmark.

Open PlayerDetailsViewController.swift and add the following within unwindWithSelectedGame(segue:):

if let gamePickerViewController = segue.source as? GamePickerViewController,
   let selectedGame = gamePickerViewController.gamesDataSource.selectedGame {
  game = selectedGame
}

If the source view controller of the segue is of type GamePickerViewController and there is a valid selected game in the data source, then set the game to the new one.

Build and run, and choose a game other than chess. The value Chess is not updated! Why is that?

Simulator: Choosing a Game

Synchronizing Segues With Actions

When you select a cell, the unwind segue is triggered first, and the PlayerDetailsViewController is reading the selected game from the data source before tableView(_:didSelectRowAt:) in GamePickerViewController is executed. That’s the reason nothing has changed.

Automatically triggered segues are not meant to work hand-in-hand with actions when the order of execution is important. To fix this, you need to tell the view controller when to trigger the segue.

UIViewController has performSegue(withIdentifier:sender:). Its job is to allow you to trigger a segue when you want it triggered.

To fix the issue, first remove the segue that the cell triggers on selection. Select the Exit icon in Choose Game Scene, and from the Connections inspector remove the action connected to the Selection outlet by clicking on the small x.

The segue needs to connect from the view controller itself so nothing else triggers it. To create a segue from the controller Control-drag from the View Controller icon to the Exit icon. Give this new segue the identifier unwind to reference it from the code.

Renaming Segue

Open GamePickerViewController.swift and, at the end of tableView(_:didSelectRowAt:), add the following:

performSegue(withIdentifier: "unwind", sender: cell)

Build and run and try to change the game. It works this time. :]

Selecting a Game

Time to resume the task of saving a new player.

Saving the New Player

The last step is to actually save the new Player object when the user taps Done. These are the actions needed to finish this step:

  1. Only when the segue connected to the Done button is triggered, construct a new Player object and store it in the controller.
  2. When the unwind segue of the Done button is performed, append that stored object to the data source.

Open Main.storyboard and, under Add Player Scene, select the unwind segue triggered by Done. Its name is Unwind segue to savePlayerDetail: in the Document Outline. Give it the identifier SavePlayerDetail.

Open PlayerDetailsViewController.swift. Add this property at the top of the class:

var player: Player?

Then, add the following at the beginning of prepare(for:sender:) in the same file:

if segue.identifier == "SavePlayerDetail",
   let playerName = nameTextField.text, 
   let gameName = detailLabel.text {
  player = Player(name: playerName, game: gameName, rating: 1)
}

This creates a new Player if the form is complete.

In PlayersViewController.swift, add the following as the implementation of savePlayerDetail(_:)

guard 
  let playerDetailsViewController = segue.source as? PlayerDetailsViewController,
  let player = playerDetailsViewController.player 
  else {
    return
}
playersDataSource.append(player: player, to: tableView)

Build and run, then add a new player in your app. It works like a charm! :]

Adding a Player