「一部だけ角丸にする」をSwiftUIで実現する

## はじめに
UIKitではiOS11からUIViewの一部だけを簡単に角丸にすることができるAPIが提供されました。
これをSwiftUIでも同じように利用しようとすると、標準のAPIでは実現できないことがわかります。

本記事では、UIKitと同じようなインターフェースで、
SwiftUIのViewの一部だけを角丸にする方法をご紹介します。

※ 最終的にはこんな感じで使えるようになります

```swift
Rectangle()
    .cornerRadius(12, maskedCorners: [.layerMinXMinYCorner, .layerMinXMaxYCorner])
```

サンプルコードは以下に置いております。
https://github.com/chocoyama/PartlyRoundedCornerView

## UIKitでの一部のみ角丸処理
UIKitではCALayerに以下のプロパティが用意されています。

```swift
@available(iOS 11.0, *)
open var maskedCorners: CACornerMask
```

このプロパティをcornerRadiusプロパティを設定した上で利用することで、
ビューの一部の角のみを丸くすることが簡単にできます。

```swift
uiView.layer.cornerRadius = 20
uiView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] // 左上と、右上のみを角丸にする
```

（表示）
<img width="200" alt="UIKitSample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/1156fe62-afe0-b409-948f-bd32c3c9e23d.png">


## SwiftUIでの角丸処理
では、SwiftUIではどうでしょうか。

SwiftUIには以下のModifierが用意されており、Viewの全ての角を丸くすることは容易にできます。

```swift
@inlinable public func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View
```

```swift
struct ContentView: View {
    var body: some View {
        Rectangle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .cornerRadius(20)
    }
}
```

（表示）
<img width="200" alt="SwiftUISample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/2eec378e-7276-cb11-26c3-1e34fb568798.png">


しかし、提供されているAPIは上記のものだけであり、
UIKitで利用できた `maskedCorners` を指定することができないことが問題となります。

## UIViewRepresentableを利用したワークアラウンド
では、SwiftUIでも同様のインターフェースでViewの一部の角丸くするにはどうすればよいでしょうか。
今回採用した方法は「`UIViewRepresentable` を利用し、一部のみ角丸にしたUIViewを利用してSwiftUIのViewに`mask`をかける」という方法です。

### UIViewRepresentable
UIViewRepresentableは、UIKitで作られたビューをSwiftUIで利用するためのprotocolで、以下のような形で呼び出します。
細かい点については解説しませんが、ここでは `makeUIView` でUIViewが初期化されることだけ分かっていれば問題ありません。

```swift
struct ContentUIView: UIViewRepresentable {
    func makeUIView(context: UIViewRepresentableContext<ContentUIView>) -> UIView {
        let uiView = UIView()
        // initialization
        return uiView
    }
    
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ContentUIView>) {
    }
}
```

### mask
`mask Modifier` はSwiftUIのViewが呼び出せるもので、その名の通りViewにマスクをかけることができます。
下記のサンプルコードは角丸の四角形でImageをマスクしています。

```swift
Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    .mask(RoundedRectangle(cornerRadius: 20))
```

（表示）
<img width="200" alt="maskSample" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/9778c93a-c1f0-dfcb-2dc6-381a1dd1ad6f.png">


### UIViewRepresentableとmaskを組み合わせる
それでは上記の2つを組み合わせてみましょう

以下の実装を行っていきます
1. 一部のみを角丸にしたUIViewを、UIViewRepresentableを利用してSwiftUIから呼び出せるようにする
2. SwiftUIViewのmask Modifierに、1で作成したビューを受け渡す
3. 結果的にSwiftUIのViewの一部の角のみを丸くできる

1 

```swift
struct PartlyRoundedCornerView: UIViewRepresentable {
    let cornerRadius: CGFloat
    let maskedCorners: CACornerMask
    
    func makeUIView(context: UIViewRepresentableContext<PartlyRoundedCornerView>) -> UIView {
        // 引数で受け取った値を利用して、一部の角のみを丸くしたViewを作成する
        let uiView = UIView()
        uiView.layer.cornerRadius = cornerRadius
        uiView.layer.maskedCorners = maskedCorners
        uiView.backgroundColor = .white
        return uiView
    }
    
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PartlyRoundedCornerView>) {
    }
}
```

2 

```swift
Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
　　 // 角丸処理を施したUIViewをmaskに利用する
    .mask(PartlyRoundedCornerView(cornerRadius: 20,
                                  maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
```

3 
このようにSwiftUIのViewの一部を角丸にすることができました:tada:
<img width="200" alt="representableAndMask" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/b338028d-0b93-eee6-093b-94a0d3c8bed0.png">


## カスタムModifierを作成して、よりSwiftUIっぽく仕上げる
それでは仕上げです。
現状の実装でもやりたいことは既に実現できていますが、よりSwiftUIっぽい記述ができるようにしてみましょう。
利用するのは `カスタムModifier` です。

### カスタムModifier
SwiftUIでは標準で用意されたModifierとは別に、自前でModifierを定義することも可能です。
具体的には、`ViewModifier`プロトコルに準拠させたstructを用意し、`body`メソッドを実装してあげることでこれを実現します。
先ほど作成したコードから、マスクをかけている部分をカスタムModifierに切り出します。

```swift
struct PartlyRoundedCornerModifier: ViewModifier {
    let cornerRadius: CGFloat
    let maskedCorners: CACornerMask
    
    func body(content: Content) -> some View {
        content.mask(PartlyRoundedCornerView(cornerRadius: self.cornerRadius,
                                             maskedCorners: self.maskedCorners))}
}
```

さらに、Viewの`modifier`にViewModifierに適合しているインスタンスを受け渡すと、bodyメソッド内の処理が実行されます。
こうすることで `mask Modifier` の実行がカスタムModifierのbodyメソッド内に隠蔽されました。

```swift
Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    //.mask(PartlyRoundedCornerView(cornerRadius: 20,
    //                              maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
    .modifier(PartlyRoundedCornerModifier(cornerRadius: 20,
                                          maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
```

### Extensionの利用

この状態だとまだ切り出したメリットを実感することができませんが、
Viewにextensionを生やすことでスマートな記述にすることが可能です。

```swift
extension View {
    func cornerRadius(_ radius: CGFloat, maskedCorners: CACornerMask) -> some View {
        self.modifier(PartlyRoundedCornerModifier(cornerRadius: radius,
                                                  maskedCorners: maskedCorners))
    }
}
```

```swift
Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    //.modifier(PartlyRoundedCornerModifier(cornerRadius: 20,
    //                                      maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
    .cornerRadius(20, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner])
```

このような実装を加えることで、標準実装されている `cornerRadius` を記述すると
先ほど実装したmodifierが補完されるようになり、呼び出しのコードも簡潔になりました:tada:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/70cb2ec2-b492-17ab-65ec-d5cd5b088a51.png)

## 最終形

```swift
.cornerRadius(20, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner])
```

## おわりに
SwiftUIではUIKitでは出来ていた処理が出来ないといったことに度々直面します。
公式で対応してくれるのが一番望ましいですが、それまではこういったワークアラウンドで、SwiftUIらしさを保ったままコーディングできるよう工夫していくしかなさそうです。
よりいい方法などがあれば是非教えてください:pray:

https://github.com/chocoyama/PartlyRoundedCornerView