Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using @IBSegueAction with UINavigationController

Thanks to AD Progress's excellent research and answer below, I finally figured out what's going on with IBSegueActions.

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…

enter image description here

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 IBSegueActions.

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?

like image 959
Ashley Mills Avatar asked Oct 24 '19 16:10

Ashley Mills


Video Answer


2 Answers

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.

Recreate the Segue

Next select present Modally and name your segue with an identifier

Present modally

Segue 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.

Select ViewController from which you pass data

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.

StoryBoard

That's it.

You should get a result like this once you tap any of the cells in the first View Controller

Sample

Here is the sample project GitHub

I also found more information about IBSegueAction here

like image 171
AD Progress Avatar answered Oct 24 '22 05:10

AD Progress


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:

right clicking navigation controller's relationship segue

like image 1
malhal Avatar answered Oct 24 '22 04:10

malhal