📜

[WWDC2023] iOS17におけるScrollViewの新機能  その1

2023/06/26に公開

SwiftUI

iOS 17では多くの新しいScrollViewモディファイヤが追加され、アプリ開発体験を大幅に向上させることを約束している。その新機能を使ってみて、どのように動作するかを本記事で示す。

以下で4つのモディファイヤについて詳しく説明する。

  1. .containerRelativeFrame(_:alignment:)
  2. .scrollTargetLayout(isEnabled:)
  3. .scrollTargetBehavior(_:)
  4. .scrollPosition(id:)

もしこの記事が気に入ったら、ぜひいいねやフォローをお願いします。
それでは、始めよう!

1. .containerRelativeFrame(_:alignment:)

この新しいモディファイヤは、ビューの高さ、幅、または両方をコンテナビューに関連して指定することができる。例では、ZStackにこれを使用している。ここで、その高さと幅をコンテナビュー、この場合はScrollViewに対して設定している。

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        // ...
                    }
                    .padding(16)
                    .containerRelativeFrame(.horizontal)
                    .containerRelativeFrame(.vertical)
                }
            }
        }
    }
}

以下のように簡略化することができる:

.containerRelativeFrame([.vertical, .horizontal])

そして、結果は以下のようになる:

Example01

このモディファイヤはScrollViewだけでなく、他のコンテキストでも使用できるようだ。Appleのドキュメンテーションによると、以下の状況で使うことができる:

  • iPadOSまたはmacOSでビューを表示するウィンドウ、またはiOSのデバイスの画面。
  • NavigationSplitViewの列
  • NavigationStack
  • TabViewのタブ
  • ScrollViewやListのようなスクロール可能なビュー

2. .scrollTargetLayout(isEnabled:)

このスクロールターゲットレイアウトモディファイヤは、ScrollView内のメインビュー(コンテナビュー)、この場合はZStackに添付される。これにより、ScrollViewはどこに揃えるべきかを決定できる。これにより、特定のScrollViewのスクロールターゲットが設定される。デフォルトでは、この設定はtrueに設定されている

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        // ...
                    }
                    .padding(16)
                    .containerRelativeFrame([.vertical, .horizontal])
                }
            }
            .scrollTargetLayout()
        }
    }
}

このモディファイヤ自体はビューに影響を与えない、それを3.と4.のモディファイヤと組み合わせて使用しないといけない。

3. .scrollTargetBehavior(_:)

それでは、セクション2でのターゲット設定から、ScrollViewの動作を定義する段階へと移ろう。これは、ScrollViewがどのように機能するかを決定することを意味する。これには、.paging.viewAlignedのようなScrollViewの動作を選択する作業が含まれる。それぞれについて詳しく見ていこう。

.paging

.paging動作は、ScrollViewをページングインターフェースに変換する。これはTikTokの縦スクロールやスムーズなオンボーディング体験を思い浮かべてみてください。この動作は、ビューの高さと幅を利用して次のページへスムーズに遷移し、ビューの一部が切り取られることなく常にフルビューが表示されるようにする。

.pagingを実装するとき、この動作がコンテナビューが全画面幅を占めることを要求するため、LazyHStackのスペースを0に設定することが重要です。

スペースが16に設定されている場合(ページング動作が乱れている)

Example04

スペースが0に設定されている場合(通常のページング動作)

Example02

コード:

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        RoundedRectangle(cornerRadius: 16)
                            .fill(.black.gradient)
                        
                        Text(string)
                            .font(.system(size: 92))
                            .fontWeight(.bold)
                            .foregroundStyle(.white)
                        
                    }
                    .padding(16)
                    .containerRelativeFrame([.vertical, .horizontal])
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.paging)
    }
}

.viewAligned

スペースを加えたり、より複雑なレイアウトを作成したい場合には、.viewAligned動作が便利です。これにより、ScrollViewの開始位置をカスタマイズして、スクロール時にコンテナビューとスムーズに整列することができる。この機能は、レイアウトに柔軟性を追加する。

こんな感じです:

Example05

コード:

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            HStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        RoundedRectangle(cornerRadius: 16)
                            .fill(.black.gradient)
                        
                        Text(string)
                            .font(.system(size: 92))
                            .fontWeight(.bold)
                            .foregroundStyle(.white)
                        
                    }
                    .frame(width: 300)
                    .containerRelativeFrame(.vertical)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
    }
}

4. .scrollPosition(id:)

ScrollViewに表示されている項目を追跡することができます。これを使うのはとても簡単です。@State変数を作成します:@State private var scrollPosition そして、この変数をモディファイアにバインドします: .scrollPosition(id: $scrollPosition)。これにより、スクロールするとScrollViewは表示している値を追跡する。つまり、この値を他のビューにリンクすることができる。

こんな感じです:

Example07

コード:

struct ScrollViewExampleScrollPosition: View {
    @State private var scrollPosition: String?
    let strings: [String] = ["1", "2", "3", "4", "5"]
    var body: some View {
        GeometryReader { geo in
            let size = geo.size
            VStack {
                ScrollView(.horizontal) {
                    HStack(spacing: 16) {
                        ForEach(strings, id: \.self) { string in
                            ZStack(alignment: .center) {
                                RoundedRectangle(cornerRadius: 16)
                                    .fill(.black.gradient)
                                
                                Text(string)
                                    .font(.system(size: 92))
                                    .fontWeight(.bold)
                                    .foregroundStyle(.white)
                                
                            }
                            .frame(width: 300, height: 500)
                            .padding(.vertical, 16)
                            .padding(.horizontal, (size.width - 300) / 2)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $scrollPosition)
                
                VStack {
                    if let scrollPosition {
                        Text(scrollPosition)
                            .font(.largeTitle)
                    }
                }
            }
        }
    }
}

または、ボタンを使用して、表示したい番号に直接ジャンプすることもできる。

こんな感じ:

Example06

コード:

struct ScrollViewExampleScrollPosition: View {
    @State private var scrollPosition: String?
    let strings: [String] = ["1", "2", "3", "4", "5"]
    var body: some View {
        GeometryReader { geo in
            let size = geo.size
            VStack {
                ScrollView(.horizontal) {
                    HStack(spacing: 16) {
                        ForEach(strings, id: \.self) { string in
                            ZStack(alignment: .center) {
                                RoundedRectangle(cornerRadius: 16)
                                    .fill(.black.gradient)
                                
                                Text(string)
                                    .font(.system(size: 92))
                                    .fontWeight(.bold)
                                    .foregroundStyle(.white)
                                
                            }
                            .frame(width: 300, height: 500)
                            .padding(.vertical, 16)
                            .padding(.horizontal, (size.width - 300) / 2)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $scrollPosition)
                
                VStack {
                    ForEach(strings, id: \.self) { string in
                        Button("Scroll to \(string)") {
                            withAnimation {
                                scrollPosition = string
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
        }
    }
}

ここまで読んでくれてありがとうございました。
その2楽しみにしてください!

Spacely, Inc. App Div.
Dean Thompson

Follow me!
Twitter

参考

Discussion