【SwiftUI】よくあるインジケーター表示切り替えを、ProgressViewとカスタム ViewModifierで使いやすくする

## はじめに

SwiftUIには、ローディング中とかにぐるぐるするインジケーターが`ProgressView`として用意されています。

大抵こういうViewは共通化されると思うので、使いやすくするためにProgressViewの カスタム ViewModifierを作ってみました。

普通にViewにしてもいいと思ったのですが、
Viewにすると多分こうなってしまうと思います↓

使う側では、毎回`ZStack`で囲わないといけないので、面倒な気がします。

```swift
ZStack {
  Text("Hello World")

  if isShownProgressView {
    CustomProgressView()
  }
}
```

そうではなくこうやって使えた方が、`ZStack`とか不要だし便利だろうと思ったので、ViewModifierで実装してます。↓

```swift
Text("Hello World")
  .customProgressView($isShownProgressView)
```

## 成果物

よくあるローディング中画面が表示されています。
動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。

https://github.com/kamimi01/CustomProgressViewModifier

`Color`を`ProgressView`の前の階層に入れているので、この画面が表示されているときは後ろは操作不可にしてます。
`ProgressView`だけでは、後ろは操作不可にしてくれませんので、自分で実装必要でした。

[実装](#実装)を見ればわかりますが、
「読み込み中」のテキストや色、背景色は自由にカスタマイズできます。

![Simulator Screen Shot - iPhone 12 - 2021-09-08 at 19.30.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/ecbe3960-f2da-5d38-53d9-df7fac19d94d.png)

## 実装

まずカスタム ViewModifierの実装です。

```swift
import SwiftUI

struct CustomProgressView: ViewModifier {

    @Binding var isShownProgressView: Bool

    func body(content: Content) -> some View {
        // contentはこのカスタムViewModifierを使用する対象Viewのプロキシ
        ZStack { content
            if isShownProgressView {
                // ProgressViewの背景をタップ不可にするために、Colorを使用
                Color.gray.opacity(0.2)

                VStack(spacing: 6) {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .gray))
                        .scaledToFit()
                        .frame(width: 22, height: 22)

                    Text("読み込み中")
                        .foregroundColor(Color.gray)
                        .font(.caption2)
                }
            }
        }
    }
}
```

最初`content`を書くのを忘れてしまい、`ProgressView`は表示されるが背景にあるはずのViewが全く表示されないという現象で悩んでました。
`content`は
> 修飾する対象のViewのプロキシ

だそうなので、これがないと修飾する対象のViewが表示されないのは当然でした。。

なので、当然`if`文の外に`content`が存在してる必要があります。
そうでないと、`isShownProgressView`が`false`の時には修飾する対象のViewも一緒に非表示になってしまいます。

ちなみに`Text`のViewを使わなくても、
`ProgressView("読み込み")`というようにテキストを設定できるのですが、
フォントサイズや色など細かくカスタマイズしたい場合は、`Text`のViewを自分で作らないといけないみたいです。
`ProgressView(Text("読み込み").font(.caption2))`というようには書けなかったです。


では次に、ViewのExtensionを実装します。

```swift
extension View {
    func customProgressView(_ isShownProgressView: Binding<Bool>) -> some View {
        self.modifier(CustomProgressView(isShownProgressView: isShownProgressView))
    }
}
```

これで、初めに書いたように
`.customProgressView($isShownProgressView)`
とModifierをつければ例の画面が表示できるようになります。

Extensionでラップしない場合は
`.modifier(CustomProgressView(showProgressView: $showProgressView))`
と書くことになると思います。（実際にこれで動作確認はしていないのですが動くはず。。）

これはこれでいいかもですが、私はラップした方が他の標準Modifierと同じように使えてシンプルかなと思っています。

今更な説明ですが、親View側で`showProgressView`の真偽を切り替えることになると思うので、`@Binding`を使っています。

では早速使ってみる。

```swift
struct ContentView: View {
    @ObservedObject private var viewModel = ContentViewModel()
    
    var body: some View {
        
        VStack(spacing: 50) {
            Text("テスト1")
            Text("テスト2")
        }
        .background(Color.yellow)
        .customProgressView($viewModel.isShownProgressView)
        .onAppear {
            viewModel.onAppear()
        }
    }
}
```

viewModelクラスのプロパティとして、`isShownProgressView`を`@Published`で持っています。

```swift
class ContentViewModel: ObservableObject {
    
    @Published var isShownProgressView = false
    
    func onAppear() {
        showProgressView = true
        
        // 3秒後待ってインジケータを削除（実際はAPI通信したり、画像取得したり）
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            self.isShownProgressView = false
        }
    }
}
```

今回は3秒待つだけにしていますが、実際はAPIと通信したりとかとにかく何か時間がかかる処理が実装されることになると思います。

その処理完了の前後で`isShownProgressView`の真偽を切り替え、その値を監視することによって、
今回のローディング画面の切り替えを行なっています。

## 参考にした記事

- [SwiftUIのViewModifierを使ってViewをカスタマイズする](https://qiita.com/k_awoki/items/107773d6b14107954b91)
- [【SwiftUI】カスタムModifierの作成](https://capibara1969.com/1520/)
- [How to create custom View modifiers for better code reusability in SwiftUI](https://blog.techchee.com/how-to-create-custom-view-modifiers-for-better-code-reusability-in-swiftui/)→かなり参考にさせていただきました。ProgressViewではなくもっとUI的に凝ったローディング画面を作りたい場合にはこの記事がおすすめです。

## おわりに

Custom ViewModifierを勉強する良い機会になりました。
まだまだSwiftUI初心者ですが、ちょっとずつ進歩してることを願ってます。
誤り、もっと良いやり方あるなどありましたら、是非コメントで教えてください :blush: 

誰かの役に立てば幸いです。