0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI】iOS15 ScrollViewのrefreshableを実装

Last updated at Posted at 2023-09-09

はじめに

iOS15以後、Listに対し、refreshableのモディファイが利用できます。
iOS16の場合、ScrollViewに対してrefreshableのモディファイも利用できます。
今回問題になったのは、iOS15の場合ScrollViewにrefreshableのモディファイをつけても、実際は何の反応もしないこと。

アプローチ

下記の記事には複数の方法でPull to Refreshを実現しました。
https://qiita.com/ruwatana/items/0598af785f19ed907e81
その中「Introspectを使ってSwiftUIのListからUITableViewを取り出す」に着目しました。

IntrospectとはUIViewRepresentableで内包したUIKitのViewを探し出して、動作をカスタマイズするものです。Introspectについて、いろんな紹介記事があるが、最後の参考リンクに追加します。
開発上OSSを利用できない場合は多々あります。今回はIntrospectを利用せず、その原理を利用してScrollViewのrefreshableを実装します。

実装

UIViewRepresentableで見えないViewをScrollViewにOverlayとして追加。そのUIViewRepresentable内、 ScrollViewに内包したUIScrollViewを探し出して、UIRefreshControlを追加するという流れです。

import SwiftUI
import UIKit

struct ScrollViewInspectorView: UIViewRepresentable {
    // 呼び出し元のViewからのハンドラを設定
    var onRefresh: () -> Void
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView(frame: .zero)
        view.isHidden = true
        view.isUserInteractionEnabled = false
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.async {
            guard let viewHost = uiView.superview, let superView = viewHost.superview else { return }
            let targetView = findChild(type: UIScrollView.self, in: superView)
            if let view = targetView as? UIScrollView {
                context.coordinator.scrollView = view
                // UIRefreshControlを入れる
                view.refreshControl = UIRefreshControl()
                view.refreshControl?.addTarget(
                    context.coordinator,
                    action: #selector(context.coordinator.handleRefreshControl),
                    for: .valueChanged
                )
            }
        }
    }
    
    func findChild<T: UIView>(type: T.Type, in view: UIView) -> UIView? {
        for subview in view.subviews {
            if let foundView = subview as? T {
                return foundView
            } else if let foundView = findChild(type: type, in: subview) {
                return foundView
            }
        }
        return nil
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onRefresh: onRefresh)
    }
    
    class Coordinator: NSObject {
        var onRefresh: () -> Void
        var scrollView: UIScrollView?
        
        init(onRefresh: @escaping () -> Void, scrollView: UIScrollView? = nil) {
            self.onRefresh = onRefresh
            self.scrollView = scrollView
        }
        
        @objc func handleRefreshControl() {
            onRefresh()
            self.scrollView?.refreshControl?.endRefreshing()
        }
    }
}

呼び出し元:

struct ContentView: View {
    var body: some View {
        ScrollView {
            ForEach(1..<11) { number in
                Text(String(number) + "行目")
            }
        }
        .overlay {
            ScrollViewInspectorView {
                print("refresh")
            }
            .frame(width: 0, height: 0)
        }
    }
}

実際の動作

Simulator Screen Recording - iPhone 13 mini - 2023-09-09 at 20.12.50.gif

終わり

今回はiOS15だけのため、ScrollViewのrefreshableを実装しました。
呼び出し元のoverlayをモディファイのすることをおすすめします。

参考リンク

https://qiita.com/ruwatana/items/0598af785f19ed907e81
https://yanamura.hatenablog.com/entry/swiftui-introspect

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?