Skip to content
This repository has been archived by the owner on Sep 3, 2023. It is now read-only.

Latest commit

 

History

History
363 lines (273 loc) · 11.8 KB

File metadata and controls

363 lines (273 loc) · 11.8 KB

APIから商品の一覧を読み込む

chapter3が終わってない人は、いったん出来たところまでコミットしてください。 その後

git checkout ch4-start

とすると、このchapterが開始できるところまでコードが進みます。

前章ではXcode Previewでダミーのデータを表示するところまで作りました。 この章ではAPIを叩いた結果をリストに表示します。 さらにプレビューでなくシミュレータで表示することを目指します。

この章ではAPIから取得した結果をViewに反映させる作業を通して、

  • イベントに反応して処理を進める方法
  • apollo-clientを使ってネットワークリクエストを送るを方法
  • ViewにStateを持たせ、それを表示に反映させる方法

を、学びます

講義: 状態とデータフロー

APIから引いてきた結果を表示するためには、Viewが「状態」を持つ必要があります。 SwiftUIでも外部からのイベントやユーザーのアクションによって、データを変更し、その影響を受ける部分を更新する仕組みが備わっています。

いくつかの手段が用意されていて、実装と特徴を紹介していきます。

State

Viewのプロパティの変更を検知する仕組みです。 @State属性がつけられたプロパティの値が変わると、そのプロパティを参照しているViewがSwiftUIのフレームワークによって再描画されます。

struct MyView: View {
    @State var count: Int = 0
    var body: some View {
        VStack {
            Text("\(count)")
            Button(action: { self.count += 1 }) { 
                Text("Increment")
            }
        }
    }
}

ObservableObject + (Environment|Observed|State)Object

別のクラスのインスタンスのプロパティの変更を検知する仕組みです。 例として、現在値を持つCounterクラスのcountプロパティの変更を監視して、Viewの再描画を行うケースを考えます。

class Counter {
    var count: Int = 0
    func increment() {
        count += 1
    }
}

struct MyView: View {
    var counter: Counter
    var body: some View {
        VStack {
            Text("\(counter.count)")
            // counter.incrementが呼び出されたら再描画されてほしい
            Button(action: { counter.increment() }) { 
                Text("Increment")
            }
        }
    }
}

まず、監視対象のクラスはObservableObjectプロトコルに準拠させる必要があります。そのクラスが持つプロパティの内、@Published属性がつけられたプロパティのみが監視の対象になります。

このような実装になります。

import SwiftUI
import Combine

class Counter: ObservableObject {
    var step: Int = 1 // これが変わってもViewは再描画されない
    @Published var count: Int = 0 // @Publishedがつけられているので変更されるとViewが再描画される
    func increment() {
        count += step
    }
}

このように定義したクラスをViewにプロパティとして持たせて利用します。その際、@ObservedObject, @StateObject, @EnvironmentObjectの3つのどれかの属性をつけることで、@Published属性がつけられたプロパティの変更時にViewが再描画されるようになります。

それぞれ特徴があるので、以下で説明していきます。

ObservedObject

ObservableObjectに準拠したクラスのインスタンスを監視します。 どこか別の場所で保持されているインスタンスを親Viewから渡してもらう等して、受け取る必要がある。 CounterクラスはこのView自身が作ってもうまく動きません。Viewの再描画とともにインスタンスも再生成されてしまうからです。

struct MyView: View {
    @ObservedObject var counter: Counter // = Counter() としてはダメ
    var body: some View {
        VStack {
            Text("\(counter.count)")
            Button(action: { counter.increment() }) { 
                Text("Increment")
            }
        }
    }
}

struct SomeView: View {
    var body: some View {
        MyView(counter: Counter.shared) // 1例としてsingletonを渡しているだけ。こうする必要はない
    }
}

StateObject

ObservableObjectに準拠したクラスのインスタンスを監視します。 ObservedObjectと違い、View自身が直接ObservableObjectをインスタンス化することができます。

struct MyView: View {
    @StateObject var counter = Counter()
    var body: some View {
        VStack {
            Text("\(counter.count)")
            Button(action: { counter.increment() }) { 
                Text("Increment")
            }
        }
    }
}

EnvironmentObject

ObservableObjectに準拠したクラスのインスタンスを監視します。 自分より上位の階層でenvironmentObject`モディファイアで該当のクラスのインスタンスを渡しておく必要があります。 これによりStateをバケツリレーしていくことを防げます。

struct MyView: View {
    @EnvironmentObject var counter: Counter
    ...
}
...

struct MyApp: App {
    @StateObject var counter = Counter()
    var body: some Scene {
        WindowGroup {
            NavigationView {
                MyView()
            }
            .environemntObject(counter)
        }
    }
}

Binding

@State@Publishedでマークした変数を別のViewに更新させることもできます。

struct IncrementButton: View {
    @Binding var count: Int
    var body: some View {
        Button(action: { self.count += 1 }) {
            Text("Increment")
        }
    }
}

struct MyView {
    @State var count: Int = 0
    @ObservedObject var counter: Counter
    var body: some View {
        VStack {
            VStack {
                Text("\(count)")
                // @State属性の変数を@Binding属性の変数にわたす場合`$`をつける
                IncrementButton(count: $count)
            }
            VStack {
                Text("\(counter.count)")
                IncrementButton(count: $counter.count)
            }
        }
    }
}

参考資料

ハンズオン

それでは、これらを利用してAPIから取ってきた結果をViewに反映させるようにしてみます

資料は@Stateで進めますが、ObservableObjectを使ってみてもかまいません。

シミュレータでの実行でProductListPageViewが表示されるようにする

現在シミュレータを実行して表示されるのはContentViewというHelloWorldが表示される画面です。 先程作ったProductListPageViewを表示させるようにします。

MiniMartApp.swiftを開き、ContentViewProductListPageViewにします。

@main
struct MiniMartApp: App {
    var body: some Scene {
        WindowGroup {
-           ContentView()
+           ProductListPageView()
        }
    }
}

⌘+Rでデバッグ実行をし、空っぽの画面が表示されればOKです。

が、動いているのかいないのかよくわかりませんね。 画面のタイトルを表示するのにはNavigationViewが便利です。 NavigationView配下のViewは、.navigationTitleモディファイアで画面上部にタイトルを表示できるようになります。 画面のタイトルを表示するようMiniMartApp.swift, ProductListPageView.swiftをそれぞれ以下のようにします。

MiniMartApp.swift

@main
struct MiniMartApp: App {
    var body: some Scene {
        WindowGroup {
-           ProductListPageView()
+           NavigationView {
+               ProductListPageView()
+           }
        }
    }
}

ProductListPageView.swift

    var body: some View {
        List(products, id: \.id) { product in
            // 中略
        }
+       .navigationTitle("MiniMart")
    }

⌘+Rでデバッグ実行をし、MiniMartと表示されればOKです。

このMiniMartという文字列が表示されているエリアをNavigationBarと呼びます。NavigationBarにはタイトル等を表示することで、「ユーザーがどこにいるのか」を示すことが役割です。

APIからデータを取得して表示する

まずは更新時にViewが再描画されるようにproducts@State属性をつけます。

struct ProductListPageView: View {
+   @State var products: [FetchProductsQuery.Data.Product] = []
-   var products: [FetchProductsQuery.Data.Product] = []

次に、画面が表示されたタイミングでAPIのリクエストを飛ばし、結果をproductsに代入します。

画面が表示されたを受け取るためにはonAppearというモディファイアを利用します。

    var body: some View {
        // 中略
        }
+       .onAppear {
+       }
        .navigationTitle("MiniMart")
    }

ここに画面が表示された際の処理を書けるので、apiから商品の一覧を引いてくる処理を書きます。

    var body: some View {
        // 中略
        }
        .onAppear {
+           Network.shared.apollo.fetch(query: FetchProductsQuery()) { result in
+               switch result {
+               case let .success(graphqlResult):
+                   self.products = graphqlResult.data?.products ?? []
+               case .failure:
+                   break
+               }
            }
+       }
        .navigationTitle("MiniMart")
    }

ネットワーク周りのコードの解説をします。 Network.shared.apolloはchapter2で作成したApolloClientのSingletonで、第一引数に渡しているFetchProductsQueryは同じくchapter2で生成したgaphqlのクエリを表すクラスです。 fetchは非同期に動作し、結果が返ってくると後置されているクロージャがコールバックされます。

クロージャの第一引数に渡ってきてるresult変数は、Result型と呼ばれる型になっていて、「成功か失敗かどちらかの値を持つ型」を表現することができます。 定義としてはこのようになっています。

public enum Result<Success, Failure> where Failure: Error {
    case success(Success)
    case failure(Failure)
}

これを活用することで、「成功の時の値もエラーの値も両方あるかもしれない/どちらもないかもしれない」というのを防ぐことができ、コードの見通しが良くなります。

今回のケースではSuccessにはGraphQLResult 型が入ってきています。 GraphQLResult型のdataという変数はfetchメソッド呼び出し時のクエリの型の内部構造体のDataという型になるように作られています。 今回はFetchProductsQuery.Data型です。 dataはOptionalなので、もしdataがnilだったら単にから配列を入れるようにしています。

ここまででシミュレータを実行してみましょう。しばらくまった後に結果が表示されればOKです。

とても長い道のりでしたが、これでAPIと通信してリスト画面を表示する機能が出来ました!


Chapter5へ進む