【SwiftUI】Pull to refresh(UIRefreshControl)を実装する

UIKit ではテーブルデータのリロード機能の実装に UIRefreshControl を使用していました。

SwiftUI では代わりとなる UI がまだ提供されていません(2021年4月現在)。

UIRefreshControl をラップした UIViewRepresentable プロトコルに沿った View を作成することで実現することができるようですが、

純粋な SwiftUI の機能の組み合わせだけで実現することもできます。

【SwiftUI】Pull to refresh(UIRefreshControl)を実装する

以下のような RefreshControl という View 部品を作成しました。

struct RefreshControl: View {
    
    @State private var isRefreshing = false
    var coordinateSpaceName: String
    var onRefresh: () -> Void
    
    var body: some View {
        GeometryReader { geometry in
            if geometry.frame(in: .named(coordinateSpaceName)).midY > 50 {
                Spacer()
                    .onAppear() {
                        isRefreshing = true
                    }
            } else if geometry.frame(in: .named(coordinateSpaceName)).maxY < 10 {
                Spacer()
                    .onAppear() {
                        if isRefreshing {
                            isRefreshing = false
                            onRefresh()
                        }
                    }
            }
            HStack {
                Spacer()
                if isRefreshing {
                    ProgressView()
                } else {
                    Text("⬇︎")
                        .font(.system(size: 28))
                }
                Spacer()
            }
        }.padding(.top, -50)
    }
}

ポイントは、GeometryReader です。

GeometryReader は、親View の位置や、子View の相対的な位置などを取得するための View の一種です。

GeometryReader についてはこちらの Qiita 記事が大変参考になりますのでご一読ください。

この RefreshControl は ScrollView の子View として利用します。GeometryReader 情報(GeometryProxy)を使って親である ScrollView の位置を把握し、PullToRefreshの動きをコントールしています。

また、RefreshControl に取っては親がどの View なのかはわからないため、予め親View の frame に名前(coordinateSpaceName)を設定し、その名前の frame を元に判断をしています。

PullToRefresh の簡単な流れは以下のようになります。

  1. ScrollView を下に引っ張る
  2. ScrollView の Y位置が 50 を超えたら isRefreshing が true になる
  3. ScrollView から指を離す
  4. ScrollView の Y位置が 10未満になった時 isRefreshing が true になっていたら onRefresh を実行

実際に利用する時は以下のように使います。

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack {
                RefreshControl(coordinateSpaceName: "RefreshControl", onRefresh: {
                    print("doRefresh()")
                })
                Text("test01")
                Text("test02")
                Text("test03")
                Text("test04")
                Text("test05")
                Text("test06")
                Text("test07")
                Text("test08")
                Text("test09")
            }
        }.coordinateSpace(name: "RefreshControl")
    }
}

ScrollView の coordinateSpace(name: String) プロパティに名前を設定し RefreshControl 側で ScrollView を把握しますので、RefreshControl に渡す coordinateSpaceName同じにしておかなければなりませんので注意してください。

以上