swiftui-expert-skill - previews
SwiftUI 预览参考目录Preview 宏带模拟数据的预览Previewable 属性包装器常见诊断汇总清单Preview 宏#Preview宏Swift 5.9、Xcode 15是声明预览的现代方式。传统的PreviewProvider协议仍然有效新代码优先使用#Preview因为它更简洁并支持内联 traits。基本用法// 现代#Preview 宏#Preview{ContentView()}// 命名预览#Preview(Dark Mode){ContentView().preferredColorScheme(.dark)}// 传统PreviewProvider —— 仍然有效但对新代码来说过于冗长structContentView_Previews:PreviewProvider{staticvarpreviews:someView{ContentView()}}多个预览为每个有意义的渲染状态声明一个#Preview让每个状态在画布中独立渲染#Preview(Default){SettingsRow(title:Notifications,isOn:true)}#Preview(Off State){SettingsRow(title:Notifications,isOn:false)}#Preview(Long Title){SettingsRow(title:Enable Push Notifications for All Events,isOn:true)}预览 TraitsTraits 无需修改视图本身即可配置预览环境// 固定尺寸#Preview(traits:.fixedLayout(width:300,height:100)){CompactBanner(message:Welcome)}// 适合内容的尺寸#Preview(traits:.sizeThatFitsLayout){BadgeView(count:5)}// 横屏方向#Preview(traits:.landscapeLeft){DashboardView()}在 NavigationStack 内部预览将预览的目标视图包裹在其导航容器中以便工具栏项、标题和返回按钮正确渲染#Preview{NavigationStack{DetailView(item:.sample)}}带模拟数据的预览预览必须能够在没有外部依赖的情况下编译和渲染。实时服务、网络调用和磁盘 I/O 会使预览变慢、不稳定或损坏请使用自包含的示例数据。静态示例数据将示例值作为静态属性暴露在模型本身上这样任何预览都可以复用它们而无需内联重建值structItem:Identifiable{letid:UUIDvarname:Stringvarprice:Double}extensionItem{staticletsampleItem(id:UUID(),name:Widget,price:9.99)staticletsamples:[Item][Item(id:UUID(),name:Widget,price:9.99),Item(id:UUID(),name:Gadget,price:19.99),Item(id:UUID(),name:Doohickey,price:4.99),]}#Preview{ItemListView(items:Item.samples)}模拟可观察模型对于由Observable模型驱动的视图基础见state-management.md在模型本身上暴露预配置的实例ObservableMainActorfinalclassCartModel{varitems:[Item][]varisLoadingfalsestaticvarpreview:CartModel{letmodelCartModel()model.itemsItem.samplesreturnmodel}staticvaremptyPreview:CartModel{CartModel()}staticvarloadingPreview:CartModel{letmodelCartModel()model.isLoadingtruereturnmodel}}#Preview(With Items){CartView().environment(CartModel.preview)}#Preview(Empty){CartView().environment(CartModel.emptyPreview)}#Preview(Loading){CartView().environment(CartModel.loadingPreview)}带环境依赖的预览注入视图依赖的任何环境值让预览反映真实的运行时上下文#Preview{OrderDetailView(order:.sample).environment(CartModel.preview).environment(\.locale,Locale(identifier:ja_JP)).environment(\.dynamicTypeSize,.xxxLarge)}模拟异步数据源当视图依赖网络或数据服务时为依赖提供协议抽象让预览可以注入一个立即返回示例数据的同步 mock。这是一种方法——根据周围代码库已经使用的模式进行调整。protocolDataFetching{funcfetchItems()asyncthrows-[Item]}structLiveDataFetcher:DataFetching{leturl:URLfuncfetchItems()asyncthrows-[Item]{let(data,_)tryawaitURLSession.shared.data(from:url)returntryJSONDecoder().decode([Item].self,from:data)}}structMockDataFetcher:DataFetching{varresult:Result[Item],Error.success(Item.samples)funcfetchItems()asyncthrows-[Item]{tryresult.get()}}#Preview{ItemListView(fetcher:MockDataFetcher())}#Preview(Error State){ItemListView(fetcher:MockDataFetcher(result:.failure(URLError(.notConnectedToInternet))))}Previewable 属性包装器PreviewableiOS 18、Xcode 16让你可以直接在#Preview块内使用State、FocusState和其他属性包装器无需用包装视图承载交互式状态。交互式状态// Previewable在预览中内联交互式开关#Preview{PreviewableStatevarisOnfalseToggle(Notifications,isOn:$isOn)}// 没有 Previewable需要包装视图structTogglePreviewWrapper:View{StateprivatevarisOnfalsevarbody:someView{Toggle(Notifications,isOn:$isOn)}}#Preview{TogglePreviewWrapper()}多个交互控件#Preview{PreviewableStatevarnameAlicePreviewableStatevarage25.0VStack{TextField(Name,text:$name)Slider(value:$age,in:0...100,step:1){Text(Age:\(Int(age)))}Text(Hello,\(name)! Age:\(Int(age)))}.padding()}带 FocusState 的 Previewable在预览内设置初始焦点时优先使用.defaultFocus而不是从.onAppear写入FocusState。.onAppear可能与初始渲染竞争焦点赋值可能丢失。底层原理请参阅focus-patterns.md。#Preview{PreviewableFocusStatevarisFocused:BoolTextField(Search,text:.constant()).focused($isFocused).defaultFocus($isFocused,true)}iOS 18 之前目标的回退如果项目的最低部署目标低于 iOS 18Previewable不可用。回退到包装视图privatestructSliderPreview:View{Stateprivatevarvalue0.5varbody:someView{CustomSlider(value:$value)}}#Preview{SliderPreview()}常见诊断症状原因修复#Previewbody 类型不匹配闭包返回非View类型确保最终表达式是ViewPreviewable仅在 iOS 18 可用部署目标较低时使用Previewable使用包装视图或用#available门控预览崩溃并提示缺少环境未注入Environment(SomeType.self)值在预览中添加.environment(SomeType.preview)预览挂起或渲染空白视图依赖永远不会解析的异步数据注入一个立即返回示例数据的 mock从非隔离上下文访问MainActor隔离的模型预览辅助函数在主 actor 之外触碰仅主 actor 的 API将辅助函数或预览 body 标记为MainActor汇总清单新预览优先使用#PreviewPreviewProvider对旧代码仍然有效为每个有意义的渲染状态提供命名预览默认、空、错误、加载目标是 iOS 18 时交互式预览使用Previewable否则用包装视图在模型上暴露静态的.sample/.preview数据让预览不必内联重建值当视图依赖异步数据时通过协议注入 mock 服务预览中绝不依赖实时网络或磁盘 I/O在预览中设置FocusState时优先使用.defaultFocus而不是.onAppear写入