如何为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
相关产品推荐
相关产品推荐

