SwiftUIで部分角丸を実現する

SwiftUIでは角丸を実現するために `cornerRadius` というAPIが存在しますが、これは全ての角に角丸を適用するものであり残念ながら部分ごとに角丸を適用出来るものではありません。

本投稿ではSwiftUIの機能のみで部分角丸を実現させてみます。

## 実装方法

SwiftUIにはCoreGraphics同様のAPIを提供する`Path`というモジュールが提供されています。
また、Viewに対して`mask`を呼び出すことでmaskをかけることも出来ます。
これらの機能を合わせることで角丸を実現させます。

```swift
struct RoundedView: View {
    let radius: CGFloat = 10

    var body: some View {
        Rectangle()
            .fill(Color.black)
            .frame(width: 200, height: 200)
            // Viewに対してマスクをかける
            .mask(
                // マスク対象Viewのフレームにアクセス出来るようにGeometoryReaderを利用
                GeometryReader { geo in
                    Path { path in
                        let frame = geo.frame(in: .local)

                        path.move(to: CGPoint(x: frame.minX, y: frame.midY))
                        // 各角に弧を描いていき、角丸にする
                        path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.minY),
                                    tangent2End: CGPoint(x: frame.midX, y: frame.minY),
                                    radius: self.radius)
                        path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.minY),
                                    tangent2End: CGPoint(x: frame.maxX, y: frame.midY),
                                    radius: self.radius)        
                        path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.maxY),
                                    tangent2End: CGPoint(x: frame.midX, y: frame.maxY),
                                    radius: self.radius)
                        path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.maxY),
                                    tangent2End: CGPoint(x: frame.minX, y: frame.midY),
                                    radius: self.radius)
                    }
                }
            )
    }
}
```

## 実用的にする

上記の例では全ての角毎にradiasを指定しなければならないので、使い勝手がよくありません。
Extensionを作ることで角毎に角丸が適用出来るようにします。

```swift
// 角丸対象の角
enum RoundableCorner {
    /// 右上
    case topLeading
    /// 左上
    case topTrailing
    /// 右下
    case bottomLeading
    /// 左下
    case bottomTrailing
}

extension View {
    /// 指定した角を丸める
    /// - Parameters:
    ///   - radius: 角丸の半径
    ///   - corner: 丸める角
    /// - Returns: 角丸を適用したあとのView
    func cornerRadius(_ radius: CGFloat, corner: RoundableCorner) -> some View {
        self.cornerRadius(radius: CGFloat, corners: [corner])
    }

    /// 指定した複数の角を丸める
    /// - Parameters:
    ///   - radius: 角丸の半径
    ///   - corner: 丸める角
    /// - Returns: 角丸を適用したあとのView
    func cornerRadius(_ radius: CGFloat, corners: Set<RoundableCorner>) -> some View {
        self.mask(
            GeometryReader { geo in
                Path { path in
                    let frame = geo.frame(in: .local)

                    path.move(to: CGPoint(x: frame.minX, y: frame.midY))
                    path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.minY),
                                tangent2End: CGPoint(x: frame.midX, y: frame.minY),
                                radius: corners.contains(.topLeading) ? radius : 0)
                    path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.minY),
                                tangent2End: CGPoint(x: frame.maxX, y: frame.midY),
                                radius: corners.contains(.topTrailing) ? radius : 0)
                    path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.maxY),
                                tangent2End: CGPoint(x: frame.midX, y: frame.maxY),
                                radius: corners.contains(.bottomTrailing) ? radius : 0)
                    path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.maxY),
                                tangent2End: CGPoint(x: frame.minX, y: frame.midY),
                                radius: corners.contains(.bottomLeading) ? radius : 0)
                }
            }
        )
    }
}
```

上記のようなExtensionを定義しておくことで、部分角丸を指定したいときは以下のようなコード書くだけでよくなります。

```swift
struct Content: View {
    var rectangle: some View {
        Rectangle()
            .fill(Color.black)
            .frame(width: 100, height: 100)
    }

    var body: some View {
        VStack {
            Text("指定箇所にまとめて角丸を適用")
            self.rectangle
                .cornerRadius(10, corners: [.topLeading, .bottomTrailing])

            Text("部分毎に異なる角丸を適用")
            self.rectangle
                .cornerRadius(30, corner: .topLeading)
                .cornerRadius(10, corner: .bottomLeading)
        }
    }
}
```

これは以下のように表示されます。

![スクリーンショット 2020-12-29 0.32.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28406/33dc34e5-bc44-47b2-2a41-f1b10efe8167.png)