はじめに
SwiftUIには、ローディング中とかにぐるぐるするインジケーターがProgressView
として用意されています。
大抵こういうViewは共通化されると思うので、使いやすくするためにProgressViewの カスタム ViewModifierを作ってみました。
普通にViewにしてもいいと思ったのですが、
Viewにすると多分こうなってしまうと思います↓
使う側では、毎回ZStack
で囲わないといけないので、面倒な気がします。
ZStack {
Text("Hello World")
if isShownProgressView {
CustomProgressView()
}
}
そうではなくこうやって使えた方が、ZStack
とか不要だし便利だろうと思ったので、ViewModifierで実装してます。↓
Text("Hello World")
.customProgressView($isShownProgressView)
成果物
よくあるローディング中画面が表示されています。
動画で見たい場合は、以下のGithubリポジトリで公開しているので、そのREADMEを確認ください。
Color
をProgressView
の前の階層に入れているので、この画面が表示されているときは後ろは操作不可にしてます。
ProgressView
だけでは、後ろは操作不可にしてくれませんので、自分で実装必要でした。
実装を見ればわかりますが、
「読み込み中」のテキストや色、背景色は自由にカスタマイズできます。
実装
まずカスタム ViewModifierの実装です。
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を実装します。
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
を使っています。
では早速使ってみる。
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
で持っています。
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をカスタマイズする
- 【SwiftUI】カスタムModifierの作成
- How to create custom View modifiers for better code reusability in SwiftUI→かなり参考にさせていただきました。ProgressViewではなくもっとUI的に凝ったローディング画面を作りたい場合にはこの記事がおすすめです。
おわりに
Custom ViewModifierを勉強する良い機会になりました。
まだまだSwiftUI初心者ですが、ちょっとずつ進歩してることを願ってます。
誤り、もっと良いやり方あるなどありましたら、是非コメントで教えてください
誰かの役に立てば幸いです。