iOS 14 的 Diffable Data Source 讓你輕鬆建立和更新大量資料


本篇原文(標題:Handling iOS 14 Diffable Data Sources)刊登於作者 Medium,由 Anupam Chugh 所著,並授權翻譯及轉載。

在 iOS 13 中,Apple 除了引入了 Swift UI 這個宣告式 (declarative) UI 框架外,還為 UIKit 框架添加了不少新功能,當中最重要的就是 UICollectionView 的改善。

準確來說,新的 Compositional Layouts 和 Diffable Data Sources APIs,讓我們更容易構建進階 CollectionView 佈局和集中的資料源。

iOS 14 更進一步,帶來了新的 Cell registration API,並在 UICollectionView 內為 UITableView 提供開箱即用的支援。

但更重要的是,現在 iOS 14 的 Diffable data sources 新增了 section snapshot,讓你可以以一個 Section 為基礎更新 Data。在構建 iOS 14 推出的新層次設計的 Outline Styled 列表,這個功能十分有用。

今年,Diffable data source 的另一個新功能是 First-class reordering。

目標

  • 快速重溫 Diffable data source
  • 了解如何實作 section snapshots
  • 深入探究新的 reordering API

快速重溫 Diffable data source

在這個新的宣告式 API 推出前,開發者需要使用 numberOfItemsInSectioncellForItemAt 方法,來建立資料源;而要更新資料,就要使用 performBatchUpdates()reloadData() 方法。

這個方法雖然可以建立和更新 data,但就會導致資料源分散。更壞的是,reloadData() 方法會讓我們無法展示漂亮的動畫,而 performBatchUpdates 就會無意地導致一些常見錯誤,例如 NSInternalInconsistencyException

有了新的 Diffable data source,我們可以透過 Snapshot 提供資料,以獲得集中的資料源。

Snapshot 代表一種資料的狀態,不是依賴 index path 來更新 item,而是依靠型式安全 (type-safe) 的唯一識別符 (identifier),來識別唯一的 Section 和 Item。

更好的是,你可以使用 apply 方法,在 NSDiffableDataSourceSnapshot 實例中設定為 UITableViewUICollectionView 的資料源,並讓其處理動畫。有趣的是,apply 方法也可以從後台線程執行。

總括來說,Diffable data source 可以計算差異,並讓我們可以在 UICollectionViewUITableView 佈局中更輕鬆地管理資料源。您可以存取 dataSource.snapshot() 來讀取 UI 元件的當前狀態,並相應地添加或刪除 item。

iOS 14 引入的 SectionSnapshots

在 iOS 14 之前,要在 iOS 13 填充 item 和 section,我們需要在NSDiffableDataSourceSnapshot 使用以下的方法:

var snapshot = NSDiffableDataSourceSnapshot<String, String>()
snapshot.appendSections(["1", "2"])

如果要在一個 Section 添加 Item,我們就會使用這個方法:

snapshot.appendItems(["1.1"], toSection: "1")
snapshot.appendItems(["2.1"], toSection: "2")

那麼,既然我們已經可以以一個 Section 為基礎添加 Item,新的 NSDiffableDataSourceSectionSnapshot API 又可以為列表帶來甚麼呢?

簡單來說,就是客製化大綱列表 (outlined list) 或展開式列表視圖 (expandable list)。

我們可以利用 NSDiffableDataSourceSectionSnapshot API,輕鬆地建立及更新展開式集合視圖,讓視圖可以展開及折疊某些 Section。如此一來,我們就可以方便地建立階層式 (hierarchical) 資料。

以下是 iOS 14 新的 Section Snapshot 提供的方法:

diffable-data-source-1

現在,讓我們利用全新的 Section Snapshot 來提供資料源,以建立一個 iOS 14 CollectionView

建立你的資料模型

我們的資料源會保存字串 (string) 的階層式資料。因此,讓我們使用 childItems 陣列創建兩個 item 的結構:

struct Child : Hashable{
 let item: String
}
struct Parent: Hashable {
 let item: String
 let childItems: [Child]
}

因為 parent item (標題 (header))和 childItems 都是字串,所以我們需要一個方法,為它們會進入的 UICollectionViewCell 區分兩者的型別。讓我們為它們創建 case 列舉 (enum):

enum OutlineItem: Hashable {
case parent(Parent)
case child(Child)
}

現在我們已經準備好了資料模型,以下是這次用到的虛擬資料,我們將會用這些資料來填充 UICollectionView

diffable-data-source-2

建立我們的 Diffable data source

有了新的 iOS 14 Cell 註冊技巧,要初始化 UICollectionViewCell,我們不再需要使用傳統的 Cell identifier 方法。

我們可以如此在 iOS 14 UICollectionView.CellRegistration 建立和顯示內容,並傳遞到 UICollectionViewDiffableDataSource

func makeDataSource() -> UICollectionViewDiffableDataSource<String, OutlineItem> {
         
        
        let parentRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Parent> { cell, indexPath, item in
            
            var content = cell.defaultContentConfiguration()
            content.text = item.item
            cell.contentConfiguration = content
            
            let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
            cell.accessories = [.outlineDisclosure(options:headerDisclosureOption)]
            
        }

        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Child> { cell, indexPath, item in
            
            var content = cell.defaultContentConfiguration()
            content.text = item.item
            cell.indentationLevel = 2
            cell.contentConfiguration = content
        }
        
        return UICollectionViewDiffableDataSource<String, OutlineItem>(
                    collectionView: collectionView,
                    cellProvider: { collectionView, indexPath, item in
    
                        
                    switch item{
                        case .parent(let parentItem):
                            
                            let cell = collectionView.dequeueConfiguredReusableCell(
                                using: parentRegistration,
                                for: indexPath,
                                item: parentItem)
    
                            return cell

                        case .child(let childItem):
    
                            let cell = collectionView.dequeueConfiguredReusableCell(
                                using: cellRegistration,
                                for: indexPath,
                                item: childItem)
    
                            return cell
                    }
        })
}

我們已經註冊了兩個 Cell,一個是每個 Section 的 Root,並包含 Disclosure Indicator;另一個則是用來顯示每個 child item 的內容。

現在我們的資料源已經準備好了,讓我們在 CollectionView 上設定它吧!

private lazy var dataSource = makeDataSource()

最後,我們把 Snapshot 套用到以上的資料源。

建立 section snapshots

我們可以如此構建一個 section snapshot:

collectionView.dataSource = dataSource

var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<OutlineItem>()

for data in hirerachicalData{

    let header = OutlineItem.parent(data)
    sectionSnapshot.append([header])
    sectionSnapshot.append(data.childItems.map { OutlineItem.child($0) }, to: header)

    sectionSnapshot.expand([header])
}

dataSource.apply(sectionSnapshot, to: "Root", animatingDifferences: false, completion: nil)

我們迭代了 hierarchical 資料,並將父實例設置為每個 Section 的標題,並在其中設置 childItems。此外,我們還展開了每個標題的 Section,以顯示所有項目(你可以配置為僅隱藏/展開特定 Section)。

最後,把 Section Snapshot 應用於 UICollectionView 的 Root Section。在模擬器上運行時,App 看起來會是這樣:

app-demo

如果想要 UICollectionView + diffable data source 和 Section Snapshot 的完整源程式碼,可以到 GitHub 參考。

你也可以在 dataSource 上設置 sectionSnapshotHandlers,來客製化各個 Section Item 的展開狀態。SectionSnapshotHandler<Item> 提供了不同的閉包,例如 shouldCollapseItemwillCollapseItemwillExpandItem、和 ShouldExpandItem

使用新的 Reordering API

Section Snapshop 不但可以讓我們構建展開式列表,並確定 item 的嵌套級別 (nested level)(列表的 root),它還有一個 Reordering API,可以快速地插入到我們的 diffable data source 中。

具體來說,要啟用重新排序,我們需要定義以下兩個閉包:

reordering-api-clousures

然後,你需要如此設置附件 (accessory) 來註冊 Cell:

cell.accessories = [.reorder(displayed:.always)]

請注意,為簡便起見,我們將重新排序圖標設置為總是顯示 (always be displayed)。但是,我建議你設置一個 Edit 按鈕,以便在 whenEditingwhenNotEditing 狀態之間切換,以啟用/停用重新排序。

reordering-api

didReorderwillReorder 閉包會傳遞一個 NSDiffableDataSourceTranscation 新型別。

Transaction 會包含所有更新 diffable data source 所需的資訊:

diffable-data-source-transaction

CollectionDifference 是 Swift 5.1 引入的一個新型別,描述了兩個 Collection State 之間 item 的插入和刪除。

因此,你可以在 didReorder 閉包內,按已重新排序的 transcation 來簡單地更新原來的資料源:

originalDataSource.applying(transaction.difference)

總結

這篇文章說明了 iOS 14 Diffable data sources 的轉變。你可以利用 Section Snapshot 和新的重新排序 API,輕鬆地在 CollectionViews 建立和更新大量資料。

謝謝你的閱讀。

本篇原文(標題:Handling iOS 14 Diffable Data Sources)刊登於作者 Medium,由 Anupam Chugh 所著,並授權翻譯及轉載。

作者簡介:Anupam Chugh,深入探索 ML 及 AR 的 iOS Developer。喜愛撰寫關於想法、科技、與程式碼的文章。歡迎到我的 Blog 閱讀更多文章,或在 LinkedIn 上關注我。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This