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:
- 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.
- 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:
-
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) -
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).
-
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).
-
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
}


