Как создать перелистываемый UITabBar с нуля
Опубликовано: 2022-03-11Как вы знаете, iOS SDK от Apple содержит множество встроенных компонентов пользовательского интерфейса. Кнопки, контейнеры, навигация, макеты с вкладками, вы называете это — почти все, что вам когда-либо понадобится, есть. Или это?
Все эти базовые компоненты позволяют нам создавать базовые структурированные пользовательские интерфейсы, но что произойдет, если возникнет необходимость выйти за рамки стандартного? когда разработчику iOS нужно создать какое-то поведение, которое по умолчанию не поддерживается в SDK?
Один из таких случаев — UITabBar , где у вас нет возможности свайпа между вкладками, а также у вас нет анимации переключения между вкладками.
Поиск простого исправления UITabBar
После изрядных поисков мне удалось найти только одну полезную библиотеку на Github. К сожалению, библиотека создавала много проблем при запуске приложения, хотя на первый взгляд выглядела как элегантное решение.
Другими словами, я обнаружил, что библиотека очень проста в использовании, но содержит ошибки, которые явно перевешивают ее простоту использования и, как правило, вызывают проблемы. Если вы все еще заинтересованы, библиотеку можно найти по этой ссылке.
Итак, после некоторых размышлений и долгих поисков, я начал реализовывать свое собственное решение и сказал себе: «Эй, а что, если мы будем использовать контроллер просмотра страницы для свайпа и собственный UITabBar. Что, если мы сгруппируем эти две вещи вместе и будем обрабатывать индекс страниц, проводя пальцем по экрану или нажимая на панель вкладок?»
В конце концов, я нашел решение, хотя оно оказалось довольно сложным, как я объясню позже.
Продуманное решение UITabBar
Представьте, что у вас есть три элемента панели вкладок, которые нужно построить, что автоматически означает, что у вас есть три страницы/контроллера, которые будут отображаться для каждого элемента вкладки.
В этом случае вам нужно будет создать экземпляр этих трех контроллеров представления, а также вам понадобятся два заполнителя/пустых контроллера представления для панели вкладок, чтобы создавать элементы панели вкладок, изменять их состояние при нажатии вкладки или когда пользователь хочет изменить индекс вкладки программно.
Для этого давайте покопаемся в Xcode и напишем пару классов, просто чтобы посмотреть, как все это работает.
Пример свайпа между вкладками
На этих снимках экрана вы можете видеть, что первый элемент панели вкладок окрашен в синий цвет, затем пользователь проводит пальцем по правой вкладке, которая имеет желтый цвет, а последний экран показывает, что выбран третий элемент, поэтому вся страница отображается желтым цветом.
Программное использование перелистываемой панели вкладок
Итак, давайте углубимся в эту функцию и напишем простой пример панели вкладок с возможностью прокрутки для iOS. Прежде всего, нам нужно создать новый проект.
Предпосылки, необходимые для нашего проекта, довольно просты: инструменты сборки Xcode и Xcode, установленные на вашем Mac.
Чтобы создать новый проект, откройте приложение Xcode на своем Mac и выберите «Создать новый проект Xcode», затем назовите свой проект и, наконец, выберите тип создаваемого приложения. Просто выберите «Single View App» и нажмите «Далее».
Как видите, на следующем экране от вас потребуется предоставить некоторую основную информацию:
- Название продукта: я назвал его SwipeableTabbar.
- Команда: Если вы хотите запустить это приложение на реальном устройстве, вам потребуется учетная запись разработчика. В моем случае я буду использовать для этого свою учетную запись.
Примечание. Если у вас нет учетной записи разработчика, вы также можете запустить ее в симуляторе.
- Название организации: я назвал ее Toptal .
- Идентификатор организации: я назвал его com.toptal .
- Язык: выберите Swift.
- Снимите флажки: «Использовать основные данные», «Включить модульные тесты» и «Включить тесты пользовательского интерфейса».
Нажмите кнопку «Далее», и вы готовы приступить к созданию панели вкладок с возможностью прокрутки.
Простая архитектура
Как вы уже знаете, когда вы создаете новое приложение, у вас уже есть класс Main ViewController
и Main.Storyboard
.
Прежде чем мы начнем проектирование, давайте сначала создадим все необходимые классы и файлы, чтобы убедиться, что у нас все настроено и работает, прежде чем мы перейдем к части работы с пользовательским интерфейсом.
Где-то внутри вашего проекта просто создайте несколько новых файлов -> TabbarController.swift , NavigationController.swift , PageViewController.swift .
В моем случае это выглядит так.
В файле AppDelegate оставьте только didFinishLaunchingWithOptions
, так как вы можете удалить все остальные методы.
Внутри didFinishLaunchingWithOptions
просто скопируйте и вставьте следующие строки:
window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true
Удалите все из файла с именем ViewController.swift . Мы вернемся к этому файлу позже.
Во-первых, давайте напишем код для NavigationController.swift .
import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }
При этом мы только что создали простой UINavigationController
, где у нас есть полупрозрачная полоса с серым цветом TintColor. Это все здесь.
Теперь мы можем перейти к PageViewController
.
В нем нам нужно кодировать немного больше, чем в предыдущих файлах, которые мы обсуждали.
Этот файл содержит один класс, один протокол, некоторый источник данных UIPageViewController
и методы делегата.
Полученный файл должен выглядеть так:
Как видите, мы объявили наш собственный протокол с именем PageViewControllerDelegate
, который должен сообщать контроллеру панели вкладок, что индекс страницы был изменен после обработки смахивания.
import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }
Затем нам нужно создать новый класс с именем PageViewController
, который будет содержать наши контроллеры представления, выбирать страницы по определенному индексу, а также обрабатывать свайпы.
Давайте представим, что первым выбранным контроллером при первом запуске должен быть контроллер центрального вида. В этом случае мы присваиваем значение нашего индекса по умолчанию, равное 1.
class PageViewController: UIPageViewController { weak var swipeDelegate: PageViewControllerDelegate? var pages = [UIViewController]() var prevIndex: Int = 1 override func viewDidLoad() { super.viewDidLoad() self.dataSource = self self.delegate = self } func selectPage(at index: Int) { self.setViewControllers( [self.pages[index]], direction: self.direction(for: index), animated: true, completion: nil ) self.prevIndex = index } private func direction(for index: Int) -> UIPageViewController.NavigationDirection { return index > self.prevIndex ? .forward : .reverse } }
Как видите, у нас есть pages
переменных, которые будут содержать ссылки на все наши контроллеры представлений.
Переменная prevIndex
используется для хранения последнего выбранного индекса.
Вы можете просто вызвать метод selectPage
, чтобы установить выбранный индекс.
Если вы хотите отслеживать изменения индекса страницы, вы должны подписаться на swipeDelegate
, и при каждом пролистывании страницы вы будете уведомлены об изменении индекса страницы, а также получите текущий индекс.
Направление метода вернет направление UIPageViewController
. Последней частью головоломки в этом классе являются реализации делегата/источника данных.
К счастью, эти реализации очень просты.

extension PageViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let previousIndex = viewControllerIndex - 1 guard previousIndex >= 0 else { return nil } guard pages.count > previousIndex else { return nil } return pages[previousIndex] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let nextIndex = viewControllerIndex + 1 guard nextIndex < pages.count else { return nil } guard pages.count > nextIndex else { return nil } return pages[nextIndex] } } extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed { guard let currentPageIndex = self.viewControllers?.first?.view.tag else { return } self.prevIndex = currentPageIndex self.swipeDelegate?.pageDidSwipe(to: currentPageIndex) } } }
Как вы можете видеть выше, в игре есть три метода:
- Первый находит индекс и возвращает предыдущий контроллер представления.
- Второй находит индекс и возвращает следующий контроллер представления.
- Последний проверяет, закончилось ли пролистывание, устанавливает текущий индекс в локальное свойство
prevIndex
а затем вызывает метод делегата, чтобы уведомить родительский контроллер представления об успешном завершении пролистывания.
Теперь мы наконец можем написать нашу реализацию UITabBarController
:
import UIKit class TabbarController: UITabBarController { let selectedColor = UIColor.blue let deselectedColor = UIColor.gray let tabBarImages = [ UIImage(named: "ic_music")!, UIImage(named: "ic_play")!, UIImage(named: "ic_star")! ] override func viewDidLoad() { view.backgroundColor = .gray self.delegate = self tabBar.isTranslucent = true tabBar.tintColor = deselectedColor tabBar.unselectedItemTintColor = deselectedColor tabBar.barTintColor = UIColor.white.withAlphaComponent(0.92) tabBar.itemSpacing = 10.0 tabBar.itemWidth = 76.0 tabBar.itemPositioning = .centered setUp() self.selectPage(at: 1) } }
Как видите, мы создаем TabbarController
со свойствами и стилем по умолчанию. Нам нужно определить два цвета для выбранных и невыбранных элементов панели. Кроме того, я представил три изображения для элементов панели вкладок.
В viewDidLoad
я просто устанавливаю конфигурацию нашей панели вкладок по умолчанию и выбираю страницу №1. Это означает, что начальной страницей будет страница номер один.
private func setUp() { guard let centerPageViewController = createCenterPageViewController() else { return } var controllers: [UIViewController] = [] controllers.append(createPlaceholderViewController(forIndex: 0)) controllers.append(centerPageViewController) controllers.append(createPlaceholderViewController(forIndex: 2)) setViewControllers(controllers, animated: false) selectedViewController = centerPageViewController } private func selectPage(at index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) guard let PageViewController = (self.viewControllers?[1] as? PageViewController) else { return } PageViewController.selectPage(at: index) }
Как видите, внутри метода setUp мы создали два контроллера представления-заполнителя. Эти контроллеры-заполнители необходимы для UITabBar
потому что количество элементов панели вкладок должно быть равно количеству имеющихся у вас контроллеров представления.
Если вы помните, мы используем UIPageViewController
для отображения контроллеров, но для UITabBar
, если мы хотим сделать его полностью работоспособным, нам нужно создать экземпляры всех контроллеров представления, чтобы элементы панели работали при нажатии на них. Итак, в этом примере placeholderviewcontroller #0 и #2 являются пустыми контроллерами представления.
В качестве центрального контроллера представления мы создаем PageViewController
с тремя контроллерами представления.
private func createPlaceholderViewController(forIndex index: Int) -> UIViewController { let emptyViewController = UIViewController() emptyViewController.tabBarItem = tabbarItem(at: index) emptyViewController.view.tag = index return emptyViewController } private func createCenterPageViewController() -> UIPageViewController? { let leftController = ViewController() let centerController = ViewController2() let rightController = ViewController3() leftController.view.tag = 0 centerController.view.tag = 1 rightController.view.tag = 2 leftController.view.backgroundColor = .red centerController.view.backgroundColor = .blue rightController.view.backgroundColor = .yellow let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) guard let pageViewController = storyBoard.instantiateViewController(withIdentifier: "PageViewController") as? PageViewController else { return nil } pageViewController.pages = [leftController, centerController, rightController] pageViewController.tabBarItem = tabbarItem(at: 1) pageViewController.view.tag = 1 pageViewController.swipeDelegate = self return pageViewController } private func tabbarItem(at index: Int) -> UITabBarItem { return UITabBarItem(title: nil, image: self.tabBarImages[index], selectedImage: nil) }
Первый и второй методы, изображенные выше, являются методами инициализации нашего контроллера просмотра страниц .
Элемент tabbar
метода просто возвращает элемент tabbar
по индексу.
Как видите, в createCenterPageViewController()
я использую теги для каждого контроллера представления. Это помогает мне понять, какой контроллер появился на экране.
Далее мы подходим к, возможно, нашему самому важному методу, handleTabbarItemChange
.
private func handleTabbarItemChange(viewController: UIViewController) { guard let viewControllers = self.viewControllers else { return } let selectedIndex = viewController.view.tag self.tabBar.tintColor = selectedColor self.tabBar.unselectedItemTintColor = selectedColor for i in 0..<viewControllers.count { let tabbarItem = viewControllers[i].tabBarItem let tabbarImage = self.tabBarImages[i] tabbarItem?.selectedImage = tabbarImage.withRenderingMode(.alwaysTemplate) tabbarItem?.image = tabbarImage.withRenderingMode( i == selectedIndex ? .alwaysOriginal : .alwaysTemplate ) } if selectedIndex == 1 { viewControllers[selectedIndex].tabBarItem.selectedImage = self.tabBarImages[1].withRenderingMode(.alwaysOriginal) } }
В этом методе я использую контроллер представления в качестве параметра. Из этого контроллера представления я получаю тег как выбранный индекс. Для панели вкладок нам нужно установить выбранные и невыбранные цвета.
Теперь нам нужно перебрать все наши контроллеры и проверить, если i == selectedIndex
Затем нам нужно отрендерить изображение как исходный режим рендеринга , в противном случае нам нужно отрендерить изображение как режим шаблона .
Когда вы визуализируете изображение в режиме шаблона , оно наследует цвет от цвета оттенка элемента.
Мы почти закончили. Нам просто нужно ввести два важных метода из UITabBarControllerDelegate
и PageViewControllerDelegate
.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { self.selectPage(at: viewController.view.tag) return false } func pageDidSwipe(to index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) }
Первый вызывается при нажатии на любой элемент вкладки, а второй вызывается при свайпе между вкладками.
Подведение итогов
Когда вы соберете весь код вместе, вы заметите, что вам не нужно писать собственную реализацию обработчиков жестов, и вам не нужно писать много кода для обеспечения плавной прокрутки/перелистывания между элементами панели вкладок.
Обсуждаемая здесь реализация не идеальна для всех сценариев, но это причудливое, быстрое и относительно простое решение, позволяющее создавать эти функции с помощью небольшого кода.
Наконец, если вы хотите попробовать мой подход, вы можете использовать мой репозиторий GitHub. Удачного кодирования!