SwiftUIでスクロールビューの挙動を決める defaultScrollAnchor の第二引数 ScrollAnchorRole

2024/09/18に公開

はじめに

iOS18のSwiftUIに defaultScrollAnchor(_ anchor: UnitPoint?, for role: ScrollAnchorRole) という面白い引数を持つものが登場したので紹介します。

これまでのあらすじ

iOS17にて導入された defaultScrollAnchor(_ anchor: UnitPoint?) はスクロールビューの表示範囲よりも内容の方が小さいときに表示範囲のどこに表示するか設定できる画期的なものでした。

import SwiftUI

struct OldStyle: View {

    let items: [String] = [
        "aaa", "bbb", "ccc", "ddd", "eee"]
    
    var body: some View {
        
        Spacer()
        
        ScrollView {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .font(.title)
            }
        }
        .defaultScrollAnchor(.bottom) //ここです
        .frame(width: 200, height:300)
        .padding()
        .border(.black)
        
        Spacer()
    }

}

#Preview {
    OldStyle()
}

iOS18で追加された for という引数

従来の仕様ではちょっとした問題があります。
野球の試合の速報をイメージしましょう。3回の裏が終わった時点で表示は左のようになります(試合の内容は省く)。上から表示する自然な表示です。

9回の裏の攻撃中に画面を開くと右のようになりました。

ユーザーは現在の内容を見たいのですが、表示されていません(もちろんスクロールすれば表示します)。

そこでこの新しいメソッドが出てきます。
defaultScrollAnchor(_ anchor: UnitPoint?, for role: ScrollAnchorRole)
です。
この2番目の引数 ScrollAnchorRole はRoleという名の通り、1番目の引数がどういう役割か指定します。
役割は3種類あります。initialOffset sizeChanges alignment です。それぞれを説明します。

initialOffset

今度は表示内容>表示範囲のときにどこで揃えるかというものです。先程の例でいうと .top なら1回表の位置で揃え、.bottom なら9回裏の位置で揃えます。

sizeChanges

これはスクロールビューの大きさを変えたときに、表示内容のどこを固定するかというもののようです。表示内容>表示範囲のときに関係するという点で initialOffset と似ているのですが、initialOffset は表示した瞬間どうするか、sizeChanges はその後のサイズ変更に対する挙動というものです。

alignment

これは、表示内容<表示範囲のときにどの場所に表示するかというものです。 1番目の引数が .top なら上に表示、.bottom なら下に表示という意味になります。

試合はまだ終わっていない

さっそくこれを適用して9回裏を表示させましょう。

出来ました。

struct NewStyle: View {

    let items: [String] = [
        "1回表", "1回裏", "2回表", "2回裏", "3回表", "3回裏",
        "4回表", "4回裏", "5回表", "5回裏", "6回表", "6回裏",
        "7回表", "7回裏", "8回表", "8回裏", "9回表", "9回裏",
    ]
    
    var body: some View {
        
        Spacer()
        
        ScrollView {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .font(.title)
            }
        }
        .defaultScrollAnchor(.top, for: .alignment)
        .defaultScrollAnchor(.bottom, for: .initialOffset)
        .frame(width: 200, height:300)
        .padding()
        .border(.black)
        
        Spacer()
    }
    
}

Appleによる別の書き方

iOS17からあった

.defaultScrollAnchor(.bottom)

という書き方は上の3つの役割をすべて設定するものです。よって

.defaultScrollAnchor(.bottom)
.defaultScrollAnchor(.topLeading, for: .alignment)

という書き方も出来るようです。

sizeChangesの記事を追加しました

挙動が複雑なのでこれだけで独立してます
https://zenn.dev/samekard_dev/articles/9a6981930c8cc0

Discussion