Contents
Chào bạn đến với Fx Studio,
Chúng ta lại tiếp tục với phần Combine vs. UIKit. Đọc qua cái tên bài, có thể bạn suy nghĩ đó là UINavigationController. Thì cái này không phải nha. Phần này sẽ tổng hợp các cách điều hướng View/ViewController cho các trường hợp chung nhất. Bên cạnh đó còn giải quyết vấn đề truyền tải dữ liệu giữa các View/ViewController.
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- iOS 13.0
Tiếp tục sử dụng lại project của bài trước để làm ví dụ demo code cho bài này. Ta thêm một màn hình SettingsViewController để:
- Cập nhật lại số đếm một cách nhanh nhất
- Truyền dữ liệu lại cho HomeViewController biết sự cập nhật
1. Present view
Bắt đầu bằng câu chuyện là hiển thị 1 Alert View để thông báo cho người dùng biết thông tin hay lỗi phát sinh trong quá trình sử dụng. Chúng ta giải quyết bài toán này bằng Combine nha, để cho có luồng gió mới.
Việc đầu tiên là ta cần 1 function và 1 Publisher. Đó là những gì cần thiết nhất. Mở file HomeViewController
tiến hành khai báo 1 function như vậy
func alert(title: String, text: String?) -> AnyPublisher<Void, Never> { // .... }
Bạn chú ý, đối tượng lần này chúng ta sử dụng là AnyPublisher
. Với
- Output là
Void
, thường sử dụng cho việc thông báo sự kiện - Failure là
Never
, không bao giờ có lỗi
Tiến hành implement code logic của UI Control
func alert(title: String, text: String?) -> AnyPublisher<Void, Never> { let alertVC = UIAlertController(title: title, message: text, preferredStyle: .alert) }
Vẫn không có thay đổi chi mới ở đây, bạn có thể tạo ra các View khác. Update các property của chúng … trước khi sử dụng.
Giờ qua phần chính là Publisher trả về. Tiếp tục với đoạn code sau:
func alert(title: String, text: String?) -> AnyPublisher<Void, Never> { let alertVC = UIAlertController(title: title, message: text, preferredStyle: .alert) return Future { resolve in alertVC.addAction(UIAlertAction(title: "Close", style: .default, handler: { _ in resolve(.success(())) })) self.present(alertVC, animated: true, completion: nil) } .handleEvents(receiveCancel: { self.dismiss(animated: true, completion: nil) }) .eraseToAnyPublisher() }
Tới đây thì phải căng não ra cho thẳng, nếu không đọc code xí là xoắn não liền.
- Sử dụng
Future
để đảm bảo việc phát ra dữ liệu ở tương lai thực thi. - Output của Future là 1
closue
, ta sẽ coding vào đó - Bạn sẽ hiển thị Alert ở
closure
đó, bên cạnh đó bạn cài đặt cho button của Alert ởclosure
với việc phát đisuccess
- Sử dụng toán tử
handleEvents
với tham sốreceiveCancel
. Có nghĩa khi kết thúc Future thì sẽdismiss
alert. - Cuối cùng, quan trọng nhất là
eraseToAnyPublisher
–> xoá đi dấu viếtFuture
, biến nó thành 1 Publisher
Cuối cùng là việc sử dụng nó. Vì return về là Publisher nên cần phải subscribe để dùng.
self.alert(title: "Error", text: error.localizedDescription) .sink { _ in // tự sướng trong này } .store(in: &self.subscriptions)
Thực sự thì trong trường hợp hiển thị Alert này, cũng không cần thiết phải return về Publisher. Tuy nhiên, với các trường hợp hiển thị các Custom View thì cũng nên cần.
Xem code sử dụng ở function lưu dữ liệu. Sau khi có call back
trở về.
@objc func save() { DataManagement.share.save(value: self.countPublisher.value) .sink(receiveCompletion: { [unowned self] completion in if case .failure(let error) = completion { self.alert(title: "Error", text: error.localizedDescription) .sink { _ in // tự sướng trong này } .store(in: &self.subscriptions) } }) { [unowned self] id in print("SAVED SUCCESS!") self.alert(title: "HOME", text: "SAVED SUCCESS!").sink { _ in // tự sướng trong này }.store(in: &self.subscriptions) } .store(in: &subscriptions) }
Mình sẽ hoàn thiện nó hơn ở 1 bài Custom View với Combine (ở thời gian nào đó). Chúng ta tiếp tục sang phần tiếp theo.
2. Talking to other ViewController
Bắt đầu, bằng việc cài đặt function để push
một ViewController khác vào HomeViewController. Ta chọn BarButtonItem. Mở file HomeViewController, tại viewDidLoad thêm đoạn code sau:
let settingsBarButton = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(gotoSettingsVC)) self.navigationItem.rightBarButtonItem = settingsBarButton
Thêm một function để điều hướng sang màn hình SettingsViewController.
@objc func gotoSettingsVC() { // vc let settingsVC = SettingsViewController() // push self.navigationController?.pushViewController(settingsVC, animated: true) }
Chúng ta sẽ có vấn đề đầu tiên là truyền dữ liệu từ A sang B.
Tạm thời ta sẽ gọi A là HomeViewController và B là SettingsViewController. Bước đầu tiên là truyền dữ liệu từ A sang B. Trong ví dụ của mình thì TextField của mình sẽ hiện thị giá trị count
ở HomeViewController.
- Thêm property
count
cho SettingsViewController
class SettingsViewController: UIViewController { @IBOutlet weak var countTexyField: UITextField! var count: Int = 0 override func viewDidLoad() { super.viewDidLoad() countTexyField.text = "\(count)" } @IBAction func done(_ sender: Any) { } }
- Truyền dữ liệu A sáng B
@objc func gotoSettingsVC() { // vc let settingsVC = SettingsViewController() settingsVC.count = countPublisher.value // push self.navigationController?.pushViewController(settingsVC, animated: true) }
Giải quyết cái này khá là đơn giản, bạn chỉ cần gán
các giá trị cho các thuộc tính của B. Hoặc gọi function setup/config data cho B với đối số là các giá trị mà bạn muốn truyền từ A sang B.
Vì đối tượng B được tạo ở function của đối tượng A.
Mọi việc sẽ phức tạp khi chiều truyền dữ liệu là từ B sang A.
Trước đây, để giải quyết vấn đề này chúng ta sử dụng con trỏ.
Tạo con trỏ A trong class B.
Nói cho nó sang chãnh vậy, chứ Swift thì dùng Protocol
thôi. Hay các delegate và datasouce mà lâu nay chúng ta đã dùng. Rồi chúng ta tiến hoá lên việc call back
bằng closure.
Vâng, tất cả vẫn là Non-Combine code. Giờ chúng ta đang ở trong thời đại mới rồi. Nên phải sử dụng được Combine Code vào để giải quyết vấn đề truyền dữ liệu này.
Giải pháp như thế nào?
Theo tư tưởng của Combine thì:
B sẽ là publisher và A sẽ là subscriber
Tư tưởng là như vậy. Nhưng không đời nào lại đi biến cả 1 UIViewController thành 1 publisher. Quá nhiều thứ dư thừa. Chúng ta chỉ cần cài đặt cho dữ liệu nào cần thiết mà thôi. Cụ thể là chúng ta tạo ra các các property
là các publisher.
Quay lại ví dụ giả tưởng trên, ta khai báo thêm các đoạn code sau trong class B:
var countPublisher = PassthroughSubject<Int, Never>()
var count: Int = 0 {
didSet {
countPublisher.send(count)
}
}
Giải thích:
countPublisher
dùng để phát dữ liệu đi. Nó là 1subject
- Khi có dữ liệu mới được gán cho
count
thì sử dụngcountPublisher
để phát đi. - Ngoài ra, bạn có thể lượt bỏ đi biến
count
, vìsubject
cũng có khả năng lưu trữ dữ liệu. Chính là thuộc tínhvalue
của nó.
Phát dữ liệu đi ở B như sau:
@IBAction func done(_ sender: Any) {
guard let value = Int(countTexyField.text ?? "0") else { return }
count = value
self.navigationController?.popViewController(animated: true)
}
Vấn đề đơn giản là gán dữ liệu mới count
. Vì theo trên thì didSet
của count
đã sử dụng publisher. Nên ta không cần cài đặt gì thêm ở đây nữa. Sau đó, thoát B để về A.
Khi không muốn gởi gì hết đi, kết thúc câu chuyện tình này thì code như sau:
countPublisher.send(completion: .finished)
Nhận dữ liệu như thế nào?
Còn 1/2 câu chuyện nữa cần giải quyết. Tới đây thì bạn quay lại function gotoSettingsVC
tại A. Việc tiếp theo là bạn phải subscribe tới publisher của B. Xem tiếp ví dụ code:
@objc func gotoSettingsVC() {
// vc
let settingsVC = SettingsViewController()
settingsVC.count = countPublisher.value
// publisher
let publisher = settingsVC.countPublisher
// subscriptions
publisher
.sink { value in
self.countPublisher.value = value
}
.store(in: &subscriptions)
// push
self.navigationController?.pushViewController(settingsVC, animated: true)
}
Bạn sẽ thấy:
- Tạo 1 đối tượng tham chiếu tới publisher của B
- Nếu cần biến đổi dữ liệu thì sử dụng
map
- Sau đó là
subscribe
, có thể dùngassign
hoặcsink
- Cuối cùng là lưu trữ subscription
Cài đặt trước các hành động, mọi thứ sẽ phản ứng lại đúng như ý đồ của chúng ta. Đó là tư tưởng code của Combine để giải quyết vấn đề này.
3. Multiple subscriptions
Bạn hay nghe câu nói:
Đời không như là mơ.
Thì đời cũng như code vậy. Nó không đơn giản khi mỗi function thực hiện 1 nhiệm vụ. Hay 1 ViewController chỉ cần giải quyết 1 vấn đề. Mà đôi khi từ 1 dữ liệu chung, bạn cần phải giải quyết nhiều việc nữa. Quay về câu chuyện tình giữa A và B.
gotoSettingsVC
này:@objc func gotoSettingsVC() { // vc let settingsVC = SettingsViewController() settingsVC.count = countPublisher.value // publisher let publisher = settingsVC.countPublisher // subscription 2 publisher .sink { value in self.countPublisher.value = value } .store(in: &subscriptions) // subscription 2 publisher .map { "\($0)" } .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions) // push self.navigationController?.pushViewController(settingsVC, animated: true) }
Bạn dễ dàng thấy được có tới 2 subscription tới cùng 1 publisher để thực hiện 2 việc.
- Lấy giá trị của nó
- Biến đổi giá trị của nó
Bạn có đảm bảo được tính toàn vẹn của các Publisher đó hay không?
Chúng ta đều biết các Transforming Operators, giúp biến đổi Publisher để đạt cái mà mình mong muốn. Nên rất có thể sau 1 vài operator thì Publisher của mình đã không còn như trước nữa.
Quay lại function trên, ta edit đoạn này:
let publisher = settingsVC.countPublisher.share()
Toán tử share()
có đề cập trong phần operator. Sử dụng khi có nhiều subscription tới cùng 1 publisher và cũng cùng trỏ tới publisher gốc. Giúp publisher gốc khi emit
dữ liệu cho nhiều subscriber thì dữ liệu được giở đi an toàn hơn.
4. Binding
Ở bài trước, chúng ta đã tìm hiểu về call back
. Phần này sẽ là binding
. Dành cho bạn nào chưa hiểu lắm thì cứ hiểu đơn giản như sau:
Binding là việc ràng buộc 2 đối tượng tượng với nhau. Khi dữ liệu của đối tượng này thay đổi thì đối tượng kia cũng sẽ thay đổi theo.
@Published
Thật là không sai khi nói Apple đã có âm mưu ngay từ đầu. Và thật là tinh tế, khi đã gài và cài cắm Combine code và trong các framework truyền thống. Mà quan trọng là nó không thay đổi gì nhiều code cũ.
Có 2 từ khoá mới:
@Published
@ObservedObject
Đã được khai sinh ra vào bạn có thể dùng trong bất class/struct/enum nào cũng được. Trong phần này chúng ta chỉ tìm hiểu về @Published
thôi. Vậy nó là gì?
- Cách đơn giản nhất và nhanh nhất để bạn tạo ra 1 property là publisher
- Không ảnh hưởng gì tới code của class chứa nó, chỉ khai báo thêm từ khoá
@Publisher
phía trước - Với Output là cùng kiểu dữ liệu với property đó. Và không bao giờ có lỗi.
- Vừa lưu trữ được giá trị và phát đi được giá trị
- Real-time, bất cứ khi nào bạn thay đổi giá trị thì đồng thời nó sẽ phát đi giá trị đó cho các subscriber
private
haypublic
để được- Phải yêu cầu có giá trị lúc khai báo
Ví dụ cú pháp khai báo:
struct Person {
@Published var age: Int = 0
}
Sử dụng thì
var person = Person()
person.$age
.sink { ... }
.store(...)
Thêm toán tử dấu $
để truy cập tới nó.
Áp dụng vào cái ví dụ trên
Ở class B, chỉnh sửa lại một chút như sau:
import UIKit import Combine class SettingsViewController: UIViewController { @IBOutlet weak var countTexyField: UITextField! // var countPublisher = PassthroughSubject<Int, Never>() // // var count: Int = 0 { // didSet { // countPublisher.send(count) // } // } @Published var count: Int = 0 override func viewDidLoad() { super.viewDidLoad() countTexyField.text = "\(count)" } @IBAction func done(_ sender: Any) { guard let value = Int(countTexyField.text ?? "0") else { return } count = value self.navigationController?.popViewController(animated: true) } }
Trong đó:
- Xoá đi
countPublisher
- Thêm
@Published
vào trước khai báo biếncount
Mọi thứ trong B vẫn giữ nguyên như cũ. Giờ sang A:
- Xoá đi các subscription tới
countPublisher
, vì nó không còn nữa - Thêm subscription mới như sau
settingsVC.$count .sink { value in self.countPublisher.value = value }.store(in: &subscriptions)
Run project và test lại mọi thứ đã hoạt động đúng ý đồ chưa.
OKAY! Bạn đã đủ kiến thức để quản lý việc điều hướng và truyền tải dữ liệu giữa các View. Mình xin kết thúc bài viết này và bạn có thể download code demo tại đây.
Tạm kết
- Sử dụng Future để handle việc hiển thị View trong việc điều hướng
- Quản lý truyền dữ liệu giữa 2 view với Combine
- Quản lý tương tác nhiều subscribe tới cùng 1 Publisher
- Binding dữ liệu theo chiều ngược lại.
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
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- 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
Archives
- September 2024 (1)
- 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)