Contents
Chào bạn đến với Fx Studio,
Bài viết này sẽ là bài viết đầu tiên trong chuỗi các bài viết về việc kết hợp Combine với UIKit. Đối tượng tấn công trước hết chính là UIViewController.
Nếu bạn chưa biết về Combine là gì, thì có thể bắt đầu với phần Basic Combine Framework. Còn nếu bạn là fan của Fx Studio và đã đọc các phần trước rồi thì …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- iOS 13.0
Chúng ta sẽ sử dụng một project iOS để làm code ví dụ và giải thích ý nghĩa trong bài này. Bạn có thể tạo project có hay không có Storyboard cũng đều được. Project của chúng ta như sau:
Trong đó:
- 1 UIViewController
- Có 1 UILabel để hiển thị dữ liệu
- 2 UIButton, 1 cái để tăng giá trị và 1 cái để giảm giá trị
1. import
Công việc đầu tiên là gõ dòng lệnh vào vào file ViewController.
import Combine
Mặc dù Combine đã được Apple âm thầm gài gắm vào nhiều framework. Nhưng để sử dụng full tính năng của nó thì cần import đúng thư viện. Tới đây thì bạn đã hoàn thành 50% công việc rồi đó.
OKAY, I’M FINE!
2. Subscriptions
Làm việc với ứng dụng thì vấn đề đầu tiên cần chú ý đó là quản lý bộ nhớ.
Với phong cách lập trình theo Reactive Programming của RxSwift hay Asynchronous Programming của Combine đó là quản lý các subscriptions
của các subscribers tới các publisher.
Muốn quán lý chúng, bạn cần khai báo 1 property cho ViewController
var subscriptions = Set<AnyCancellable>()
Mỗi khi bạn subscribe một Publisher nào đó, sẽ 1 token
cho đối tượng Cancellable. Mà chúng ta đã biết thì Class Subscriber cũng kế thừa Cancellable. Công với việc subscribe (bằng sink
hay assign
) thì cũng để sinh ra một đối tượng cancellable
.
Do đó, tốt nhất là lưu trữ các cancellable
vào 1 tập hợp với kiểu là Set
để tránh trùng lặp các token
. Cái này có lợi ích như sau:
- Tập trung các subscription phát sinh trong ViewController
- Khi ViewController bị giải phóng, thì các subscription lưu trữ này cũng giải phóng theo
- Tối ưu được bộ nhớ của ứng dụng
Để cho dễ liên tưởng thì bên RxSwift có anh chàng
disposebag
, thì cái này tương tự vậy.
3. Publisher
Nhân vật chính bây giờ mới xuất hiện. Publisher là trái tim của Combine. Nó là nguồn phát đi các giá trị nên là thành phần không thể thiếu được trong việc sử dụng Combine Code vào trong UIKit.
Publisher sẽ điều khiển luồng dữ liệu trong ViewController.
Quay lại project của chúng ta. Ta có 1 UILabel để hiện thị biến count
. Do đó, ta sẽ cần có 1 Publisher cho count
này.
Lựa chọn ở đây thì bạn sẽ phải chọn kiểu của Publisher thuộc 1 trong 2 kiểu sau:
- CurrentValueSubject
- Ưu điểm của nó khi có subscription tới thì subscriber sẽ có ngay dữ liệu liền
- Hiệu quả khi lưu trữ dữ liệu
- Cần cung cấp giá trị của Output ban đầu khi khai báo subject này
- PassthroughSubject
- Ưu điểm là có cái gì là ném cái đó đi. Chứ không quan tâm phải gởi dữ liệu ngay từ đầu cho subscriber
- Hiệu quả trong việc gởi các sự kiện hoặc call back
- Không cần cung cấp giá trị ban đầu cho việc khai báo subject này
Vì luồng dữ liệu của ta là luồng dữ liệu bất đồng bộ, ta không biết thời điểm nào người dùng bấm vào 2 button đó. Cho nên giá trị count
cũng không biết thời điểm nào sẽ tăng hay giảm. Và Subject
là lựa chọn hàng đầu. Nó có nhiều ưu điểm như sau:
- Vừa lưu trữ được dữ liệu và vừa phát dữ liệu đi được
- Kết nối được phần Combine code với Non-Combine code
- Có thể phát đi các giá trị theo mong muốn
- Nhiều subscriber có thể subscribe tới nó
- Tự do trong việc sử dụng kiểu dữ liệu cho Output
- Handle Error một cách dễ dàng
Tiếp tục với file HomeViewController.swift
, thêm 1 property sau:
var countPublisher = CurrentValueSubject<Int, Never>(0)
Trong đó:
- Output : Int
- Failure : Never
Thử xem cách dùng của nó như thế nào?
- Cách 1:
set
lại giá trị cho thuộc tínhvalue
của Subject
countPublisher.value = 1
- Cách 2:
send
giá trị mới
let newValue = countPublisher.value + 1 countPublisher.send(newValue)
Tiếp tục là vấn đề mới, bạn để code send dữ liệu của Publsher ở đâu?. Việc này thì càng dễ hơn. Nơi nào có sự kiện thì tại đó sẽ thực hiện việc phát dữ kiệu đi. Có 2 loại sự kiện:
- Sự kiện người dùng, thông qua các IBAction
- Sự thay đổi trạng thái của các View, thông qua Delegate & Datasource của các View đó
Trong bài ví dụ, ta sẽ tăng và giảm giá trị của count
một cách đơn giản như sau:
@IBAction func increase(_ sender: Any) { countPublisher.value += 1 } @IBAction func reduce(_ sender: Any) { countPublisher.value -= 1 }
Để kiểm chứng thì ta thử subscription như trước đây. Vào function viewDidLoad
thêm đoạn code này vào:
countPublisher .print("Publisher:" ) .sink(receiveCompletion: { (completion) in print(completion) }) { (value) in print(value) } .store(in: &subscriptions)
Build project và nhấn test thử, ta sẽ đc kết quả như sau được in ra ở console
Publisher:: receive subscription: (CurrentValueSubject) Publisher:: request unlimited Publisher:: receive value: (0) 0 Publisher:: receive value: (1) 1 Publisher:: receive value: (2) 2 Publisher:: receive value: (3) 3 Publisher:: receive value: (4) 4 Publisher:: receive value: (5) 5 Publisher:: receive value: (6) 6 Publisher:: receive value: (7) 7 Publisher:: receive value: (8) 8 Publisher:: receive value: (9) 9 Publisher:: receive value: (10) 10 ...
Toán tử print
trong Publisher sẽ giúp bạn debug được dữ liệu hay sự kiện mà liên quan tới Publisher đó.
Chờ một chút …
Tới đây, nhiều bạn sẽ liên tưởng tới RxSwift hay họ hàng nhà Rx. Là nó có
button.tag
, còn Combine sao không có? Việc gì phải dùng nó ở IBAction?
Phần này mình xin phép không lý giải. Nhưng bạn phải hiểu được là bạn đang sử dụng Combine trên nên tảng nào. Với UIKit, thì để UIKit quản lý các action
của nó, còn Combine giúp bạn biến đổi phần còn lại của thế giới hỗn tạp này.
Nếu bạn vẫn còn thấy ấm ức, thì hãy đợi vài phần nữa, mình sẽ tới bài SwiftUI
, khi đó mọi thứ sẽ sáng tỏ.
4. Subscribe
Chúng ta đã có nguồn phát dữ liệu và thấy được dữ liệu thay đổi như thế nào sau mỗi lần có sự kiện người dùng tác động lên UI. Tiếp theo công việc là đưa nó lên giao diện.
Bạn sử dụng tiếp đoạn code trên và tiến hành gán giá trị cho IBOutlet
của ViewController.
override func viewDidLoad() { super.viewDidLoad() countPublisher .print("Publisher:" ) .sink(receiveCompletion: { completion in print(completion) }) { value in self.counterLabel.text = "\(value)" } .store(in: &subscriptions) }
Build và test chương trình của chúng ta hoạt động ra sao. Ta thấy:
- 2 IBAction chỉ có trách nhiệm xét lại giá trị
- Phần UI sẽ hiển thị dựa theo dữ liệu mà Publisher phát đi
Đó chính là nguyên lý đầu tiên khi sử dụng Combine trong UIKit. Ta tiến hành subscribe
tới các Publisher và công việc này sẽ khai báo tất cả (subscriptions) tại lúc ViewController được khởi tạo (đó là viewDidLoad).
Khi người dùng có hành động, ta chỉ cần thay đổi dữ liệu từ nguồn phát và không quan tâm gì thêm. Mọi thứ còn lại sẽ dựa vào đó mà biến đổi tiếp.
Quay lại ví dụ code của chúng ta. Về bản chất hoạt động khi subscribe
thì đã OKE rồi. Tuy nhiên, bạn nhận thấy công việc update dữ liệu lên UI được thực hiện ở closure của sink
, để biến đổi giá trị Int
thành String
và sau đó là phép gán lên thuộc tính text
của UILabel
. Chúng ta cần nâng cấp một chút nữa.
Xoá đoạn code trên và chỉnh sửa một số chỗ như sau.
- Thay đổi lại Output của Publisher và thêm 1 property để lưu trữ giá trị
var countPublisher = CurrentValueSubject<String?, Never>("0") var count = 0
- Update lại subscribe tại viewDidLoad
override func viewDidLoad() { super.viewDidLoad() countPublisher .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions) }
- Edit lại chỗ thay đổi dữ liệu và phát dữ liệu đi bằng Publisher tại các function IBAction
@IBAction func increase(_ sender: Any) { count += 1 countPublisher.send("\(count)") } @IBAction func reduce(_ sender: Any) { count -= 1 countPublisher.send("\(count)") }
OKAY, run project và test lại xem ntn. Mình giải thích 1 chút
- Dùng
assign
vì chúng ta cần đưa dữ liệu trực tiếp từ giá trị mà Publisher phát đi tới UI Control - 2 đầu này phải cùng kiểu dữ liệu
- Output của Publisher phải là
String?
text
của UILabel thì đương nhiên đã làString?
- Output của Publisher phải là
- Edit lại cách phát dữ liệu, thay vì Int như lúc nãy mà bây giờ là String
assign
để đưa dữ liệu nhận được tới thẳng proprety của đối tượng
Đây là cách
binding
dữ liệu lên View.
Một điều nữa bạn cần chú ý khi sử dụng assign
là Failure của Publisher phải là Never
.
5. Operators
Tới đây, chắc nhiều bạn thắc mắc tiếp là Combine đâu có gì hay đâu. Cũng bình thường và cũng phải tốn nhiều property của ViewController mới hoạt động được. Vâng, chúng ta lại nâng cấp tiếp chương trình của mình.
Quay về đoạn code trước khi thay đổi Output của Publisher từ Int
thành String?
. Và chỉnh sửa phần subscribe
như sau:
countPublisher .map { "\($0)"} .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions)
Build project và test lại xem ViewController của chúng ta đã hoạt động được hay không.
Bạn cũng biết món ăn ngon được thì phải cần chế biến nguyên liệu, sau đó là nấu. Quá trình đó biến thịt, cá .. thành các món đồ ăn thơm ngon bổ dưỡng. Nhân câu chuyện đó thì mình muốn nhắn gởi là:
Phải có biến đổi dữ liệu và sẽ biến đổi nhiều lần để được dữ liệu mà mình mong muốn.
Xem đoạn code trên bạn sẽ thấy toán tử map
. Nó thuộc họ hành nhà Transforming Operators, chuyên đi biến đổi dữ liệu. Trong đoạn code trên, khi subject
phát đi 1 int
, nhưng chúng ta cần chính là 1 cái string
mà thôi. Nên sẽ viết hàm biến đổi dữ liệu tại đó.
Vì vậy, Operators là thành phần quan trọng tiếp theo trong Combine cũng đã được sử dụng vào trong ví dụ cực kì cơ bản này rồi.
6. Handle Events
Ở trên là thao tác trên luồng dữ liệu
. Tất nhiên, bạn còn phải thao tác với luồng sự kiện
nữa. Và trong ứng dụng, khi bạn tác động tới 1 UI Control trên giao diện, thì có thể hành động đó của bạn sẽ gây ảnh hưởng tới rất nhiều UI Control khác.
Vì vậy, cần phải nắm được thời cơ mà dữ liệu có sự thay đổi. Thời cơ đó chính là lúc:
Subscriber nhận được giá trị mà phát ra bởi Publisher.
Chúng ta sử dụng toán tử handleEvents
của Publisher. Chúng ta có thể bắt được các loại sự kiện phát ra khi có sự thay đổi trên nguồn phát như sau:
- receiveSubscription
- receiveOutput
- receiveCompletion
- receiveCancel
- receiveRequest
Quay lại với ví dụ của chúng ta. Edit lại đoạn code subscribe
ở viewDidLoad
countPublisher .handleEvents(receiveOutput: { [weak self] value in // Viết code ở đây // hoặc // Gọi các function khác ở đây // ví dụ nha self?.view.backgroundColor = (value % 2 == 0) ? UIColor.white : UIColor.lightGray }) .map { "\($0)"} .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions)
Chú ý bạn nên sử dụng weak self
để chương trình mình đẹp hơn. Còn tại sao dùng nó thì mình sẽ có giải thích ở các bài sau.
OKAY. Tới đây thì mình xin kết thúc bài viết này. Và bạn bây giờ đã có đủ tự tin mà áp dụng Combine Code vào trong ViewController của bạn rồi. Mã nguồn của ví dụ demo thì bạn có thể download tại đây.
Tạm kết
- Thực hiện binding dữ liệu lên View
- Kết hợp với các thành phần cơ bản như IBOutlet & IBAction
- Tương tác được với luồng dữ liệu & luồng sự kiện
- Quản lý các subcriptions trong ViewController
- Biến đổi dữ liệu bằng các Operators
Cảm ơn bạn đã đọc bài viết này!
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
- Lập trình hướng giao thức (POP) với Swift
Archives
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)