特定の角のみ ContainerRelativeShape を適用したい [SwiftUI]

iOS 14 から登場した自動角丸 `ContainerRelativeShape` を、特定の角にだけ利用したいという記事です。

# ContainerRelativeShape とは

iPhone 11 の端末自体や、 iOS14 から登場した Widget など、各箇所固有の角丸UIが増えてきています。
それぞれの箇所で適切な角丸半径で表示してくれるのが、`ContainerRelativeShape` です。

<blockquote class="twitter-tweet" data-conversation="none" data-theme="light"><p lang="en" dir="ltr">SwiftUI gives you ContainerRelativeShape() in iOS 14, which automatically makes a corner radius concentric to its parent shape without needing to manually specify corner radius values ✨ <a href="https://t.co/MO2IQuE7nx">pic.twitter.com/MO2IQuE7nx</a></p>&mdash; Jordan Singer (@jsngr) <a href="https://twitter.com/jsngr/status/1276931917628219398?ref_src=twsrc%5Etfw">June 27, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 目的

| 左上に配置して | ContainerRelativeShape 適用 | 拡張して左上だけ角丸にする！ |
|:--:|:--:|:--:|
| ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42826/0e3d5402-6aed-00b2-5747-91a130e717ec.png) | ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42826/cc43a81a-b529-2598-1c2e-18cfab5b98d9.png) | ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42826/03ddbc53-b438-0456-472f-c0e395f920a9.png) |

- 特定の角だけ角丸にしたい
- なおかつただの角丸ではなく自動計算の ContainerRelativeShape を使いたい

これを実現した、一番右の画像の実装を解説します。

# コード

## カスタム Shape

`Shape` Protocol を用い、
角の情報を扱うちょうどよい `UIRectCorner` OptionSet があるので利用して、
独自の `ContainerRelativeShapeSpecificCorner` Struct を定義します。

```swift
struct ContainerRelativeShapeSpecificCorner: Shape {
    
    private let corners: [UIRectCorner]
    
    init(corner: UIRectCorner...) {
        self.corners = corner
    }
    
    func path(in rect: CGRect) -> Path {
        var p = ContainerRelativeShape().path(in: rect)
        
        if corners.contains(.allCorners) {
            return p
        }
        
        if !corners.contains(.topLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.topRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        return p
    }
}
```

## 利用例

```swift
// 本来
Image("camera")
    .clipShape(ContainerRelativeShape())

// 今回のやつ (特定の角だけ角丸)
Image("camera")
    .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft, .topRight))

// 全コードサンプル
struct SampleView: View {
    var body: some View {
        Group {
            Image("camera")
                .resizable()
                .scaledToFill()
                .frame(width: 80, height: 80)
                .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .padding(8)
    }
}
```