だいたい死んでる

渋谷で働いているWEBプログラマーです。

SwiftUIでMVVM

この記事は ウィルゲート Advent Calendar 2019 の22日目の記事です。

ConbainとSwiftUIが登場したことで、データバインディングの仕組みを公式がサポートしました。 サンプルとしてMVVMでアプリを作ってみようと思います。

作るアプリ

天気を一覧で見れるアプリを作ってみる。 天気情報取得には以下のAPIを使用しました。

openweathermap.org

ディレクトリ構成について

app
- View
- ViewModel
- Model

上表のように各役割によってディレクトリを分ける。 役割ごとに依存の方向は下図のようにしていきます。

f:id:MikaE:20191216000004p:plain

Viewについて

ユーザーへ機能を提供するために描画を担当します。また、ユーザーからの入力を受け取りViewModelへ渡します。 またViewModelのデータとデータバインディングすることでデータの変更と描画の変更を同期させます。

ViewModelについて

Viewを描画するための状態の保持と、Viewから受け取った入力を適切な形に変換してModelに伝達する役目を持っています。 Viewとの通信はデータバインディングの機構を使って行われます。

Modelについて

アプリケーションのドメインを持つのがModelです。ビジネスロジックを書くところになります。また、データの永続化を行う箇所でもあります。

コードについての説明

Model

APIのレスポンス値からCodableのコードを作成してくれる、quicktypeを使用して自動生成できます。 自動生成したので基本的に何も変えずに使用します。

// MARK: - Weathers
struct Weathers: Codable {
    let coord: Coord
    let weather: [Weather]
    let base: String
    let main: Main
    let visibility: Int
    let wind: Wind
    let clouds: Clouds
    let dt: Int
    let sys: Sys
    let id: Int
    let name: String
    let cod: Int
}

// MARK: - Clouds
struct Clouds: Codable {
    let all: Int
}

// MARK: - Coord
struct Coord: Codable {
    let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
    let temp: Double
    let pressure, humidity: Int
    let tempMin, tempMax: Double

    enum CodingKeys: String, CodingKey {
        case temp, pressure, humidity
        case tempMin = "temp_min"
        case tempMax = "temp_max"
    }
}

// MARK: - Sys
struct Sys: Codable {
    let type, id: Int
    let message: Double
    let country: String
    let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
    let id: Int
    let main, weatherDescription, icon: String

    enum CodingKeys: String, CodingKey {
        case id, main
        case weatherDescription = "description"
        case icon
    }
}

// MARK: - Wind
struct Wind: Codable {
    let speed: Double
    let deg: Int
}

ViewModel

データをAPIから取得する処理とModelの型に合わせて変数へ格納する処理を書いていきます。 また、Viewへデータバインドしたいので、PassthroughSubjectとObservableObjectを使用してバインドするための準備もしています。

class WeatherViewModel: Identifiable {
    
    let id = UUID()
    
    let weather: Weather
    
    init(weather: Weather) {
        self.weather = weather
    }
    
    var main: String {
        return self.weather.main
    }
}

class WeathersViewModel: ObservableObject {
    
   let didChange = PassthroughSubject<MemosViewModel,Never>()
    
    init() {
        fetchTopHeadlines()
    }
    
    var weathers = [MemoViewModel]() {
        didSet {
            didChange.send(self)
        }
    }
    
    private func fetchTopHeadlines() {
        let apKey = ""
        let baseURL = "https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid="
        guard let url = URL(string: baseURL + apKey) else {
            fatalError("URL is not correct!")
        }
        
        ApiCommon().loadTopHeadlines(url: url) { weathers in
            
            if let weathers = weathers {
                self.weathers = weathers.map(MemoViewModel.init)
            }
            
        }
        
    }
}

ViewModelをからデータをバインドするためObservedObjectを使用してオブザーバーを使用できるようにします。 こうすることでデータが変更されると自動的にViewが更新されるようになります。

View

import SwiftUI

struct WeathersView: View {
    @ObservedObject var model = WeathersViewModel()
    
    var body: some View {
        List(model.weathers) { weather in
            Text(weather.main)
        }
    }
}

struct MemoView_Previews: PreviewProvider {
    static var previews: some View {
        MemoView()
    }
}

まとめ

SwiftUIを使うことでこれまでよりも簡単にMVVMの実装をすることが出来ました。 その他のアーキテクチャについても実装をしてみます。

明日はcuresevenさんで、『アドベントカレンダー戦略』です。 お楽しみに!