SwiftUIでローディング中のスケルトン（プレースホルダー）を適用する方法

# はじめに
`List`や`LazyVGrid`などの画面初期化時にリスト数が決まっていないViewに対し、YoutubeやFacebookで使われているような、ロード中のスケルトンを表示する方法を書いてみました。

ちなみにスケルトンスクリーンとは、このようなやつです。

https://ws-design.net/skeleton_screen/

実装イメージは下記です。

<img width="360" alt="dummy" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/8c198097-adc1-d606-879d-b430468345b5.png"> <img width="360" alt="real" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/d041db6a-08d6-38b7-ac4b-5fbfed06e503.png">

# 結論
当たり前っちゃ当たり前なのですが、
リスト形式の場合、画面表示時には、表示数が0件なので、スケルトンを適用しても効果がありません。

なので、適当なDummyデータを作っておき、APIなどで本来表示すべき値が取得できたら、Dummyデータと置き換えて表示してあげればうまくいきます。

# 実装
結論で書いたことが、全てですので、簡単な実装だけご紹介します。
実際にAPIなどへリクエストすることを想定し、`Combine`を利用していますが、ここではそれについては触れません。

`Combine`については、以前この記事で書かせていただきましたのでもし良かったら、ご覧いただければと思います。

https://qiita.com/ctnc_67/items/394e668ee8c2a0b3327a

まず、ロード中にスケルトンを表示するためには、`redacted(reason:)`が標準で用意されているので、それを使います。

https://developer.apple.com/documentation/swiftui/view/redacted(reason:)

```swift:ContentViewModel.swift
struct UserInfo: Identifiable {
    let id: Int
    let name: String
    let age: Int
    let email: String
}

class ContentViewModel: ObservableObject {
    var cancellables = Set<AnyCancellable>()
    
    @Published var userInfoList = dummyData
    @Published var isFirstLoading = true
    
    let fetchUserSubject = PassthroughSubject<Void, Never>()
    
    static var dummyData: [UserInfo] {
        return (1...10).map {
            UserInfo(id: $0, name: "DummyName", age: 99, email: "dummy@test.com")
        }
    }
    
    init() {
        fetchUserSubject
            .flatMap({ _ -> AnyPublisher<[UserInfo], Never> in
                let userInfoList = (1...3).map {
                    UserInfo(id: $0, name: "RealName", age: 10, email: "real@real.com")
                }
                return Just(userInfoList).eraseToAnyPublisher()
            })
            .delay(for: 2, scheduler: DispatchQueue.main)
            .sink(receiveValue: { userInfoList in
                self.userInfoList = userInfoList
                self.isFirstLoading = false
            })
            .store(in: &cancellables)
    }
}

```

`static var dummyData: [UserInfo]`を定義して、`@Published var userInfoList = dummyData`のようにして、初期化時はダミーデータが入るようにします。

ViewModelでは、`let fetchUserSubject = PassthroughSubject<Void, Never>()`を購読していますので、View側から値が`send()`されたら、リアルデータをクラス変数にセットする処理が走ります。

本来は、APIによってデータを取得しますが、今回は、2秒後に固定値のリアルデータが帰ってくるように、処理を書いています。


```swift: ContentView.swift
struct ContentView: View {
    
    @ObservedObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack {
            List(viewModel.userInfoList) { user in
                HStack {
                    Text(user.name)
                    Spacer()
                    Text("\(user.age)歳")
                    Spacer()
                    Text(user.email)
                }
            }
            .redacted(reason: viewModel.isFirstLoading ? .placeholder : [])
        }
        .onAppear {
            viewModel.fetchUserSubject.send(())
        }
    }
}
```

`.redacted(reason: viewModel.isFirstLoading ? .placeholder : [])`
を`List`に対して適用することで、`List`の`{}`で囲まれたViewがすべてスケルトン適用されます。

`viewModel.isFirstLoading`は、リアルデータが取得できた時に`false`になるようにしているので、データが取れたら、自動的にスケルトンが解除されます。


以上となります。
List形式のViewで、ロード中のスケルトンを実装したい方の参考になれば幸いです。

# 参考にさせていただいた記事
https://qiita.com/MaShunzhe/items/4ed187a140259775c23c