Swift 程式語言

還在猶豫應該用哪種導航形式?教你實作連 Apple 都喜歡用的 UITabBar!

還在猶豫應該用哪種導航形式?教你實作連 Apple 都喜歡用的 UITabBar!
還在猶豫應該用哪種導航形式?教你實作連 Apple 都喜歡用的 UITabBar!
In: Swift 程式語言
本篇原文(標題:Creating a UITabBarController Framework)刊登於作者 Medium,由 Malcolm Kumwenda 所著並授權翻譯及轉載。

導航 (navigation) 是所有應用程式中最重要的一個部分,選擇如何將用戶導向不同頁面,正正就決定了你的應用程式能否成功。

從 AppStore 觀察使用者資訊,我們很清楚哪種導航方式最多人使用,你可以叫它們做漢堡選單 (Hamburger menus)、或是滑動選單 (Sliding menus)、又或是側邊欄 (Sidebars),這種導航形式幾乎成為各種應用程式的首選。就我個人而言,就發現 Sidebar 有很多缺陷,但我不是 UX 設計師,因此你在本文中會讀到更多設計面向 (design-oriented) 的評論。

你可能已經從文章的標題中看出,我最喜歡的應用程式導航方式是 UITabBar。看看我們親愛的蘋果公司的應用程式,大多數都會使用 UITabBar,例如:App Store、Watch、Clock 和 Music ── 全部都是使用 UITabBar 進行導航的。要詳細了解使用它的 UX 優勢,請參閱這篇教程

為何要創建一個框架 (framework)?

在 Swift 中使用 UIKit 實現 UITabBar 非常容易,那麼為什麼我們要創建整個框架呢?因為標準的 UITabBar 對我來說有點無聊,我想替這個組件添加更多的功能。加上,創建一些可重用的物件是好事,將來不用改變太多程式碼就可以進行樣式化和動畫化。

UITabBarController

在這篇文章中,我不會介紹創建 CocoaTouch 框架的步驟。要從創建可重用框架開始,請閱讀由 Alec O’Connor 所著的這篇精彩文章

程式碼

現在,我們已經知道創建框架的原因,亦闡述了想要實現的內容,接下來看看程式碼吧!我們希望框架遵守一些原則 (Principles) 和概念 (Concepts)。

原則與概念

  • SOLID
  • Protocol Oriented(協定導向)

先決條件

在開始之前,建議你先了解以下幾個原則:

  • Access modifiers
  • DataSource & Delegation
  • Programmatic constraints

這些核心原則,我們在設計和開發框架架構時必須記住。在下文,我說到相關程式碼範例時,將會進一步解釋每個概念。

結構

在此時,你已經創建了自己的框架專案,並為它取名如 MDVTabBarController。第一步,讓我們說說基本的組織結構。

Project Group Structure

MDVTabBarController

TabBarController 基本上由三種元件組合而成,包括:用來呈現每個 tab 的 TabBarItems、置放各個 item 的 TabBar、以及一個 TabBarController,TabBarController 顧名思義就是用來控制前面提到的元件。我們會從最小的元件開始構建框架,而在最後,我們希望框架可以使用插件動畫 (plugin animations)。

MDVTabBarItem

創建一個名為 MDVTabBarItemable 的檔案,將其放置在 Protocol 檔案夾中。然後,創建一個名為 MDVTabBarItem 的檔案並將其放入 MDVTabBar 檔案夾中。

MDVTabBarItemable.swift

MDVTabBarItemable 是我們希望所有 TabBarItems 遵循的協定,我們將用一個 UIView 來代替標準的 UITabBar 和 UITabBarItem 來提高靈活性。這個協定定義了兩種配置方法,用於設置 TabBarItemsetState。最後一行程式碼則設置 TabBarItem 為選取 (selected) 或未選取 (unselected) 狀態。類型別名 (typealias) MDVTabBarContainer 定義了 Animator 將使用的介面。

MDVTabBarItem.swift

MDVTabBarItem 需要一個有圖象的 UITabBar 物件,來創建一個中心放置圖片的 UIView。到目前為止,MDVTabBarItem 仍不會處理 title label,但這要添加起來非常容易,只要創建 UILabel 並將其放入 UIView 中即可。你會看到 required init 尚未實作,這是 Storyboard 用於初始化元件的 initialiser,我們不會使用 Storyboard,所以不需要實作它。

MDVTabBar

MDVTabBar 是一個相當複雜的類別,所以我們將分段研究它,首先讓我們看看屬性 (properties) 和初始化的部分。創建一個名為 MDVTabBar 的檔案,並將其放入 MDVTabBar 檔案夾中。

MDVTabBar properties

MDVTaBarDelegateMDVTabBarDataSource 是我們稍後將定義的協定。在創建可重用框架時,請記住可訪問性修飾符 (accessibility modifiers) 的使用。首先將變量盡可能設置為私有 (private),如果需要在其定義類別以外的地方修改變量,請在類別內創建一個 public setter 方法來處理修改需求。在 Swift中,private(set) 使我們能夠將屬性保持私有,同時亦擁有一個 internal scope getter。

Animator

我已經介紹過 Animator,所以讓我們直接深入實作。創建一個名為 MDVTabBarAnimatable 的檔案,將其放置在 Protocol 檔案夾中。

MDVTabBarAnimatable.swift

我們有兩個方法:

  1. prepareForAnimation  ── 這個方法在 TabBar 初始化時調用,將為 TabBar 提供任何你想要的樣式或動畫所需的所有元件。
  2. performAnimation  ── 這個方法在用戶點擊 TabBar 上的按鈕時調用,以在 index 切換間執行動畫。

這個協定用來定義所有 Animator 應遵循的 interface,任何人都可以透過實作這個協定來簡單創建動畫,這意味著我們的框架無需改動程式碼,就可以輕鬆擴展支持更多的 Animator。

In object-oriented programming, the open/closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”; that is, such an entity can allow its behaviour to be extended without modifying its source code. (reference)

MDVTabBarDataSource+Delegate

創建一個名為 MDVTabBarDataSource+Delegate 的檔案,並將其放置在 Protocol 檔案夾中。

MDVTabBarDataSource+Delegate.swift

現在回到 MDVTabBar,並完成該類別的實作。從 delegate 和 datasource didSet 方法開始。

Implementation of MDVTabBar delegate and datasource

每次調整 DataSource 之後,我們要再次進行配置,這可能是由於 Animator 改變、又或是由於某種原因讓 tabBarItems 改變了。同時,每當 delegate 被調整時,我們也希望將 icon 設置為初始狀態,這將使我們的 TabBar 處於正確的狀態,並選擇初始索引。現在讓我們替 MDVTabBar 實作缺少了的方法。

MDVTabBar methods

  1. configure  ── 此方法確保我們的 DataSource 不是 nil,雖然這情況在此範例不可能發生。然後,從 DataSource 設置 tabBarItems 和 Animator 。
  2. createContainerRects ── 此方法負責為每個 tabBarItem 創建框架。
  3. createTabBarItemContainerRect ── 從 createContainerRects 創建的框架會被傳遞給此方法,以創建一個 UIView,它將為 tabBarItem 提供 container/holder view 的用途。
  4. createTabBarItems ── 接下來,我們使用上述 containerRects 創建 MDVTabBarItems。
  5. touchUpInsideForTabBarButton ── 現在我們需要為每個 tabBarButton 提供一個觸控事件 (touch event),所有 tabBarButton 調用相同的方法,但我們使用 tag 來標識正在點擊哪個按鈕。
  6. changeIconState ── 最後,我們需要一種方法在來切換 icon 的選取和預設狀態。

上述這些方法結合在一起就可以創建一個 UITabBarItem,設定所在位置並給它一個 selector,每種方法至多只會執行一項工作。

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.(reference)

MDVTabBarController

這個類別也是相當重要的,所以我們來看看如何創建它。創建一個名為 MDVTabBarController 的檔案,並將其放在 MDVTabBar 檔案夾中。

MDVTabBarController properties

除了尚未定義的 DataSource 和 Delegate 之外,應該沒有什麼不尋常的地方。請注意這個類別被宣告為 open

MDVTabBarController method implementation

我們只需要用兩個方法就可以創建 tabBarController。

  1. configureViews  ── 我們將 DataSource 解包,隱藏標準規格 tabBar,因為要將客製的置放在這裡。從 DataSource 中設置視圖控制器 (viewControllers),初始化 MDVTabBar 並使用 constraint 設置它。最後,我們將 tabBarController 設為 MDVTabbar 的 Delegate 和 DataSource。
  2. setupTabBarConstraints ── 我們使用 constraint 來設定 tabBar 的 Layout。

現在返回 mdvTabBarControllerDataSource 並如下實作 didSet 方法:

didSet {
    self.configureViews()
}

實作 MDVTabBarDelegate 和 MDVTabBarDataSource

創建一個名為 MDVTabBarControllerDataSource+Delegate 的檔案,並將其放置在 Protocol 檔案夾中。

MDVTabBarControllerDataSource+Delegate.swift

這些是我們想要公開的 API 方法,可供任何人實作我們的框架。由於 MDVTabBarController 屬性為 open,因此可以被繼承。這個類別將會是我們的 Delegate 和 Datasource 來實作這些方法。我們來看一下如何使用這些方法。

  1. tabBarControllerInitialIndex ── 用於設置 tabBar 的初始索引。
  2. tabBarControllerViewControllers ── 為 tabBar 提供視圖控制器。
  3. tabBarHeight  ── 為 tabBar 提供所需的高度。對!我們可以使 tabBar 比標準 UIKit 更小或更高。請時常關注 Apple 的 HIG
  4. tabBarBackgroundColor ── 用於設置 tabBar 的背景顏色。
  5. tabBarAnimator ── 提供 Animator 來控制動畫。
  6. didSelectIndex  ── 這是選取 tabBarItem 時調用的委任方法。當 Tab 被選取時,就可以使用此方法來執行自定義操作。例如,打開一個 share-sheet、或是使用相機觸發權限的視圖。

MDVTabbableViewController

在上述第二點方法中有用到 MDVTabbableViewController,它是我們尚未定義的類別。創建一個名為 MDVTabbableViewController 的檔案,並將其放置在 Protocol 檔案夾中,這是我們最後一個協定和類別。

MDVTabbableViewController.swift

這個協定和類型別名的組合是用來做我們 TabBarControllerDataSource 的保護機制,確保任何視圖控制器顯示在 tabBarController 時,都應該提供一個 tabBarItem,否則它無法編譯。

完成了

我們的框架終於完成了!剩下來要做的就是創建一個專案來試用框架,並實作一個很酷的動畫。我不想再延長這篇文章,所以在此將提供一個 Github 連結,舉例說明如何使用框架。

Animator 範例

import UIKit

public class MDVUnderlineAnimator: MDVTabBarAnimatable {
    
    private var underlineColor: UIColor = .clear
    private var underlineHeight: CGFloat = 0
    private var xPosition: CGFloat = 0
    private var yPositionOffset: CGFloat = 0
    private var containerWidth: CGFloat = 0
    private var underlineView: UIView = UIView(frame: .zero)
    
    private let duration: Double
    private let delay: Double
    private let damping: CGFloat
    private let velocity: CGFloat
    
    
    public init(withUnderlineColor underlineColor: UIColor = .red,
         underlineHeight: CGFloat = 8,
         yPositionOffset: CGFloat = 0,
         duration: Double = 0.3,
         delay: Double = 0,
         damping: CGFloat = 0.5,
         velocity: CGFloat = 0.6){
        self.underlineColor = underlineColor
        self.underlineHeight = underlineHeight
        self.yPositionOffset = yPositionOffset
        
        self.damping = damping
        self.duration = duration
        self.delay = delay
        self.velocity = velocity
    }
    
    public func prepareForAnimation(onMDVTabBar tabBar: UIView, withContainers containers: [MDVTabBarContainer], andInitialIndex initialIndex: Int) {
        xPosition = CGFloat(containers[initialIndex].frame.origin.x)
        let yPosition = CGFloat(tabBar.frame.height - underlineHeight)
        containerWidth = containers[initialIndex].frame.width
        self.underlineView = UIView(frame: CGRect(x: xPosition,
                                                  y: yPosition,
                                                  width: containerWidth,
                                                  height: self.underlineHeight))
        self.underlineView.backgroundColor = self.underlineColor
        tabBar.insertSubview(self.underlineView, at: 0)
    }
    
    public func performAnimation(fromIndex: Int,
                          toIndex: Int,
                          onMDVTabBar tabBar: UIView,
                          withContainers containers: [MDVTabBarContainer],
                          completion: @escaping () -> Void) {
        
        UIView.animate(withDuration: duration,
                       delay: delay,
                       usingSpringWithDamping: damping,
                       initialSpringVelocity: velocity,
                       options: .curveLinear,
                       animations: {
                        self.xPosition = CGFloat(containers[toIndex].frame.origin.x)
                        self.underlineView.frame.origin.x = self.xPosition
                        completion()
        })
    }
}

總結

我們建立了一個 tabBarController,它具有即插即用的動畫和樣式。我不會說這個框架是完整的,相較於 ESTabBarControllerRAMAnimatedTabBarController 這種 tabBarController libraries 的完整解決方案,這個專案還差很還。

我建議讀者去看看這個 repository,可以參考它來創建你自己的動畫,看到你們提出不同想法定必更加有趣。在接下來的幾周,我會發佈更多 Animator 的範例,例如本文開頭的動畫,以及如何實現它們。

感謝您的閱讀,歡迎留下你們對本框架的意見。

資源

ESTabBarController
RAMAnimatedTabBarController
How To Create A Custom Tab Bar

本篇原文(標題:Creating a UITabBarController Framework)刊登於作者 Medium,由 Malcolm Kumwenda 所著並授權翻譯及轉載。
作者簡介:Malcolm,來自南非的自學 iOS 開發者,喜歡開發能夠幫助大家的 App,熱衷於教學及分享知識,閒暇時喜歡踢踢足球、看看球賽。
譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。聯絡方式:電郵 [email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。