見出し画像

SwiftUIでSkeleton Screenを作る

最近YouTubeやSlackなどのアプリでよく見かけるSkeleton Screen(ローディング中のプレースホルダー表示)をSwiftUIで実装してみた。

Skeleton Screenとは?

Skeleton Screenとは、ローディング中に表示される骨組みのようなデザインのことで、スピナーと比べるとどんなコンテンツが表示されるのかがわかりやすいため、ローディング中の心理的負担が軽減されると言われている。また、シマーアニメーションと組み合わせて動きを付けることも多い。

SwiftUIでの実装方法

SwiftUIにはredacted(reason:)という便利なmodifierがあり、これを使えば簡単にViewをプレースホルダーで表示させることができる。

Text("テキスト")
    // .placeholderでプレースホルダーが表示され、空配列の場合は通常表示
    .redacted(reason: shouldBeRedacted ? .placeholder : [])

また、シマーアニメーションはSwiftUI-Shimmerという軽量なライブラリを使えば簡単に適用できる。ちなみに、ライブラリ内の実装では、ViewModifierを用意していて、LinearGradientのMaskを適用させて、Animation.linear(duration:)でグラデーションのハイライト箇所を移動させている。

Text("テキスト")
    .redacted(reason: shouldBeRedacted ? .placeholder : [])
    // これだけでシマーアニメーションを適用できる
    .shimmering()

サンプルコード

import SwiftUI
import Shimmer

struct ListItem: Identifiable {
    var id: Int
    var iconName: String
    var title: String
    var text: String
}

struct ContentView: View {
    @State private var isLoading = false
    
    let items = Array(0..<20).map {
        ListItem(id: $0,
                 iconName: "heart",
                 title: "\\($0+1)",
                 text: "List item\\($0+1)")
    }
    
    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    VStack(alignment: .leading, spacing: 8) {
                        Text(item.title)
                            .foregroundColor(isLoading ? .blue : .white)
                            .font(.system(size: 12))
                            .frame(minWidth: 20)
                            .padding(2)
                            .background(.blue)
                            .cornerRadius(4)
                        
                        Text(item.text)
                    }
                    
                    Spacer()
                    
                    Image(systemName: item.iconName)
                }
                // isLoading中にプレースホルダーを表示する
                .redacted(reason: isLoading ? .placeholder : [])
                // シマーアニメーションを適用
                .shimmering(active: isLoading)
            }
        }
        .listStyle(.plain)
        
        .task {
            isLoading = true
            defer { isLoading = false }
            try? await Task.sleep(for: .seconds(5))
        }
    }
}

リポジトリ

参考


この記事が気に入ったらサポートをしてみませんか?