SwiftUIで角丸のボーダーを実装する際、ScrollViewなどで角丸ボーダーViewを呼び出すとボーダーが見切れてしまう問題に遭遇したことはないでしょうか。
角丸ボーダーの基本的な実装
まず、SwiftUIで角丸のボーダーを実装します。
このコードで角丸ボーダーを作ることができます。
.overlay(RoundedRectangle(cornerRadius: 8).stroke(.red, lineWidth: 1))
うーん!ちゃんと作成できてそうですね🎉
角丸ボーダーの実装については、こちらの記事で詳しく解説されています!
しかし、これをScrollViewやLazyVGridなどで呼び出すとレイアウト崩れが起きてしまうのです。
ボーダーViewが見切れてしまう
早速ボーダーViewをScrollView内で呼び出してみます。
struct ContentView: View {
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 16) {
                ForEach(0..<10) { _ in
                    ListContentView()
                }
            }
        }
        .scrollIndicators(.hidden)
    }
}
private struct ListContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(.red, lineWidth: 1)
            )
    }
}
上記のコードを描画したものがこちらです。
少し分かりにくいかもですが、上下のボーダーが見切れてしまっているのが伝わるでしょうか。
解決方法
結論から書きますが.strokeの代わりに.strokeBorderモディファイアを利用することで回避できます。
private struct ListContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .strokeBorder(.red, lineWidth: 1) // ここを置き換える
            )
    }
}
仕組みを理解する
Appleの公式ドキュメントによると、.strokeBorderは以下のような仕組みで動作します。
Returns a view that is the result of insetting self by style.lineWidth / 2, stroking the resulting shape with style, and then filling with the foreground color.
日本語訳:
style.lineWidth / 2分だけselfを内側に縮小し、その結果得られた図形をstyleでストロークし、フォアグラウンドカラーで塗りつぶしたビューを返します。
なるほど、つまり↓ということだと思います。
- 例えば半径100の円があるとする
 - この半径98の円に線幅4の線を描くとする
 - style.lineWidth / 2分だけselfを内側に縮小 = 円を4 ÷ 2 = 2だけ内側に縮小 → 半径98の円になる
 - 結果、線は半径内側2pt(97,98)と外側2pt(99,100)の範囲に描画されるので、完全に元の円の内側に収まる!☝️
 
まとめ
- 
.stroke()は境界線を中心として内外に描画するため、はみ出す可能性がある - 
.strokeBorder()は事前に図形を縮小してから描画するため、完全に内側に収まる - ScrollViewなどでボーダーが見切れる場合は
.strokeBorder()を使用する 
余談
この問題は下記のようにボーダー分の.paddingを追加で付与することでも解決はできますが、.strokeBorderの方がよりシンプルで直感的な解決方法と言えるでしょう。
private struct ListContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(.red, lineWidth: 1)
            )
            .padding(1) // ボーダーの分余白
    }
}


