+6

Đơn giản hoá mô hình MVVM + SwiftUI

1. Tổng quan

SwiftUI là một framework cung cấp cho chúng ta các công cụ để xây dựng các giao diện người dùng trên các nền tảng khác nhau, bao gồm iOS, macOS và watchOS. Các ứng dụng được xây dựng trên SwiftUI có thể sử dụng một số kiến trúc khác nhau tùy thuộc vào yêu cầu và quy mô của dự án (MVVM, Redux, IVO ...). Tuy nhiên, kiến trúc MVVM là một kiến trúc được khuyến khích bời Apple và được sử dụng rộng rãi trong các dự án SwiftUI.

Vậy nên, trong bài viết hôm nay chúng ta cùng tìm hiểu về kiến trúc này một cách thật đơn giản nhé. Let's do it !!!!

2. Quan hệ giữa các thành phần trong mô hình MVVM

mvvm-swiftui-architecture.png

  • Model có nhiệm vụ:

    • Định nghĩa ra các cấu trúc dữ liệu ánh xạ từ Local Database (lưu trên thiết bị: Realm, CoreData, SQLite, ... ) hoặc Remote Database (API, ...)
    • Tương tác với dữ liệu (CRUD)
  • ViewModel:

    • ViewModel chứa các logic nghiệp vụ của ứng dụng, thường được triển khai là một ObservableObject có chứa các @Published properties.
    • Tầng này có thể lắng nghe các sự thay đổi từ Model và thể hiện các sự thay đổi đó lên View.
  • View:

    • View có nhiệm vụ định nghĩa ra các trúc của UI (View, Text, Button, ...), animation, etc.
    • View liên kết với ViewModel thông qua @ObservedObject để có thể lắng nghe các sự thay đổi được phản hồi từ ViewModel.
    • Ngoài ra, View còn có nhiệm vụ nhận các sự kiện tương tác từ người dùng để truyền về ViewModel xử lý logic

3. Các triển khai MVVM với SwiftUI

Chúng ta sẽ cùng nhau triển khai kiến trúc MVVM kết hợp với SwiftUI một các đơn giản thông qua ví dụ dưới đây

3.1 Tạo một Model

  • Tạo ra struct Item để định nghĩa cấu trúc dữ liệu của data (có thể là từ API hoặc Local Database)
  • Tạo ra class ItemStore có nhiệm vụ gọi dữ liệu bằng method fetchItems() và lưu dữ liệu vào property items
import SwiftUI

/**
 Model
 * Item model define the data
 * ItemStore define how to fetch the datas
 */

struct Item: Identifiable {
    let id = UUID()
    let name: String
}

class ItemStore {
    @Published var items = [Item]()

    func fetchItems() {
        // Fetch items from server
        items = [Item(name: "Item 1"), Item(name: "Item 2")]
    }
}

Lưu ý: property items trong class ItemStore được bọc bởi @Published để ViewModel có thể lắng nghe được sự thay đổi của dữ liệu từ tầng Model

(ở đây chính là danh sách Item được lấy từ API về và lưu trong biến items)

3.2 Tạo một ViewModel

  • Bước 1: Chúng ta tạo ra 1 lớp ItemViewModel conform ObservableObject protocol.
  • Bước 2: Chúng tao tạo ra biến items có kiểu dữ liệu là mảng Item và được bọc bởi @Published.
  • Bước 3: khởi tạo object itemStore từ class ItemStore (lớp này được định nghĩa ở tầng Model) có nhiệm vụ tương tác với dữ liệu tại tầng Model
  • Bước 4: tạo ra 1 biến cancellables là một Set các AnyCancellable giúp chúng ta quản lý các subscription và đảm bảo rằng các subscription này sẽ được huỷ khi không cần sử dụng đến. (subscription ở trong ví dụ này chính là itemStore.$items)
  • Bước 5 (bước cuối cùng): tại hàm init của class ItemViewModel chúng ta assign cho items trong lớp ItemViewModel bằng dữ liệu sau khi được gọi từ API ở tầng Model
import SwiftUI
import Combine

/**
 ViewModel
 * ViewModel (ItemViewModel) that observes changes in the Model and exposes properties to the View
 */

class ItemViewModel: ObservableObject {
    @Published var items = [Item]()

    private var itemStore = ItemStore()

    func fetchItems() {
        itemStore.fetchItems()
    }

    private var cancellables = Set<AnyCancellable>()

    init() {
        itemStore.$items
            .assign(to: \.items, on: self) // assign data from API to items
            .store(in: &cancellables)
    }
}

3.3 Tạo một View

Trên tầng View, đầu tiên chúng tạo ra 1 ItemListView conform View protocol, sau đó bên trong struct ItemListView chúng ta tạo ra một biến viewModel có kiểu dữ liệu là ItemViewModel và được bọc bởi @ObservedObject . Điều này nhằm đảm bảo mỗi khi viewModel thay đổi thì View hoàn toàn có thể lắng nghe được.

import SwiftUI

/**
 ViewModel
 * View (ItemListView) that binds to the ViewModel and displays the data
 */

struct ItemListView: View {
    @ObservedObject var viewModel: ItemViewModel

    var body: some View {
        List(viewModel.items) { item in
            Text(item.name)
        }
        .onAppear {
            viewModel.fetchItems()
        }
    }
}

struct ItemListView_Previews: PreviewProvider {
    static var previews: some View {
        ItemListView(viewModel: ItemViewModel())
    }
}

Giải thích behavior cho ví dụ trên:

  • Đầu tiên khi khởi tạo ItemListViewviewModel danh sách items dưới ViewModel sẽ rỗng -> tức là View cũng sẽ hiển thị 1 danh sách rỗng.
  • Khi ItemListView xuất hiện (Appear) hàm fetchItems() ở tầng ViewModel sẽ được gọi -> sau một khoảng thời gian, khi nhận được dữ liệu [Item] từ API trả về thì ItemListView sẽ được hiển thị với 1 danh sách có số lượng phần tử bằng với số lượng phần tử có trong response của API.

Trong ví dụ này, do chúng ta mock và return ngay 1 mảng Item ở trong hàm fetchItems() nên mắt chúng ta không kịp nhận ra sự khác biệt trong quá trình load dữ liệu. Trong tình huống thực tế, đôi khi mạng chúng ta chậm hoặc câu truy vấn của chúng ta phức tạp thì sự khác biệt này sẽ được thể hiện rõ ràng.

3.4 Unit Test

Việc viết Unit test trong ứng dụng thực tế vô cùng quan trọng. Dưới đây là cách triển khai Unit test ViewModel của một ứng dụng SwiftUI kết hợp mô hình MVVM.

Ở trong ví dụ trên, khi hàm testFetchItems() được gọi thì danh sách items sẽ được gán bởi 2 phần tử [Item(name: "Item 1"), Item(name: "Item 2")]. Vì vậy chúng ta viết hàm test_fetchItems() để kiểm tra xem sau khi gọi hàm testFetchItems() thì danh sách items đã thực sự có dữ liệu hay chưa.

Gét gô !!! Easy lắm.

import XCTest
@testable import simple_mvvm_swiftui

class ItemViewModelTest: XCTestCase {
    
    var viewModel: ItemViewModel!
    
    override func setUp() {
        super.setUp()
        viewModel = ItemViewModel()
    }
    
    func test_fetchItems() {
        // act
        viewModel.fetchItems()
        
        // assert
        XCTAssertTrue(!viewModel.items.isEmpty)
    }
}

4. Tổng kết & tài liệu tham khảo

  • Qua bài viết này, chúng ta đã cùng nhau tìm hiểu và các thành phần của mô hình kiến trúc MVVM và cách triển khai kiến trúc MVVM với framework SwiftUI. Nếu thấy bài viết hay và bổ ích các bạn hãy cho mình 1 upvote và follow để mình có thêm động lực viết bài nha ❤️
  • Gửi các bạn code tham khảo: https://github.com/thuannv-1/simple-mvvm-swiftUI

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.