background vs. clipShape vs. mask

# はじめに
SwiftUI で形状指定をしたいとき、`background` や `clipShape` あるいは `mask` といくつかの候補が浮かんで「あれ、どれを使えばいいんだ？」と思ったことはありませんか？ 使い分けが紛らわしいので簡単なチャットバブルを例に比べてみます。

# 標準図形を描画できるか
ひとまず SwiftUI の標準図形たちを描画できるかどうかで実力を測ってみることにします
今回作りたい簡単なチャットバブルはこちら

![スクリーンショット 2024-09-10 3.17.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/b8d0d192-ff5b-4381-01d3-bab76a4c48ef.png)


- 円形 `Circle`（楕円形 `Ellipse`）
- カプセル `Capsule`
- 四角形 `Rectangle`（角丸四角形 `RoundedRectangle` `UnevenRoundedRectangle`）

といったシンプルな図形をカバーしています

![スクリーンショット 2024-09-10 3.16.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/77ef55df-b7f4-f82e-799b-f3d49176804f.png)

# 結論

これらの標準的な図形の描画については、全部いけます。
というのも、それぞれ `Shape` プロトコルに準拠した型を受け取ることが可能だからです。
具体的な実装を見ていきましょう。

### `.background`
背景に `View` 全般を描画できるので、当然 `Shape` に準拠した標準図形も引数に取れます

```swift: 
.background(
  // View
)

// Circle
Image("hamster_transparent") // *1
  .resizable()
  .aspectRatio(contentMode: .fit)
  .background(
    Circle().fill(.white)
  )

// Capsule 
Text("Hello, world.")
  .background(
    Capsule().fill(.white) // *2
  )

// RoundedRectangle
Text("Lorem ipsum dolor sit amet, ...")
  .background(
    RoundedRectangle(cornerRadius: 16).fill(.white)
  )
```

:::note info
`*1` 
- `hamster`: 背景あり画像
- `hamster_transparent`: 背景なし画像

`*2`
`Capsule` は `style: .continuous` を適用した `RoundedRectangle` とほぼ等価なため、以降まとめて `RoundedRectangle` を使用する。

![スクリーンショット 2024-09-10 4.17.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/3d3009ea-72af-404a-f78e-69a5a6c4f9fe.png)


:::

### `.clipShape`
形状に合わせてビューそのものを切り取るもので、`Shape` に準拠した型のみを受け取ります

```swift: 
.clipShape(
  // Shape
)

// Circle
Image("hamster") 
  .resizable()
  .aspectRatio(contentMode: .fit)
  .clipShape(
    Circle()
  )

// Capsule & RoundedRectangle
Text("Lorem ipsum dolor sit amet, ...")
  .background(.white)
  .clipShape(
    RoundedRectangle(cornerRadius: 16)
  )
```

### `.mask`
指定したビューの透明部分を使い、元のビューを特定の形に見せるもので、`View` 全般を受け取れます

```swift: 
.mask(
  // View
)

// Circle
Image("hamster") 
  .resizable()
  .aspectRatio(contentMode: .fit)
  .mask(
    Circle()
  )

// Capsule & RoundedRectangle
Text("Lorem ipsum dolor sit amet, ...")
  .background(.white)
  .mask(
    RoundedRectangle(cornerRadius: 16)
  )
```

# 使い分けを考える
:::note info
### `background` & `clipShape` vs. `mask`
:::
`mask` は複雑な形や透明度を操作するため実は処理がかなり重くなります。対して背景を追加するだけの `background` と、ビューを切り取る `clipShape` は処理が軽いため、**シンプルな形状なら**前者を使うのが適していると言えます。

:::note info
### `background` vs. `clipShape`
:::
背景を操作する `background` とビューを切り取る `clipShape` は、得意とする用途が全く違うと言えます。`background` は背後に要素を重ねるだけでビュー自体を切り取ったり形を変えるわけではありません。**元のビューのレイアウトや形状には影響を与えず**、単に背景に色や画像を追加する場合に適しています。
一方、明確に**元のビューを切り取りたいだけ**であれば、`clipShape` を使用するのが適しているでしょう。

:::note info
### `clipShape` vs. `mask`
:::
先にみたように、（`Shape` 型に準拠した）単純な形を作りたい場合は、処理が軽量な前者の `clipShape` が適しています。
一方で、**複雑な形状に変えたい場合**は `mask` の方が適切です。
とはいえ、複雑な形状って何でしょう？

例えば、こういったチャットバブルを作るには `mask` が適切です（とはいえ、チャットバブルとして成り立たせるには工夫が必要そうです）

![スクリーンショット 2024-09-10 5.54.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/c9711bc6-3262-9d64-ee1d-3af7c7ed7b7e.png)

![スクリーンショット 2024-09-10 5.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512675/c8bd4687-7ee4-de9d-5567-dcb3e9fb8ac3.png)

```swift
// あくまでイメージだが、png 画像の透明エリアを使って形状を作っている
Text(text)
  .background(.white)
  .mask(
    Image("hamster_transparent") 
      .resizable()
      .aspectRatio(contentMode: .fill)
    )
  .multilineTextAlignment(.center)
```

# まとめ

::: note info
### `background` vs. `clipShape` vs. `mask` の使い分け
- `background`: 背後に色や画像の `View` を追加するシンプルで軽量な処理。元のビューの形状には影響を与えない。

- `clipShape` : 元のビューそのものを指定した `Shape` でクリップする。処理は軽量で、シンプルな形状を適用したいときに最適。

- `mask` : 任意のビューを使用して、透明度をもとに形を変えたように見せる。複雑な形状にも対応できるが、処理はやや重くなる。

#### これらを踏まえて、パフォーマンスと形状の複雑さの観点で使い分けるとよい。
:::