You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为SwiftUI视图实现通用骨架屏(Placeholder)效果?

SwiftUI通用Redacted骨架屏实现方案

在SwiftUI中,当视图依赖异步数据时,传统写法需要为每个子视图添加if-else判断来切换真实内容和占位视图,面对复杂视图时会非常繁琐。以下是两种通用实现方案,无需重复编写条件判断即可统一添加骨架屏效果:


方案一:自定义ViewModifier + 加载状态管理

1. 封装加载状态对象

首先定义一个通用的加载状态管理类,统一处理异步数据的加载、状态更新和错误处理:

class LoadState<T>: ObservableObject {
    @Published var value: T?
    @Published var isLoading = true
    
    func load(_ task: @escaping () async throws -> T) {
        Task {
            do {
                value = try await task()
            } catch {
                // 根据需求添加错误处理逻辑
                print("加载失败:\(error.localizedDescription)")
            } finally {
                isLoading = false
            }
        }
    }
}

2. 自定义占位样式Modifier

创建一个ViewModifier,将redacted样式和加载状态绑定,同时提供便捷的View扩展方法:

struct RedactedPlaceholderModifier: ViewModifier {
    var isLoading: Bool
    
    func body(content: Content) -> some View {
        content
            .redacted(reason: isLoading ? .placeholder : [])
            .disabled(isLoading) // 加载时禁用交互
    }
}

extension View {
    func redactedPlaceholder(when isLoading: Bool) -> some View {
        modifier(RedactedPlaceholderModifier(isLoading: isLoading))
    }
}

3. 使用示例

在视图中只需绑定加载状态,给父容器添加一次Modifier即可实现全局占位效果:

struct ContentView: View {
    @StateObject private var textLoadState = LoadState<String>()
    
    var body: some View {
        VStack(spacing: 16) {
            Text(textLoadState.value ?? "占位文本")
            // 复杂视图示例
            HStack(alignment: .center, spacing: 12) {
                Circle()
                    .frame(width: 40, height: 40)
                VStack(alignment: .leading, spacing: 4) {
                    Text(textLoadState.value ?? "用户名")
                        .font(.headline)
                    Text(textLoadState.value ?? "用户简介内容")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding()
        .redactedPlaceholder(when: textLoadState.isLoading)
        .onAppear {
            textLoadState.load {
                try await fetchAsyncData()
            }
        }
    }
    
    // 模拟异步数据请求
    func fetchAsyncData() async throws -> String {
        try await Task.sleep(nanoseconds: 2_000_000_000)
        return "加载完成的真实内容"
    }
}

方案二:@ViewBuilder封装通用加载容器

如果不需要复杂的状态管理,可以直接用@ViewBuilder封装一个通用容器,简化占位逻辑:

struct LoadingView<Content: View>: View {
    let isLoading: Bool
    @ViewBuilder let content: () -> Content
    
    var body: some View {
        content()
            .redacted(reason: isLoading ? .placeholder : [])
            .disabled(isLoading)
    }
}

使用示例

直接用LoadingView包裹需要添加占位的内容,只需传入加载状态即可:

struct ContentView: View {
    @State private var asyncText: String?
    private var isLoading: Bool { asyncText == nil }
    
    var body: some View {
        LoadingView(isLoading: isLoading) {
            VStack(spacing: 16) {
                Text(asyncText ?? "占位文本")
                HStack {
                    Circle().frame(width: 40, height: 40)
                    VStack(alignment: .leading) {
                        Text(asyncText ?? "用户名")
                        Text(asyncText ?? "用户简介")
                    }
                }
            }
            .padding()
        }
        .task {
            asyncText = try await fetchAsyncData()
        }
    }
    
    func fetchAsyncData() async throws -> String {
        try await Task.sleep(nanoseconds: 2_000_000_000)
        return "真实内容"
    }
}

方案优势

  • 避免重复编写if-else条件判断,复杂视图下代码更简洁
  • 统一管理占位样式和加载状态,便于全局修改样式逻辑
  • 支持交互禁用等额外逻辑,提升用户体验

内容的提问来源于stack exchange,提问作者xTwisteDx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:15:17