По причинам, в которые я не буду вдаваться, я создаю свою собственную реализацию UITabBarController. Это подкласс UIViewController. Я добавляю UITabBar внизу и в зависимости от выбранной вкладки отображаю соответствующий контроллер представления, точно так же, как UITabBarController. Мои потребности гораздо более сложны, но я сузил этот вопрос и его код до одной проблемы, с которой я столкнулся при попытке воспроизвести стандартное поведение.
В моем тестовом приложении я создаю собственный контроллер панели вкладок с несколькими вкладками, каждая из которых состоит из UINavigationController с подклассом контроллера табличного представления в качестве корневого контроллера представления. Все работает, за исключением того, что я не получаю эффект появления края прокрутки при прокрутке до нижней части таблицы. В iOS 18 или более ранней версии со стандартным UITabBarController панель вкладок становится ясной (как и панель навигации) при прокрутке до конца таблицы.
В iOS 26 проблема аналогична, но немного другая. В моей пользовательской реализации я не получаю эффект плавного затухания в строках, отображаемых за панелью вкладок, как в стандартном UITabBarController.
Мой вопрос сводится к тому, чего не хватает в моем коде, который позволяет эффекту появления края прокрутки (iOS 18)/эффекту мягкого затухания (iOS 26) работать так же, как в стандартном UITabBarController?
Ниже немного код для настройки UITabBar в моем классе контроллера панели вкладок. Полностью рабочий код показан ниже.
Код: Выделить всё
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
}
}
Добавьте новый файл с именем MyTabBarController.swift и вставьте в него следующий полный код:
Код: Выделить всё
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 problem 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)
}
}
Код: Выделить всё
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)
}
}
Код: Выделить всё
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:
Прокрутите вниз одну из вкладок, чтобы увидеть всю строку 20. Обратите внимание, что панель вкладок не становится четкой. Сравните с тем, как меняется панель навигации при прокрутке вверх (видна вся строка 1).
iOS 26:
Обратите внимание, что строки за панелью вкладок вообще не блекнут. Прокрутите немного вниз и обратите внимание, как строки, идущие за панелью навигации, исчезают. То же самое должно происходить со строками за панелью вкладок.
Для сравнения отредактируйте ViewController.swift так, чтобы класс ViewController расширял UITabBarController вместо MyTabBarController. Создайте и запустите еще раз и обратите внимание, как ведет себя панель вкладок. Я пытаюсь воспроизвести такое же поведение на панели вкладок.
Я просто не знаю, какой фрагмент кода отсутствует в MyTabBarController для настройки его UITabBar. Что делает UITabBarController, а я нет? Я искал SO, на форумах разработчиков Apple и на GitHub и до сих пор не нашел ни одного примера, который бы касался этой части функциональности.
Вот набор изображений, демонстрирующих разные результаты.
Код: Выделить всё
ViewController
Код: Выделить всё
ViewController
Код: Выделить всё
ViewController
Код: Выделить всё
ViewController
Код: Выделить всё
ViewController
Код: Выделить всё
ViewController
Подробнее здесь: https://stackoverflow.com/questions/796 ... -uitabbarc
Мобильная версия