【SwiftUI】Pathまとめ

# これは何？
独自の図形を描画する際にPathを使えばいいというのは分かっていたが、実際どういうAPIが存在しているのか把握してなかったので、[Pathのドキュメント](https://developer.apple.com/documentation/swiftui/path)を上から下まで読んで自分なりにまとめたものです。

# Pathとは
2Dの形状の輪郭（アウトライン）を示すもの。
Path自体は塗りつぶされた図形のことを指さず、その形の輪郭・外枠を示す。

# Pathの基本
Pathは親Viewの左上を原点とし、右に行くほどXが+、下に行くほどYが+になる。
詳細は後述するが、以下が基本的な構造で、Pathのクロージャ内でmove(to:)で描画開始位置に移動し、addLine(to:)で線を引いてcolosePath()で閉じるといった感じ。

```Swift
struct ContentView: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 50, y: 10))
            path.addLine(to: CGPoint(x: 150, y: 10))
            path.addLine(to: CGPoint(x: 100, y: 110))
            path.closeSubpath()
        }
        .stroke(.green, lineWidth: 5)
    }
}
```

![スクリーンショット 2024-10-12 16.05.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/7f956c29-ce89-97ba-57e1-7a2bb5d493ee.png)

空のPathを生成してからaddしていくことも可能。

```Swift
struct ContentView: View {
    var triangle: Path {
        var path = Path()
        path.move(to: CGPoint(x: 50, y: 10))
        path.addLine(to: CGPoint(x: 150, y: 10))
        path.addLine(to: CGPoint(x: 100, y: 110))
        path.closeSubpath()
        return path
    }
    
    var body: some View {
        triangle
            .stroke(.green, lineWidth: 5)
    }
}
```
実行結果は前のと同じ。

## Shapeプロトコルに準拠した定義
Shapeに準拠させることで動的に図形を調整しやすくなるので、実際はこのように使うことが多いかも。

```Swift
struct CustomShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
            path.closeSubpath()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            CustomShape()
                .stroke(.green, lineWidth: 5)
                .frame(width: 100, height: 100)
            CustomShape()
                .stroke(.green, lineWidth: 5)
                .frame(width: 200, height: 300)
        }
    }
}
```

![スクリーンショット 2024-10-12 16.27.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/15d421bf-9e74-f3d6-b9d0-2d830b9e6060.png)

# 基本的な描画操作
Pathの基本操作をまとめる

## move(to:) と closeSubpath()
Pathにはsubpathという概念が存在する。
これは1つのPathオブジェクトで複数の図形を描画するために存在し、描画する図形数に応じてPathに複数のsubpathが生成される。

`move(to:)`は描画開始位置へ移動すると共に新しいsubpathを作成する。
`closeSubpath()`でsubpathを閉じることができ、更にsubpathの開始位置へ自動的に線を引くため、冒頭の三角を描画するコードでは2回の`addLine(to:)`で済んでいる。

```Swift
        Path { path in
            // 描画開始位置へ移動すると共に新しいsubpathを作成
            path.move(to: CGPoint(x: 50, y: 10))
            // 線を描画
            path.addLine(to: CGPoint(x: 150, y: 10))
            path.addLine(to: CGPoint(x: 100, y: 110))
            // subpathを閉じる
            // 開始位置へ自動的に線を引く
            path.closeSubpath()
        }
```

`addRect(_:transform:)`のような形状がそれだけで完結するものに関しては`move(to:)`や`closeSubpath()`は不要

## 線の描画
### addLine(to:)
1本の線を描画する

```Swift
        Path { path in
             path.move(to: CGPoint(x: 10, y: 0))
             path.addLine(to: CGPoint(x: 100, y: 0))
             path.addLine(to: CGPoint(x: 100, y: 100))
             path.addLine(to: CGPoint(x: 10, y: 100))
             path.closeSubpath()
         }
         .stroke(.green, lineWidth: 5)
```
![スクリーンショット 2024-10-13 12.07.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/d6e94836-918a-e73a-a6ad-75f084ddf923.png)

### addLines(_:)
複数の線を描画する

```Swift
        Path { path in
            path.move(to: CGPoint(x: 10, y: 0))
            path.addLines([
                CGPoint(x: 100, y: 0),
                CGPoint(x: 100, y: 100),
                CGPoint(x: 10, y: 100),
                CGPoint(x: 10, y: 0)
            ])
            path.closeSubpath()
        }
        .stroke(.green, lineWidth: 5)
```
上記コードでは先ほどと異なり最後に始点であるCGpoint(x: 10, y: 10)を指定している。
addLines(_:)ではaddLine(to:)と異なり、最後に始点まで指定しないと思い通りに描画されないので注意。

```Swift
        Path { path in
            path.move(to: CGPoint(x: 10, y: 0))
            path.addLines([
                CGPoint(x: 100, y: 0),
                CGPoint(x: 100, y: 100),
                CGPoint(x: 10, y: 100),
                // 始点まで指定しないと三角になる
                // CGPoint(x: 10, y: 0)
            ])
            path.closeSubpath()
        }
```
![スクリーンショット 2024-10-13 12.44.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/e1de245e-4e31-daef-77b9-1a214b37da3d.png)

## 短形の描画
### addRect(_:transform:)
1つの短形を描画する。

```Swift
        Path { path in
            path.addRect(CGRect(x: 10, y: 10, width: 100, height: 200))
        }
        .stroke(.green, lineWidth: 5)
```
![スクリーンショット 2024-10-13 12.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/a33ef88e-bf96-40b3-22d0-fb2f7596c1bb.png)

transform引数ではCGAffineTransformで回転・拡大・並行移動・傾斜・反転などができる。デフォルト引数で`.identity`(単位行列)が指定されている。


### addRects(_:transform:)
複数の短形を描画する

```Swift
        Path { path in
            path.addRects([
                CGRect(x: 10, y: 10, width: 100, height: 200),
                CGRect(x: 150, y: 10, width: 100, height: 200),
            ])
        }
```

### init(roundedRect:cornerRadius:style:)
角丸の短形を描画する

```Swift
        Path(
            roundedRect: CGRect(x: 10, y: 10, width: 200, height: 100),
            cornerRadius: 10,
            style: .circular
        )
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-13 21.44.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/3669bb96-4546-43ad-839b-cd08a417c368.png)

styleは角丸の形状を示す。
- circular
    - 直線からカーブにかけてシャープになる
- continuous
    - 直線からカーブにかけて滑らか。circularより滑らか

↓緑の.circularの角丸と赤の.continuousの角丸をZStackで重ねたもの。circularの方が直線からカーブにかけて.continuousに比べて若干外に出てるが、ぱっと見違いは無さそう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/0fd18c47-6748-036e-9c2b-c62f1e71691d.png" width="200">

### init(roundedRect:cornerSize:style:)
角の丸みが均一ではない角丸短形を描画できる
1つ前のcornerRadiusでは角の丸みの半径をCGFloatで指定していたので丸みが均一になっていた。このcornerSizeでは角の丸みをCGSizeで指定できるので、楕円形に丸めることが可能になる。

```Swift
        Path(
            roundedRect: CGRect(x: 10, y: 10, width: 200, height: 100),
            cornerSize: CGSize(width: 20, height: 50),
            style: .circular
        )
        .stroke(.green, lineWidth: 5)
```
![スクリーンショット 2024-10-13 22.17.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/13f15b98-6634-fa4d-ea9b-a2d713417ecb.png)

例えばwidthとheightを50, 50にするとcornerRadiusと同じように丸みが均一化される。

![スクリーンショット 2024-10-13 22.16.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/9adccf31-7db3-499b-fec3-9458db056840.png)

### init(roundedRect:cornerRadii:style:)
4つの角ごとに異なる丸みを指定可能な短形を描画する。

```Swift
        Path(
            roundedRect: CGRect(x: 10, y: 10, width: 200, height: 100),
            cornerRadii: RectangleCornerRadii(
                topLeading: 10,
                bottomLeading: 20,
                bottomTrailing: 30,
                topTrailing: 40
            ),
            style: .continuous
        )
        .stroke(.green, lineWidth: 5)
```

### addRoundedRect(in:cornerSize:style:transform:)
角丸短形をpathに追加する

```Swift
        Path { path in
            path.addRoundedRect(
                in: CGRect(x: 10, y: 10, width: 100, height: 200),
                cornerSize: CGSize(width: 30, height: 30)
            )
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-13 22.43.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/df34592d-51e9-16d9-cc4f-a2b833cc28ed.png)

### addRoundedRect(in:cornerRadii:style:transform:)
角の丸みが均一ではない角丸短形をpathに追加する

```Swift
        Path { path in
            path.addRoundedRect(in: CGRect(x: 10, y: 10, width: 100, height: 200),
                                cornerRadii: RectangleCornerRadii(
                                    topLeading: 10,
                                    bottomLeading: 20,
                                    bottomTrailing: 30,
                                    topTrailing: 40
                                ))
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-13 22.46.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/2ab4530c-1043-fb73-6a76-d42a0c833e9c.png)

## 楕円の描画

### init(ellipseIn:)
楕円のPathを描画
CGRectが正方形の場合は均等な円を描画できる

```Swift
        VStack {
            Path(ellipseIn: CGRect(x: 10, y: 10, width: 200, height: 100))
                .stroke(.green, lineWidth: 5)
            Path(ellipseIn: CGRect(x: 10, y: 10, width: 200, height: 200))
                .stroke(.green, lineWidth: 5)
        }
```

![スクリーンショット 2024-10-13 23.01.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/75b7cac4-49c4-49d0-c42a-f693255a9cd6.png)

### addEllipse(in:transform:)
Pathに楕円を追加する

```Swift
        Path { path in
            path.addEllipse(in: CGRect(x: 50, y: 50, width: 200, height: 100))
            path.addEllipse(in: CGRect(x: 50, y: 200, width: 200, height: 200))
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-13 23.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/db3bf1a1-b9d3-fac6-97f1-78094160eae2.png)

## 弧の描画
### addArc(center:radius:startAngle:endAngle:clockwise:transform:)
弧を描画する

```Swift
        Path { path in
             path.addArc(
                 center: CGPoint(x: 100, y: 100), // 中心
                 radius: 50, // 半径
                 startAngle: .degrees(40), // 開始角度。時計の4時あたり
                 endAngle: .degrees(180), // 終了角度。時計の9時あたり
                 clockwise: true // 時計回りに描画
             )
         }
         .stroke(Color.green, lineWidth: 5)
```

![スクリーンショット 2024-10-13 23.18.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c712b26c-931a-63fd-05fc-d8ee6e379085.png)

clockwiseをfalseにすると反時計回りに描画される

![スクリーンショット 2024-10-13 23.20.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/ffbe4cec-e4dd-c734-b666-1ba890936a44.png)

### addArc(tangent1End:tangent2End:radius:transform:)
特定のポイントに向かって弧を描く

```Swift
        Path { path in
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 50))
            path.addArc(
                tangent1End: CGPoint(x: 300, y: 200),
                tangent2End: CGPoint(x: 200, y: 200),
                radius: 100
            )
            path.addLine(to: CGPoint(x: 50, y: 200))
        }
        .stroke(.green, lineWidth: 5)

```
![スクリーンショット 2024-10-13 23.41.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/8b3c45bf-25d5-7c53-efa5-60ed896f9d66.png)

描画開始位置からtangent2Endに向かって弧を描画するが、tangent1Endに向かってカーブを描く。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/5fb37ddc-ab37-ec73-3b3c-65f52ae72ed8.png" width="400">

### addRelativeArc(center:radius:startAngle:delta:transform:)
相対的な弧を描画する。
`addArc(center:radius:startAngle:endAngle:clockwise:transform:)`では開始角度と終了角度を明示的に示したが、addRelativeArcでは開始角度とそこからどの程度描画するかを指定する。

以下の例では40度（時計の4時あたり）から180度分時計回りに描画している。

```Swift
        Path { path in
            path.addRelativeArc(
                center: CGPoint(x: 100, y: 100),
                radius: 50,
                startAngle: .degrees(40),
                delta: .degrees(180)
            )
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-14 0.16.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/275235f3-e2e7-0b9d-2bb3-726fb22aa641.png)

## ベジェ曲線の描画

### addQuadCurve(to:control:)
2次ベジェ曲線（1つの制御点を持つ曲線）を描画

```Swift
        Path { path in
            path.move(to: CGPoint(x: 0, y: 100)) // 始点
            path.addQuadCurve(
                to: CGPoint(x: 300, y: 100), // 終点
                control: CGPoint(x: 150, y: 0) // 制御点
            )
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-14 13.16.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/b7841ee5-72e5-2383-02d9-701e5c38b787.png)

制御点が中央上に来るように配置している。
イメージ的には以下のような感じ。

```Swift
    var linePath: Path {
        Path { path in
            path.move(to: CGPoint(x: 0, y: 100))
            path.addLine(to: CGPoint(x: 150, y: 0))
            path.addLine(to: CGPoint(x: 300, y: 100))
        }
    }
    
    var controlPath: Path {
        Path { path in
            path.addArc(center: CGPoint(x: 150, y: 0), radius: 5, startAngle: .zero, endAngle: .degrees(360), clockwise: true)
        }
    }
    
    var body: some View {
        ZStack {
            linePath
                .stroke(.red, lineWidth: 5)
            controlPath
                .stroke(.blue, lineWidth: 5)
            // ベジェ曲線
            Path { path in
                path.move(to: CGPoint(x: 0, y: 100)) // 始点
                path.addQuadCurve(
                    to: CGPoint(x: 300, y: 100), // 終点
                    control: CGPoint(x: 150, y: 0) // 制御点
                )
            }
            .stroke(.green, lineWidth: 5)
        }
    }

```
![スクリーンショット 2024-10-14 13.14.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/bc40d2d5-efa8-96e4-e11c-b03edcadae74.png)


### addCurve(to:control1:control2:)
3次ベジェ曲線（2つの制御点を持つ曲線）を描画

```Swift
        Path { path in
            path.move(to: CGPoint(x: 0, y: 100)) // 始点
            path.addCurve(
                to: CGPoint(x: 300, y: 100), // 終点
                control1: CGPoint(x: 150, y: 0), // 1つ目の制御点
                control2: CGPoint(x: 150, y: 200) // 2つ目の制御点
            )
        }
        .stroke(.green, lineWidth: 5)
```
![スクリーンショット 2024-10-14 13.10.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/1264d487-b440-157d-238d-8bc86df8bd4b.png)

2つの制御点は中央の上下に来るように配置している。
イメージ的には以下のような感じ。

```Swift
    var linePath: Path {
        Path { path in
            path.move(to: CGPoint(x: 0, y: 100))
            path.addLine(to: CGPoint(x: 150, y: 0))
            path.addLine(to: CGPoint(x: 150, y: 200))
            path.addLine(to: CGPoint(x: 300, y: 100))
        }
    }
    
    var controlPath: Path {
        Path { path in
            path.addArc(center: CGPoint(x: 150, y: 0), radius: 5, startAngle: .zero, endAngle: .degrees(360), clockwise: true)
            path.addArc(center: CGPoint(x: 150, y: 200), radius: 5, startAngle: .zero, endAngle: .degrees(360), clockwise: true)
        }
    }
    
    var body: some View {
        ZStack {
            linePath
                .stroke(.red, lineWidth: 5)
            controlPath
                .stroke(.blue, lineWidth: 5)
            // ベジェ曲線
            Path { path in
                path.move(to: CGPoint(x: 0, y: 100)) // 始点
                path.addCurve(
                    to: CGPoint(x: 300, y: 100), // 終点
                    control1: CGPoint(x: 150, y: 0), // 1つ目の制御点
                    control2: CGPoint(x: 150, y: 200) // 2つ目の制御点
                )
            }
            .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 12.48.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/60848985-1100-37ee-a894-bc3c5372f3f1.png)

# move(to:)とcloseSubpath()の重要性
先述した通り、`move(to:)`で描画開始位置へ移動してsubpathを作成し、`closeSubpath()`でsubpathを閉じることができる。
例えば以下のようにPath内で2つの図形を描画するとする。

```Swift
        Path { path in
            // 1つ目の図形
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addLine(to: CGPoint(x: 50, y: 150))
            path.closeSubpath()

            // 2つ目の図形
            path.move(to: CGPoint(x: 200, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 150))
            path.addLine(to: CGPoint(x: 200, y: 150))
            path.closeSubpath()
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-14 13.38.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/54013e96-a000-5f4d-cae5-2d8b5a60d275.png)

もし1つ目の図形で`closeSubpath()`しない場合、始点までの線は描画されない。subpathが閉じられないままなので`.fill`などで塗りつぶした際に正常に塗りつぶされない可能性がある。

![スクリーンショット 2024-10-14 13.42.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c1423d9f-689a-011c-ffaa-451b64763e43.png)

今度は2つ目の図形で`move(to:)`せずに`addLine(to:)`など続けてしまうと、前のsubpathと同じ始点からsubpathが暗黙的に作成される。

```Swift
        Path { path in
            // 1つ目の図形
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addLine(to: CGPoint(x: 50, y: 150))
            path.closeSubpath()

            // 2つ目の図形
            // path.move(to: CGPoint(x: 200, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 150))
            path.addLine(to: CGPoint(x: 200, y: 150))
            path.closeSubpath()
        }
        .stroke(.green, lineWidth: 5)
```

![スクリーンショット 2024-10-14 13.49.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/3dcc26d5-36f9-4f70-2a67-f15d552d4678.png)

1つ目の図形で`move(to:)`しない場合、線が描画されない。これは`move(to:)`により作成されていたsubpathが存在しないため。

```Swift
        Path { path in
            // 1つ目の図形
            // path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addLine(to: CGPoint(x: 50, y: 150))
            path.closeSubpath()

            // 2つ目の図形
            path.move(to: CGPoint(x: 200, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 50))
            path.addLine(to: CGPoint(x: 300, y: 150))
            path.addLine(to: CGPoint(x: 200, y: 150))
            path.closeSubpath()
        }
        .stroke(.green, lineWidth: 5)
```
![スクリーンショット 2024-10-14 13.51.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/92d3f04c-3634-8776-deba-b41161e27410.png)

`addRects(_:transform:)`などで`move(to:)`をしなくていいのは、内部でsubpathの作成とcloseを両方して完結しているため。
Pathの始点と終点を指定する以下3つを使う場合は気をつけよう。
- `addLine(to:)`
- `addQuadCurve(to:control:)`
- `addCurve(to:control1:control2:)`

# Path.Element
PathのElementとして以下のcaseがある
- Path.Element.move(to:)
- Path.Element.line(to:)
- Path.Element.curve(to:control1:control2:)
- Path.Element.quadCurve(to:control:)
- Path.Element.closeSubpath

## forEach(_:)
これらのPath.ElementをforEachで取り出すことができる。
なのでPath内の各要素を取り出して任意の処理を実行することが可能。
例えば`move(to:)`した値に合わせて動的に`addLine(to:)`の値を調整したりなどできそう。

```Swift
    let path = Path { path in
        path.move(to: CGPoint(x: 50, y: 50))
        path.addLine(to: CGPoint(x: 150, y: 50))
        path.addLine(to: CGPoint(x: 150, y: 150))
        path.addQuadCurve(to: CGPoint(x: 50, y: 150), control: CGPoint(x: 100, y: 200))
        path.closeSubpath()
    }
    
    func printEachElement() {
        path.forEach { element in
            switch element {
            case .move(let point):
                print("Move to \(point)")
            case .line(let point):
                print("Line to \(point)")
            case .quadCurve(let point, let control):
                print("Quad curve to \(point), control: \(control)")
            case .curve(let point, let control1, let control2):
                print("Curve to \(point), control1: \(control1), control2: \(control2)")
            case .closeSubpath:
                print("Close subpath")
            }
        }
    }
```

```
Move to (50.0, 50.0)
Line to (150.0, 50.0)
Line to (150.0, 150.0)
Quad curve to (50.0, 150.0), control: (100.0, 200.0)
Close subpath
```

# Pathの操作
## Pathの追加
`addPath(_:transform:)`で現在のPathに別のPathを追加する。
1つのPathを複数回使用する場合や、複雑な図形を作る時にパーツごとでPathを生成したりなどで使えそう。
以下の例では楕円を1つ描画し、transformを用いて同じ楕円の位置をずらして表示している。

```Swift
    var ellipsePath: Path {
        Path { path in
            path.addEllipse(in: CGRect(x: 50, y: 50, width: 200, height: 100))
        }
    }
    
    var body: some View {
        Path { path in
            path.addPath(ellipsePath)
            let transform = CGAffineTransform(translationX: 50, y: 50)
            path.addPath(ellipsePath, transform: transform)
        }
        .stroke(Color.green, lineWidth: 2)
    }
```

![スクリーンショット 2024-10-14 15.49.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/5a2d5f66-fec4-aea7-82d6-0a69f732bc42.png)

以下の例では楕円を1つ描画し、transformを用いて同じ楕円の位置をずらして表示している

## 共通領域の取得
`intersection(_:eoFill:)`で2つのPathの共通領域を取得する
例えばrectとellipseを同時に描画すると以下のようになるとする

```Swift
    var rectPath = Path { path in
        path.addRect(CGRect(x: 50, y: 50, width: 100, height: 100))
    }
    
    var ellipsePath = Path { path in
        path.addEllipse(in: CGRect(x: 70, y: 70, width: 100, height: 100))
    }
    
    var body: some View {
        Path { path in
            path.addPath(rectPath)
            path.addPath(ellipsePath)
        }
        .stroke(.green, lineWidth: 5)
    }
```

![スクリーンショット 2024-10-14 15.06.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/2f4d9c81-e4a9-c85a-518c-ccdd346a3f90.png)

これに`intersection(_:eoFill:)`を適用した場合

```Swift
    var body: some View {
        HStack {
            rectPath
                .intersection(ellipsePath)
                .stroke(.green, lineWidth: 5)
            ellipsePath
                .intersection(rectPath)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/74e95bea-5a11-6003-378f-7473b3fdbdc2.png)

どちらのPathに対して行っても共通領域は同じなので結果も同じ。

## 非共通領域の取得
`subtracting(_:eoFill:)`で共通領域ではない部分を取得する。

```Swift
    var body: some View {
        HStack {
            rectPath
                .subtracting(ellipsePath)
                .stroke(.green, lineWidth: 5)
            ellipsePath
                .subtracting(rectPath)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.19.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/dec8e221-ed7b-0a72-fe5d-b814cc50202a.png)

## 共通領域の線を取得
`lineIntersection(_:eoFill:)`で共通領域の線のみを取得する

```Swift
    var body: some View {
        HStack {
            rectPath
                .lineIntersection(ellipsePath)
                .stroke(.green, lineWidth: 5)
            ellipsePath
                .lineIntersection(rectPath)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.18.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c1d3f6c0-79f7-5688-f50d-62531dfec878.png)

## 非共通領域の線を取得
`lineSubtraction(_:eoFill:)`で共通領域ではない線を取得する。

```Swift
    var body: some View {
        HStack {
            rectPath
                .lineSubtraction(ellipsePath)
                .stroke(.green, lineWidth: 5)
            ellipsePath
                .lineSubtraction(rectPath)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.18.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/76a06a87-0b88-ecb6-344e-0fff3135959c.png)

## normalized(eoFill:)
自己交差しているpathを正規化し、自己交差がないPath（弱単純パス: weakly-simple path）にする。
例えば以下のようなpathは数字の8を描くように自身のlineで交差している。これが自己交差。この状態では正しく塗りつぶしや描画が行われない可能性がある。

```Swift
    let path = Path { path in
        path.move(to: CGPoint(x: 50, y: 50))
        path.addLine(to: CGPoint(x: 150, y: 150))
        path.addLine(to: CGPoint(x: 50, y: 150))
        path.addLine(to: CGPoint(x: 150, y: 50))
    }

    var body: some View {
        path
            .stroke(.green, lineWidth: 5)
    }
```

![スクリーンショット 2024-10-14 15.31.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/03ac861b-6026-8a66-cfd4-cbb23cd2e250.png)

そこでnormalized(eoFill:)を行うことで、自己交差しないpathに内部的に変更を加えてくれる。

```Swift
    var body: some View {
        path
            .normalized()
            .stroke(.green, lineWidth: 5)
    }
```
見た目的にはcloseSubpath()された（一番上にlineが追加された）ようにしか見えないが、先述したpathの要素をforEachで出力してみると、交差されないようにpathが調整されているのがわかる。

```Swift
		path.normalized().forEach { element in
        switch element {
        case .move(let point):
            print("Move to \(point)")
        case .line(let point):
            print("Line to \(point)")
				...
				}
		}
```
`.normalized()`前

```
Move to (50.0, 50.0)
Line to (150.0, 150.0)
Line to (50.0, 150.0)
Line to (150.0, 50.0)
```
`.normalized()`後
```
Move to (100.0, 100.0)
Line to (150.0, 150.0)
Line to (50.0, 150.0)
Line to (100.0, 100.0)
Line to (50.0, 50.0)
Line to (150.0, 50.0)
Close subpath
```
交差はしてないが(100, 100)で接触はしてるので「完全な単純パス」ではなく「弱単純パス」とドキュメントに示されているのだと思う。

> The returned path is a weakly-simple path, has no self-intersections, and has a normalized orientation. 
https://developer.apple.com/documentation/swiftui/path/normalized(eofill:)

## 対象差の取得
`symmetricDifference(_:eoFill:)`でどちらか一方に含まれるが、両方には含まれない領域、つまり対象差を取得する。

```Swift
    var body: some View {
        HStack {
            rectPath
                .symmetricDifference(ellipsePath)
                .fill(.green)
            ellipsePath
                .symmetricDifference(rectPath)
                .fill(.green)
        }
    }
```
![スクリーンショット 2024-10-14 15.37.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/88581871-1401-ffc8-b910-77b835e0f88e.png)

## 領域の合成
`union(_:eoFill:)`で2つの領域を合成する

```Swift
    var body: some View {
        HStack {
            rectPath
                .union(ellipsePath)
                .fill(.green)
            ellipsePath
                .union(rectPath)
                .fill(.green)
        }
    }
```

![スクリーンショット 2024-10-14 15.37.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/96319edf-a9fb-ad05-d720-770d9dffa4ee.png)

strokeで見るとPathが合成されているのがわかる。

```Swift
    var body: some View {
        HStack {
            rectPath
                .union(ellipsePath)
                .stroke(.green, lineWidth: 5)
            ellipsePath
                .union(rectPath)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.38.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/f1f75619-e6af-ee3e-eae7-3d716a5b14f2.png)

# PathのTransform

## applying(_:)
Pathに対してCGAffineTransformを適用する。
```Swift
    var roundedRect: Path {
        Path(roundedRect: CGRect(x: 10, y: 10, width: 100, height: 100), cornerRadius: 10)
    }
    
    var body: some View {
        ZStack {
            roundedRect
                .stroke(.green, lineWidth: 5)
            roundedRect
                .applying(CGAffineTransform(translationX: 200, y: 0)) // 200pt移動
                .applying(CGAffineTransform(rotationAngle: .pi / 4)) // 45度回転
                .applying(CGAffineTransform(scaleX: 1, y: 1.5)) // 縦に1.5倍
                .stroke(.green, lineWidth: 5)
        }
    }
```
![スクリーンショット 2024-10-14 15.55.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/4bf766da-6ccf-9b07-b11c-f74c07ad4be7.png)

## offsetBy(dx:dy:)
Pathを並行移動する

```Swift
    var body: some View {
        ZStack {
            roundedRect
                .stroke(.green, lineWidth: 5)
            roundedRect
                .offsetBy(dx: 50, dy: 50)
                .stroke(.green, lineWidth: 5)
        }
    }
```

![スクリーンショット 2024-10-14 15.57.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/0ad2a0e5-f940-5b46-da89-1b0788fd0460.png)


## trimmedPath(from: to:)
Pathの一部を切り取る。
切り取る開始位置(from)から終了位置(to)に0~1.0の値を指定することで、Path全体の長さに対して相対的に切り取れる。
以下の例では短形の0.1~1.0の部分のみを切り取って表示している

```Swift
    var body: some View {
        Path(roundedRect: CGRect(x: 10, y: 10, width: 100, height: 100), cornerRadius: 10)
            .trimmedPath(from: 0.1, to: 1.0)
            .stroke(.green, lineWidth: 5)
    }
```
![スクリーンショット 2024-10-14 16.00.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/a9f814d0-1833-f22a-290b-53f32ca005a1.png)

# Pathのプロパティ

## boundingRect
Pathが描画される領域の最小の短形を返す。
以下のコードではPathで描画した四角(ひし形)の短形をCGRectで返している

```Swift
    var rect = Path { path in
        path.move(to: CGPoint(x: 50, y: 0))
        path.addLine(to: CGPoint(x: 100, y: 50))
        path.addLine(to: CGPoint(x: 50, y: 150))
        path.addLine(to: CGPoint(x: 0, y: 50))
        path.closeSubpath()
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            rect
                .stroke(Color.green, lineWidth: 5)
            VStack {
                Text("\(rect.boundingRect.width)")
                Text("\(rect.boundingRect.height)")
            }
        }
    }
```

![スクリーンショット 2024-10-14 17.14.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/33371d73-1d89-b8e0-122e-d2b8deabeaea.png)

## cgPath
PathからcgPathを取得できるのでCoreGraphicsベースの描画にも使える

```Swift
    var rect: Path {
        Path { path in
            path.move(to: CGPoint(x: 50, y: 0))
             ...
            path.closeSubpath()
        }
    }
    
    var cgPath: CGPath {
        rect.cgPath
    }
```

## contains(_:eoFill:)
CGPointで指定した位置がPathの内部に含まれているかを返す
eoFillがtrueの場合は偶奇ルール、falseの場合は非ゼロワインディングルールを適用して判定をする。詳細は後述する。

```Swift
    var rect: Path {
        Path { path in
            path.move(to: CGPoint(x: 50, y: 0))
             ...
            path.closeSubpath()
        }
    }
    
    var isInside: Bool {
        let point = CGPoint(x: 50, y: 50)
        return rect.contains(point, eoFill: true)
    }
```

## currentPoint
Pathの最後に描かれたポイントを示す。
以下の例だと(x: 100, y: 50)を最後にcurrentPointしているので(x: 100, y: 50)が出力される。条件に基づいてPathの描画を動的に変更する場合などに使えたり、Pathの描画を途中でやめて再開する場合にも使えそう。

```Swift
    var rect: Path {
        Path { path in
            path.move(to: CGPoint(x: 50, y: 0))
            path.addLine(to: CGPoint(x: 100, y: 50))
            print("\(path.currentPoint)") // (x: 100, y: 50)が出力
            ...
        }
    }
```

## isEmpty
Pathに線や図形が何もない場合はtrue

# Pathへのstyle適用

## stroke(_:lineWidth:)
Pathの中心に対して線を描画する

```Swift
        Path(roundedRect: CGRect(x: 10, y: 10, width: 100, height: 100), cornerRadius: 20)
        .stroke(.green, lineWidth: 5)
```

## stroke(style:)
Pathに対して線のスタイルを適用する

```Swift 
        Path { path in
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addLine(to: CGPoint(x: 50, y: 150))
            path.closeSubpath()
        }
        .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round, dash: [10, 5]))
```

![スクリーンショット 2024-10-14 17.34.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/805a09ce-80d3-5111-76c3-145afbf019f2.png)


## strokedPath(_:)
Pathに対してStrokeStyle（線のスタイル）を適用する。

```Swift
    var rect: Path {
        Path { path in
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addLine(to: CGPoint(x: 50, y: 150))
            path.closeSubpath()
        }
        .strokedPath(StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round, dash: [10, 5]))
    }
```
見た目としては`stroke(style:)`と同じ。

`stroke(style:)`との違いは、`stroke(style:)`はshapeのモディファイアなのに対し、`strokedPath(_:)`はPathのモディファイアであること。つまり`stroke(style:)`を使用した場合Shapeとして返すが、`strokedPath(_:)`はPathを返してくれる。

`strokedPath(_:)`を使用すると、実際のsubpathの内部も変更される。
例えば上の例で`strokedPath(_:)`を適用する前は以下のようなpathだったが、

```
Move to (50.0, 50.0)
Line to (150.0, 50.0)
Line to (150.0, 150.0)
Line to (50.0, 150.0)
Close subpath
```
適用後は以下のようになる。
```
Move to (50.0, 52.5)
Line to (60.0, 52.5)
Curve to (62.5, 50.0), control1: (61.38071187457698, 52.5), control2: (62.5, 51.38071187457698)
Curve to (60.0, 47.5), control1: (62.5, 48.61928812542302), control2: (61.38071187457698, 47.5)
Line to (50.0, 47.5)
Curve to (47.5, 50.0), control1: (48.61928812542302, 47.5), control2: (47.5, 48.61928812542302)
Curve to (50.0, 52.5), control1: (47.5, 51.38071187457698), control2: (48.61928812542302, 52.5)
Close subpath
Move to (65.0, 52.5)
Line to (75.0, 52.5)
Curve to (77.5, 50.0), control1: (76.38071187457699, 52.5), control2: (77.5, 51.38071187457698)
...
```
なので例えば破線のような図形を描画したい場合、`addLine(to:)`を繰り返すのではなく`strokedPath(_:)`を使うことで1本のLineだけで完結できる。

`stroke(style:)`の適用後は各pathごとに取得できないので内部的なpathがどうなっているのかは不明。

## fill(_:)
塗りつぶす

```Swift
        Path { path in
            ...
        }
        .fill(.green)
```

## fill(_:style:)
塗りつぶしのstyle(ルール)を適用できる

```Swift
        HStack {
             // 奇遇ルール
             Path { path in
                 path.addRect(CGRect(x: 50, y: 50, width: 100, height: 100))
                 path.addRect(CGRect(x: 75, y: 75, width: 100, height: 100))
             }
             .fill(Color.blue, style: FillStyle(eoFill: true))

             // 非ゼロワインディングルール
             Path { path in
                 path.addRect(CGRect(x: 50, y: 50, width: 100, height: 100))
                 path.addRect(CGRect(x: 75, y: 75, width: 100, height: 100))
             }
             .fill(Color.red, style: FillStyle(eoFill: false))
         }
```

![スクリーンショット 2024-10-14 17.30.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/6779b4af-768c-3109-dad2-5fda36a8dce9.png)

## foregroundStyle(_:)
お馴染みの`foregroundStyle(_:)`も使えるが、グラデーションする際は`.frame`で指定する必要がありそう。
```Swift
            Path(roundedRect: CGRect(x: 10, y: 10, width: 100, height: 100), cornerRadius: 20)
            .foregroundStyle(LinearGradient(
                gradient: Gradient(colors: [.red, .green]),
                startPoint: .top,
                endPoint: .bottom
            ))
            .frame(width: 100, height: 100)
```

![スクリーンショット 2024-10-14 18.07.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/d4438253-5e92-a377-0ed9-e2a6bf3e18a3.png)

# .degreesと.radians
どこから開始するのかわからなくなるのでメモ

```Swift
        Path { path in
            path.addArc(
                center: CGPoint(x: 50, y: 50),
                radius: 30,
                startAngle: .zero,
                endAngle: .degrees(90),
                clockwise: false
            )
        }
        .stroke(.green, lineWidth: 5)
```
上記コードのように`clockwise: false`の場合、`endAngle`でそれぞれ以下を指定した場合の結果をまとめる。

|`.degrees(90)`|`.degrees(180)`|`.degrees(270)`|`.degrees(360)`|
|--|--|--|--|
|![スクリーンショット 2024-10-14 18.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/1a2f1a48-bc91-13f1-7723-db003092483d.png)|![スクリーンショット 2024-10-14 18.27.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/54ab85b0-acfe-01c6-c823-3979f710b982.png)|![スクリーンショット 2024-10-14 18.28.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/07a621ae-1a9f-5aaa-5953-433166af25d7.png)|![スクリーンショット 2024-10-14 18.28.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c9a58031-75b7-5644-fc88-49f2ef9dbff4.png)|

|`.radians(.pi * 0.5)`|`.radians(.pi)`|`.radians(.pi * 1.5)`|`.radians(.pi * 2)`|
|--|--|--|--|
|![スクリーンショット 2024-10-14 18.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/1a2f1a48-bc91-13f1-7723-db003092483d.png)|![スクリーンショット 2024-10-14 18.27.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/54ab85b0-acfe-01c6-c823-3979f710b982.png)|![スクリーンショット 2024-10-14 18.28.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/07a621ae-1a9f-5aaa-5953-433166af25d7.png)|![スクリーンショット 2024-10-14 18.28.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c9a58031-75b7-5644-fc88-49f2ef9dbff4.png)|

# eoFill
Pathの引数として何回か出てきてるが、これは多角形の内部・外部の判定をする際に使用するルールを指定するためのパラメータ。
- true：偶奇ルール（Even-Odd Rule）
- false：非ゼロワインディングルール（Non-Zero Winding Rule）

ルールによっては特定の点が内部・外部と変わる可能性がある。つまり図形を塗りつぶす際にルールによって塗り潰されたりされなかったりする。

## 偶奇ルール（Even-Odd Rule）
このルールでは、一方向に引いた直線とPathの交差回数が**偶数**か**奇数**かを考慮して決定する。
1. 判定したい点から任意の方向に無限に伸びる直線を引く
2. その直線がPathと交差する回数を数える
3. 偶数回交差する場合、その領域は外部。奇数回交差する場合、その領域は内部

![偶奇ルール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/50dbc650-e6ab-e2c3-dcb8-4dd6da498251.png)

## 非ゼロワインディングルール（Non-Zero Winding Rule）
このルールでは、一方向に引いた直線を横切るPathの**方向**と**回数**を考慮して決定する。ワインディング数はある領域を基準として、多角形がその領域の周りを何回巻いているかを示す整数値。

1. 判定したい点から任意の方向に無限に伸びる直線を引く
2. その直線と交差するPathが下から上に交差する場合、ワインディング数を+1する。上から下に交差する場合、ワインディング数を-1する
3. ワインディング数が0でない場合、その領域は多角形の内部、0の場合は外部

![非ゼロ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/9775a186-d4c7-6a54-fa01-e88b0d5ff676.png)

```Swift
    var star = Path { path in
        path.move(to: CGPoint(x: 100, y: 10))
        path.addLine(to: CGPoint(x: 150, y: 180))
        path.addLine(to: CGPoint(x: 10, y: 80))
        path.addLine(to: CGPoint(x: 190, y: 80))
        path.addLine(to: CGPoint(x: 50, y: 180))
        path.closeSubpath()
    }
    
    var body: some View {
        HStack {
            star
            .fill(.yellow, style: FillStyle(eoFill: true))
            star
                .fill(.yellow, style: FillStyle(eoFill: false))
        }
    }
```
![スクリーンショット 2024-10-14 18.46.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/f0589560-413d-394c-b36e-d9116985be84.png)

```Swift
    // 内側も外側も両方とも時計回りに描画したarc
    var arc = Path { path in
        path.addArc(center: CGPoint(x: 100, y: 100), radius: 50, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: true)
        path.addArc(center: CGPoint(x: 100, y: 100), radius: 100, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: true)
        path.closeSubpath()
    }
    // 内側は反時計回り、外側は時計回りに描画したarc
    var arc2 = Path { path in
        path.addArc(center: CGPoint(x: 100, y: 100), radius: 50, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: false)
        path.addArc(center: CGPoint(x: 100, y: 100), radius: 100, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: true)
        path.closeSubpath()
    }
    
    var body: some View {
        HStack {
            VStack {
                arc
                    .fill(.yellow, style: FillStyle(eoFill: true))
                arc
                    .fill(.yellow, style: FillStyle(eoFill: false))
            }
            VStack {
                arc2
                    .fill(.yellow, style: FillStyle(eoFill: true))
                arc2
                    .fill(.yellow, style: FillStyle(eoFill: false))
            }
        }
    }
```

![スクリーンショット 2024-10-14 18.46.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237700/c004212f-294f-2e71-bfea-81b8dfeacf43.png)

# おわりに
現時点（2024年10月）で出ているPathのAPIを全てまとめてみました。こうやって全て試してみると意外となんでも作れそうな気がします。
ドキュメントを全て見たところ、PathはSwiftUI登場のiOS13からありますが、その後追加されたAPIはiOS17でごく一部なのであまり進化がないようです。また、WWDCのセッションでもPathについて解説してるものもなく、SwiftUI登場時のセッションで軽く触れられている程度でした。
間違いなどあればコメントいただけると助かります。

# 参考資料
https://developer.apple.com/documentation/swiftui/path

https://techlife.cookpad.com/entry/2023/06/21/162523

https://www.akamoz.jp/you/uni/ps/clipping.htm