はじめに
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)
}
}
}
実際の動作
終わり
今回はiOS15だけのため、ScrollViewのrefreshableを実装しました。
呼び出し元のoverlayをモディファイのすることをおすすめします。
参考リンク
https://qiita.com/ruwatana/items/0598af785f19ed907e81
https://yanamura.hatenablog.com/entry/swiftui-introspect