Home

Diffable Data Source

Word count: 1,504 / Reading time: 9 min
2021/01/21 Share

DiffableDataSource là một API mới của UITableView và UICollectionView được giới thiệu tại WWDC19 trên IOS 13 để thay thế cho UITableViewDataSource và UICollectionViewDataSource.

Trong bài viết này, chúng ta sẽ tìm hiểu sơ lược về UITableViewDataSource, sau đó c sẽ đi thẳng đến API mới và xem nó hoạt động như thế nào với một ví dụ.

UITableViewDataSource

Trước đây, khi làm việc với UITableView hoặc UICollectionView, bạn cần phải implement protocol UITableViewDataSource để chỉ định chi tiết những dữ liệu được hiển thị trên cell hay các supplementary views như headers and footers.

1
2
3
4
5
6
7
8
9
// Providing number of Rows and Sections
func tableView(UITableView, numberOfRowsInSection: Int) -> Int
func numberOfSections(in: UITableView) -> Int

// Providing Cells, Headers, and Footers
func tableView(UITableView, cellForRowAt: IndexPath) -> UITableViewCell
func tableView(UITableView, titleForHeaderInSection: Int) -> String?
func tableView(UITableView, titleForFooterInSection: Int) -> String?
// other datasource methods

bất cứ khi nào dữ liệu ở controller thay đổi, thông thường chúng ta phải reload toàn bộ các thành phần của tableView bằng cách gọi reloadData() hoặc performBatchUpdates(_:completion:)... để cập nhật (insert, delete, move) các item hay section cụ thể, tương tự như sau:

1
2
3
4
self.tableView.performBatchUpdates({
self.tableView.deleteRows(at: deletedIndexPaths, with: .fade)
self.tableView.insertRows(at: insertedIndexPaths, with: .right)
}, completion: nil)

Nhưng bạn cần phải cẩn thận khi thực hiện các thay đổi và đảm bảo các thay đổi được áp dụng theo đúng thứ tự.
Nếu không, khi cập nhật sẽ bị lỗi, bạn sẽ nhận được một cái gì đó như:

Mặc dù cách tiếp cận UITableViewDataSource đơn giản và linh hoạt nhưng nó cũng dễ xảy ra lỗi và là nguồn phát sinh crash phổ biến khi có sự sai lệch giữa trạng thái UI hiện tại với data do controller quản lý.

Diffable data source

Giờ đây với UITableViewDiffableDataSource, bạn có thể tạo dataSource và apply các thay đổi giữa các state một cách an toàn hơn bằng cách thao tác với snapshot (một khái niệm mới đại diện cho trạng thái hiện tại của tableView).

Để hiển thị hoặc cập nhật dữ liệu, bạn chỉ cần tạo một đối tượng của NSDiffableDataSourceSnapshot với dữ liệu đã được cập nhật và cung cấp nó cho dataSource thông qua việc gọi phương thức apply (_: animatingDifferences :), nó sẽ so sánh sanpshot hiện tại ( rendered models ) với snapshot mới để xem sự khác biệt sau đó hiển thị lên tableView.

Getting Started

Bây giờ chúng ta hãy xem một ví dụ về màn hình hiển các contact list với thanh tìm kiếm cho phép người dùng tìm kiếm danh bạ của mình theo tên.

Như đã đề cập, với cách tiếp cận mới, chúng tôi chỉ cần hai loại

  • UITableViewDiffableDataSource có hai kiểu chung: Kiểu item và kiểu section, để chỉ định tableView cách hiện thị cell và các supplementary views.

  • NSDiffableDataSourceSnapshot <SectionIdentifierType, ItemIdentifierType>: đại diện cho dataSource mới sẽ được hiển thị.

Create the model

Bắt đầu bằng cách tạo 1 model Contact, nó là dữ liệu sẽ được hiển thị trong TableView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Contact : Hashable {

var id = UUID()
var firstName: String
var lastName: String
var emailAddress: String

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

static func == (lhs: Contact, rhs: Contact) -> Bool {
lhs.id == rhs.id
}
}

Note: Diffable data source yêu cầu SectionIdentifierType và ItemIdentifierType (model) phải kế thừa Hashable để cho phép dataSource so sánh giữa các snapshot với nhau để tìm ra sự khac biệt (để biết chính xác những gì đã được chèn, xóa hoặc di chuyển).

Setting up the data source

Đầu tiên, tạo 1 Section type chúng sẽ được sử dụng dưới dạng SectionIdentifierType, gồm 2 section:

1
2
3
4
enum Section : Int , CaseIterable {
case friendsContacts
case allContacts
}

Sau đó, tạo và tuỳ chỉnh diffable data source để cung cấp cho TableView với chi tiết về cách hiện thị cells va các supplementary view ( section headers and footers )

1
2
3
4
5
6
7
8
9
10
11
12
typealias DataSource = UITableViewDiffableDataSource<Section, Contact>
private lazy var dataSource = makeDataSource()
// ...
func makeDataSource() -> DataSource {
let dataSource = DataSource(tableView: tableView, cellProvider: {( tableView, indexPath, contact) -> UITableViewCell? in

let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableCell.reuseIdentifier, for: indexPath) as? ContactTableCell
cell?.configure(with: contact)
return cell
})
return dataSource
}

Note: bạn thường làm các việc trên bằng cách triển khai cellForRowAt, titleForHeaderInSection, v.v. với UITableViewDataSource

Creating a Snapshot

Bây giờ TableView đã biết chính xác cách hiển thị dữ liệu, chúng ta cần cung cấp cho TableView dữ liệu để hiển thị. Đây là lúc NSDiffableDataSourceSnapshot xuất hiện.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private var friendsContacts : [Contact] = Contact.friendsContacts
private var allContacts : [Contact] = Contact.allContacts
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Contact>

override func viewDidLoad(){
super.viewDidLoad()
// some other UI Configurations
applySnapshot(animatingDifferences: false)
}

func applySnapshot(animatingDifferences: Bool = true) {
var snapshot = Snapshot()
snapshot.appendSections( Section.allCases )
snapshot.appendItems(friendsContacts, toSection: .friendsContacts)
snapshot.appendItems(allContacts, toSection: .allContacts)
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

Tạo một snapshot với các section và item sẽ được hiển thị. Bất cứ khi nào apply một snapshot mới, TableView sẽ so sánh với snapshot hiện tại để biết chính xác những gì được cập nhật, sau đó hiển thị và tạo các animation cho các cập nhật đó.

Add UISearchController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private var friendsContacts : [Contact] = Contact.friendsContacts
private var allContacts : [Contact] = Contact.allContacts
private var searchController = UISearchController(searchResultsController: nil)

override func viewDidLoad(){
super.viewDidLoad()
// some other UI Configurations
configureSearchController()
}

extension ContactsTableViewController: UISearchResultsUpdating {

func configureSearchController() {
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Contacts"
navigationItem.searchController = searchController
definesPresentationContext = true
}

func updateSearchResults(for searchController: UISearchController) {

let myContacts = Contact.allContacts
let myFriends = Contact.friendsContacts
if let text = searchController.searchBar.text, !text.isEmpty {
allContacts = myContacts.filter { contact in
return contact.firstName.contains(text) || contact.lastName.contains(text)
}
friendsContacts = myFriends.filter { contact in
return contact.firstName.contains(text) || contact.lastName.contains(text)
}
} else {
allContacts = myContacts
friendsContacts = myFriends
}
applySnapshot()
}
}

Bất cứ khi nào người dùng thay đổi text trong searchBar, chúng ta sẽ cập nhật danh sách allContacts và friendsContacts cho phù hợp sau đó apply một snapshot mới, và các danh sách cập nhật sẽ được hiển thị.

Supplementary Views

Cuối cùng hãy header cho cả hai section.

1
2
3
class SectionHeaderReusableView: UITableViewHeaderFooterView {
// whatever how it looks
}

Sau đó implement các method cần thiết của UITableViewDelegate như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// MARK: - UITableViewDelegate

extension ContactsTableViewController {

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30
}

override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}

override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = tableView.dequeueReusableHeaderFooterView(
withIdentifier: SectionHeaderReusableView.reuseIdentifier) as? SectionHeaderReusableView
else {
return nil
}

if section == Section.allContacts.rawValue {
header.titleLabel.text = "Your Contacts"
} else {
header.titleLabel.text = "Friends Contacts"
}

return header
}

override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}

}

Final

CATALOG
  1. 1. UITableViewDataSource
    1. 1.1. Diffable data source
  2. 2. Getting Started
    1. 2.1. Create the model
    2. 2.2. Setting up the data source
    3. 2.3. Creating a Snapshot
    4. 2.4. Add UISearchController
  3. 3. Supplementary Views
  4. 4. Final