LoginSignup
85
71

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-25

はじめに

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

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

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

備忘録

@Stateとは?

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

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

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

toggle_sample

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

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

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

@ObservedObjectとは?

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

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

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

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

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("スコアをつける")
            }
        }
    }
}

このようになります。
score_sample

@EnvironmentObjectとは?

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

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

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

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

class UserSettings: ObservableObject {
    @Published var score = 0
}

アプリ内のどこからでも共有インスタンスにアクセスできるようにしたいので、SceneDelegate.swiftでこのインスタンスを作成して設定します。

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

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

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

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

@EnvironmentObject var settings: UserSettings

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

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する")
            }
        }
    }
}

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

score_environment_sample

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

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

備考

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

Qiita記事取得サンプルについて

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

ディレクトリ構成

スクリーンショット 2019-09-25 19.13.17.png
このように変更させていただきました。

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

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

まとめ

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

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

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

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

参考

github

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

85
71
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
71