DBD
DBD

Reputation: 23223

UISplitViewController, reuse DetailViewController (Swift)

Setup: Create a boilerplate iOS Master-Detail app in Xcode and run it. Everytime you click on a master item it re-creates the detailVC and configures it to display the new data.

Premise: Often this is what I want, but other times it does little more than change the text in a single label. Doesn't it make more sense to re-use the existing DetailVC? (at least in this case)

So what's the best way to do this?

Contemplation: When I look at the boilerplate code I see the MasterVC creates class scoped var for the detailVC.

var detailViewController: DetailViewController? = nil

It sets this value in viewDidLoad, but it then never uses it for anything. Huh? In prepare(for:sender:) we get this code

let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController

which creates a new instance of the DetailViewController. If you put a break point in after it's created and compare it to the class var detailViewController you can see they are different.

The UIStoryboardSegue is creating my new DetailViewController which can easily be confirmed by looking at the documentation.

You do not create segue objects directly. Instead, the storyboard runtime creates them when it must perform a segue between two view controllers.

I could remove the segue on row select and manually call the method on my detailVC and that's probably the easiest, but segues are so purty and visual in IB. I could could probably create a custom segue. What else could I do and is there a clear "best" way?

Upvotes: 0

Views: 338

Answers (2)

malhal
malhal

Reputation: 30746

First remove the var detailViewController that is a remnant from when the template was using UINavigationController and won't work with the UISplitViewController because if a new master is init when collapsed, say after a selection in a root VC then it isn't possible to find the previous detail. iOS 14 added a new method to easily find the previous detail even if the split is currently collapsed, viewController(for:) and you could use it like this:

// only perform when collapsed, otherwise find the detail and update it's item.
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
    if identifier != "showDetail" { return true }
    guard let indexPath = tableView.indexPathForSelectedRow else { return true }
    guard let svc = splitViewController else { return true }
    if svc.isCollapsed { return true }
    
    let secondaryViewController = svc.viewController(for: .secondary)
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController,
    let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return true }
    
    let object = fetchedResultsController.object(at: indexPath)
    topAsDetailController.detailItem = object
    
    return false // cancels the segue
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
        if let indexPath = tableView.indexPathForSelectedRow {
            let object = fetchedResultsController.object(at: indexPath)
            let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
            controller.detailItem = object
            controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
            controller.navigationItem.leftItemsSupplementBackButton = true
        }
    }
}

Upvotes: 1

DBD
DBD

Reputation: 23223

In case someone else has this question, here is my solution until someone comes up with something better.

Go to Interface Builder and remove the showDetail segue which goes from the master tableview to the detailVC. Then go into the MasterViewController class and remove the prepare(for:sender:) function.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
        if let indexPath = tableView.indexPathForSelectedRow {
            let object = objects[indexPath.row] as! NSDate
            let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
            controller.detailItem = object
            controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
            controller.navigationItem.leftItemsSupplementBackButton = true
        }
    }
}

Create a new function in Table View area (this new function is part of UITableViewDelegate)

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
     detailViewController?.detailItem = object
}

Upvotes: 0

Related Questions