LoginSignup
1
3

More than 1 year has passed since last update.

SwiftUIでswipe→progress view表示→データ更新→更新完了次第 progress view非表示を実現する

Last updated at Posted at 2021-06-06

環境

Xcode…Version 12.5 (12E262)

やりたかったことと経緯

ScrollViewにデータを表示しておいて、スワイプされたらデータ更新。
progress view(くるくる)を表示する。
新しいデータを取得完了次第progress viewを表示終了する。

というのを実現したかったのだが、そもそもSwiftUIはprogress view単体はあるもの(ProgessViewというstruct)の、「データ更新終了次第消す」機能どころか「スワイプされたらくるくるを出す」機能(androidでいうSwipeRefreshLayout、 UIKitのiOSでいうUIRefreshControl?)すらまだデフォルトでの用意はないらしい。なので自作した。

コード

1.progress viewを表示する側の準備をする①

struct TopView: View {
    @StateObject var apiModel :ApiModel 


    var body: some View {
        VStack{
            if(self.apiModel.memos.count>0){
                MemoList(memos: self.apiModel.memos).environmentObject(apiModel)
            }else{
                EmptyView()
            }
        }
        .coordinateSpace(name: "parent")
        .onAppear(){
            // 表示したら読み込み開始
            self.apiModel.load()
        }
    }
}

class ApiModel: ObservableObject {
    @Published var memos : Array<String> = []

    init() {

    }


    private let API_BASE =
        "https://hogehoge"

    private let API_PATH_GET_ALL_DATA="/apiNameGetAll"


    public func load(memosContainer : Binding<Array<String>>? = nil) {
        let url = URL(string: API_BASE+API_PATH_GET_ALL_DATA)!


        URLSession.shared.dataTask(with: url) { data, response, error in
            print("data: \(String(describing: data))")
            do{
                let allData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
                print(allData) // Jsonの中身を表示
            }
            catch {
                print(error)
            }
            DispatchQueue.main.async {
                if(memosContainer == nil){
                    self.memos = try! JSONDecoder().decode(Array<String>.self, from: data!)
                }else{
                    memosContainer!.wrappedValue =    try! JSONDecoder().decode(Array<String>.self, from: data!)
                }
            }
        }.resume()
    }

}

  • TopView : 大元のView。MemoListはScrollViewを持ち、中でデータをViewにして表示している。(具体的コードは後述。)
  • ApiModel : http通信をして外からデータを取得してくるメソッドload(memosContainer : Binding<Array<String>>? = nil)と取得してきたデータの入っている@Published var memos : Array<String>を持つ。
  • load(memosContainer : Binding<Array<String>>? = nil)は、memosContainerが渡されなかったら取得してきたデータを@Published var memos : Array<String>に入れるが、not-nilだったら、memosContainerにデータを入れる。
  • 以上のsnipetでのポイントは.coordinateSpace(name: "parent")。これがあとでprogress viewを出す時にきいてくる。

2.progress viewを表示するための準備②+データ更新終了時にprogress viewの表示を消す部分の実装

struct  MemoList:View {
    var memos : Array<String>
    @EnvironmentObject var apiModel :ApiModel

    init(memos : Array<String>) {
        self.memos = memos
    }

    var body: some View{
        var swipe : SwipeRefreshController?

        // 中間橋渡しをしてくれるオブジェクトを作ることで、データが更新されたらクルクルを消せるようにする。
        let p = Binding<Array<String>>(get : {
            return self.apiModel.memos
        },set : { memos in
            swipe?.finishRefresh() // データ取得が終わったのでクルクルを止める
            self.apiModel.memos = memos

        })

        swipe = SwipeRefreshController(coordinateSpaceName:"parent"){
            self.apiModel.load(memosContainer: p)
        }

        return ScrollView(.vertical){
            VStack{
                swipe

                ForEach(self.memos, id: \.self){ memo in
                    Text(memo)


                }
            }
        }
    }
}
  • MemoList : ScrollViewにデータを表示していくView。
  • また、今回の主役のprogress viewをもつViewであるSwipeRefreshControllerもScrollViewの中に入れて表示する。
  • SwipeRefreshControllerはinit時に(coordinateSpaceName : String,onRefresh : @escaping (()->Void)を引数に取る。これは引っ張られたかどうか判断するために親のcoordinateSpaceのnameを渡すためのcoordinateSpaceName : Stringと、progress viewを表示した時の表示開始(正確にはスワイプされた時)コールバックのonRefresh : @escaping (()->Void)。(コード詳細後述)
  • また、SwipeRefreshControllerはfinishRefresh()を呼ぶとprogress viewを表示終了する。(コード詳細後述)
  • 以上のsnipetでのポイントは、まず、「progress viewに表示するための準備①」でTopViewに設定したcoordinateSpace nameの"parent"SwipeRefreshControllerインスタンス作成時に渡していること。
  • 更に
let p = Binding<Array<String>>(get : {
            return self.apiModel.memos
        },set : { memos in
            swipe?.finishRefresh() // データ取得が終わったのでクルクルを止める
            self.apiModel.memos = memos

        })

を作成し、SwipeRefreshControllerのprogress view表示開始コールバックにself.apiModel.load(memosContainer: p)を設定したこと。これにより、スワイプされてprogress viewが表示されたら、データの更新ApiModel#load(memosContainer : Binding<Array<String>>? = nil)が呼ばれ、さらに今回はmemosContainer!=nilなのでデータ更新され次第pにとってきたデータが入り、p#setが呼ばれる。

  • そしてp#setの中でSwipeRefreshController#finishRefresh()を呼ぶことで、「データの更新が終了し次第progress viewの表示を終了」を実現。さらにself.apiModel.memos = memosを呼ぶことでTopViewMemoListが表示に使用しているおおもとのデータも更新する。

3.スワイプされたら感知してprogress viewを表示するViewの作成

class RefreshManager : ObservableObject {
    @Published var isRefreshing = false
}

// スワイプしたら更新中のクルクルをだしてリフレッシュコールバックを呼ぶためのView
struct SwipeRefreshController: View {
    init(coordinateSpaceName : String,onRefresh : @escaping (()->Void)) {
        // クロージャがスコープ外でも生きる時は@escapeが必要 https://qiita.com/mishimay/items/1232dbfe8208e77ed10e#%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E3%81%A8%E3%81%8D%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%8B
        self.coordinateSpaceName = coordinateSpaceName
        self.onRefresh = onRefresh
        //isRefreshing = false
    }

    @ObservedObject var refreshManager = RefreshManager() // @Stateでただのbool値にしていると、外からfalseにしても反映されなかったので、ObservaleObjectクラスを作ってその中のプロパティにする。

    // クルクルを消す
    public func finishRefresh(){
        print("finishRefresh")
        self.refreshManager.isRefreshing = false
    }
    var coordinateSpaceName: String
    var onRefresh: () -> Void // スワイプをした時のコールバック


    var body: some View {
        return GeometryReader { geometry in
            VStack{
                if geometry.frame(in: .named(coordinateSpaceName)).midY > 100 {
                    Spacer()
                        .onAppear() {
                            //isRefreshing = true
                            self.refreshManager.isRefreshing = true
                            onRefresh()
                        }
                }
                if (self.refreshManager.isRefreshing) {
                    HStack {
                        Spacer()

                        ProgressView()

                        Spacer()
                    }
                }
            }
        }
    }
}
  • 渡されたcoordinateSpaceNameと、GeometryReaderでスワイプされたかどうか判定して、スワイプされたらclass RefreshManager : ObservableObjectに持たせたデータ更新中フラグの@Published var isRefreshingをたて、init時に渡されていたスワイプ時コールバックを呼ぶ。
  • また、isRefreshingフラグが立ったらprogress viewを表示し、おろされたら表示を終了する。

以上で、
ScrollViewにデータを表示しておいて、スワイプされたらデータ更新。
progress view(くるくる)を表示する。
新しいデータを取得完了次第progress viewを表示終了する。

を実現できました。

参考にしたもの

https://www.yururiwork.net/%E3%80%90swiftui%E3%80%91pull-to-refresh%EF%BC%88uirefreshcontrol%EF%BC%89%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B/
https://qiita.com/masa7351/items/0567969f93cc88d714ac

1
3
0

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
1
3