重复if条件引发Swift编译器类型检查失败,求原因与解决办法
背景
跟着教程开发时,添加了一个仅在选中按钮时显示的导航链接,随后出现编译错误:
CompileDylibError: Failed to build DayView.swift
Compiling failed: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
.../DayView.1.preview-thunk.swift:35:76: error: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
@_dynamicReplacement(for: body) private var __preview__body: some View {
^
代码里包含两处if selectedDate == date {…}语句,将任意一处改为if true {…}后错误就会消失。想搞清楚:
- 这个问题的成因是什么?是不是不允许使用两个相同的if语句?
- 有什么解决办法?
- 是否是对@State变量的理解有误,意外创建了无限循环?
原始代码
struct DayView: View { // 可选的日期列表 @State var dates = [ Calendar.current.date(byAdding: .hour, value: 1, to: Date())!, Calendar.current.date(byAdding: .hour, value: 2, to: Date())!, Calendar.current.date(byAdding: .hour, value: 3, to: Date())!, Calendar.current.date(byAdding: .hour, value: 4, to: Date())!, ] // 记录选中的日期 @State var selectedDate: Date? // 可选类型,可能没有选中的日期 var body: some View { ScrollView { VStack { Text("Today's Date") Divider() .padding(.vertical) Text("Select A Time") .font(.largeTitle) .bold() ForEach(dates, id:\.self) { date in HStack { Button { withAnimation { selectedDate = date } } label: { Text(date.timeFromDate()) .bold() .padding() .frame(maxWidth: .infinity) .foregroundColor(selectedDate == date ? .white : .blue) .background( ZStack { // 将此处改为if true可消除错误 1/2 if selectedDate == date { // 按钮选中状态 RoundedRectangle(cornerRadius: 10) .fill(.blue.opacity(0.2)) } else { // 按钮未选中状态 RoundedRectangle(cornerRadius: 10) .stroke() } } ) } // 将此处改为if true可消除错误 2/2 if selectedDate == date { // 选中时显示导航按钮 NavigationLink { EmptyView() } label: { Text("Next") .bold() .padding() .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) } } } } } } .navigationTitle("Day Of The Week") .navigationBarTitleDisplayMode(.inline) } }
第二处if selectedDate == date {…}用于添加深蓝色的NavigationLink按钮,仅在选中对应时间按钮时显示。
更新记录
12月25日
制作了最小可复现示例(MRE),发现只要移除/修改标注的5行中的任意一行,预览就能成功编译。比如去掉.bold()就能通过编译。
12月26日
今早尝试编译去掉.bold()的示例,结果失败;取消.bold()注释等编译失败后再注释掉,编译又成功了,结果不稳定。
执行Product > Clean Build Folder并重启Xcode后,错误依旧。
开发环境:
- MacBook Pro (Retina, 15-inch, Mid 2015)
- MacOS 12.7.2 (21G1974)
- Xcode Version 14.2 (14C18)
- 模拟器:iOS 16.2 (20C52)
最小可复现代码
import SwiftUI struct DayView: View { // 可选的日期列表 @State var dates = [ Calendar.current.date(byAdding: .hour, value: 1, to: Date())! ] // 记录选中的日期 @State var selectedDate: Date? var body: some View { VStack { ForEach(dates, id:\.self) { date in HStack { Button { selectedDate = date } label: { Text(date.timeFromDate()) // 改为Text("test")可编译成功 1/5 .bold() // 移除该行可编译成功 2/5 .padding() .frame(maxWidth: .infinity) .foregroundColor(selectedDate == date ? .white : .blue) // 移除该行可编译成功 3/5 .background( ZStack { if selectedDate == date { // 改为if true可编译成功 4/5 RoundedRectangle(cornerRadius: 10) .fill(.blue.opacity(0.2)) } else { RoundedRectangle(cornerRadius: 10) .stroke() } } ) } if selectedDate == date { // 改为if true可编译成功 5/5 NavigationLink { EmptyView() } label: { Text("Next") .padding() .frame(maxWidth: .infinity) } } } } } } } extension Date { func timeFromDate() -> String { let formatter = DateFormatter() formatter.dateFormat = "HH:mm a" return formatter.string(from: self) } } struct DayView_Previews: PreviewProvider { static var previews: some View { NavigationStack { DayView() } } }
解答
原因分析
这个错误不是因为使用了两个相同的if语句,也不是@State导致的无限循环,本质是SwiftUI编译器的类型检查过载。
SwiftUI的body是一个复杂的表达式树,当同一个闭包(此处为ForEach的闭包)内多次引用同一个条件判断selectedDate == date,再叠加文本样式、条件视图、颜色判断等多个动态元素时,编译器需要推导的类型组合会指数级增长,超出了它在合理时间内的处理上限。
旧版Xcode(14.2)对这类复杂视图的类型检查优化不如新版本,再加上2015款Mac的性能限制,更容易触发该问题。编译结果不稳定(有时修改后能过,有时不行)也是编译器过载时的偶发表现。
解决办法
核心思路是拆分复杂表达式,减少编译器的类型推导压力:
提前计算选中状态
在ForEach闭包内先把selectedDate == date的结果存为布尔变量,避免重复编写条件判断:ForEach(dates, id:\.self) { date in let isSelected = selectedDate == date // 提前计算选中状态 HStack { Button { withAnimation { selectedDate = date } } label: { Text(date.timeFromDate()) .bold() .padding() .frame(maxWidth: .infinity) .foregroundColor(isSelected ? .white : .blue) .background( ZStack { if isSelected { RoundedRectangle(cornerRadius: 10) .fill(.blue.opacity(0.2)) } else { RoundedRectangle(cornerRadius: 10) .stroke() } } ) } if isSelected { NavigationLink { EmptyView() } label: { Text("Next") .bold() .padding() .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) } } } }提取子视图
把按钮和导航链接拆成独立的子视图,进一步简化body的复杂度:struct TimeButton: View { let date: Date let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { Text(date.timeFromDate()) .bold() .padding() .frame(maxWidth: .infinity) .foregroundColor(isSelected ? .white : .blue) .background( ZStack { if isSelected { RoundedRectangle(cornerRadius: 10) .fill(.blue.opacity(0.2)) } else { RoundedRectangle(cornerRadius: 10) .stroke() } } ) } } } struct NextLink: View { var body: some View { NavigationLink { EmptyView() } label: { Text("Next") .bold() .padding() .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) } } }之后在
DayView的body中调用:ForEach(dates, id:\.self) { date in let isSelected = selectedDate == date HStack { TimeButton(date: date, isSelected: isSelected) { withAnimation { selectedDate = date } } if isSelected { NextLink() } } }其他辅助优化
- 避免重复创建
DateFormatter,将其改为静态属性:extension Date { private static let timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm a" return formatter }() func timeFromDate() -> String { Date.timeFormatter.string(from: self) } } - 清理Xcode缓存:除了
Clean Build Folder,还可以删除~/Library/Developer/Xcode/DerivedData目录下的缓存文件,再重启Xcode。
- 避免重复创建
内容的提问来源于stack exchange,提问作者Purp

