はじめに
iOS の ScrollView には、コンテンツの端までスクロールするとバウンドをするようなアニメーションがデフォルトで実装されています。
しかし、アプリによってはコンテンツの端までスクロールしてもバウンドしないように設定したい場合もあると思います。
その場合、どのように実装すればいいのでしょうか?
UIKit の場合 (UIScrollView)
scrollView.bounces = false
を設定することで対応できます。
let scrollView = UIScrollView()
scrollView.bounces = false
SwiftUI の場合 (ScrollView)
2023年11月時点では、ScrollView の bounces が無効となるように設定する modifier が (おそらく) 存在しません。
そのため、bounces を無効化したい場合は少し工夫をする必要があります。
iOS 16.4 以降では scrollBounceBehavior(_:axes:) という modifier が使えますが、これを使っても scrollView.bounces = false
と全く同じ設定にすることはできません
対応策
SwiftUI で ScrollView の bounces を無効化するためには、以下のような方法があります。
- ScrollView の部分を UIScrollView で実装する
-
UIScrollView.appearance().bounces = false
を設定する - SwiftUIIntrospect を利用する
1. ScrollView の部分を UIScrollView で実装する
SwiftUI の ScrollView では実装ができないので、ScrollView の部分を UIScrollView で実装して UIViewRepresentable を使って表示する方法です。
メリット
- 一番安全で確実な方法
- Apple が提供している API だけで実装ができる
- 影響範囲が 1つの ScrollView に限られる
- bounces 以外のプロパティを使った細かい調整もやりやすい
デメリット
- ScrollView で表示するコンテンツを UIView で実装する必要がある
- 中身のコンテンツは SwiftUI で作ることも可能だが、UIHostingController を使って UIView に変換する手間がかかる
2. UIScrollView.appearance().bounces = false
を設定する
SwiftUI の ScrollView は内部で UIScrollView を生成しており、それを wrap して SwiftUI の API として提供しています。
そのため、UIScrollView.appearance()
を使ってアプリ全体の UIScrollView の設定を変更すると SwiftUI ScrollView も変更の影響を受けます。
これを利用して、bounces を無効化します。
struct ContentView: View {
@Environment(\.dismiss) var dismiss
func customDismiss() {
// `dismiss()` を実行すると .onDisappear が呼び出されない
// そのため、カスタムのメソッドを作成し dismiss 実行前に bounces の設定をもとに戻す
UIScrollView.appearance().bounces = true
dismiss()
}
var body: some View {
ScrollView {
...
}
.onAppear {
UIScrollView.appearance().bounces = false
}
.onDisappear {
// UIScrollView.appearance はアプリ全体に影響するので、設定をもとに戻す
UIScrollView.appearance().bounces = true
}
}
}
メリット
-
UIScrollView.appearance().bounces = false
を呼び出すだけで bounces の制御ができるので、実装が簡単
デメリット
-
scrollView.bounces = false
の設定がアプリ内全ての ScrollView に影響してしまう- デグレが発生していないかを確認するのがめちゃくちゃ大変
-
UIScrollView.appearance().bounces = true
を呼び出すタイミングがずれたりするだけで、アプリ全体の動作が大きく変わってしまう
3. SwiftUIIntrospect を利用する
上で説明した通り、SwiftUI ScrollView は UIScrollView を wrap しています。
そのため、ScrollView 内部の UIScrollView を取得することができれば、UIScrollView の API を使って動作を制御することができます。
SwiftUIIntrospect というライブラリを使うことで、SwiftUI の内部で呼び出されている UIKit のクラスに簡単にアクセスすることができるようになります。
import SwiftUI
import SwiftUIIntrospect
...
var body: some View {
ScrollView {
...
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { scrollView in
scrollView.bounces = false
}
}
メリット
-
.introspect()
modifier を呼び出すだけで UIScrollView のインスタンスを取得できるので、実装が簡単 - プロパティの変更が 1つの ScrollView に限定される
デメリット
- UI の実装がサードパーティーのライブラリに大きく依存する
- ライブラリのアップデートによって UI の動作が大きく変わってしまう可能性がある
- アプリを実行する端末の iOS バージョンによって動作差異が発生することも
- SwiftUI 内部の UIKit を無理やり掘り出して利用しているので、Apple の思想上あまり良くない感じはする
- 多分大丈夫だけど、将来的に reject 対象になる可能性もゼロではないかも
まとめ
ScrollView の bounces を無効化することは、UIKit では簡単に対応できますが SwiftUI ではかなりめんどくさい対応になることがわかりました。
今後は SwiftUI を使って iOS アプリの開発を行うことが一般的になると思いますが、SwiftUI はかゆいところに手が届かないことが多いのでまだまだ UIKit の知識は必要不可欠だということを改めて痛感しました。
個人的には、デザインや機能が複雑な UI については潔く UIKit で実装してしまい、Apple から対応された API が提供されたタイミングで SwiftUI に移行するのが一番いいんじゃないのかなと思います。
参考資料