Search code examples
iosswiftuikit

Override show(_:sender:) of UIViewController freezes the UI


I can't override the show(_:sender:) method of UIViewController or else my app freezes when the method is called (Xcode 15.3 simulator, iOS 17.4, macOS Sonoma 14.3.1, MacBook Air M1 8GB).

Here is a table view that helps illustrate the UI freeze when you tap the right bar button item, which calls an overridden show(_:sender:) method:

class ViewController: UIViewController {
    let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        navigationItem.leftBarButtonItem = .init(title: "Present", style: .plain, target: self, action: #selector(presentViewController))
        navigationItem.rightBarButtonItem = .init(title: "Show", style: .done, target: self, action: #selector(showViewController))
        
        view.addSubview(tableView)
        tableView.frame = view.bounds
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    @objc func presentViewController() {
        present(AnotherViewController(), animated: true)
    }

    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        super.present(viewControllerToPresent, animated: flag, completion: completion)
        
    }
    
    @objc func showViewController() {
        show(AnotherViewController(), sender: self)
    }
    
    override func show(_ vc: UIViewController, sender: Any?) {
        super.show(vc, sender: sender)
        
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        var contentConfiguration = cell.defaultContentConfiguration()
        contentConfiguration.text = "Cell"
        cell.contentConfiguration = contentConfiguration
        return cell
    }
}

class AnotherViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
    }
}

As you can see, you may call an overridden present(_:animated:completion) (by tapping the left bar button item), but you may not call an overridden show(_:sender:).

Is there any workaround?


Solution

  • From the documentation for UIViewController show(_:sender:):

    The default implementation of this method calls the targetViewController(forAction:sender:) method to locate an object in the view controller hierarchy that overrides this method. It then calls the method on that target object, which displays the view controller in an appropriate way. If the targetViewController(forAction:sender:) method returns nil, this method uses the window’s root view controller to present vc modally.

    The problem with your code is that targetViewController(forAction:sender:) is returning self since it is ViewController that is overriding show(_:sender:). So you end up in an infinite recursive loop of calls to show(_:sender:). You can see this if you put a breakpoint in your overridden show(_:sender:) and let the breakpoint be hit a couple of times. You can also add:

    override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
        let res = super.targetViewController(forAction: action, sender: sender)
        print(res === self)
        return res
    }
    

    and you will see "true" printed over and over.

    When you override show(_:sender:) you should actually perform the logic to show the given view controller instead of calling super.show(vc, sender: sender).

    Overriding present to call super.present doesn't have the same problem because the logic of the default implementation of present doesn't use targetViewController(forAction:sender:) to find the implementor of present like show does.

    You should only override show(_:sender:) in a custom container view controller class that needs to display the requested view controller is some specific way not handled by the normal framework.