LoginSignup
1
1

SwiftUI ScrollView の bounces を無効化する (したかった)

Last updated at Posted at 2023-11-27

はじめに

iOS の ScrollView には、コンテンツの端までスクロールするとバウンドをするようなアニメーションがデフォルトで実装されています。

参考画像

ScrollViewDemo.gif

しかし、アプリによってはコンテンツの端までスクロールしてもバウンドしないように設定したい場合もあると思います。
その場合、どのように実装すればいいのでしょうか?

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 を無効化するためには、以下のような方法があります。

  1. ScrollView の部分を UIScrollView で実装する
  2. UIScrollView.appearance().bounces = false を設定する
  3. 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 に移行するのが一番いいんじゃないのかなと思います。

参考資料

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