Cum să creați o bară UITabBar care poate fi glisată de la zero
Publicat: 2022-03-11După cum știți, SDK-ul iOS de la Apple conține o multitudine de componente UI încorporate. Butoane, containere, navigații, machete cu file, cum vrei să-l numești — aproape tot ce vei avea nevoie vreodată este acolo. Sau este?
Toate aceste componente de bază ne permit să creăm interfețe de utilizare structurate de bază, dar ce se întâmplă dacă este nevoie să ieșim în afara cutiei; când un dezvoltator iOS trebuie să creeze un fel de comportament care nu este acceptat în SDK în mod implicit?
Unul dintre aceste cazuri este UITabBar , unde nu aveți posibilitatea de a glisa între file și nici nu aveți animații pentru comutarea între file.
Căutarea unei remedieri UITabBar ușoare
După o mulțime de căutări, am reușit să găsesc o singură bibliotecă utilă pe Github. Din păcate, biblioteca a creat o mulțime de probleme în timpul rulării aplicației, deși a apărut ca o soluție elegantă la prima vedere.
Cu alte cuvinte, mi s-a părut că biblioteca este foarte ușor de utilizat, dar cu erori, ceea ce, evident, a depășit ușurința în utilizare și tinde să provoace probleme. În cazul în care încă sunteți interesat, lib-ul poate fi găsit sub acest link.
Așa că, după câteva gânduri și multe căutări, am început să implementez propria mea soluție și mi-am spus: „Hei, ce se întâmplă dacă folosim controlerul de vizualizare a paginii pentru glisare și UITabBar nativ. Ce se întâmplă dacă grupăm aceste două lucruri împreună, gestionăm indexul paginii glisând sau atingând bara de file?”
În cele din urmă, am venit cu o soluție, deși s-a dovedit oarecum complicată, așa cum voi explica mai târziu.
O soluție elaborată UITabBar
Imaginați-vă că aveți trei elemente din bara de file de creat, ceea ce înseamnă automat că aveți trei pagini/controlere de afișat pentru fiecare element de filă.
În acest caz, va trebui să instanțiați acele trei controlere de vizualizare și veți avea nevoie, de asemenea, de două substituente/controlere de vizualizare goale pentru bara de file, pentru a crea elemente din bara de file, pentru a le schimba starea atunci când este apăsată fila sau când utilizatorul dorește să schimbe indexul filei în mod programatic.
Pentru aceasta, să cercetăm Xcode și să scriem câteva clase, doar pentru a vedea cum funcționează aceste lucruri.
Un exemplu de trecere între file
În aceste capturi de ecran, puteți vedea că primul element din bara de file este albastru, apoi utilizatorul glisează spre fila din dreapta, care este galbenă, iar ultimul ecran arată că al treilea element este selectat, astfel încât întreaga pagină este afișată ca galben.
Utilizarea programatică a barei de file cu glisare
Deci, haideți să ne aprofundăm în această funcție și să scriem un exemplu ușor de bară de file care poate fi glisată pentru iOS. În primul rând, trebuie să creăm un nou proiect.
Cerințele necesare pentru proiectul nostru sunt destul de de bază: instrumentele de compilare Xcode și Xcode instalate pe Mac.
Pentru a crea un nou proiect, deschideți aplicația Xcode pe Mac și selectați „Creați un nou proiect Xcode”, apoi denumiți proiectul și, în final, alegeți tipul de aplicație care trebuie creat. Pur și simplu selectați „Single View App” și apăsați Next.
După cum puteți vedea, următorul ecran vă va cere să furnizați câteva informații de bază:
- Nume produs: l-am numit SwipeableTabbar.
- Echipa: Dacă doriți să rulați această aplicație pe un dispozitiv real, va trebui să aveți un cont de dezvoltator. În cazul meu, voi folosi propriul meu cont pentru asta.
Notă: dacă nu aveți un cont de dezvoltator, îl puteți rula și pe Simulator.
- Numele organizației: L-am numit Toptal .
- Identificator organizație: l-am numit com.toptal .
- Limbă: selectați Swift.
- Debifați: „Utilizați datele de bază”, „Includeți teste unitare” și „Includeți teste UI”.
Apăsați butonul Următorul și sunteți gata să începeți să construiți bara de file care poate fi glisată.
Arhitectură simplă
După cum știți deja până acum, atunci când creați o nouă aplicație, aveți deja clasa Main ViewController
și Main.Storyboard
.
Înainte de a începe proiectarea, să creăm mai întâi toate clasele și fișierele necesare pentru a ne asigura că avem totul configurat și rulează înainte de a trece la partea UI a jobului.
Undeva în proiectul dvs., creați pur și simplu câteva fișiere noi -> TabbarController.swift , NavigationController.swift , PageViewController.swift .
În cazul meu, așa arată.
În fișierul AppDelegate , lăsați doar didFinishLaunchingWithOptions
, deoarece puteți elimina toate celelalte metode.
În didFinishLaunchingWithOptions
, pur și simplu copiați și lipiți liniile de mai jos:
window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true
Eliminați totul din fișierul numit ViewController.swift . Vom reveni la acest fișier mai târziu.
Mai întâi, să scriem cod pentru NavigationController.swift .
import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }
Cu aceasta, tocmai am creat un simplu UINavigationController
, unde avem o bară translucidă cu o culoare gri TintColor. Asta e tot aici.
Acum, putem trece la preluarea PageViewController
.
În el, trebuie să codificăm puțin mai mult decât în fișierele anterioare despre care am discutat.
Acest fișier conține o clasă, un protocol, o sursă de date UIPageViewController
și metode delegate.
Fișierul rezultat trebuie să arate astfel:
După cum puteți vedea, am declarat propriul nostru protocol numit PageViewControllerDelegate
, care ar trebui să spună controlerului barei de file că indexul paginii a fost schimbat după ce glisarea este gestionată.
import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }
Apoi trebuie să creăm o nouă clasă, numită PageViewController
, care va păstra controlerele noastre de vizualizare, va selecta paginile la un index specific și, de asemenea, va gestiona glisările.
Să ne imaginăm că primul controler selectat la prima noastră rulare ar trebui să fie controlerul de vedere centrală. În acest caz, atribuim valoarea implicită a indexului, egală cu 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 } }
După cum puteți vedea aici, avem pages
variabile, care vor conține referințe ale tuturor controlerelor noastre de vizualizare.
Variabila prevIndex
este folosită pentru a stoca ultimul index selectat.
Puteți apela pur și simplu metoda selectPage
pentru a seta indexul selectat.
Dacă doriți să ascultați modificările indexului paginii, trebuie să vă abonați la swipeDelegate
, iar la fiecare glisare de pagină, veți fi anunțat că indexul paginii s-a schimbat, plus veți primi și indexul curent.
Direcția metodei va returna direcția de glisare a UIPageViewController
. Ultima piesă a puzzle-ului din această clasă sunt implementările delegat/surse de date.
Din fericire, aceste implementări sunt foarte simple.
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) } } }
După cum puteți vedea mai sus, există trei metode în joc:

- Primul găsește indexul și returnează controlerul de vizualizare anterior.
- Al doilea găsește indexul și returnează următorul controler de vizualizare.
- Ultimul verifică dacă glisarea sa încheiat, setează indexul curent la proprietatea locală
prevIndex
și apoi apelează metoda delegatului pentru a notifica controlerului de vizualizare părinte că glisarea a fost încheiată cu succes.
Acum putem scrie în sfârșit implementarea noastră 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) } }
După cum puteți vedea, creăm TabbarController
, cu proprietăți și stil implicit. Trebuie să definim două culori, pentru elementele din bara selectate și deselectate. De asemenea, am introdus trei imagini pentru elementele din bara de file.
În viewDidLoad
, doar configurez configurația implicită a barei noastre de file și selectez pagina #1. Acest lucru înseamnă că pagina de pornire va fi pagina numărul unu.
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) }
În cadrul metodei setUp, vedeți, că am creat două controlere de vizualizare substituentă. Aceste controlere de substituent sunt necesare pentru UITabBar
deoarece numărul de elemente din bara de file trebuie să fie egal cu numărul de controlere de vizualizare pe care le aveți.
Dacă vă amintiți, folosim UIPageViewController
pentru a afișa controlere, dar pentru UITabBar
, dacă vrem să o facem pe deplin funcțională, trebuie să avem toate controlerele de vizualizare instanțiate, astfel încât elementele din bară să funcționeze când le apăsați. Deci, în acest exemplu, placeholderviewcontroller #0 și #2 sunt controlere de vizualizare goale.
Ca controler de vizualizare centrată, creăm un PageViewController
cu trei controlere de vizualizare.
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) }
Prima și a doua metodă descrise mai sus sunt metode init ale controlerului nostru de vizualizare a paginii.
Elementul din bara de tabele metodei tabbar
doar elementul din bara de tabbar
la index.
După cum puteți vedea, în createCenterPageViewController()
folosesc etichete pentru fiecare controler de vizualizare. Acest lucru mă ajută să înțeleg ce controler a apărut pe ecran.
În continuare, ajungem la ceea ce este posibil cea mai importantă metodă a noastră, 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) } }
În această metodă, folosesc controlerul de vizualizare ca parametru. Din acest controler de vizualizare, primesc o etichetă ca index selectat. Pentru bara de file, trebuie să setăm culorile selectate și neselectate.
Acum trebuie să parcurgem toate controlerele noastre și să verificăm dacă i == selectedIndex
Apoi trebuie să redăm imaginea ca mod de randare originală , în caz contrar, trebuie să redăm imaginea ca mod șablon .
Când redați o imagine utilizând modul șablon , aceasta va moșteni culoarea din culoarea nuanței elementului.
Aproape am terminat. Trebuie doar să introducem două metode importante din 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) }
Primul este apelat atunci când apăsați pe orice element de filă, în timp ce al doilea este apelat atunci când glisați între file.
Încheierea
Când puneți tot codul împreună, veți observa că nu trebuie să vă scrieți propria implementare a gestionatorilor de gesturi și nu trebuie să scrieți mult cod pentru a oferi defilare/glisare lină între elementele din bara de file.
Implementarea discutată aici nu este ceva care va fi ideal pentru toate scenariile, dar este o soluție ciudată, rapidă și relativ ușoară, care vă permite să creați aceste caracteristici cu puțin cod.
În cele din urmă, dacă doriți să încercați abordarea mea, puteți utiliza depozitul meu GitHub. Codare fericită!