🔬

ScrollAnchorRole.sizeChanges 調査隊

2024/09/19に公開

環境

Sequoia 15.0
Xcode 16.0

ScrollAnchorRole.sizeChangesとは

スクロールビューの大きさやその表示内容が変化したときに、変化前/変化後の違和感を軽減するもの。
一番使われるのは、表示内容の一番最後の要素に特に意味があるとき、サイズが変化しても一番最後のものを表示し続ける使い方かと。

前提知識

そもそもScrollAnchorRoleで表されるの3つの役割を知らないといけない。
次の記事の内容が前提になります。

https://zenn.dev/samekard_dev/articles/4b08f7e2e777cb

先にまとめ

あまりにややこしいので先にまとめる。
大きいルールは3つ

基本はトップ
.sizeChanges = .topのときはもちろん、他のときでも条件が合わなければトップの位置を基準にする

.sizeChanges = .centerのとき
現在の表示位置がセンターであると判断されれば、サイズの変化時にセンターを基準にする

.sizeChanges = .bottomのとき
一番最後の要素が表示されていれば、一番最後の要素を表示し続ける挙動をする

.sizeChanges = .top

トップ位置を基準にする。

initialOffset alignment 結果
top top OK
top center OK
top bottom OK
center top OK
center center OK
center bottom OK
bottom top OK
bottom center OK
bottom bottom OK

OKというのは、予想通りの挙動ですわ、という意味。

.sizeChanges = .center

以下のルールAからCが適用されたときはセンター基準の挙動
それ以外はトップ基準の挙動

ルールA
内容<表示領域となりalignment = .topが適用されてから、内容>表示領域になったのち、手動でスクロールしない

ルールB
内容<表示領域となりalignment = .centerが適用されてから、内容>表示領域になったのち、手動でスクロールしない

ルールC
はじめに表示したときに内容>表示領域で、initialOffset = .centerが適用されたのち、手動でスクロールしない

◯はそのルールの適用がありえるという意味。

initialOffset alignment ルールA ルールB ルールC
top top
top center
top bottom 常にトップ基準の挙動
center top
center center
center bottom
bottom top
bottom center
bottom bottom 常にトップ基準の挙動

.alignment = .bottomのときにルールAやBのようなものが適用されないが、問題になることもあまりなさそう。
.sizeChanges = .centerにする人はすべて.centerにして使うことが多いのかなという気がする。
スクロールしてしまうと「現在センターである」という状態が失われる(と推測)。

.sizeChanges = .bottom

以下のルールDからFが適用されたときボトム基準の挙動
それ以外はトップ基準の挙動

ルールD
内容>表示領域であり、一番下の要素が表示されているとき

ルールE
内容<表示領域となりalignment = .topが適用されたのち、内容>表示領域に変わったとき(その後はルールDに以降)

ルールF
内容<表示領域となりalignment = .bottomが適用されたのち、内容>表示領域に変わったとき(その後はルールDに以降)

initialOffset alignment ルールD ルールE ルールF
top top
top center *
top bottom
center top
center center *
center bottom
bottom top
bottom center *
bottom bottom
  • やり方を変えたらルールEやFに似たものが適用されるかも

調査コード

import SwiftUI

struct ContentView: View {
    
    let items: [String] = [
        "aaa", "bbb", "ccc", "ddd", "eee", "fff",
        "ggg", "hhh", "iii", "jjj", "kkk", "lll",
        "mmm", "nnn", "ooo", "ppp", "qqq", "rrr",
        "sss", "ttt", "uuu", "vvv", "www", "xxx",
        "yyy", "zzz"]
    
    //これらのパラメータははじめに表示されたときの内容と領域の大小をコントロールするものです
    @State var size1 = 0.0
    @State var size2 = 100.0
    
    let initialOffsetUP: UnitPoint = .bottom
    let sizeChangesUP: UnitPoint = .bottom
    let alignmentUP: UnitPoint = .bottom

    var body: some View {
        HStack {
            VStack {
                Button("larger") {
                    size2 -= 20
                    if size2 < 0 {
                        size2 = 0
                    }
                }
                .padding()
                Button("smaller") {
                    size2 += 20
                }
                .padding()
                Spacer()
                    .frame(height: size2)
                
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                }
                .defaultScrollAnchor(initialOffsetUP, for: .initialOffset)
                .defaultScrollAnchor(sizeChangesUP, for: .sizeChanges)
                .defaultScrollAnchor(alignmentUP, for: .alignment)
                
                .padding()
                .border(.black)
                Spacer()
                    .frame(height: size2)
            }
            VStack {
                Button("larger") {
                    size1 -= 20
                    if size1 < 0 {
                        size1 = 0
                    }
                }
                .padding()
                Button("smaller") {
                    size1 += 20
                }
                .padding()
                Spacer()
                    .frame(height: size1)
                
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                }
                .defaultScrollAnchor(initialOffsetUP, for: .initialOffset)
                .defaultScrollAnchor(sizeChangesUP, for: .sizeChanges)
                .defaultScrollAnchor(alignmentUP, for: .alignment)
                
                .padding()
                .border(.black)
                Spacer()
                    .frame(height: size1)
            }
        }
    }
}

#Preview {
    ContentView()
}

Discussion