Contents
Chào bạn đến với Fx Studio!
Chúng ta lại tiếp tục mô hình MVVM với Combine Framework. Bạn nên dành ra một chút thời gian để review lại kiến thức về mô hình mới này.
-
- Tổng quát : Combine vs. MVVM – Overview
- Quản lý & hiển thị dữ liệu : Combine vs. MVVM – Binding
Và bài viết này sẽ nói về chủ đề sự kiện người dùng trong mô hình MVVM với Combine. Đây cũng là phần cần nắm tiếp theo trong mô hình MVVM, bất chấp là MVVM truyền thống hay đã biến tấu đi. Còn bây giờ thì …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- iOS 13.0
Chúng ta vẫn sử dụng lại project demo của bài trước, trong bài viết này chưa cần phải thêm màn hình nào nữa. Công việc tiếp theo của chúng ta sẽ là cải tiến lại code và thêm các sự kiện cần thiết.
1. Sự kiện người dùng
Xem lại sơ đồ tổng quát của mô hình MVVM với Combine, ở mức độ chi tiết cho View & ViewModel. Trong mô hình, người dùng đóng vài trò quan trọng, là tác nhân chính xúc tác sự thay đổi trạng thái của ứng dụng.
Công việc đầu tiên là mô hình sẽ hiển thị dữ liệu cho người dùng xem. Công việc tiếp theo, là sẽ chờ và đón nhận các sự kiện từ người dùng, sau đó sẽ xử lý chúng. Bạn cũng nhận ra một điều là chúng ta không biết chính xác khi nào người dùng sẽ tác động lên ứng dụng. Vì vậy,
Sự kiện người dùng cũng được xem là luồng dữ liệu bất đồng bộ.
Đây cũng chính là căn nguyên thúc đẩy sự phát triển trở lại của đa vũ trụ Reactive Programming trong thập kỉ vừa qua. Ta đi vào phân tích các bước của 1 sự kiện người dùng là như thế nào.
- Bước 1: View nhận sự kiện
- View là thực thể đầu tiên sẽ tiếp nhận sự kiện từ phía người dùng
- Các loại function được cài đặt để đón nhận bao gồm:
- Sự kiện cảm ứng
- Delegate
- IBAction
- Sau khi nhận được, thì View sẽ kích hoạt một thứ được gọi là
luồng sự kiện
. Luồng này sẽ đi qua nhiều đối tượng trong mô hình và cuối cùng là phản hồi lại cho người dùng biết thông qua giao diện.
- Bước 2: ViewModel tiếp nhận & xử lý
- Trong mô hình MVVM, ViewModel sẽ là thành phần chịu trách nhiệm cho việc xử lý các luồng sự kiện
- Tương ứng với mỗi sự kiện bên View, thì ViewModel sẽ có các Action tương ứng
- Điểm khó khăn nhất, chính là ViewModel sẽ phản hồi lại như thế nào sau khi xử lý xong
- Bước 3: phản hồi lại người dùng
- Việc phản hồi lại người dùng sẽ thông qua giao diện ứng dụng. Nó sẽ cập nhật trạng thái hay điều hướng … ứng với mỗi tác động của người dùng
- Công việc này bắt đầu từ phía ViewModel và mình sẽ gọi bằng cái tên là
luồng dữ liệu
- Với luồng dữ liệu đồng bộ thì chúng ta không cần xử lý việc phản hồi
- Với luồng dữ liệu bất đồng bộ thì cần phải kiểm tra và quản lý các đối tượng tiếp nhận phản hồi
- Các loại cài đặt cho việc phản hồi
- Delegate & DataSource
- Closure
- KVO
- Publisher (mới)
Bạn đã nắm sơ qua ý nghĩa của sự kiện người dùng tác động tới mô hình như thế nào rồi. Bây giờ chúng ta chuyển sang việc xử lý từng loại nha.
2. Action without callback
Có rất nhiều sự kiện không cần sự phản hồi ngược lại. Đa số trong chúng đều là tương tác đồng bộ với nhau về mặt dữ liệu, trên cùng một Thread. Cụ thể ở đây là chính Main Thread, nơi quản lý về mặt UI của ứng dụng.
Ta lấy ví dụ từ chính màn hình LoginViewController, có 1 button là clear. Nhiệm vụ của nó là xoá đi nội dung của 2 ô TextField. Do trước đó (bài trước), chúng ta đã cài đặt việc cập nhật dữ liệu của 2 TextField thông qua subscription tới 2 thuộc tính của ViewModel. Nên sự tác động của người dùng thì ứng dụng đơn giản là xoá đi dữ liệu của 2 thuộc tính của ViewModel và không cần làm gì thêm nữa.
Bạn mở file LoginViewModel, thêm một function với tên là clear
như sau:
func clear() { username = "" password = "" }
Quá dễ hiểu và không cần gì giải thích ở đây nữa. Giờ sang file LoginViewController thôi, tại function clear
cho UIBarButtonItem, bạn tiến hành gọi function vừa tạo của ViewModel.
@objc func clear() { viewModel.clear() }
Build project và kiểm tra xem hoạt động đúng theo yêu cầu chưa. Nếu oke thì bạn sẽ thấy dữ liệu ở 2 TextField sẽ bị xoá ngay và Button Login cũng bị disable. Trong khi, bạn không cần phải code quản lý đám giao diện đó và cập nhật lại trạng thái cho tụi nó.
Hầu hết loại này, bạn có sử dụng Combine hay Non-Combine thì cũng không khác nhau nhiều lắm.
3. Callback
Đây chính là phần bạn nên tập trung kĩ vào trong việc xử lý các yêu cầu với mô hình MVVM mới này. Cái khó ở đây chính là:
Callback với việc sử dụng Combine Code.
Về điểm lý thuyết callback
sử dụng với Combine, mình đã trình bày ở Phần 2 của toàn serire, bạn có thể quay lại đó để đọc kĩ hơn.
Còn giờ tìm hiểu là tại sao phải dùng nó?
Việc phản hồi là điều bắt buộc phải có trong ứng dụng. Bạn sẽ cảm thấy cực kì khó chịu khi táp
liên tục vào 1 cái button. Nhưng không có chuyện gì xảy ra. Bùm và sau đó ứng dụng của bạn bị crash
… Có hàng ngàn trường hợp như thế này, nó tạo là một trải nghiệm xấu, một ấn tượng xấu cho người dùng khi sử dụng ứng dụng của bạn tạo ra.
Và nếu như bạn thao tác với bất đồng bộ thì callback
là cái phải có, để mọi thành phần trong mô hình hoạt động đúng như ý đồ đặt ra.
Chúng ta quay lại với Project demo của mình, yêu cầu tiếp theo là nhấn vào Button Login. Vì hiện tại mình sử dụng dữ liệu giả để test, nên xử lý xử kiện này chúng ta cũng giả định như sau:
- Kiểm tra
username
&password
với dữ liệu giả cho trước - Sau 3 giây, sẽ phản hồi lại cho người dùng biết
- Thành công sẽ chuyển qua HomeViewController
- Thất bại sẽ báo alert với thông tin lỗi
Mở file LoginViewModel, thêm function cho việc xử lý sự kiện login.
func login() -> AnyPublisher<Bool, Never> { }
Điều chú ý ở đây chính là việc return
về một Publisher, với:
- Output là
Bool
, cho biết việc đăng nhập thành công hay thất bại - Failure là
Never
Việc trả về là Publisher thì đây chính là callback
trong Combine. Các thao tác có thể ở 1 thời điểm nào đó trong tương lai, việc còn lại là bạn cần phải subscribe
tới nó và đón chờ giá trị phát ra.
Vấn đề
error
, mình sẽ không đề cập trong bài này, nhằm tiết kiện thời gian và công sức. Bạn có thể xem lại Phần 2 với việc Handling Error trong bài Networking.
Tiếp theo, thêm phần code xử lý logic cho việc đăng nhập. Bạn thêm đoạn code sau vào function vừa mới tạo ra:
func login() -> AnyPublisher<Bool, Never> { if isLoading { return $isLoading.map { !$0 }.eraseToAnyPublisher() } isLoading = true // test let test = username == "fxstudio" && password == "123456" let subject = CurrentValueSubject<Bool, Never>(test) return subject.delay(for: .seconds(3), scheduler: DispatchQueue.main).eraseToAnyPublisher() }
Trong đó:
isLoading
sẽ hiển thị cái view tròn tròn xoay xoay, cho người dùng biết trạng thái đang chờ xử lý- Kiểm tra với 2 dữ liệu giả là
fxstudio
&123456
(thông tin đăng nhập) - Toán tử
delay
để sau 3 giây thì phát ra giá trị - Bạn đừng quên toán tử
eraseToAnyPublisher
để xoá các dấu vết lưu lại
Cuối cùng là phần sử dụng function này ra sao. Bạn mở file LoginViewController, tại function IBAction của button login, bạn tiến hành gọi function login
mới của ViewModel.
@IBAction func loginButtonTouchUpInside(_ sender: Any) { viewModel.login() .sink { [weak self] done in self!.viewModel.isLoading = false if done { let vc = HomeViewController() self?.navigationController?.pushViewController(vc, animated: true) } else { self?.alert(title: App.Text.appName, text: "Login failed.") } } .store(in: &subscriptions) }
Vì giá trị trả về của login
là Publisher, nên việc xử lý ở View chỉ là subscribe
nó thôi. Sử dụng sink
vì có phần xử lý error trong này. Bạn kiểm tra điều kiện done
, để có hành động tương tứng.
- Đúng sẽ là
push
màn hình HomeViewController vào - Sai sẽ hiển thị
alret
(được đề cập trong phần Overview của MVVM mới)
Build ứng dụng và tiến hành kiểm tra hoạt động, bạn nên test với vài trường hợp thành công và thất bại.
Trong bài sử dụng dữ liệu giả, nếu thay chúng nó bằng việc tương tác API và nhận dữ liệu từ server trả về. Thì chúng ta đã hoàn thiện việc xử lý sự kiện người dùng cộng với tương tác API rồi đó.
4. Handle actions
Chắc bạn cũng thắc mắc cho các phần ở trên:
Thèn Combine đầu rồi, sao toàn là View gọi function của ViewModel hết vậy.
Để lý giải thì 2 phần trên cho bạn cách tạo function xử lý và cách gọi chúng. Đơn giản, chúng ta cần có sự tích hợp được Combine Code trong project của bạn. Nó sẽ thật sự phát huy hiệu quả cao nhất, khi bạn có thể dùng nó cho nhiều trường hợp nhất.
Bây giờ, chúng ta sẽ quản lý tập trung và theo tư tưởng Combine để giải quyết bài toán sự kiện người dùng này. Bạn mở file LoginViewModel và tới function processAction
, edit lại như sau:
private func processAction(_ action: Action) { switch action { case .login: print("ViewModel -> Login") _ = login().sink { done in self.isLoading = false if done { self.state.value = .logined } else { self.state.value = .error(message: "Login failed.") } } case .clear: self.clear() } }
Tương ứng với mỗi giá trị của action
phát đi, thì chúng ta sẽ gọi các function xử lý tương ứng. Cũng không quá khó hiểu ở đây. Cuối cùng, tiến hành cập nhật thêm việc phát giá trị của action
tại LoginViewController.
- Các phản ứng với hành động đã được cài đặt theo
state
của đối tượng ViewModel (ở bài trước)
@IBAction func loginButtonTouchUpInside(_ sender: Any) { viewModel.action.send(.login) } @objc func clear() { viewModel.action.send(.clear) }
Mọi thử đã đi vào lại quỹ đạo của mô hình MVVM với Combine. Các sự kiện người dùng đều được quản lý tập trung. Các phản hồi sẽ tự động phản ứng lại với dữ liệu nhận được từ các Publisher.
Ngoài ra, cũng tách biệt việc điều hướng trong luồng sự kiện
. Giúp cho việc debug và quản lý View dễ dàng hơn. Cũng như sau này bạn có khái niệm về việc nâng cấp tiếp mô hình từ MVVM lên VIPER.
Bạn build lại project và tận hưởng kết quả nào!
OKAY! Mọi thứ đã ổn và mình xin hết thúc bài viết này ở đây. Bạn đã giải quyết cơ bản được các sự kiện người dùng, cũng như việc quản lý tập trung chúng. Và phần hay vẫn còn ở sau. Bạn hãy đón chờ xem nó.
Bạn có thể checkout project demo tại đây:
Tạm kết
- Sơ đồ hoạt động của
luồng sự kiện
và cáchluồng dữ liệu
di chuyển - Các cách cài đặt các function xử lý sự kiện người dùng với callback và không dùng callback
- Quản lý tập trung các sự kiện người dùng trong mô hình MVVM với Combine
Nếu bạn thấy bài viết này hay và hưu ích, thì hãy share cho nhiều người cùng đọc. Nếu bạn muốn đóng góp hoặc góp ý cho mình, thì hãy để lại comment hoặc email hoặc theo contact của website.
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)