Come creare una UITabBar scorrevole da zero
Pubblicato: 2022-03-11Come sai, l'SDK iOS di Apple contiene una miriade di componenti dell'interfaccia utente integrati. Pulsanti, contenitori, navigazioni, layout a schede, lo chiami: quasi tutto ciò di cui avrai bisogno è lì. O è?
Tutti questi componenti di base ci consentono di creare UI strutturate di base, ma cosa succede se è necessario uscire dagli schemi; quando uno sviluppatore iOS deve creare una sorta di comportamento che non è supportato nell'SDK per impostazione predefinita?
Uno di questi casi è UITabBar , in cui non hai la possibilità di scorrere tra le schede e inoltre non hai animazioni per passare da una scheda all'altra.
Alla ricerca di una facile correzione UITabBar
Dopo una discreta quantità di ricerche, sono riuscito a trovare solo una libreria utile su Github. Sfortunatamente, la libreria ha creato molti problemi durante l'esecuzione dell'applicazione, anche se a prima vista sembrava una soluzione elegante.
In altre parole, ho trovato la libreria molto facile da usare, ma piena di bug, che ovviamente superava la sua facilità d'uso e tendeva a causare problemi. Se sei ancora interessato, la lib può essere trovata a questo link.
Quindi, dopo un po' di riflessione e molte ricerche, ho iniziato a implementare la mia soluzione e mi sono detto: “Ehi, e se usiamo il controller di visualizzazione della pagina per lo swipe e UITabBar nativa. E se raggruppiamo queste due cose insieme, gestiamo l'indice della pagina scorrendo o toccando la barra delle schede?"
Alla fine, ho trovato una soluzione, anche se si è rivelata alquanto complicata, come spiegherò più avanti.
Un'elaborata soluzione UITabBar
Immagina di avere tre elementi della barra delle schede da creare, il che significa automaticamente che hai tre pagine/controller da visualizzare per ogni elemento della scheda.
In questo caso, dovrai creare un'istanza di questi tre controller di visualizzazione e avrai anche bisogno di due segnaposto/controller di visualizzazione vuoti per la barra delle schede, per creare elementi della barra delle schede, modificarne lo stato quando viene premuta la scheda o quando l'utente desidera modificare l'indice delle schede a livello di codice.
Per questo, esaminiamo Xcode e scriviamo un paio di classi, solo per vedere come funzionano queste cose.
Un esempio di scorrimento tra le schede
In questi screenshot, puoi vedere che il primo elemento della barra delle schede è blu, quindi l'utente scorre sulla scheda destra, che è gialla, e l'ultima schermata mostra che il terzo elemento è selezionato, quindi l'intera pagina viene visualizzata in giallo.
Uso programmatico della barra delle schede a scorrimento
Quindi, tuffiamoci in questa funzione e scriviamo un semplice esempio di una barra delle schede a scorrimento per iOS. Prima di tutto, dobbiamo creare un nuovo progetto.
I prerequisiti necessari per il nostro progetto sono abbastanza basilari: Xcode e Xcode build tools installati sul tuo Mac.
Per creare un nuovo progetto, apri l'applicazione Xcode sul tuo Mac e seleziona "Crea un nuovo progetto Xcode", quindi dai un nome al tuo progetto e infine scegli il tipo di applicazione da creare. Seleziona semplicemente "App visualizzazione singola" e premi Avanti.
Come puoi vedere, la schermata successiva richiederà di fornire alcune informazioni di base:
- Nome prodotto: l'ho chiamato SwipeableTabbar.
- Team: se desideri eseguire questa applicazione su un dispositivo reale, dovrai disporre di un account sviluppatore. Nel mio caso, userò il mio account per questo.
Nota: se non disponi di un account sviluppatore, puoi eseguirlo anche su Simulator.
- Nome organizzazione: l'ho chiamato Toptal .
- Identificatore dell'organizzazione: l'ho chiamato com.toptal .
- Lingua: seleziona Swift.
- Deseleziona: "Usa dati di base", "Includi test unitari" e "Includi test dell'interfaccia utente".
Premi il pulsante Avanti e sei pronto per iniziare a creare la barra delle schede a scorrimento.
Architettura semplice
Come ormai saprai, quando crei una nuova app, hai già la classe Main ViewController
e Main.Storyboard
.
Prima di iniziare la progettazione, creiamo prima tutte le classi e i file necessari per assicurarci di avere tutto impostato e funzionante prima di procedere alla parte dell'interfaccia utente del lavoro.
Da qualche parte all'interno del tuo progetto, crea semplicemente alcuni nuovi file -> TabbarController.swift , NavigationController.swift , PageViewController.swift .
Nel mio caso, sembra così.
Nel file AppDelegate , lascia solo didFinishLaunchingWithOptions
, poiché puoi rimuovere tutti gli altri metodi.
All'interno didFinishLaunchingWithOptions
, copia e incolla semplicemente le righe seguenti:
window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true
Rimuovere tutto dal file chiamato ViewController.swift . Torneremo su questo file più tardi.
Innanzitutto, scriviamo il codice per NavigationController.swift .
import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }
Con questo, abbiamo appena creato un semplice UINavigationController
, dove abbiamo una barra traslucida con un TintColor grigio. Questo è tutto qui.
Ora possiamo procedere ad assumere il PageViewController
.
In esso, abbiamo bisogno di codificare un po' di più rispetto ai file precedenti di cui abbiamo discusso.
Questo file contiene una classe, un protocollo, alcune origini dati UIPageViewController
e metodi delegati.
Il file risultante deve assomigliare a questo:
Come puoi vedere, abbiamo dichiarato il nostro protocollo chiamato PageViewControllerDelegate
, che dovrebbe indicare al controller della barra delle schede che l'indice della pagina è stato modificato dopo la gestione dello swipe.
import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }
Quindi dobbiamo creare una nuova classe, chiamata PageViewController
, che conterrà i nostri controller di visualizzazione, selezionerà le pagine in un indice specifico e gestirà anche gli swipe.
Immaginiamo che il primo controller selezionato nella nostra prima esecuzione dovrebbe essere il controller della vista centrale. In questo caso, assegniamo il nostro valore di indice predefinito, pari a 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 } }
Come puoi vedere qui, abbiamo pages
variabili, che conterranno i riferimenti di tutti i nostri controller di visualizzazione.
La variabile prevIndex
viene utilizzata per memorizzare l'ultimo indice selezionato.
Puoi semplicemente chiamare il metodo selectPage
per impostare l'indice selezionato.
Se vuoi ascoltare le modifiche all'indice della pagina, devi iscriverti a swipeDelegate
e ad ogni swipe della pagina ti verrà notificato che l'indice della pagina è cambiato, inoltre riceverai anche l'indice corrente.
La direzione del metodo restituirà la direzione di scorrimento di UIPageViewController
. L'ultimo pezzo del puzzle in questa classe sono le implementazioni del delegato/origine dati.
Fortunatamente, queste implementazioni sono molto semplici.
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) } } }
Come puoi vedere sopra, ci sono tre metodi in gioco:

- Il primo trova l'indice e restituisce il controller di visualizzazione precedente.
- Il secondo trova l'indice e restituisce il controller di visualizzazione successivo.
- L'ultimo controlla se lo scorrimento è terminato, imposta l'indice corrente sulla proprietà locale
prevIndex
e quindi chiama il metodo delegato per notificare al controller di visualizzazione padre che lo scorrimento è stato terminato correttamente.
Ora possiamo finalmente scrivere la nostra implementazione 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) } }
Come puoi vedere, creiamo il TabbarController
, con proprietà e stile predefiniti. Dobbiamo definire due colori, per gli elementi della barra selezionati e deselezionati. Inoltre, ho introdotto tre immagini per gli elementi della scheda.
In viewDidLoad
, sto semplicemente impostando la configurazione predefinita della nostra barra delle schede e selezionando la pagina n. Ciò significa che la pagina di avvio sarà la pagina numero uno.
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) }
All'interno del metodo setUp, puoi vedere che abbiamo creato due controller di visualizzazione segnaposto. Questi controller segnaposto sono necessari per UITabBar
perché il numero di elementi della barra delle schede deve essere uguale al numero di controller di visualizzazione di cui disponi.
Se puoi ricordare, utilizziamo UIPageViewController
per visualizzare i controller, ma per UITabBar
, se vogliamo renderlo completamente funzionante, dobbiamo avere tutti i controller di visualizzazione istanziati, in modo che gli elementi della barra funzionino quando li tocchi. Quindi, in questo esempio, placeholderviewcontroller n. 0 e n. 2 sono controller di visualizzazione vuoti.
Come controller di visualizzazione centrato, creiamo un PageViewController
con tre controller di visualizzazione.
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) }
Il primo e il secondo metodo illustrati sopra sono metodi init del nostro controller di visualizzazione di pagina.
L'elemento della barra delle schede del metodo tabbar
semplicemente l'elemento della tabbar
delle schede all'indice.
Come puoi vedere, all'interno di createCenterPageViewController()
sto usando tag per ogni controller di visualizzazione. Questo mi sta aiutando a capire quale controller è apparso sullo schermo.
Successivamente, arriviamo a quello che è forse il nostro metodo più importante, 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) } }
In questo metodo, sto usando il controller di visualizzazione come parametro. Da questo controller di visualizzazione, ottengo un tag come indice selezionato. Per la barra delle schede, dobbiamo impostare i colori selezionati e non selezionati.
Ora dobbiamo scorrere tutti i nostri controller e verificare se i == selectedIndex
Quindi dobbiamo renderizzare l'immagine come modalità di rendering originale , altrimenti dobbiamo renderizzare l'immagine come modalità modello .
Quando esegui il rendering di un'immagine utilizzando la modalità modello , erediterà il colore dal colore della tinta dell'elemento.
Abbiamo quasi finito. Abbiamo solo bisogno di introdurre due metodi importanti da UITabBarControllerDelegate
e 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) }
Il primo viene chiamato quando si preme su qualsiasi elemento della scheda, mentre il secondo viene chiamato quando si scorre tra le schede.
Avvolgendo
Quando metti insieme tutto il codice, noterai che non è necessario scrivere la propria implementazione dei gestori di gesti e non è necessario scrivere molto codice per offrire uno scorrimento/scorrimento fluido tra gli elementi della barra delle schede.
L'implementazione discussa qui non è qualcosa che sarà l'ideale per tutti gli scenari, ma è una soluzione bizzarra, rapida e relativamente semplice che consente di creare queste funzionalità con un po' di codice.
Infine, se vuoi provare il mio approccio, puoi usare il mio repository GitHub. Buona codifica!