Como criar um UITabBar deslizante do zero

Publicados: 2022-03-11

Como você sabe, o iOS SDK da Apple contém uma infinidade de componentes de interface do usuário integrados. Botões, contêineres, navegações, layouts com guias, você escolhe - quase tudo o que você precisa está lá. Ou é?

Todos esses componentes básicos nos permitem criar UIs estruturadas básicas, mas o que acontece se houver necessidade de sair da caixa; quando um desenvolvedor iOS precisa construir algum tipo de comportamento que não é suportado no SDK por padrão?

Um desses casos é o UITabBar , onde você não tem a capacidade de deslizar entre as abas, e também não tem animações para alternar entre as abas.

Procurando por uma correção fácil de UITabBar

Depois de uma boa pesquisa, consegui encontrar apenas uma biblioteca útil no Github. Infelizmente, a biblioteca criou muitos problemas durante a execução do aplicativo, embora pareça uma solução elegante à primeira vista.

Em outras palavras, achei a biblioteca muito fácil de usar, mas cheia de bugs, o que obviamente superava sua facilidade de uso e tendia a causar problemas. Caso você ainda esteja interessado, a lib pode ser encontrada neste link.

Então, depois de pensar e pesquisar muito, comecei a implementar minha própria solução e disse a mim mesmo: “Ei, e se usarmos o controlador de visualização de página para swipe e o UITabBar nativo. E se agruparmos essas duas coisas, lidar com o índice da página enquanto desliza ou toca na barra de guias?”

Por fim, encontrei uma solução, embora tenha se mostrado um pouco complicada, como explicarei mais tarde.

Uma solução elaborada UITabBar

Imagine que você tenha três itens da barra de guias a serem construídos, o que significa automaticamente que você tem três páginas/controladores a serem exibidos por cada item de guia.

Nesse caso, você precisará instanciar esses três controladores de exibição e também precisará de dois placeholders/controladores de exibição vazios para a barra de guias, para criar itens da barra de guias, alterar seu estado quando a guia for pressionada ou quando o usuário quiser alterar o índice da guia programaticamente.

Para isso, vamos mergulhar no Xcode e escrever algumas classes, apenas para ver como essas coisas funcionam.

Um exemplo de deslizar entre as guias

Exemplo de guias deslizantes no iOS

Nessas capturas de tela, você pode ver que o primeiro item da barra de guias é azul, então o usuário desliza para a guia direita, que é amarela, e a última tela mostra que o terceiro item está selecionado, então toda a página é exibida em amarelo.

Uso programático da barra de guias deslizante

Então, vamos mergulhar nesse recurso e escrever um exemplo fácil de uma barra de guias deslizante para iOS. Primeiro de tudo, precisamos criar um novo projeto.

Os pré-requisitos necessários para o nosso projeto são bastante básicos: Ferramentas de compilação Xcode e Xcode instaladas no seu Mac.

Para criar um novo projeto, abra o aplicativo Xcode no seu Mac e selecione “Criar um novo projeto Xcode”, depois nomeie seu projeto e, finalmente, escolha o tipo de aplicativo a ser criado. Basta selecionar “Single View App” e pressionar Next.

Captura de tela do Xcode

Como você pode ver, a próxima tela exigirá que você forneça algumas informações básicas:

  • Nome do produto: eu o chamei de SwipeableTabbar.
  • Equipe: Se você deseja executar este aplicativo em um dispositivo real, precisará ter uma conta de desenvolvedor. No meu caso, usarei minha própria conta para isso.

Observação: se você não tiver uma conta de desenvolvedor, também poderá executá-la no Simulador.

  • Nome da Organização: Eu a chamei de Toptal .
  • Identificador da organização: chamei-o de com.toptal .
  • Idioma: selecione Swift.
  • Desmarque: "Usar dados principais", "Incluir testes de unidade" e "Incluir testes de interface do usuário".

Pressione o botão Avançar e você estará pronto para começar a criar sua barra de guias deslizante.

Arquitetura Simples

Como você já sabe, ao criar um novo aplicativo, você já tem a classe Main ViewController e Main.Storyboard .

Antes de começarmos a projetar, vamos primeiro criar todas as classes e arquivos necessários para garantir que tudo esteja configurado e funcionando antes de prosseguirmos para a parte da interface do usuário do trabalho.

Em algum lugar dentro do seu projeto, simplesmente crie alguns novos arquivos -> TabbarController.swift , NavigationController.swift , PageViewController.swift .

No meu caso, fica assim.

Captura de tela: Controladores Xcode

No arquivo AppDelegate , deixe apenas didFinishLaunchingWithOptions , pois você pode remover todos os outros métodos.

Dentro didFinishLaunchingWithOptions , basta copiar e colar as linhas abaixo:

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

Remova tudo do arquivo chamado ViewController.swift . Voltaremos a este arquivo mais tarde.

Primeiro, vamos escrever o código para NavigationController.swift .

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

Com isso, acabamos de criar um UINavigationController simples, onde temos uma barra translúcida com um TintColor cinza. Isso é tudo aqui.

Agora, podemos prosseguir para assumir o PageViewController .

Nele, precisamos codificar um pouco mais do que nos arquivos anteriores que discutimos.

Este arquivo contém uma classe, um protocolo, alguma fonte de dados UIPageViewController e métodos delegados.

O arquivo resultante precisa ficar assim:

Captura de tela do Xcode: métodos

Como você pode ver, declaramos nosso próprio protocolo chamado PageViewControllerDelegate , que deve informar ao controlador da barra de guias que o índice da página foi alterado após a manipulação do dedo.

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

Em seguida, precisamos criar uma nova classe, chamada PageViewController , que conterá nossos controladores de exibição, selecionará páginas em um índice específico e também manipulará swipes.

Vamos imaginar que o primeiro controlador selecionado em nossa primeira execução deve ser o controlador de exibição central. Nesse caso, atribuímos nosso valor de índice padrão, 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 você pode ver aqui, temos a variável pages , que conterá referências de todos os nossos controladores de visão.

A variável prevIndex é usada para armazenar o último índice selecionado.

Você pode simplesmente chamar o método selectPage para definir o índice selecionado.

Se você quiser ouvir as alterações do índice da página, você deve se inscrever em swipeDelegate e, em cada passagem de página, você será notificado de que o índice da página foi alterado, além de receber o índice atual.

A direção do método retornará a direção do furto de UIPageViewController . A última peça do quebra-cabeça nesta classe são as implementações de delegado/fonte de dados.

Felizmente, essas implementações são muito 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 você pode ver acima, existem três métodos em jogo:

  • O primeiro encontra o índice e retorna o controlador de exibição anterior.
  • O segundo encontra o índice e retorna o próximo controlador de visualização.
  • O último verifica se o swipe terminou, define o índice atual para a propriedade local prevIndex e, em seguida, chama o método delegate para notificar o controlador de exibição pai que o swipe foi finalizado com sucesso.

Agora podemos finalmente escrever nossa implementação 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 você pode ver, criamos o TabbarController , com propriedades e estilo padrão. Precisamos definir duas cores, para itens de barra selecionados e desmarcados. Além disso, introduzi três imagens para itens da barra de guias.

Em viewDidLoad , estou apenas configurando a configuração padrão de nossa barra de guias e selecionando a página #1. O que isso significa é que a página de inicialização será a página número um.

 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 do método setUp, você vê, que criamos dois controladores de exibição de espaço reservado. Esses controladores de espaço reservado são necessários para UITabBar porque o número de itens da barra de guias deve ser igual ao número de controladores de exibição que você possui.

Se você puder se lembrar, usamos UIPageViewController para exibir controladores, mas para UITabBar , se quisermos torná-lo totalmente viável, precisamos ter todos os controladores de exibição instanciados, para que os itens da barra funcionem quando você tocar neles. Portanto, neste exemplo, placeholderviewcontroller #0 e #2 são controladores de exibição vazios.

Como um controlador de exibição centralizado, criamos um PageViewController com três controladores de exibição.

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

O primeiro e o segundo método descritos acima são métodos init do nosso controlador de exibição de página .

O método tabbar item apenas retorna o item tabbar no índice.

Como você pode ver, dentro de createCenterPageViewController() estou usando tags para cada controlador de visualização. Isso está me ajudando a entender qual controlador apareceu na tela.

Em seguida, chegamos ao que é possivelmente nosso método mais 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) } }

Nesse método, estou usando o controlador de exibição como parâmetro. A partir deste controlador de exibição, recebo uma tag como um índice selecionado. Para a barra de guias, precisamos definir as cores selecionadas e não selecionadas.

Agora precisamos percorrer todos os nossos controladores e verificar se i == selectedIndex

Em seguida, precisamos renderizar a imagem como um modo de renderização original , caso contrário, precisamos renderizar a imagem como um modo de modelo .

Ao renderizar uma imagem usando o modo de modelo , ela herdará a cor da tonalidade do item.

Estamos quase terminando. Só precisamos introduzir dois métodos importantes de 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) }

O primeiro é chamado quando você pressiona em qualquer item da guia, enquanto o segundo é chamado quando você desliza entre as guias.

Empacotando

Ao juntar todo o código, você notará que não precisa escrever sua própria implementação de manipuladores de gestos e não precisa escrever muito código para fornecer rolagem/deslizamento suave entre os itens da barra de guias.

A implementação discutida aqui não é algo que será ideal para todos os cenários, mas é uma solução peculiar, rápida e relativamente fácil que permite criar esses recursos com um pouco de código.

Finalmente, se você quiser experimentar minha abordagem, pode usar meu repositório GitHub. Boa codificação!