作りながら学ぶ！ SwiftUIアニメーション インジケーター編

# はじめに

作りながら学ぶ! アニメーション インジケーター編です。

完成するとこのようなアニメーションになります。
![ylcjk-49v22.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/a7a7976c-45ce-cccc-2840-a341d7b30ac5.gif)
 


## 作ろう

### 円を作る

   まずは土台となる円を作ります。

   ```swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        Circle()
    }
}
   ```

   <img width="300" alt="スクリーンショット 2020-02-04 13.28.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/6773c617-f8f4-e9d9-d416-a0e1eeb00597.png">


### 円をくり抜いて輪を作る

 円を輪にするには`stroke`メソッドを使います。

   ```swift
func stroke<S>(S, lineWidth: CGFloat) -> View
func stroke<S>(S, style: StrokeStyle) -> View
func stroke(lineWidth: CGFloat) -> Shape
func stroke(style: StrokeStyle) -> Shape
   ```
https://developer.apple.com/documentation/swiftui/shape

ここの`<S>`には`ShapeStyle`に準拠している型を指定することができます。

ColorやGradientといった色や色のグラデーションを指定することができます。
https://developer.apple.com/documentation/swiftui/shapestyle

最初はわかりやすいように色をグリーンにします。

```swift
struct ContentView: View {
    var body: some View {
            Circle()
                .stroke(Color.green)
    }
}
```
![スクリーンショット 2020-02-09 23.52.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/4c572f9a-4916-718a-5f81-ddb73b7b3040.png)

これでは線が細くて見づらいので、線の幅を広げて見やすいようにしましょう。
そのために`StrokeStyle`を当てていきます。

## StrokeStyle
境界線または仕切りの色、幅、およびスタイルを定義するオブジェクト。
https://developer.apple.com/documentation/apple_news/strokestyle

```swift
        StrokeStyle(lineWidth: CGFloat,
                    lineCap: CGLineCap,
                    lineJoin: CGLineJoin,
                    miterLimit: CGFloat,
                    dash: [CGFloat],
                    dashPhase: CGFloat)
```

### lineWidth
線の幅 デフォルトは1 

ですので線幅を大きくするために、`lineWidth`を8にします。

```swift
struct ContentView: View {
    var body: some View {
            Circle()
                .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
    }
}
```

![スクリーンショット 2020-02-10 0.24.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/fef6809a-9626-8fe0-559c-491ff7ddb4cc.png)

円のままだと、回転してもわからないので`trim`メソッドを使ってトリミングしていきます。

### `trim(from:to:)`

```swift
func trim(from startFraction: CGFloat = 0, to endFraction: CGFloat = 1) -> some Shape
```

https://developer.apple.com/documentation/swiftui/shape/3365372-trim


```swift
struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.4)
            .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
    }
}
```
![スクリーンショット 2020-02-11 15.47.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/1ea0e10c-ca5d-742e-1d3b-f99b74ea2fd1.png)

現在のままだと円が大きいので、`frame`メソッドを使い調整します。

```swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
            .frame(width: 48, height: 48)
    }
}

```
![スクリーンショット 2020-02-11 15.54.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/9fbf3797-e479-d892-d713-2780279714f5.png)


インジケーターっぽいサイズになってきました！

![スクリーンショット 2020-02-10 0.59.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/c3608a3f-34c1-feda-b49f-4f7afe0373c8.png)

角が尖っているのを、丸くしていきます。
そのために`StrokeStyle`の`lineCap`を利用します。

### CGLineCap (iOS 2.0+)
|  CGLineCap  | 線の端 | 末端 | 画像 |
| :---------: | :----: | :--: | :--: |
|  **.butt**|  四角  |  指定されたパスのエンドポイントまで  |    ![スクリーンショット 2020-02-08 22.37.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/62075154-d25e-52db-79e3-adef84abb7d8.png)|
| **.round**  |   丸   |  指定されたパスの端点を超える    |   ![スクリーンショット 2020-02-08 22.37.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/9977f82c-50b9-75e1-b878-55406fe57f4d.png)|
| **.square** |  四角  |    指定されたパスの端点から線幅の半分だけ超える  |    ![スクリーンショット 2020-02-08 22.37.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/14419571-8f44-cf93-5fb7-14dca6ac58d9.png)|
https://developer.apple.com/documentation/coregraphics/cglinecap

```swift
struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(lineWidth: 8, lineCap: .round))
            .frame(width: 48, height: 48)
    }
}
```

![スクリーンショット 2020-02-10 1.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/64ecab99-a7bf-94f3-877c-f374b708aba0.png)

### CGLineJoin (iOS 2.0+)
以下のように角がある図形だと変化がわかります。
しかし、今回作成するインジケーターは円弧なので軽く流します。

|          | .miter | .round | .bevel |
| -------- | ------ | ------ | ------ |
| lineJoin |   ![スクリーンショット 2020-02-09 0.25.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/b6108ad8-c981-fabc-78ae-7380575daf5b.png)|![スクリーンショット 2020-02-09 0.25.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/f71328d0-9094-8856-f1a2-0deb64e4795c.png)|     ![スクリーンショット 2020-02-09 0.29.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/09626874-acfc-fa61-1333-02401cb098fe.png)|

### miterLimit
先端部の形状に`.miter`を適用を決める閾値(デフォルト 10)

### dash
破線の形状を配列で指定します。

```swift
[線の長さ, 空白の長さ, 2番目の線の長さ, 2番目の空白の長さ, ...]
```


```swift
struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.4)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            .frame(width: 48, height: 48)
    }
}
```
![スクリーンショット 2020-02-10 2.45.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/95a832d4-f402-d7d6-00ff-8361b2647350.png)


### dashPhase 
破線の開始位置を変更します。デフォルトは０

### 回転させよう
いよいよアニメーションです。
2次元の回転系のアニメーションには、`rotatioinEffect`を使います。

## `rotationEffect(_:anchor:)`

```swift
func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View
```
https://developer.apple.com/documentation/swiftui/text/3276966-rotationeffect
`angle`には回転する角度を指定します。
`anchor`には回転する中心を指定します。デフォルトは`.center`です。

### Angle
https://developer.apple.com/documentation/swiftui/angle
Angleには`degrees`と`radians`を渡すことができます。
今回は角度を渡すことにするので`degrees`を引数にとります。

```swift
struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: 180))
    }
}
```
真ん中を中心に180度回転しました。
![スクリーンショット 2020-02-10 3.16.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/abb21dd2-fe1b-8296-325d-7cb994aed6e8.png)

しかしこれでは、アニメーションとは言えません。
ですので、時間経過とともに変化するようにしていきます。

### onAppear
まずはじめに、どのタイミングでアニメーションが発火するか決めます。
ボタンを押したら、スクロールしたら、など色々ありますが、今回は対象となるViewが表示されたアニメーションが発火するようにします。
そこでonAppearメソッドを利用します。

```swift
func onAppear(perform action: (() -> Void)? = nil) -> some View
```
このメソッドを呼ぶことでViewが用事されたときにactionクロージャを実行します。

https://developer.apple.com/documentation/swiftui/text/3276931-onappear

次に肝心のアニメーションの処理です。
今回は　`withAnimation`メソッドを利用します。

### withAnimation
指定したアニメーションとともにViewを更新します。
https://developer.apple.com/documentation/swiftui/3279151-withanimation
bodyにはアニメーションとともに変化させたい状態変数をクロージャに渡します。
## Animation

| Animatioin | 説明                                       | gif  |
| ---------- | ------------------------------------------ | ---- |
| default    | デフォルト                                 |![default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/c5e993ba-c9ea-06aa-17bd-f9c1823c4a94.gif)|
| linear     | 直線的　一定の割合                         |![lineaer.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/29634f47-95c8-896e-3f01-b6c50c36101b.gif)|
| easeIn     | 徐々に早くなる                             |![easeIn.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/0eb6813d-962f-fb42-73b7-95a81228c84c.gif)|
| easeOut    | 徐々に遅くなる                             | ![easeOut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/38420e2a-f067-3c06-5cdf-450c765a471e.gif)|
| easeInOut  | 開始は遅く、中盤で加速し終盤でまた遅くなる |![easeInOut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/8dbfe1e6-dd28-e8c2-2fcb-db9d6dc213b1.gif)|


今回は一定速度で回り続けて欲しいので`linear`を利用します。

lienarメソッドにはアニメーション長さを指定することができます。
`lienar(duration:)`を利用します。


### @State 
状態変数をセットします。
これを`withAnimation`のクロージャ内で切り替えてあげることで状態が変化します。

```swift
struct ContentView: View {
    
    @State var isAnimation = false
    
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            
            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
            .onAppear() {
                withAnimation(
                    Animation
                        .linear(duration: 1)) {
                            self.isAnimation.toggle()
                }
        }
    }
}
```
 
このままですと、一度回転しただけで終わってしまいますので、`repeatForever(autoreverses:)`メソッドを利用します。

### `repeatForever(autoreverses:)`
https://developer.apple.com/documentation/swiftui/animation/3263783-repeatforever

| autoreverses |      |
| :----------: | :--: |
|     true     |   ![true.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/f6b8341b-359f-f849-9ed1-8021a9195618.gif)|
|    false     |      ![false.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/7fd10871-0fbd-8a18-533b-085a8658f810.gif)|

リバースしなくて良いので、`false`を指定します。

```swift
struct ContentView: View {
    
    @State var isAnimation = false
    
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            
            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
            .onAppear() {
                withAnimation(
                    Animation
                        .linear(duration: 1)
                        .repeatForever(autoreverses: true)) {
                            self.isAnimation.toggle()
                }
        }
    }
}
```
### RoundedRectangle


### グラデーション

```swift
Gradient(colors: [.gray, .white])
```


| グラデーション | 見た目 |
| :------------: | :----: |
|  Linear 線形   |    ![スクリーンショット 2020-02-11 11.37.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/1b5aed42-5d34-4923-fbf5-810baafdcbac.png)|
|  Angular 円形  |    ![スクリーンショット 2020-02-11 11.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/5b092e28-ee5e-3664-7c8c-f1d1dbb0ed15.png)|
| Radical 放物状 |     ![スクリーンショット 2020-02-11 11.37.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/15d9cf01-5270-d1c3-9f28-1c9ad7f926b1.png)|

`.stroke`メソッドの中の`Color.green`を`AngularGradient(gradient: Gradient(colors: [.gray, .white])`に変更します。
そうすると以下のように破線の位置がずれていることが確認できます。

![スクリーンショット 2020-02-11 2.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/858f79d4-5fca-e253-0cb3-38744d0099c9.png)

そこで`StrokeStyle`の`dashPhase`の値をかえます。

`dashPhase: 8`にすることで、

![スクリーンショット 2020-02-11 2.07.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/a712ec90-955b-5391-ec03-d36a99a0e889.png)

綺麗に描画されました。

##　完成済みソースコード
```swift
import SwiftUI

struct ContentView: View {
    
    @State var isAnimation = false
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .frame(width: 200, height: 120, alignment: .center)
                .foregroundColor(Color.gray)
            
            VStack {
                Spacer()
                Circle()
                    .trim(from: 0, to: 0.6)
                    .stroke(AngularGradient(gradient: Gradient(colors: [.gray, .white]), center: .center),
                            style: StrokeStyle(
                                lineWidth: 8,
                                lineCap: .round,
                                dash: [0.1, 16],
                                dashPhase: 8))
                    
                    .frame(width: 48, height: 48)
                    .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
                    .onAppear() {
                        withAnimation(
                            Animation
                                .linear(duration: 1)
                                .repeatForever(autoreverses: false)) {
                                    self.isAnimation.toggle()
                        }
                }
                
                Text("読み込み中")
                    .foregroundColor(.white)
                    .font(.system(size: 12, weight: .medium, design: .rounded))
                    .lineLimit(1)
                    .padding(.top)
                Spacer()
            }
        }
    }
}
```

### 課題

strokestykeのdashにアニメーション当てれなかった；；
dashの幅を変えて、アニメーションをeaseOutでアニメーションの終わりに一つ一つの玉が近くアニメーションを作りたかった；；
誰か教えてくださいmm