如何從頭開始創建可滑動的 UITabBar

已發表: 2022-03-11

如您所知,Apple 的 iOS SDK 包含無數的內置 UI 組件。 按鈕、容器、導航、選項卡式佈局,應有盡有——幾乎所有你需要的東西都在那裡。 或者是嗎?

所有這些基本組件都允許我們創建基本的結構化 UI,但如果需要跳出框框會發生什麼? 當 iOS 開發人員需要構建 SDK 默認不支持的某種行為時?

其中一種情況是UITabBar ,您無法在選項卡之間滑動,也沒有用於在選項卡之間切換的動畫。

尋找一個簡單的 UITabBar 修復

經過大量搜索,我在 Github 上只找到了一個有用的庫。 不幸的是,該庫在運行應用程序時產生了很多問題,儘管乍一看它似乎是一個優雅的解決方案。

換句話說,我發現這個庫很容易使用,但是有問題,這顯然超過了它的易用性,而且往往會導致問題。 如果您仍然感興趣,可以在此鏈接下找到該庫。

所以,經過一番思考和大量搜索,我開始實現自己的解決方案,我對自己說: “嘿,如果我們使用頁面視圖控制器進行滑動和原生 UITabBar 會怎樣。 如果我們將這兩個東西組合在一起,在滑動或點擊標籤欄時處理頁面索引呢?”

最終,我想出了一個解決方案,儘管它被證明有些棘手,我稍後會解釋。

精心設計的 UITabBar 解決方案

假設您要構建三個選項卡項,這自動意味著每個選項卡項要顯示三個頁面/控制器。

在這種情況下,您將需要實例化這三個視圖控制器,並且您還需要兩個佔位符/空視圖控制器用於選項卡欄,以製作選項卡欄項目,在按下選項卡或用戶想要更改時更改它們的狀態以編程方式設置選項卡索引。

為此,讓我們深入研究 Xcode 並編寫幾個類,看看這些東西是如何工作的。

在選項卡之間滑動的示例

iOS 中可滑動標籤的示例

在這些截圖中,你可以看到第一個標籤欄項目是藍色的,然後用戶滑動到右邊的標籤,這是黃色的,最後一個屏幕顯示第三個項目被選中,所以整個頁面顯示為黃色。

可滑動標籤欄的編程使用

因此,讓我們深入研究這個功能,並為 iOS 編寫一個可滑動標籤欄的簡單示例。 首先,我們需要創建一個新項目。

我們項目所需的先決條件非常基本:在 Mac 上安裝 Xcode 和 Xcode 構建工具。

要創建一個新項目,請在 Mac 上打開 Xcode 應用程序並選擇“創建一個新的 Xcode 項目”,然後為您的項目命名,最後選擇要創建的應用程序的類型。 只需選擇“單一視圖應用程序” ,然後按下一步。

Xcode 截圖

如您所見,下一個屏幕將要求您提供一些基本信息:

  • 產品名稱:我將它命名為SwipeableTabbar。
  • 團隊:如果你想在真實設備上運行這個應用程序,你必須有一個開發者帳戶。 就我而言,我將為此使用自己的帳戶。

注意:如果您沒有開發者帳戶,您也可以在模擬器上運行它。

  • 組織名稱:我將其命名為Toptal
  • 組織標識符:我將其命名為com.toptal
  • 語言:選擇 Swift。
  • 取消選中: “使用核心數據”、“包括單元測試”“包括 UI 測試”。

按下一步按鈕,您就可以開始構建可滑動的標籤欄了。

簡單的架構

正如您現在已經知道的那樣,當您創建一個新應用程序時,您已經擁有了 Main ViewController類和Main.Storyboard

在我們開始設計之前,讓我們首先創建所有必要的類和文件,以確保在我們繼續工作的 UI 部分之前我們已經設置並運行了所有內容。

在您的項目中的某個地方,只需創建一些新文件 -> TabbarController.swiftNavigationController.swiftPageViewController.swift

就我而言,它看起來像這樣。

截圖:Xcode 控制器

AppDelegate文件中,只保留didFinishLaunchingWithOptions ,因為您可以刪除所有其他方法。

didFinishLaunchingWithOptions中,只需複制並粘貼以下行:

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

從名為ViewController.swift的文件中刪除所有內容。 我們稍後會回到這個文件。

首先,讓我們為NavigationController.swift編寫代碼。

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

有了這個,我們剛剛創建了一個簡單的UINavigationController ,其中我們有一個帶有灰色 TintColor 的半透明條。 就在這裡。

現在,我們可以繼續使用PageViewController了。

在其中,我們需要比之前討論的文件多編寫一些代碼。

該文件包含一個類、一個協議、一些UIPageViewController數據源和委託方法。

生成的文件需要如下所示:

Xcode 截圖:方法

如您所見,我們已經聲明了我們自己的協議,稱為PageViewControllerDelegate ,它應該告訴標籤欄控制器在處理滑動後頁面索引已更改。

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

然後我們需要創建一個名為PageViewController的新類,它將保存我們的視圖控制器,選擇特定索引處的頁面,並處理滑動。

假設我們第一次運行時選擇的第一個控制器應該是中心視圖控制器。 在這種情況下,我們分配我們的默認索引值,等於 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 } }

正如您在此處看到的,我們有變量pages ,其中將包含我們所有視圖控制器的引用。

變量prevIndex用於存儲最後選擇的索引。

您可以簡單地調用selectPage方法來設置選定的索引。

如果你想監聽頁面索引的變化,你必須訂閱swipeDelegate ,並且在每次頁面滑動時,你會收到頁面索引改變的通知,另外你也會收到當前的索引。

方法 direction 將返回UIPageViewController的滑動方向。 此類中的最後一塊拼圖是委託/數據源實現。

幸運的是,這些實現非常簡單。

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

正如您在上面看到的,有三種方法在起作用:

  • 第一個找到索引並返回前一個視圖控制器。
  • 第二個找到索引並返回下一個視圖控制器。
  • 最後一個檢查滑動是否結束,將當前索引設置為本地屬性prevIndex ,然後調用委託方法通知父視圖控制器滑動成功結束。

現在我們終於可以編寫我們的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) } }

如您所見,我們創建了具有默認屬性和样式的TabbarController 。 我們需要為選中和取消選中的條形項目定義兩種顏色。 此外,我還為標籤欄項目介紹了三個圖像。

viewDidLoad中,我只是設置標籤欄的默認配置並選擇第 1 頁。 這意味著啟動頁面將是第一頁。

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

在 setUp 方法中,您會看到,我們創建了兩個佔位符視圖控制器。 UITabBar需要這些佔位符控制器,因為標籤欄項目的數量必須等於您擁有的視圖控制器的數量。

如果你還記得的話,我們使用UIPageViewController來顯示控制器,但是對於UITabBar ,如果我們想讓它完全可用,我們需要實例化所有視圖控制器,以便當你點擊它們時欄項目將工作。 因此,在本例中, placeholderviewcontroller #0#2是空視圖控制器。

作為居中的視圖控制器,我們創建了一個具有三個視圖控制器的PageViewController

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

上面描述的第一個和第二個方法是我們的頁面瀏覽控制器的 init 方法。

tabbar item 方法只返回索引處的tabbar項目。

如您所見,在createCenterPageViewController()中,我為每個視圖控制器使用標籤。 這有助於我了解屏幕上出現了哪個控制器。

接下來,我們來看看可能是我們最重要的方法, 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) } }

在這種方法中,我使用視圖控制器作為參數。 從這個視圖控制器,我得到一個標籤作為選定的索引。 對於標籤欄,我們需要設置選中和未選中的顏色。

現在我們需要遍歷所有控制器並檢查i == selectedIndex

然後我們需要將圖像渲染為原始渲染模式,否則我們需要將圖像渲染為模板模式

當您使用模板模式渲染圖像時,它將從項目的色調顏色繼承顏色。

我們快完成了。 我們只需要從UITabBarControllerDelegatePageViewControllerDelegate中引入兩個重要的方法。

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

當您按下任何選項卡項目時調用第一個,而當您在選項卡之間滑動時調用第二個。

包起來

當您將所有代碼放在一起時,您會注意到您不必編寫自己的手勢處理程序實現,也不必編寫大量代碼來在標籤欄項目之間提供平滑的滾動/滑動。

這裡討論的實現並不適合所有場景,但它是一個古怪、快速且相對簡單的解決方案,使您能夠使用少量代碼創建這些功能。

最後,如果您想嘗試我的方法,可以使用我的 GitHub 存儲庫。 快樂編碼!