🔨

SwiftUIのスクロールビューで、要素数が多い時は設定したサイズに収め、少ない時はセンタリングするのをLayoutを使って実現

2024/09/03に公開

はじめに

SwiftUIでスクロールビューを使う時、要素数が多い時は設定したサイズ以内に収め、少ない時はセンタリングする(表示に必要な高さに縮まる)やり方がわからなかったがLayoutを使ったら限定的な用途で出来た。(他に何かいいやり方があるかもしれないが)

環境

Xcode 15.4

スクリーンショット

注意

まだ研究中なので使用する時は注意してください。
Text()に与える文字列の長さが長くて2行になるとズレます。
これは自然な大きさを問い合わせたときは1行で表示した大きさを答えるが、表示するときには2行になるからです。

コード

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack() {
            Spacer()
            Text("上の要素")
            Centered {
                ScrollView { 
                    ForEach(1..<5) {_ in 
                        Text("abcdefg")
                    }
                }
                .scrollBounceBehavior(.basedOnSize)
                .frame(maxHeight: 300.0)
            }
            .border(.gray)
            .frame(height: 300.0)
            Text("下の要素")
            Spacer()
        }
        .padding()
    }
}

struct Centered: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 1 else {
            fatalError()
        } 
        
        let subviewSize = subviews[0].sizeThatFits(.unspecified)
        
        print("proposal",proposal)
        print("subviewSize",subviewSize)
        
        print(min(proposal.width ?? CGFloat.greatestFiniteMagnitude, subviewSize.width))
        print(min(proposal.height ?? CGFloat.greatestFiniteMagnitude, subviewSize.height))
        
        return CGSize(
            width: min(proposal.width ?? CGFloat.greatestFiniteMagnitude, subviewSize.width),
            height: min(proposal.height ?? CGFloat.greatestFiniteMagnitude, subviewSize.height))
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        guard subviews.count == 1 else {
            fatalError()
        }
        
        let subviewSize = subviews[0].sizeThatFits(.unspecified)
        
        let x = bounds.minX
        let y = bounds.minY
        
        print("bounds", bounds)
        print("proposal", proposal)
        print("subviewSize", subviewSize)
        
        subviews[0]
            .place(
                at: CGPoint(x: x, y: y),
                anchor: .topLeading,
                proposal: ProposedViewSize(bounds.size)
            )
    }
}

Discussion