Aus Gründen, auf die ich nicht näher eingehen werde, erstelle ich meine eigene Implementierung von UITabBarController. Dies ist eine UIViewController-Unterklasse. Ich füge unten eine UITabBar hinzu und je nachdem, welche Registerkarte ausgewählt ist, zeige ich den entsprechenden Ansichtscontroller an, genau wie UITabBarController. Meine Anforderungen sind weitaus komplexer, aber ich habe diese Frage und ihren Code auf ein Problem eingegrenzt, das ich bei meinem Versuch habe, das Standardverhalten zu reproduzieren.
In meiner Test-App erstelle ich meinen benutzerdefinierten Tab-Bar-Controller mit einigen Tabs, die jeweils aus einem UINavigationController mit einer Unterklasse eines Table View Controllers als Root-View-Controller bestehen. Es funktioniert alles, außer dass ich beim Scrollen zum unteren Rand der Tabellenansicht nicht den Effekt des Erscheinungsbilds der Bildlaufkante erhalte. Unter iOS 18 oder früher wird mit einem standardmäßigen UITabBarController die Tab-Leiste klar (wie die Navigationsleiste), wenn Sie zum Ende der Tabelle scrollen.
Unter iOS 26 ist das Problem ähnlich, aber etwas anders. In meiner benutzerdefinierten Implementierung erhalte ich nicht den Soft-Fade-Effekt für die Zeilen, die hinter der Tab-Leiste angezeigt werden, wie bei einem Standard-UITabBarController.
Meine Frage läuft darauf hinaus, was in meinem Code fehlt, damit der Bildlaufkanteneffekt (iOS 18) / Soft-Fade-Effekt (iOS 26) wie in einem Standard-UITabBarController funktioniert?
Das Folgende ist ein Teil des Codes für die Einstellung die UITabBar in meiner benutzerdefinierten Tab-Bar-Controller-Klasse einrichten. Vollständig funktionsfähiger Code erscheint weiter unten.
Code: Select all
let tabBar = UITabBar()
private func setupTabBar() {
let std = UITabBarAppearance()
std.configureWithDefaultBackground()
tabBar.standardAppearance = std
// This appearance is never triggered. What is needed to make this work?
let se = UITabBarAppearance()
se.configureWithTransparentBackground()
tabBar.scrollEdgeAppearance = se
tabBar.delegate = self
tabBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tabBar)
NSLayoutConstraint.activate([
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
if #available(iOS 26.0, *) {
// Under iOS 26 this is now nice and simple - or at least is seems to be.
tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
} else {
// Under iOS 18 and earlier, this is the only way I could get the tab bar to
// appear correctly and fully fill-in the safe area.
tabBar.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
Fügen Sie eine neue Datei mit dem Namen MyTabBarController.swift hinzu und fügen Sie den folgenden vollständigen Code in die neue Datei ein:
Code: Select all
import UIKit
class MyTabBarController: UIViewController {
let tabBar = UITabBar()
var viewControllers: [UIViewController] {
get {
_viewControllers
}
set {
setViewControllers(newValue, animated: false)
}
}
// Assumes there will be at least one controller provided - crashes if there are none - fine for my needs
func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
// Cleanup the currently selected view controller, if any.
if !_viewControllers.isEmpty {
removeViewController(_viewControllers[selectedIndex])
}
_viewControllers = viewControllers
selectedIndex = 0
// Update the tab bar
tabBar.setItems(viewControllers.map({ $0.tabBarItem }), animated:animated)
tabBar.selectedItem = tabBar.items?.first
//
addViewController(viewControllers[selectedIndex])
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private var selectedIndex = 0
private var _viewControllers = [UIViewController]()
private func removeViewController(_ controller: UIViewController?) {
if let controller {
controller.willMove(toParent: nil)
controller.view.removeFromSuperview()
controller.removeFromParent()
}
}
private func addViewController(_ controller: UIViewController?) {
if let controller {
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
controller.didMove(toParent: self)
accountForTabBar()
// Ensure the tab bar stays on top
view.bringSubviewToFront(tabBar)
}
}
private func selectViewController(at index: Int) {
// Swap out the current view controller with the newly selected one
removeViewController(_viewControllers[selectedIndex])
selectedIndex = index
tabBar.selectedItem = tabBar.items?[index] // This is needed under iOS 26 otherwise the selected tab is gets reset to 0
let controller = _viewControllers[index]
addViewController(controller)
}
private func accountForTabBar() {
// Make sure the tab bar has been added and there is at least one controller
guard tabBar.superview != nil && !_viewControllers.isEmpty else { return }
// Ensure the current view controller's bottom safe area is adjusted to account for the tab bar
let controller = _viewControllers[selectedIndex];
// I don't like the need for a different adjustment as of iOS 26.
// Does this suggest a [url=viewtopic.php?t=26065]problem[/url] with my code?
if #available(iOS 26.0, *) {
controller.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: tabBar.frame.height - view.safeAreaInsets.bottom, right: 0)
} else {
controller.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: tabBar.frame.height, right: 0)
}
}
private func setupTabBar() {
let std = UITabBarAppearance()
std.configureWithDefaultBackground()
tabBar.standardAppearance = std
// This appearance is never triggered. What is needed to make this work?
let se = UITabBarAppearance()
se.configureWithTransparentBackground()
tabBar.scrollEdgeAppearance = se
tabBar.delegate = self
tabBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tabBar)
NSLayoutConstraint.activate([
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
if #available(iOS 26.0, *) {
// Under iOS 26 this is now nice and simple - or at least is seems to be.
tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
} else {
// Under iOS 18 and earlier, this is the only way I could get the tab bar to
// appear correctly and fully fill-in the safe area.
tabBar.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupTabBar()
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
accountForTabBar() // Update to reflect new safe area
}
override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
accountForTabBar() // Ensure initial view controller is adjusted properly
}
}
extension MyTabBarController: UITabBarDelegate {
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
// User tapped on a tab item - update which view controller is being shown
let index = tabBar.items?.firstIndex(of: item) ?? 0
selectViewController(at: index)
}
}
Code: Select all
import UIKit
// Show 20 simple rows to test scroll edge functionality with the tab bar
class TableViewController: UITableViewController {
init() {
super.init(style: .plain)
self.title = "Table"
tabBarItem.image = UIImage(systemName: "list.bullet")
navigationItem.rightBarButtonItem = editButtonItem
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
}
}
extension TableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
20
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = "Row \(indexPath.row + 1)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath)
}
}
Code: Select all
import UIKit
// Change this to extend UITabBarController to see the difference in behavior
class ViewController: MyTabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Just as an example - show a bunch of tabs
viewControllers = [
UINavigationController(rootViewController: TableViewController()),
UINavigationController(rootViewController: TableViewController()),
UINavigationController(rootViewController: TableViewController()),
UINavigationController(rootViewController: TableViewController()),
]
}
}
iOS 18:
Scrollen Sie auf einer der Registerkarten nach unten, damit Sie die gesamte Zeile 20 sehen können. Beachten Sie, dass die Registerkartenleiste nicht klar wird. Vergleichen Sie damit, wie sich die Navigationsleiste ändert, wenn Sie nach oben scrollen (die gesamte Zeile 1 ist sichtbar).
iOS 26:
Beachten Sie, dass Zeilen hinter der Tab-Leiste überhaupt nicht ausgeblendet werden. Scrollen Sie ein wenig nach unten und bemerken Sie, wie die Zeilen hinter der Navigationsleiste ausgeblendet werden. Dasselbe sollte mit Zeilen hinter der Tab-Leiste passieren.
Bearbeiten Sie zum Vergleich ViewController.swift so, dass die ViewController-Klasse UITabBarController anstelle von MyTabBarController erweitert. Erstellen Sie den Build und führen Sie ihn erneut aus. Beachten Sie dabei, wie sich die Registerkartenleiste verhält. Ich versuche, dasselbe Verhalten in der Tab-Leiste zu reproduzieren.
Ich weiß nur nicht, welcher Code in MyTabBarController fehlt, um seine UITabBar einzurichten. Was macht UITabBarController, was ich nicht tue? Ich habe SO, die Apple-Entwicklerforen und GitHub durchsucht und bisher kein Beispiel gefunden, das diese Funktionalität anspricht.
Hier sind eine Reihe von Bildern, die unterschiedliche Ergebnisse zeigen.
Code: Select all
ViewController
Code: Select all
ViewController
Code: Select all
ViewController
Code: Select all
ViewController
Code: Select all
ViewController
Code: Select all
ViewController
Mobile version