Cómo crear una UITabBar deslizable desde cero
Publicado: 2022-03-11Como sabe, el SDK de iOS de Apple contiene una gran cantidad de componentes de interfaz de usuario integrados. Botones, contenedores, navegaciones, diseños con pestañas, lo que sea, casi todo lo que necesitará está allí. ¿O es eso?
Todos estos componentes básicos nos permiten crear interfaces de usuario estructuradas básicas, pero ¿qué sucede si es necesario salirse de la caja? cuando un desarrollador de iOS necesita crear algún tipo de comportamiento que no se admite en el SDK de forma predeterminada?
Uno de estos casos es UITabBar , donde no tiene la capacidad de deslizar entre pestañas, y tampoco tiene animaciones para cambiar entre pestañas.
Buscando una solución fácil de UITabBar
Después de una buena cantidad de búsqueda, logré encontrar solo una biblioteca útil en Github. Desafortunadamente, la biblioteca creó muchos problemas al ejecutar la aplicación, aunque a primera vista parecía una solución elegante.
En otras palabras, encontré la biblioteca muy fácil de usar, pero con errores, lo que obviamente superaba su facilidad de uso y tendía a causar problemas. En caso de que todavía esté interesado, la biblioteca se puede encontrar en este enlace.
Entonces, después de pensar y buscar mucho, comencé a implementar mi propia solución y me dije a mí mismo: “Oye, ¿qué pasa si usamos el controlador de vista de página para deslizar y UITabBar nativo? ¿Qué pasa si agrupamos estas dos cosas juntas, manejamos el índice de la página mientras deslizamos o tocamos en la barra de pestañas?
En última instancia, se me ocurrió una solución, aunque resultó algo complicada, como explicaré más adelante.
Una solución UITabBar elaborada
Imagine que tiene tres elementos de barra de pestañas para crear, lo que automáticamente significa que tiene tres páginas/controladores para mostrar por cada elemento de pestaña.
En este caso, deberá crear una instancia de esos tres controladores de vista y también necesitará dos marcadores de posición/controladores de vista vacíos para la barra de pestañas, para crear elementos de la barra de pestañas, cambiar su estado cuando se presiona la pestaña o cuando el usuario quiere cambiar el índice de pestañas mediante programación.
Para esto, profundicemos en Xcode y escribamos un par de clases, solo para ver cómo funcionan estas cosas.
Un ejemplo de deslizamiento entre pestañas
En estas capturas de pantalla, puede ver que el primer elemento de la barra de pestañas es azul, luego el usuario se desliza hacia la pestaña derecha, que es amarilla, y la última pantalla muestra que se seleccionó el tercer elemento, por lo que toda la página se muestra en amarillo.
Uso programático de la barra de pestañas deslizable
Entonces, profundicemos en esta función y escribamos un ejemplo sencillo de una barra de pestañas deslizable para iOS. En primer lugar, necesitamos crear un nuevo proyecto.
Los requisitos previos necesarios para nuestro proyecto son bastante básicos: herramientas de compilación Xcode y Xcode instaladas en su Mac.
Para crear un nuevo proyecto, abra la aplicación Xcode en su Mac y seleccione "Crear un nuevo proyecto Xcode", luego asigne un nombre a su proyecto y finalmente elija el tipo de aplicación que se creará. Simplemente seleccione "Aplicación de vista única" y presione Siguiente.
Como puede ver, la siguiente pantalla requerirá que proporcione cierta información básica:
- Nombre del producto: lo llamé SwipeableTabbar.
- Equipo: si desea ejecutar esta aplicación en un dispositivo real, deberá tener una cuenta de desarrollador. En mi caso, usaré mi propia cuenta para esto.
Nota: Si no tiene una cuenta de desarrollador, también puede ejecutar esto en Simulator.
- Nombre de la organización: la llamé Toptal .
- Identificador de la organización: lo nombré com.toptal .
- Idioma: Seleccione Swift.
- Desmarque: "Usar datos básicos", "Incluir pruebas unitarias" e "Incluir pruebas de interfaz de usuario".
Presiona el botón Siguiente y estarás listo para comenzar a crear tu barra de pestañas deslizable.
arquitectura sencilla
Como ya sabe, cuando crea una nueva aplicación, ya tiene la clase Main ViewController
y Main.Storyboard
.
Antes de comenzar a diseñar, primero creemos todas las clases y archivos necesarios para asegurarnos de que tenemos todo configurado y funcionando antes de continuar con la parte de la interfaz de usuario del trabajo.
En algún lugar dentro de su proyecto, simplemente cree algunos archivos nuevos -> TabbarController.swift , NavigationController.swift , PageViewController.swift .
En mi caso, se ve así.
En el archivo AppDelegate , deje solo didFinishLaunchingWithOptions
, ya que puede eliminar todos los demás métodos.
Dentro de didFinishLaunchingWithOptions
, simplemente copie y pegue las siguientes líneas:
window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true
Elimine todo del archivo llamado ViewController.swift . Volveremos a este archivo más tarde.
Primero, escribamos código para NavigationController.swift .
import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }
Con esto, acabamos de crear un UINavigationController
simple, donde tenemos una barra translúcida con un TintColor gris. Eso es todo aquí.
Ahora, podemos proceder a asumir el PageViewController
.
En él, necesitamos codificar un poco más que en los archivos anteriores que discutimos.
Este archivo contiene una clase, un protocolo, un origen de datos UIPageViewController
y métodos delegados.
El archivo resultante debe verse así:
Como puede ver, hemos declarado nuestro propio protocolo llamado PageViewControllerDelegate
, que debería indicarle al controlador de la barra de pestañas que el índice de la página se cambió después de manejar el deslizamiento.
import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }
Luego, debemos crear una nueva clase, llamada PageViewController
, que contendrá nuestros controladores de vista, seleccionará páginas en un índice específico y también manejará los deslizamientos.
Imaginemos que el primer controlador seleccionado en nuestra primera ejecución debería ser el controlador de vista central. En este caso, asignamos nuestro valor de índice predeterminado, igual 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 } }
Como puede ver aquí, tenemos pages
variables, que contendrán referencias de todos nuestros controladores de vista.
La variable prevIndex
se utiliza para almacenar el último índice seleccionado.
Simplemente puede llamar al método selectPage
para establecer el índice seleccionado.
Si desea escuchar los cambios en el índice de la página, debe suscribirse a swipeDelegate
, y en cada deslizamiento de la página, se le notificará que el índice de la página cambió, además de que también recibirá el índice actual.
La dirección del método devolverá la dirección de deslizamiento de UIPageViewController
. La última pieza del rompecabezas en esta clase son las implementaciones de fuentes de datos/delegados.
Afortunadamente, estas implementaciones son muy simples.

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) } } }
Como puede ver arriba, hay tres métodos en juego:
- El primero encuentra el índice y devuelve el controlador de vista anterior.
- El segundo encuentra el índice y devuelve el siguiente controlador de vista.
- El último verifica si el deslizamiento ha finalizado, establece el índice actual en la propiedad local
prevIndex
y luego llama al método de delegado para notificar al controlador de vista principal que el deslizamiento finalizó correctamente.
Ahora finalmente podemos escribir nuestra implementación 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) } }
Como puede ver, creamos el TabbarController
, con propiedades y estilo predeterminados. Necesitamos definir dos colores, para elementos de barra seleccionados y deseleccionados. Además, he introducido tres imágenes para los elementos de la barra de pestañas.
En viewDidLoad
, simplemente estoy configurando la configuración predeterminada de nuestra barra de pestañas y seleccionando la página #1. Lo que esto significa es que la página de inicio será la página número 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) }
Dentro del método setUp, verá que creamos dos controladores de vista de marcador de posición. Estos controladores de marcador de posición son necesarios para UITabBar
porque la cantidad de elementos de la barra de pestañas debe ser igual a la cantidad de controladores de vista que tiene.
Si puede recordar, usamos UIPageViewController
para mostrar los controladores, pero para UITabBar
, si queremos que funcione completamente, necesitamos tener todos los controladores de vista instanciados, para que los elementos de la barra funcionen cuando los toque. Entonces, en este ejemplo, placeholderviewcontroller #0 y #2 son controladores de vista vacíos.
Como controlador de vista centrado, creamos un PageViewController
con tres controladores de vista.
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) }
El primer y segundo método descritos arriba son métodos de inicio de nuestro controlador de página vista .
El elemento de la barra de tabbar
del método simplemente devuelve el elemento de la barra de tabbar
en el índice.
Como puede ver, dentro de createCenterPageViewController()
estoy usando etiquetas para cada controlador de vista. Esto me está ayudando a entender qué controlador ha aparecido en la pantalla.
A continuación, llegamos a lo que posiblemente sea nuestro método más 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) } }
En este método, estoy usando el controlador de vista como parámetro. Desde este controlador de vista, obtengo una etiqueta como índice seleccionado. Para la barra de pestañas, necesitamos establecer colores seleccionados y no seleccionados.
Ahora necesitamos recorrer todos nuestros controladores y verificar si i == selectedIndex
Luego, debemos renderizar la imagen como un modo de renderizado original ; de lo contrario, debemos renderizar la imagen como un modo de plantilla .
Cuando renderiza una imagen usando el modo de plantilla , heredará el color del tinte del elemento.
Casi terminamos. Solo necesitamos introducir dos métodos importantes de UITabBarControllerDelegate
y 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) }
El primero se llama cuando presiona cualquier elemento de pestaña, mientras que el segundo se llama cuando se desliza entre pestañas.
Terminando
Cuando reúna todo el código, notará que no tiene que escribir su propia implementación de controladores de gestos y no tiene que escribir mucho código para ofrecer un desplazamiento/desplazamiento suave entre los elementos de la barra de pestañas.
La implementación que se analiza aquí no es algo que sea ideal para todos los escenarios, pero es una solución peculiar, rápida y relativamente fácil que le permite crear estas funciones con un poco de código.
Finalmente, si desea probar mi enfoque, puede usar mi repositorio de GitHub. ¡Feliz codificación!