Jak stworzyć Swipeable UITabBar od podstaw

Opublikowany: 2022-03-11

Jak wiecie, Apple iOS SDK zawiera mnóstwo wbudowanych komponentów interfejsu użytkownika. Przyciski, kontenery, nawigacje, układy z kartami, co tylko chcesz — jest tam prawie wszystko, czego kiedykolwiek będziesz potrzebować. Albo to jest?

Wszystkie te podstawowe komponenty pozwalają nam tworzyć podstawowe ustrukturyzowane interfejsy użytkownika, ale co się stanie, jeśli zajdzie potrzeba wyjścia poza schemat; kiedy programista iOS musi zbudować jakieś zachowanie, które nie jest domyślnie obsługiwane w SDK?

Jednym z tych przypadków jest UITabBar , w którym nie masz możliwości przesuwania między kartami, a także nie masz animacji przełączania między kartami.

Wyszukiwanie łatwej poprawki UITabBar

Po wielu poszukiwaniach udało mi się znaleźć tylko jedną użyteczną bibliotekę na Github. Niestety biblioteka spowodowała wiele problemów podczas uruchamiania aplikacji, choć na pierwszy rzut oka wydawała się eleganckim rozwiązaniem.

Innymi słowy, uznałem, że biblioteka jest bardzo łatwa w użyciu, ale zawiera błędy, co oczywiście przewyższało łatwość jej użycia i powodowało problemy. Jeśli nadal jesteś zainteresowany, bibliotekę można znaleźć pod tym linkiem.

Tak więc, po przemyśleniu i wielu poszukiwaniach, zacząłem wdrażać własne rozwiązanie i powiedziałem sobie: „Hej, a co jeśli użyjemy kontrolera widoku strony do przeciągania i natywnego UITabBar. A co, jeśli zgrupujemy te dwie rzeczy razem, zajmiemy się indeksem stron, przesuwając palcem lub stukając w pasek kart?”

Ostatecznie wymyśliłem rozwiązanie, choć okazało się to nieco podchwytliwe, co wyjaśnię później.

Rozbudowane rozwiązanie UITabBar

Wyobraź sobie, że masz do zbudowania trzy elementy paska kart, co automatycznie oznacza, że ​​na każdy element karty mają być wyświetlane trzy strony/kontrolery.

W takim przypadku będziesz musiał utworzyć wystąpienie tych trzech kontrolerów widoku, a także będziesz potrzebować dwóch symboli zastępczych/pustych kontrolerów widoku dla paska kart, aby utworzyć elementy paska kart, zmienić ich stan po naciśnięciu karty lub gdy użytkownik chce zmienić indeks kart programowo.

W tym celu zagłębimy się w Xcode i napiszmy kilka klas, aby zobaczyć, jak te rzeczy działają.

Przykład przesuwania między kartami

Przykład przesuwanych kart w iOS

Na tych zrzutach ekranu widać, że pierwszy element paska kart jest niebieski, następnie użytkownik przesuwa palcem do prawej karty, która jest żółta, a ostatni ekran pokazuje zaznaczony trzeci element, więc cała strona jest wyświetlana w kolorze żółtym.

Programowe użycie przesuwanego paska kart

Zanurzmy się więc w tę funkcję i napiszmy prosty przykład przesuwanego paska kart dla iOS. Przede wszystkim musimy stworzyć nowy projekt.

Wymagania wstępne potrzebne do naszego projektu są dość podstawowe: narzędzia do budowania Xcode i Xcode zainstalowane na komputerze Mac.

Aby utworzyć nowy projekt, otwórz aplikację Xcode na komputerze Mac i wybierz „Utwórz nowy projekt Xcode”, a następnie nazwij swój projekt i na koniec wybierz typ aplikacji, która ma zostać utworzona. Po prostu wybierz „Aplikacja pojedynczego widoku” i naciśnij Dalej.

Zrzut ekranu Xcode

Jak widać, następny ekran będzie wymagał podania podstawowych informacji:

  • Nazwa produktu: nazwałem go SwipeableTabbar.
  • Zespół: Jeśli chcesz uruchomić tę aplikację na prawdziwym urządzeniu, będziesz musiał mieć konto programisty. W moim przypadku użyję do tego własnego konta.

Uwaga: jeśli nie masz konta programisty, możesz uruchomić to również w Symulatorze.

  • Nazwa organizacji: nazwałem ją Toptal .
  • Identyfikator organizacji: nazwałem go com.toptal .
  • Język: wybierz Szybki.
  • Odznacz: „Użyj danych podstawowych”, „Dołącz testy jednostkowe” i „Dołącz testy interfejsu użytkownika”.

Naciśnij przycisk Dalej i możesz zacząć tworzyć przesuwany pasek kart.

Prosta architektura

Jak już wiesz, kiedy tworzysz nową aplikację, masz już klasę Main ViewController i Main.Storyboard .

Zanim zaczniemy projektować, najpierw utwórzmy wszystkie niezbędne klasy i pliki, aby upewnić się, że wszystko jest skonfigurowane i uruchomione, zanim przejdziemy do części dotyczącej interfejsu użytkownika.

Gdzieś w swoim projekcie po prostu utwórz kilka nowych plików -> TabbarController.swift , NavigationController.swift , PageViewController.swift .

W moim przypadku wygląda to tak.

Zrzut ekranu: Kontrolery Xcode

W pliku AppDelegate pozostaw tylko didFinishLaunchingWithOptions , ponieważ możesz usunąć wszystkie inne metody.

Wewnątrz didFinishLaunchingWithOptions po prostu skopiuj i wklej poniższe linie:

 window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true

Usuń wszystko z pliku o nazwie ViewController.swift . Wrócimy do tego pliku później.

Najpierw napiszmy kod dla NavigationController.swift .

 import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }

Dzięki temu właśnie stworzyliśmy prosty UINavigationController , w którym mamy półprzezroczysty pasek z szarym TintColor. To wszystko tutaj.

Teraz możemy przystąpić do przejęcia PageViewController .

W nim musimy kodować trochę więcej niż w poprzednich plikach, o których mówiliśmy.

Ten plik zawiera jedną klasę, jeden protokół, niektóre źródła danych UIPageViewController i metody delegatów.

Wynikowy plik musi wyglądać tak:

Zrzut ekranu Xcode: Metody

Jak widać, zadeklarowaliśmy własny protokół o nazwie PageViewControllerDelegate , który powinien informować kontroler paska kart, że indeks strony został zmieniony po przesunięciu palcem.

 import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }

Następnie musimy stworzyć nową klasę o nazwie PageViewController , która będzie przechowywać nasze kontrolery widoku, wybierać strony w określonym indeksie, a także obsługiwać przeciągnięcia.

Wyobraźmy sobie, że pierwszy wybrany kontroler podczas naszego pierwszego uruchomienia powinien być kontrolerem widoku centralnego. W tym przypadku przypisujemy naszą domyślną wartość indeksu, równą 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 } }

Jak widać tutaj, mamy zmienne pages , które będą zawierać referencje wszystkich naszych kontrolerów widoku.

Zmienna prevIndex służy do przechowywania ostatniego wybranego indeksu.

Możesz po prostu wywołać metodę selectPage , aby ustawić wybrany indeks.

Jeśli chcesz nasłuchiwać zmian w indeksie stron, musisz zasubskrybować swipeDelegate , a przy każdym przesunięciu strony zostaniesz powiadomiony o zmianie indeksu strony, a także otrzymasz bieżący indeks.

Kierunek metody zwróci kierunek przesunięcia UIPageViewController . Ostatnim elementem układanki w tej klasie są implementacje delegatów/źródła danych.

Na szczęście te implementacje są bardzo proste.

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

Jak widać powyżej, w grze są trzy metody:

  • Pierwszy z nich wyszukuje indeks i zwraca poprzedni kontroler widoku.
  • Drugi znajduje indeks i zwraca następny kontroler widoku.
  • Ostatni sprawdza, czy przeciągnięcie zostało zakończone, ustawia bieżący indeks na właściwość lokalną prevIndex , a następnie wywołuje metodę delegata, aby powiadomić kontroler widoku nadrzędnego, że przeciągnięcie zostało pomyślnie zakończone.

Teraz możemy wreszcie napisać naszą implementację 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) } }

Jak widać, tworzymy TabbarController , z domyślnymi właściwościami i stylem. Musimy zdefiniować dwa kolory, dla zaznaczonych i odznaczonych elementów paska. Wprowadziłem również trzy obrazy dla elementów tabbar.

W viewDidLoad ustawiam tylko domyślną konfigurację naszego paska kart i wybieram stronę #1. Oznacza to, że strona startowa będzie stroną numer jeden.

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

W metodzie setup widzisz, że stworzyliśmy dwa zastępcze kontrolery widoku. Te kontrolery zastępcze są potrzebne dla UITabBar , ponieważ liczba elementów paska kart musi być równa liczbie posiadanych kontrolerów widoku.

Jeśli pamiętasz, używamy UIPageViewController do wyświetlania kontrolerów, ale dla UITabBar , jeśli chcemy, aby był w pełni funkcjonalny, musimy mieć utworzone wystąpienia wszystkich kontrolerów widoku, aby elementy paska działały po dotknięciu ich. Tak więc w tym przykładzie placeholderviewcontroller #0 i #2 są pustymi kontrolerami widoku.

Jako wyśrodkowany kontroler widoku tworzymy PageViewController z trzema kontrolerami widoku.

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

Pierwsza i druga metoda opisana powyżej to metody init naszego kontrolera odsłon .

Element tabbar metody po prostu zwraca element tabbar w indeksie.

Jak widać, w ramach createCenterPageViewController() używam tagów dla każdego kontrolera widoku. Pomaga mi to zrozumieć, który kontroler pojawił się na ekranie.

Następnie dochodzimy do prawdopodobnie naszej najważniejszej metody, 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) } }

W tej metodzie używam kontrolera widoku jako parametru. Z tego kontrolera widoku otrzymuję tag jako wybrany indeks. Dla paska zakładek musimy ustawić wybrane i niezaznaczone kolory.

Teraz musimy przejść przez wszystkie nasze kontrolery i sprawdzić, czy i == selectedIndex

Następnie musimy wyrenderować obraz jako oryginalny tryb renderowania , w przeciwnym razie musimy wyrenderować obraz jako tryb szablonu .

Gdy renderujesz obraz w trybie szablonu , dziedziczy on kolor z koloru tinty elementu.

Prawie skończyliśmy. Musimy tylko wprowadzić dwie ważne metody z UITabBarControllerDelegate i 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) }

Pierwsza jest wywoływana po naciśnięciu dowolnego elementu karty, a druga jest wywoływana, gdy przesuwasz palcem między kartami.

Zawijanie

Kiedy złożysz cały kod razem, zauważysz, że nie musisz pisać własnej implementacji obsługi gestów i nie musisz pisać dużo kodu, aby zapewnić płynne przewijanie/przesuwanie między elementami paska kart.

Omawiana tutaj implementacja nie jest czymś, co będzie idealne dla wszystkich scenariuszy, ale jest dziwacznym, szybkim i stosunkowo łatwym rozwiązaniem, które umożliwia tworzenie tych funkcji przy użyciu niewielkiej ilości kodu.

Wreszcie, jeśli chcesz wypróbować moje podejście, możesz skorzystać z mojego repozytorium GitHub. Udanego kodowania!