美文网首页
IGListKit 的演化

IGListKit 的演化

作者: 苹果上的小豌豆 | 来源:发表于2019-08-23 10:16 被阅读0次

    【转载APPCODAIGListKit 的演化:一起來看 Instagram 如何逐步解決 App 問題!】

    IGListKit + MVVM 是 Instagram 對於 iOS UICollectionView UI 與數據解耦的解決方案,IGListKit 的設計理念是以數據驅動,來解決不同 Team 之間的需求,包含不同的數據與不一樣的 Layout。

    原始 UICollectionView 再用戶日益增長的 Instagram 有甚麼問題

    Instagram 是一款照片與影片的社交平台,目前月活躍使用者已超過 10 億。隨著 Instagram 的成長,因為越來越多的業務性質,Instagram 也需要更多更複雜的 Cell Layout。

    好的架構與解決方案通常都不是一開始就做好的,而是隨著產品的成長與用戶的大規模提升,架構逐漸演進而成的。

    原生 UICollectionView 的理念

    1. 可高度客製化
    2. 商業邏輯與 UI Code 解耦合
    3. Cell Reuse 資源可重複利用(我們知道創建 View 的開銷非常大)
      下列是模仿 ig 首頁貼文形式的 Layout 畫面:
    instagram-layout

    我們可以看到畫面中紅框代表 Cell 元件,目前為止我們有兩種 Cell ,上面負責顯示使用者訊息,下面負責顯示貼文。

    讓我們來撰寫程式碼吧!首先,要決定有多少個 Section:

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return postData.count
    }
    

    回應對應的 UICollectionViewCell:

    • UserInfoViewCell:負責顯示用戶大頭貼、名稱、與更多(對應 View Model – PostViewModel)
    • UserImageViewCell:負責顯示用戶貼文圖片(對應 View Model – PostImageViewModel)

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let section = indexPath.section
    
        // 判斷資料模型種類
        // 貼文類
        if let postViewModel = postData[section] as? PostViewModel {
    
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UserInfoViewCell
            cell.updateWith(object: postViewModel)
    
            return cell
    
        } else if let postImageViewModel = postData[section] as? PostImageViewModel {
    
            // 主要照片類別
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellId, for: indexPath) as! UserImageViewCell
            cell.updateWith(object: postImageViewModel)
    
            return cell
        }
    
        let cell = UICollectionViewCell()
        return cell
    }
    
    然後,我們要依照不同的 Cell 設定不同的高度:
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
        if postData[indexPath.section] is PostViewModel {
            return CGSize(width: view.frame.width, height: 50)
        } else if postData[indexPath.section] is PostImageViewModel {
            return CGSize(width: view.frame.width, height: 400)
        }
    
        return CGSize.zero
    }
    

    當資料是用戶資訊時,我們設定高度為 50;而當資料是貼文照片時,我們設定高度為 400。

    當然,完整原始碼位於最上發方說明的位置可以參考 CollectionNormalController 這是一般 CollectionView 的實現過程。

    接下來讓我們情境模擬一下,因業務需求我們需要增加下列推薦關注的元件

    layout-2

    那我們需要增加哪些 Code 呢?

    UserFocusViewCell:負責顯示推薦關注(對應 View Model – FocusViewModel)
    讓我們增加判斷回應的 Cell:

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        //...以上省略
    
    else if let focusViewModel = postData[section] as? FocusViewModel {
    
            // 關注類
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: focusCellId, for: indexPath) as! UserFocusViewCell
            cell.updateWith(object: focusViewModel)
    
            return cell
        }
    
       //...以下省略
    }
    
    看起來修改的幅度並不多,但是這裡出了幾個問題!
    1. 多人合作時,大家需要在同樣的 Function 反覆添加與修改邏輯
    2. Controller 越來越臃腫
    3. 因業務需求新增 UI 需要修改多個團隊的代碼,職責分權不明確
    4. 可讀性及可維護性下降
    5. 業務邏輯與設計需求耦合
    6. 難以增加 A/B Test 代碼
    因這些業務與成長需求,ig 不得不另外尋找解決方案,而 IGListKit 就是透過這些需求演化而成的。
    IGListKit 的理念
    • 1.增加一層 SectionController,拆分商業邏輯與 UI
    • 2.提升 Code 的可重用度
    • 3.高性能更新畫面機制 (O(n))
    IGListKit 主要提供了這些功能:
    • 不需要一次次調用 performBatchUpdates(_:, completion:) 或 reloadData()
    • 具有可重複使用的 Cell 和 Components
    • 創建具有多種數據類型的集合
    • 自定義模型的差異行為
    • 只依賴 UICollectionView
    • 可擴充的 API
    • 使用 Objective-C 編寫,並完整支援 Swift
    IGListKit 簡單範例使用

    為了表示 IGListKit 擴充性與可重用性,我們沿用上面製作好的 UI 與 ViewModel:

    • CollectionIGListKitController 為 IGListKit 使用方式
    • UserInfoViewCell:負責顯示用戶大頭貼、名稱、與更多(對應 View Model – PostViewModel)
    • UserImageViewCell:負責顯示用戶貼文圖片(對應 View Model – PostImageViewModel)
    • UserFocusViewCell:負責顯示推薦關注(對應 View Model – FocusViewModel)
    • PostData:模擬資料來源


    ViewModel

    要使用 IGListKit,我們的 ViewModel 必須遵守 ListDiffable 協定。我們先了解一下這個協定吧!ListDiffable 必須實現兩個 function:
      1. func diffIdentifier() -> NSObjectProtocol:用於定義辨識項目
      1. func isEqual(toDiffableObject object: ListDiffable?) -> Bool:用於辨識兩者是否為同一個 Model
        現在,我們可以開始實作了!首先,建立一個專屬的辨識協定 PostPageProtocol:
    protocol PostPageProtocol: ListDiffable {
       var identifier: UUID { get }
     }
    

    這個協定很簡單,遵守 ListDiffable,並且規定必須實作 identifier 用於資源比較。然後,在我們的 PostViewModel 中加入 headerImage、headerTitle、和 headerRightButtonTitle,用於顯示用戶資訊。

    class PostViewModel: PostPageProtocol {
    
    let identifier = UUID.init()
    let headerImage: String
    let headerTitle: String
    let headerRightButtonTitle: String
    
    init(headerImage: String, headerTitle: String , headerRightButtonTitle: String) {
        self.headerImage = headerImage
        self.headerTitle = headerTitle
        self.headerRightButtonTitle = headerRightButtonTitle
    }
    
    func diffIdentifier() -> NSObjectProtocol {
        return identifier as NSObjectProtocol
    }
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let object = object as? PostViewModel else {
            return false
        }
        return self.identifier == object.identifier
    }
    }
    

    再製作兩個 ViewModel PostImageViewModel 和 PostImageViewModel,用於貼文照片與推薦關注。其中 PostImageViewModel 只加入屬性 mainImage,用於顯示照片;而 FocusViewModel 就加入屬性 headerImage、headerTitle、和 headerRightButtonTitle,用於顯示推薦關注的資訊。

    class PostImageViewModel: PostPageProtocol {

    let identifier = UUID.init()
    let mainImage: String
    
    init(mainImage: String) {
        self.mainImage = mainImage
    }
    
    func diffIdentifier() -> NSObjectProtocol {
        return identifier as NSObjectProtocol
    }
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let object = object as? PostImageViewModel else {
            return false
        }
        return self.identifier == object.identifier
    }
    }
    
    class FocusViewModel: PostPageProtocol {
    
    let identifier = UUID.init()
    let headerImage: String
    let headerTitle: String
    let headerRightButtonTitle: String
    
    init(headerImage: String, headerTitle: String , headerRightButtonTitle: String) {
        self.headerImage = headerImage
        self.headerTitle = headerTitle
        self.headerRightButtonTitle = headerRightButtonTitle
    }
    
    func diffIdentifier() -> NSObjectProtocol {
        return identifier as NSObjectProtocol
    }
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let object = object as? FocusViewModel else {
            return false
        }
        return self.identifier == object.identifier
    }
    }
    

    Controller 製作

    接下來,我們來製作 Controller 吧!我們先以下列程式碼建立一個 UIViewController,注意不是 UICollectionViewController。

    //MARK:- MainViewController
    class CollectionIGListKitController: UIViewController {

    // 生成 CollectionView
    let layout = UICollectionViewFlowLayout()
    lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
    
    // 生成 updater 與 adapter
    let updater = ListAdapterUpdater()
    
    // 綁定 adapter
    lazy var adapter = ListAdapter(updater: updater, viewController: self)
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        // 定義 adapter 的 dataSource 與 collectionView
        adapter.dataSource = self
        adapter.collectionView = collectionView
    
        view.addSubview(collectionView)
        collectionView.fillToSuperview()
    
        collectionView.backgroundColor = .white
    }
    }
    

    我們首先生成一個 UICollectionView,再生成 ListAdapterUpdater 與 ListAdapter。ListAdapterUpdater 負責 row 與 section 的更新,而 ListAdapter 負責控制 CollectionView。

    ListAdapterDataSource

    因為我們的 adapter.dataSource 是指定 CollectionIGListKitController,所以必須實作 ListAdapterDataSource。

    //MARK:- ListAdapterDataSource
    extension CollectionIGListKitController: ListAdapterDataSource {
    // 資料來源
    func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
        return PostData.postData
    }
    
    // 返回合適的 ListSectionController
    func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    
        if object is PostViewModel {
            return UserInfoViewController()
        } else if object is PostImageViewModel {
            return UserImageViewController()
        } else if object is FocusViewModel {
            return FocusViewModelViewController()
        }
    
        return ListSectionController()
    }
    
    func emptyView(for listAdapter: ListAdapter) -> UIView? {
        return nil
    }
    }
    

    返回合適的 ListSectionController,就如同一開始我們返回適合的 Cell 一樣。

    所以,我們必須實作這三個 SectionController。這個步驟並不複雜,首先先定義了他們本身需要的 Model。sizeForItem 定義了這個 Cell 需要的大小,而 cellForItem 則定義了要返回哪一個 UICollectionViewCell。

    //MARK:- UserInfoView
    class UserInfoViewController: ListSectionController {
    
    var currentUserInfo: PostViewModel?
    
    override func didUpdate(to object: Any) {
        guard let userInfo = object as? PostViewModel else {
            return
        }
        currentUserInfo = userInfo
    }
    
    override func numberOfItems() -> Int {
        return 1
    }
    
    override func sizeForItem(at index: Int) -> CGSize {
        return CGSize(width: collectionContext!.containerSize.width, height: 50)
    }
    
    override func cellForItem(at index: Int) -> UICollectionViewCell {
        let cell = collectionContext!.dequeueReusableCell(of: UserInfoViewCell.self, for: self, at: index) as! UserInfoViewCell
    
        if let currentUserInfo = currentUserInfo {
            cell.updateWith(object: currentUserInfo)
        }
    
        return cell
    }
    }
    

    其他几个也类似这么写 。

    我們使用 IGListKit 後解決了甚麼問題

    iglistkit

    其實就是一開始 ig 團隊遇到的問題:

    1. 不同業務團隊只需負責自己的業務邏輯;
    2. 商業邏輯分離至 ListSectionController,解決越來越臃腫的 Controller,細分成許多 Child Controller。

    總結

    隨著業務與用戶規模的成長,App 一定會遇到許多複雜問題,而我們一定要透過架構的演進來解決新的問題。能跟著產品成長的團隊,才是好團隊!今天的範例說明了 IG 團隊在業務增長上所誕生的 IGListKit

    IGListKit 使很多團隊可以專注在自己業務邏輯面的開發,並且讓職責更加明確,大大提高可擴展性、易讀性、與易維護性,同時也為 A/B Test 做好準備。

    好的架構並不是一蹴而就,更多的是演化打磨與取捨。架構如此,人的成長也如此。感謝你的閱讀,亦強烈建議你搭配範例閱讀 IGListKitArchetype

    相关文章

      网友评论

          本文标题:IGListKit 的演化

          本文链接:https://www.haomeiwen.com/subject/opczsctx.html