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. Ở bài trước đã cung cấp cách sử dụng các thành phần cơ bản vào trong UIKit với đối tượng tương tác trực tiếp là UIViewController. Với bài này là phần tiếp theo khi chuyên về xử lý các sự kiện phát sinh trong ViewController.
Nếu bạn chưa đọc qua bài đầu tiên thì có thể ghé link sau:
Còn nếu mọi việc đã oke rồi thì …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- iOS 13.0
Sử dụng lại project ở bài viết trước. Nhiệm vụ lần này là thêm tính năng lưu trữ dữ liệu ở trong máy bằng file *.plist
. Và nâng cấp project bằng việc sử dụng UINavigationController cho rootViewController ở window.
Thêm 1 UIBarButtonItem cho navigation bar, để làm action save dữ liệu
override func viewDidLoad() { super.viewDidLoad() //title title = "HOME" //navigation let saveBarButton = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(save)) self.navigationItem.leftBarButtonItem = saveBarButton //subscription countPublisher .handleEvents(receiveOutput: { [weak self] value in self?.view.backgroundColor = (value % 2 == 0) ? UIColor.white : UIColor.lightGray }) .map { "\($0)"} .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions) }
1. Emit Data
Ở bài trước, chúng ta chọn giải pháp sau cho việc thay đổi dữ liệu:
@IBAction func increase(_ sender: Any) { countPublisher.value += 1 }
Khi thực hiện lệnh gán dữ liệu cho thuộc tính value
của Subject, nó cũng đồng thời phát đi giá trị hay gọi là emit
. Và bạn cũng biết chúng ta có thể phát đi dữ liệu ở bất kì đầu. Trong phạm vi bài viết trước, chính là sự kiện người dùng tác động lên UI Control.
Là em IBAction.
Giờ ta thử tiếp đoạn code sau:
override func viewDidLoad() { super.viewDidLoad() //title title = "HOME" //navigation let saveBarButton = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(save)) self.navigationItem.leftBarButtonItem = saveBarButton //subscription countPublisher .handleEvents(receiveOutput: { [weak self] value in self?.view.backgroundColor = (value % 2 == 0) ? UIColor.white : UIColor.lightGray }) .map { "\($0)"} .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions) // test emit countPublisher.send(10) }
Run project, bạn sẽ thấy được giá trị 10 hiển thị.
Tóm tắt một chút:
- Sử dụng Publisher là 1 Subject thì bạn có thể
emit
theo 2 cách- Xét giá trị cho thuộc tính
value
của nó. - Sử dụng function
send(:)
để phát dữ liệu đi. Đồng thời cũng cập nhật giá trị củavalue
.
- Xét giá trị cho thuộc tính
- Nơi sử dụng
- Tương tác với người dùng
- Thiết lập các giá trị ban đầu
- … ở đâu cũng được
2. handleEvents
Phần này mình liệt kê vào lại để nhóm các khái niệm liên quan trong cùng một bài. Vẫn là ví dụ code trên cho subscription. Bạn xem đoạn code sau:
countPublisher .handleEvents(receiveSubscription: { subscription in print(subscription) }, receiveOutput: { (value) in print(value) }, receiveCompletion: { (completion) in print(completion) }, receiveCancel: { print("Cancel") }, receiveRequest: { (request) in print(request.hashValue) }) .map { "\($0)"} .assign(to: \.text, on: self.counterLabel) .store(in: &subscriptions)
Mình đề cập lại vấn đề này, để dành cho bạn nào vẫn muốn lấy được sự kiện IBAction mà đã trót lỡ dùng nó cho em emit
data rồi. Phần này chỉ có vâỵ thôi. Còn sử dụng sao thì tuỳ thuộc vào ý đồ của bạn.
3. Call back
Vấn đề tiếp theo, bạn muốn nhận được phản hồi lại cho ViewController biết, sau khi thực hiện sự kiện nào đó.
Yêu cầu chức năng như sau:
Bạn lưu dữ liệu vào 1 file, sau đó báo lại việc lưu đã thành công hay là không.
Tạo thêm 1 file Model phụ trách việc lưu dữ liệu vào file *.plist
, với tên DataManager.swift
. Có các chức năng sau:
singleton
để truy cập sử dụng đối tượng mọi lúc mọi nơisave
dữ liệu, ở đây là lưu giá trịcount
vào file*.plist
load
để đọc dữ liệu lên với định dạng là Dictionary
class DataManagement { // MARK: - Singleton public static var share: DataManagement = { let dataManagement = DataManagement() return dataManagement }() private init() {} private var plistURL : URL { let documentDirectoryURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) return documentDirectoryURL.appendingPathComponent("data.plist") } // MARK: - public function func save(value: Int) { do { let dictionary = ["count" : value] try savePropertyList(dictionary) } catch { print(error) } } func load() -> [String : Int] { do { let dictionary = try loadPropertyList() return dictionary } catch { print(error) return [:] } } // MARK: - private function private func savePropertyList(_ plist: Any) throws { let plistData = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) try plistData.write(to: plistURL) } func loadPropertyList() throws -> [String : Int] { let data = try Data(contentsOf: plistURL) guard let plist = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String : Int] else { return [:] } return plist } }
Nhằm tiết kiệm thời gian nên mình đã viết sẵn 1 file Model như trên, với đầy đủ chức năng rồi. Tiếp theo, sử dụng chúng trong ViewController:
- Load dữ liệu đã lưu và hiện thị nó lên UI vào lúc khởi tạo ViewController. Tại function viewDidLoad, thêm đoạn code sau:
// load count let temp = DataManagement.share.load()["count"] ?? 0 countPublisher.send(temp)
- Save dữ liệu lại
@objc func save() { DataManagement.share.save(value: countPublisher.value) }
Run project và test xem Model và ViewController đã hoạt động ổn định chưa. Nếu đã ổn định rồi thì chúng ta sang phần chính.
Đi tìm lời giải cho câu hỏi sau:
Muốn biết được lúc nào thì dữ liệu lưu vào file
*.plist
thành công.
Với kiểu code Non-Combine cũ thì bạn sẽ thêm 1 closure vào function save
ở DataManager kia. Nhằm báo lại biết tác vụ đã hoàn thành. Ví dụ code như sau:
func save(value: Int, completion: (Error?) -> Void) { do { let dictionary = ["count" : value] try savePropertyList(dictionary) //call back completion(nil) } catch { //call back completion(error) } }
Đây chính là các thực hiện
call back
với closure truyền thống.
Với code Combine, chúng ta sẽ dùng 1 Publisher để phát đi giá trị là hoàn thành hay thất bại. Và điều quan trọng là code Combine phải kết hợp với code Non-Combine (chính là các function trong file DataManager). Vì bạn không muốn phải thay đổi lại quá nhiều code trong project của bạn.
Publisher sử dụng ở đây là
Future
.
Tại sao dùng Future
?
- Đối tượng Future là một publisher đặc biệt
- Nó chỉ phải ra duy nhất 1 lần (có thể thành công hoặc thất bại)
- Có thể lợi dụng trong các function Non-Combine code của UIKit (các function lâu nay của chúng ta). Nhằm
return
về một đối tượng Future.
Tiến hành cài đặt như sau:
Bước 1 : Mở file DataManager.swift
, import thư viện Combine và thêm function sau.
func save(value: Int) -> Future<Void, Error> { return Future { resolve in do { let dictionary = ["count" : value] try self.savePropertyList(dictionary) //call back resolve(.success(())) } catch { //call back resolve(.failure(error)) } } }
Công việc của bạn là sẽ viết 1 function với giá trị trả về là 1 Publisher. Mọi thứ vẫn không thay đổi quá nhiều. Vì:
- Với Publisher bạn có thể subscribe tại chỗ khác
- Phần code tương tác vẫn là Non-Combine code
- Giữ được cấu trúc lâu nay của project
Lựa chon Future
vì bạn chỉ cần dùng nó 1 lần và không bận tâm gì tới hiện tại hay quán khứ. Tương lai mới quan trọng. Nó cần 2 giá trị:
- success
- failure
Bước 2 : Mở file ViewController lên, tại function save
, tiến hành subscribe hành động
@objc func save() { DataManagement.share.save(value: self.countPublisher.value) .sink(receiveCompletion: { [unowned self] completion in if case .failure(let error) = completion { print(error.localizedDescription) } }) { [unowned self] id in print("SAVED SUCCESS!") } .store(in: &subscriptions) }
Vẫn là hình ảnh quen thuộc với sự kiện từ BarButtonItem. Tại function này ta thực hiện việc gọi lệnh save
số lần đếm. Vì function save (của DataManager) trả về 1 publisher. Nên ta cần phải subscribe
nó để xử lý.
Sử dụng SINK
để subscribe nó.
- Quan tâm tới 2 closure
completion
vàvalue
. - Future sẽ trả về 1 trong 2 đó.
- Tiếp tục handle các thao tác tương ứng với dữ liệu nhận được.
- Cuối cùng, là
store
lại subscription.
Run project và test lại xem ViewController đã hoạt động ổn chưa.
Có điều gì đặc biệt ở đây, khi mà bạn có thể return về 1 enum có success và failure?
Câu trả lời đó là:
Bất đồng bộ.
Mình sẽ giúp bạn hồi tưởng bí thuật lại như thế này:
Khi bạn xử lý 1 công việc theo kiểu bất đồng bộ. Thì mối quan tâm lớn nhất đó là nhận lại được kết quả.
- Protocol hay chính là Delegation Parttern là cái sẽ được ưu tiên sử dụng.
- Đơn giản nhưng không dễ hiểu.
- Đầu óc phải tưởng tượng ra một chút thì mới hình dung ra được luồng sự kiện di chuyển.
- Closure dùng làm tham số trong function.
- Với cái tên hay được đặt là
completion
haycall back
. - Dùng khá nhiều trong mô hình MVVM, phục vụ cho tương tác giữa View và ViewModel.
- Nó đơn giản hơn, dễ hiểu hơn và code xử lý tập trung đúng chỗ. Không suy nghĩ đau đầu về việc tìm cái đường di chuyển của nó.
- Với cái tên hay được đặt là
Với Combine thì sao:
Việc
call back
bây giờ chính làemit data
.
OKAY. Tới đây bạn sẽ nắm bắt được cơ bản việc xử lý sự kiện trong UIKit với Code Combine. Và biết cách call back
lại sau khi thực hiện xử lý sự kiện nào đó. 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
- Hai cách để
emit
dữ liệu đi với Subject handleEvents
của Publisher để hỗ trợ thêm cho việc xử lý sự kiện người dùng- Thực hiện
call back
với Code Combine bằng đối tượngFuture
.
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)