SwiftUI の ScrollView を CustomShape で clip すると内部の View まで clip される

下記のように ScrollView に対して clippedShae(_:) で CustomShape を適用して clip すると、 ScrollView 内の View まで clip されてしまって View が途中で切れてしまう。 Rectangle 等の組み込みの Shape や、List に対してであれば問題は生じない。 Stack Overflow にも同様の投稿があって、自分は確認していないけど iOS 13 では再現しないとのこと。

stackoverflow.com

CustomShape で clip clippedShae(_:) 無し(正常パターン)
f:id:komaji504:20210216181507g:plain
CustomShape で clip
f:id:komaji504:20210216181442g:plain
clippedShae(_:)` 無し
struct ExampleView: View {
    struct CustomShape: Shape {
        func path(in rect: CGRect) -> Path {
            let bezierPath = UIBezierPath(
                roundedRect: rect,
                byRoundingCorners: .allCorners,
                cornerRadii: CGSize(width: 20.0, height: 20.0)
            )
            let cgPath = bezierPath.cgPath
            return Path(cgPath)
        }
    }

    var body: some View {
        ScrollView {
            ForEach(0..<100) { element in
                Text("\(element)")
            }
            .frame(maxWidth: .infinity)
            .background(Color.blue)
            .padding()
        }
        .background(Color.green)
        .clipShape(CustomShape()) // ScrollView に対して CustomShape で clip
    }
}

これは、CustomShape 内の Path の init によっても再現の有無が異なっていて、各 init における今回の現象の再現有無は下記の通りだった。 もしかしたら callback や CGPath を引数にとる init は CGPath の作り方によっても挙動が異なるかも。

  • OK
    • init(_ rect: CGRect),
    • init(roundedRect rect: CGRect, cornerSize: CGSize, style: RoundedCornerStyle = .circular)
    • init(roundedRect rect: CGRect, cornerRadius: CGFloat, style: RoundedCornerStyle = .circular)
  • NG
    • init(_ callback: (inout Path) -> ())
    • init(_ path: CGPath)
    • init(_ path: CGMutablePath)
    • init(ellipseIn rect: CGRect)

解決法

ZStack 等を使って ScrollView の背面の View をおいて、ScrollView は透明にして背面の View に CustomShape を適用することで解決した。 ただ、この方法は View のレイアウト次第では使えないかも。

    var body: some View {
        ZStack {
            Color.green
                .clipShape(CustomShape()) // 背面の View に CustomShape を適用

            ScrollView {
                ForEach(0..<100) { element in
                    Text("\(element)")
                }
                .frame(maxWidth: .infinity)
                .background(Color.blue)
                .padding()
            }
        }
    }