SwiftUI:如何从View body返回遵循协议的视图并处理错误?
SwiftUI网络错误处理方案
你的代码主要存在两个问题:
- SwiftUI中直接使用
any ErrorableViewProtocol作为子视图会触发类型擦除问题,因为View是泛型协议,existential类型无法直接作为View的子节点; ErrorableView中的error参数未被使用,且值类型的normal无法直接修改其error属性。
修正后的基础代码
用泛型替代existential类型,同时通过绑定来管理错误状态:
protocol ErrorableViewProtocol: View { var error: Error? { get set } } struct ErrorableView<Content: ErrorableViewProtocol>: View { @Binding var content: Content var body: some View { if let error = content.error { ErrorView(error: error) } else { content } } } // 示例错误视图 struct ErrorView: View { let error: Error var body: some View { VStack { Text("出错了") Text(error.localizedDescription) Button("重试") { // 此处添加重试逻辑 } } } } // 示例符合协议的正常视图 struct NormalView: ErrorableViewProtocol { @State var error: Error? var body: some View { Text("正常展示内容") } }
实用的网络错误处理方案
方案1:当前位置替换显示错误视图
适合在原视图位置直接切换到错误界面,通过状态变量控制视图展示:
struct ContentView: View { @State private var networkError: Error? @State private var isLoading = false var body: some View { VStack { if let error = networkError { ErrorView(error: error, retryAction: fetchData) } else if isLoading { ProgressView("加载中...") } else { Text("网络请求成功的内容") } } .onAppear(perform: fetchData) } private func fetchData() { isLoading = true // 模拟网络请求 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { isLoading = false // 模拟错误场景 networkError = NSError(domain: "com.example", code: -1, userInfo: [NSLocalizedDescriptionKey: "网络连接失败"]) } } } // 带重试按钮的错误视图 struct ErrorView: View { let error: Error let retryAction: () -> Void var body: some View { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text("请求失败") .font(.title) Text(error.localizedDescription) .foregroundColor(.secondary) .multilineTextAlignment(.center) Button("重试", action: retryAction) .buttonStyle(.borderedProminent) } .padding() } }
方案2:导航到独立错误页面
如果需要跳转至单独的错误详情页,可结合NavigationStack(iOS 16+)使用:
struct ContentView: View { @State private var networkError: Error? @State private var isLoading = false var body: some View { NavigationStack { VStack { if isLoading { ProgressView("加载中...") } else { Text("正常内容") } } .navigationTitle("首页") .navigationDestination(item: $networkError) { error in ErrorDetailView(error: error, retryAction: { networkError = nil fetchData() }) } .onAppear(perform: fetchData) } } private func fetchData() { isLoading = true DispatchQueue.main.asyncAfter(deadline: .now() + 1) { isLoading = false networkError = NSError(domain: "com.example", code: -1, userInfo: [NSLocalizedDescriptionKey: "网络连接失败"]) } } } // 错误详情页 struct ErrorDetailView: View { let error: Error let retryAction: () -> Void var body: some View { VStack(spacing: 20) { Image(systemName: "wifi.slash") .font(.system(size: 60)) .foregroundColor(.red) Text("网络错误") .font(.largeTitle) .fontWeight(.bold) Text(error.localizedDescription) .foregroundColor(.gray) .padding(.horizontal) .multilineTextAlignment(.center) Button("返回重试", action: retryAction) .buttonStyle(.borderedProminent) } .navigationTitle("错误提示") } } // 让Error符合Identifiable协议,支持navigationDestination extension Error: Identifiable { public var id: String { localizedDescription } }
方案3:用弹出层展示错误视图
通过fullScreenCover或sheet弹出错误提示,不切换页面:
struct ContentView: View { @State private var networkError: Error? @State private var isLoading = false var body: some View { VStack { if isLoading { ProgressView("加载中...") } else { Text("正常内容") } } .fullScreenCover(item: $networkError) { error in ErrorPopoverView(error: error) { networkError = nil fetchData() } } .onAppear(perform: fetchData) } private func fetchData() { isLoading = true DispatchQueue.main.asyncAfter(deadline: .now() + 1) { isLoading = false networkError = NSError(domain: "com.example", code: -1, userInfo: [NSLocalizedDescriptionKey: "网络连接失败"]) } } } struct ErrorPopoverView: View { let error: Error let retryAction: () -> Void var body: some View { VStack(spacing: 20) { Spacer() Image(systemName: "xmark.octagon") .font(.system(size: 50)) .foregroundColor(.red) Text("请求失败") .font(.title) Text(error.localizedDescription) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) HStack { Button("取消", action: retryAction) .buttonStyle(.bordered) Button("重试", action: retryAction) .buttonStyle(.borderedProminent) } Spacer() } .padding() .background(Color.white) .cornerRadius(20) .padding(.horizontal) } }
内容的提问来源于stack exchange,提问作者Anton Tropashko
相关产品推荐
相关产品推荐

