[アーキテクチャ]MVVM


MVVM?

  • Model
  • View
  • ViewModel
  • 上記3つの要素からなるパターンをMVVMと呼ぶ.
    Model
    データ構造と処理
    View
    画面表示とユーザーインタラクションを担当
    ViewModel
    ビューの抽象化;データをビューに変換する
    ビジネスロジックは依然としてViewModelが担当しています.
    ただ論理的な観点がデータ中心に移った点には大きな違いがある.
    ビューとViewModelの依存性を解消します.
    また、データバインドとコマンドモードの使用にも注意してください.

    インプリメンテーション


    以前MVC記事で実現した機能をMVVMに変換する.
    実際の動作結果はMVCと同じであるが,内部の実現差を見るためである.
    IOSでは、MVVMは少し異質な感じがします.
    概念的には、ビューの機能がViewとViewコントローラに分かれているためです.
    SWIFTUIを使用する場合、MVVMのメリットは明らかになるはずです.

    Model

    struct Repository: Codable {
        let id: Int
        let node_id: String
        let full_name: String
        let description: String?
        let url: String
        let html_url: String
        let created_at: String
        let updated_at: String
    }
    struct SearchResponse: Codable {
        var total_count: Int = 0
        var incomplete_results: Bool = false
        var items: [Repository] = []
    }

    View

    class SearchTableViewCell: UITableViewCell {
        @IBOutlet weak var nameLabel: UILabel!
        @IBOutlet weak var descriptionLabel: UILabel!
        
        func setData(data: Repository) {
            nameLabel.text = data.full_name
            descriptionLabel.text = data.description
        }
    }
    ここで、ViewControlはViewカテゴリに相当します.
    ViewControllerは、ViewModelに関する情報を持っていますが、Input、Outputプロトコルで定義されているものに限ります.
    依存注入に変更することもできます.
    class SearchViewController: UIViewController {
        var viewModel: (SearchViewModelInput & SearchViewModelOutput) = SearchViewModel()
    }
    イベント発生時に対応するビジネスロジックを呼び出すことをビューで実現します.
    制御ロジックの位置はViewControlからViewModelに移動します.
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        self.viewModel.fetchRepositories(text: searchBar.text)
    }
    データバインディングはKVO,Notification,Rx,Combineなどで実現できる.
    ここではCombineを使いました.
    self.viewModel.repositoriesPublisher
        .receive(on: DispatchQueue.main)
        .sink { [weak self] repositories in
            self?.displayedRepositories = repositories
            self?.tableView.reloadData()
        }.store(in: &cancelBag)

    ViewModel


    データのクエリー、管理などのロジックを実現します.
    ViewModelは自分のビューを参照する存在を知らない.
    class SearchViewModel: SearchViewModelProtocol {
        @Published var repositories: [Repository] = []
        var repositoriesPublisher: Published<[Repository]>.Publisher { $repositories }
        
        var cancelBag = Set<AnyCancellable>()
        
        func fetchRepositories(text: String?) {
            var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
            urlComponents?.queryItems = [
                URLQueryItem(name: "q", value: text)
            ]
            
            URLSession.shared.dataTaskPublisher(for: (urlComponents?.url)!)
                .map { $0.data }
                .decode(type: SearchResponse.self, decoder: JSONDecoder())
                .map { $0.items }
                .replaceError(with: [])
                .assign(to: \.repositories, on: self)
                .store(in: &cancelBag)
        }
    }

    結果



    完全なコードは羽状バニラにあります.