Thanks to AD Progress's excellent research and answer below, I finally figured out what's going on with IBSegueAction
s.
If you hook up a rootViewController
segue (or any other kind, presumably) to an IBSegueAction
in its UINavigationContoller
, but it doesn't find it there, it will look at the next view controller up the responder chain, until it finds an IBSegueAction
with the correct signature. If it doesn't find one, it'll call init(with coder: NSCoder)
on the rootViewController
. If only that was documented somewhere…
Notes
Note: This is a minimal example to illustrate the problem - how to pass parameters when initialising a view controller contained in a modal UINavigationController
using an IBSegueAction
. I'm aware that this isn't how you would normally present a detail view.
Note 2: I know how to use segues with prepare(for segue: UIStoryboardSegue)
. What I want to know how is how to pass non-optional parameters when initialising a view controller contained in a modal UINavigationController
using an IBSegueAction
Note 3: I know how to use use an IBSegueAction
to a view controller not contained in a UINavigationController
(e.g. pushing on the nav stack)
Original Question
I have the following setup…
A list of people in a UITableViewController
. Tapping on a PersonCell
presents a PersonViewController
inside a UINavigationController
To avoid having an optional Person
in the PersonViewController
, I'm trying to use IBSegueAction
s.
My first thought was that I need one for the segue between the PeopleViewController
and the PersonNavController
, and the segue between the PersonNavController
and the PersonViewController
as follows…
class PeopleViewController: UITableViewController {
// Table view delegate methods here
@IBSegueAction func showPerson(_ coder: NSCoder, sender: Any?) -> PersonNavController? {
guard let cell = sender as? PersonCell else { return nil }
return PersonNavController(coder: coder, person: cell.person)
}
}
class PersonNavController: UINavigationController {
private let person: Person
required init?(coder: NSCoder, person: Person) {
self.person = person
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@IBSegueAction func root(_ coder: NSCoder) -> PersonViewController? {
return PersonViewController(coder: coder, person: person)
}
}
class PersonViewController: UIViewController {
@IBOutlet private weak var name: UILabel!
private let person: Person
required init?(coder: NSCoder, person: Person) {
self.person = person
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
name.text = person.firstName
}
}
The problem here, it turns out, is that even though you can hook up a segue action to the segue between a UINavigationController
and its rootViewController
, it doesn't get fired as, in the example above, the super.init(coder: coder)
in PersonNavController
calls PersonViewController.init(coder: NSCoder)
(which isn't implemented).
Any thoughts as to how I can get this working?
To achieve the result which you want I recreated a simple project which I will link on my GitHub.
To start off, you need to get rid of the segue you have created to your Navigation controller and recreate it.
Next select present Modally and name your segue with an identifier
Next you need to define the IBSegueAction in your first ViewController
ViewController.swift
import UIKit
class ViewController: UITableViewController {
let persons = ["Robert", "Peter", "Dave"]
var selectedPerson = 0
override func viewDidLoad() {
super.viewDidLoad()
}
@IBSegueAction
private func showPerson(coder: NSCoder, sender: Any?, segueIdentifier: String?)
-> PersonViewController? {
return PersonViewController(coder: coder, personDetails: persons[selectedPerson])
}
//MARK:- TableView Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return persons.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "personCell", for: indexPath)
cell.textLabel?.text = persons[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedPerson = indexPath.row
performSegue(withIdentifier: "ShowPerson", sender: self)
}
}
Then add the implementation of init?(coder: NSCoder, personDetails: String)
in the PersonViewController as follows.
Xcode will yell that you need to implement required init?(coder: NSCoder)
Just tap fix.
PersonViewController.swift
import UIKit
class PersonViewController: UIViewController {
//Your data passed in below as non optional constant
let personDetails: String
//The received data gets initialized below
init?(coder: NSCoder, personDetails: String) {
self.personDetails = personDetails
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@IBOutlet weak var personLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
personLabel.text = personDetails
}
}
The next step is to select the ViewController's top part in the storyboard so you can see the three icons.
Following that, you have to select the Segue which goes out from the second UINavigationController to your destination ViewController here PersonViewController and drag back from the segue to the first yellow icon of the ViewController you want to send information from like on the screenshot below. Xcode will automatically recognize the IBSegueAction's name which you created in code.
That's it.
You should get a result like this once you tap any of the cells in the first View Controller
Here is the sample project GitHub
I also found more information about IBSegueAction here
I had the same problem but managed to get it working. Instead of setting your segue action selector on your Show Detail segue, set it on Navigation Controller's Relationship Segue, as in the following screenshot:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With