HomeiOS DevelopmentUINavigationController push transition protected space bug in compact UISplitViewController on iOS 26

UINavigationController push transition protected space bug in compact UISplitViewController on iOS 26


Having a master-detail view with a navigation stack was once applied by having a UISplitViewController with 2 navigation controllers.

In compact structure these 2 controllers magically "collapse" into 1 by the UISplitViewController, and we will type a fundamental navigation stack: grasp -> element -> sub-detail -> sub-sub-detail.

This setup used to work properly earlier than iOS 26.

On iOS 26 the navigation push transition is buggy on sub-levels as a consequence of 2 separate structure points:

  1. When the transition begins, the pushed view controller structure isn’t up to date. The preliminary dimension (earlier than an replace) is .zero, and in consequence we see the way it animates to the ultimate dimension and place. Watch how on the display recording the blue view expands from .zero.
  2. When the transition begins, the protected space structure information isn’t up to date. The protected space preliminary dimension is .zero. If a pushed view controller structure depends on it, we see the way it animates to accommodate the protected space dimension and place. Watch how on the display recording the pink view shrinks from full display dimension.

These animations are usually not anticipated! The pushed view controller structure ought to be mounted by the constraints through the push transition (because it was once earlier than iOS 26).

Bug (1) might be workarounded by including a layoutIfNeeded() name inside viewDidLoad() (see SubSubDetailViewController beneath).
However (2) is difficult to workaround.

What have been tried:

  1. Substitute viewControllers setup with utilizing a more recent iOS 14 API, this had no impact – the identical bugs:

    tremendous.init(type: .doubleColumn)
    self.setViewController(MasterViewController(), for: .major)
    self.setViewController(DetailViewController(), for: .secondary)
    
  2. Use a single UINavigationController unwrapping both MasterViewController or DetailViewController from having a UINavigationController round. This has undesired results in non-compact master-detail mode (seen on iPad and enormous iPhone’s).

  3. Utilizing a prime constraint associated to the navigation bar backside anchor as a substitute of safeAreaLayoutGuide isn’t accounting for the left and proper edges notch areas (on panorama telephones).

  4. Having one particular code model on small iPhones and one other code on different units primarily based on the system mannequin is undesirable, as a result of iPad multitasking can be utilized to change to compact structure mode dynamically.

To breed create an iOS venture in Xcode 26, paste this code, and run on an iOS 26.0 small iPhone simulator:

import UIKit

class ViewController: UISplitViewController {
  required init?(coder: NSCoder) {
    tremendous.init(coder: coder)
    self.viewControllers = [
      UINavigationController(rootViewController: MasterViewController()),
      UINavigationController(rootViewController: DetailViewController()),
    ]
}

class MasterViewController: UIViewController {
  override func viewDidLoad() {
    tremendous.viewDidLoad()
    self.title = "grasp"
    self.view.backgroundColor = .purple
  }
}

class DetailViewController: UIViewController {
  override func viewDidLoad() {
    tremendous.viewDidLoad()
    self.title = "element"
    self.view.backgroundColor = .orange
    addBox(to: self.view).backgroundColor = .yellow
  }

  override func viewDidAppear(_ animated: Bool) {
    tremendous.viewDidAppear(animated)
    Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
      assert(self.navigationController != nil)
      self.navigationController?.pushViewController(SubDetailViewController(), animated: true)
    }
  }
}

class SubDetailViewController: UIViewController {
  override func viewDidLoad() {
    tremendous.viewDidLoad()
    self.title = "sub element"
    self.view.backgroundColor = .inexperienced
    addBox(to: self.view).backgroundColor = .cyan
  }

  override func viewDidAppear(_ animated: Bool) {
    tremendous.viewDidAppear(animated)
    Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
      assert(self.navigationController != nil)
      self.navigationController?.pushViewController(SubSubDetailViewController(), animated: true)
    }
  }
}

class SubSubDetailViewController: UIViewController {
  override func viewDidLoad() {
    tremendous.viewDidLoad()
    self.title = "sub sub element"
    self.view.backgroundColor = .blue
    addBox(to: self.view).backgroundColor = .magenta
  }

  override func viewWillAppear(_ animated: Bool) {
    tremendous.viewWillAppear(animated)
    self.view.layoutIfNeeded()
  }
}

func addBox(to father or mother: UIView) -> UIView {
  let field = UIView()
  field.backgroundColor = .yellow
  field.translatesAutoresizingMaskIntoConstraints = false
  father or mother.addSubview(field)

  let container = father or mother.safeAreaLayoutGuide
  NSLayoutConstraint.activate([
    NSLayoutConstraint(item: box, attribute: .top, relatedBy: .equal, toItem: container, attribute: .top, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: box, attribute: .bottom, relatedBy: .equal, toItem: container, attribute: .bottom, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: box, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: box, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1, constant: 0),
  ])

  return field
}

UINavigationController push transition protected space bug in compact UISplitViewController on iOS 26

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments