SwiftUI+Combineの備忘録とMVVM with Combineチュートリアル for iOSを模倣してQiita記事取得サンプル作ってみた

## はじめに
つい先日、@you_matz さんが[MVVM with Combineチュートリアル for iOS](https://qiita.com/you_matz/items/a3d640be2a8feaf698bd)の記事を投稿されていたので、自分の学習用に少しコードを変更して模写をさせていただきました。
(Qiitaから最新記事を取得してリスト表示するだけの実装に変更しただけです。)

まだまだSwiftUIのキャッチアップが全然できていなく、Combineもあまり理解できていないのでこういったチュートリアルを紹介していただけてとても嬉しく思います。

ただ、上記のチュートリアルは大変、勉強になりましたが、そもそも@Stateや@ObservedObjectなど、基本的なところがわからなかったので、今回は備忘録メインで書かせていただきます。

### 備忘録
## @Stateとは?
SwiftUIから追加されたバインディングで使う修飾子で、
SwiftUIだとViewはstructなので、値の更新ができないけれども
@Stateを宣言すると、値を更新することができるようになります。

以下のように宣言して使用します。

```swift
var body: some View {
    VStack {
      Toggle(isOn: $isOn) {
        Text("トグル")
      }.padding()
      if isOn {
        Text("オン!!!!!!")
      }
    }
```

<img width="300" alt="toggle_sample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/a0a89652-68ea-fad7-6928-3cf129d48ba1.gif">


トグルをタップすると@stateで宣言したプロパティが更新されます。
その後、Stateが定義されているViewが再レンダリングされるという仕組みです。

公式ドキュメントにもあるように、基本的にはViewとその子ビューからしかアクセスできないらしいので、@Stateを使用する時は、privateをつけたほうが良さそうです。

だいたい、ネットにあるサンプルにも@State privateになっていますが、僕のサンプルはprivateを入れ忘れましたorz

## @ObservedObjectとは?
@Stateは単純な値に使う場合に使用しますが、メソッドや複数のプロパティなどを扱う場合は@ObservedObjectを使用します。
こちらは@ObservedObjectを使うプロパティの型がObservableObjectプロトコルに準拠する必要があります。
この場合、プロパティの型はclassに定義します。

例えばこのようにScoreViewModelを定義し、監視対象のプロパティに@Published修飾子をつけて宣言します。
ちなみにscoreViewModelのプロパティはプライベートとして宣言されていないので、
@Satateとは違って他のビューで使用することができます。

```swift
class ScoreViewModel: ObservableObject {
    @Published var score = 0
}
```

そしてScoreViewModelに紐づくView側は以下のように記述しておきます。

```swift
struct ContentView: View {
    @ObservedObject var viewModel = ScoreViewModel()

    var body: some View {
        VStack {
            Text("下のスコアはボタンを押すごとに増えていくよ")
            Text("スコア: \(viewModel.score)")
              .padding(5)
            Button(action: {
                self.viewModel.score += 1
            }) {
                Text("スコアをつける")
            }
        }
    }
}
```
このようになります。
<img width="300" alt="score_sample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/47e001b9-b953-42b5-24d2-980114f58f36.gif">

## @EnvironmentObjectとは?
これを使用してデータを一度親ビューで定義してしまえば、他のビューでもアクセスすることができるようになります。
つまり、これにより必要なビューでモデルデータを共有することができちゃいます。
ただ、@EnvironmentObjectも@ObservedObjectと同じく、クラスにObservableObjectを準拠させる必要があります。

@ObservedObjectを使用するよりも簡潔にかけるようになります。

注意としては、@EnvironmentObjectは一番親ビュー、つまり祖先ビューに定義されている必要があります。
SwiftUIが正しいタイプの@EnvironmentObjectを見つけられない場合、クラッシュしてしまうようです。

まず大元のModelクラスを定義します。

```swift
class UserSettings: ObservableObject {
    @Published var score = 0
}
```
アプリ内のどこからでも共有インスタンスにアクセスできるようにしたいので、SceneDelegate.swiftでこのインスタンスを作成して設定します。

rootViewControllerを設定している箇所で以下のように一番親であるViewに.environmentObject()でインスタンスをセットします。

```swift
window.rootViewController = UIHostingController(rootView: ParentView().environmentObject(settings))
```

こうすることで、以降、このルートにあるビューの子や孫ビューすべてのビューでこのインスタンスを共有することができます。

あとは、使用するビュー側で以下のように@EnvironmentObject付きで定義しておくだけです。

```swift
@EnvironmentObject var settings: UserSettings
```

以下のようにビューとその子ビューを定義してみましょう。

```swift
struct ParentView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        NavigationView {
            VStack {
                Button(action: {
                    self.settings.score += 1
                }) {
                  Text("\(self.settings.score)")
                  Text("スコアを+1する")
                    .padding()
                }

                NavigationLink(destination: DetailView()) {
                    Text("子ビューを表示する")
                }
            }
        }
    }
}

struct ChildView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        VStack {
            Button(action: {
                self.settings.score -= 1
            }) {
              Text("\(self.settings.score)")
                .padding()
              Text("スコアを-1する")
            }
        }
    }
}
```

こうすると以下のようになります。

<img width="300" alt="score_environment_sample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/a44831de-5044-a136-cd75-09db6c15425d.gif">

ParentViewとChildViewでは、scoreのデータの受け渡しは行なっていませんが、それぞれの画面で変更した値がバインドされて共有されていることがわかります。

単純にデータの共有だけならstaticでグローバルで定義すれば済む話ですが、ここで重要なのはビューがその値を変更するたびに、依存するすべてのビューが自動的に更新され、同期されるということです。
まだまだ活用できていませんが、他にも面白い使い方が色々とありそうな機能ですね。

## 備考
他にも色々あるとは思いますが、まだまだキャッチアップ途中なので今回は主なバインディングの修飾子についてだけ残しています。

## Qiita記事取得サンプルについて
こちらは、ほんとに@you_matz さんの記事を模写してちょいちょいっと変更を加えただけですし、備忘録のおまけみたいなものなので、あまり参考にならないかと思いますが、さささっと変更点だけ書かせていただきます。

### ディレクトリ構成
<img width="238" alt="スクリーンショット 2019-09-25 19.13.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/6a21307d-9cb3-de30-6b03-6e9f0daa7bf5.png">
このように変更させていただきました。


### モデルクラスをQiita用に書き換え

```swift
struct QiitaData: Codable {
  var title: String
  var user: User
  var urlString: String
  
  enum CodingKeys: String, CodingKey {
    case title
    case user
    case urlString = "url"
  }
  
  struct User: Codable {
    var name: String
  }
}
```

とりあえずタイトルと、ユーザー名。
あとはsafari起動用にurlを保持するように書き換えました。

### その他
他のところは基本的には模写してリネームしているだけです。

## 完成したQiita記事取得サンプル
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/5d35f792-ff8f-5497-f678-54c5b0dfa557.png)

## まとめ
最新のSwiftUIについてのこうしたチュートリアルはとても貴重なもので大変勉強になりました。

SwiftUIのバグなどもあるようでまだまだ安定しないフレームワークではありますが、 Objective-CからSwiftに移行したように今後SwiftUIが主流になる可能性は多いにあるので、これからも日々キャッチアップに努めたいと思います。

チュートリアルを提供してくださったraywenderlich氏、またそれを紹介して和訳してくださった@you_matzさんありがとうございました。

僕もキャッチアップしながらSwiftUIについてアウトプットしていけたらと思いますのでどうぞよろしくお願いいたします。
また、ご指摘やアドバイス等などあればぜひお願いいたします。
それでは最後までご覧いただきありがとうございました。


## 参考
- raywenderlichによるMVVM-with-Combine-Tutorial-for-iOSのリポジトリ  
 https://github.com/manchan/MVVM-with-Combine-Tutorial-for-iOS
- Qiitaの記事 MVVM with Combineチュートリアル for iOS(@you_matzさんの記事)  
 https://qiita.com/you_matz/items/a3d640be2a8feaf698bd
- Qiitaの記事 SwiftでQiitaのAPIを表示させる。(@dddiskさんの記事)  
 https://qiita.com/dddisk/items/09b46c65434c117f1287
- Qiita API v2 公式ドキュメント  
 https://qiita.com/api/v2/docs

## github

参考にならないと思いますが、一応、githubにも載せてあります。
https://github.com/kazy-dev/SwiftUI-Qiita-Client-App/