SwiftUI 撸码常见错误 2 例漫谈

概述

在 SwiftUI 日常撸码过程中,头发尚且还算茂盛的小码农们经常会犯这样那样的错误。虽然犯这些错的原因都很简单,但有时想要快速准确的定位它们却并不容易。

况且这些错误还可能在模拟器和 Xcode 预览(Preview)表现的行为不甚一致,这无疑加大了"驯服"它们的难度。

在本篇博文中,您将学到如下内容:

  • 概述
  • [1. TabView 每个标签页 tag 类型需要谨慎对待](#1. TabView 每个标签页 tag 类型需要谨慎对待)
  • [2. 视图中 @FetchRequest 定义不完整导致的崩溃](#2. 视图中 @FetchRequest 定义不完整导致的崩溃)
  • 总结

本文出现的问题在 Xcode 16.1 + iOS 18.1 中仍会存在。

相信学完本课后,小伙伴们倘若以后再遇到与此类似的问题,必将胸有成竹、迎刃而解。

那还等什么呢?Let's fix it!!!😉


本文对应的视频课在此,欢迎小伙伴们恣意观赏:

SwiftUI 撸码常见错误 2 例漫谈


1. TabView 每个标签页 tag 类型需要谨慎对待

在下面这个简单的例子中,点击 TabView 内部每个标签页竟然毫无反应!

下面上源代码,你能看出是哪里的问题吗?

swift 复制代码
enum TabTag: Int {
    case home = 0, worry, game, user
}

@available(iOS 17, *)
@Observable
class Model {
    var currentSelectingTab = 0
}

@available(iOS 17.0, *)
struct Main: View {
    
    @State var model = Model()
    
    var body: some View {
        NavigationStack {
            TabView(selection: $model.currentSelectingTab) {
                Text("Home")
                    .tabItem {
                        Label("Home", systemImage: "house")
                    }
                    .tag(TabTag.home)
                
                Text("Worry")
                    .tabItem {
                        Label("Worry", systemImage: "figure.fall")
                    }
                    .tag(TabTag.worry)
                
                Text("Game")
                    .tabItem {
                        Label("游戏", systemImage: "gamecontroller.fill")
                    }
                    .tag(TabTag.game)
                
                Text("User")
                    .tabItem {
                        Label("用户", systemImage: "person.fill")
                    }
                    .tag(TabTag.user)
            }
            .navigationTitle("SwiftUI 初学者常见错误")
        }        
    }
}

其实原因很简单:问题就出在 TabView 中每个标签页的 tag 类型上。我们实际使用的是 TabTag 类型,但是向 TabView 构造器 selection 形参绑定的却是整形类型。

所以这个问题解决起来也很容易,只需要将 Model 中对应的属性改为 TabTag 类型即可:

swift 复制代码
@available(iOS 17, *)
@Observable
class Model {
    var currentSelectingTab = TabTag.home
}

再次运行代码一切都回归正常了。

但是故事到这里并没有结束。假如我们将代码修改为如下形式,却是能够选择 TabView 中每个标签页的:

swift 复制代码
struct Main: View {
    @State var currentSelectingTab = 0
    
    var body: some View {
        NavigationStack {
            TabView(selection: $currentSelectingTab) {
                Text("Home")
                    .tabItem {
                        Label("Home", systemImage: "house")
                    }
                    .tag(TabTag.home)
                
                Text("Worry")
                    .tabItem {
                        Label("Worry", systemImage: "figure.fall")
                    }
                    .tag(TabTag.worry)
                
                Text("Game")
                    .tabItem {
                        Label("游戏", systemImage: "gamecontroller.fill")
                    }
                    .tag(TabTag.game)
                
                Text("User")
                    .tabItem {
                        Label("用户", systemImage: "person.fill")
                    }
                    .tag(TabTag.user)
            }
        }
    }
}

在上面的代码中,我们仅仅将原来在 Model 中 Int 类型的 currentSelectingTab 直接放到视图 @State 中,并将其与 TabView 的选中操作绑定起来而已。但是这样的话,同样造成了 TabView 标签页 tag 类型的不一致,为何又没有问题呢?

其实,这波"走位"表面看起来貌似可以恣意切换 TabView 各个标签页,但实际却是有问题的。为了拨开迷雾见青天,我们特地为 currentSelectingTab 状态增加了 onChange 监听器:

swift 复制代码
struct Main: View {
    @State var currentSelectingTab = 0
    
    var body: some View {
        NavigationStack {
            TabView(selection: $currentSelectingTab) {
                //...
            }
        }
        .onChange(of: currentSelectingTab) {_,new in
            // 永远不会进入此闭包中
            print("\(new)")
        }
    }
}

运行代码可以发现:尽管我们可以切换到不同标签页中,但我们的 currentSelectingTab 状态却从未发生过改变!

所以,最终我们发现了 SwiftUI 中一个"不一致"的场景:在 @Observable 对象中属性类型和 TabView 标签页 tag 不匹配会导致"正确"的交互行为(标签页无法切换),但在 @State 同样的属性却不能。

希望苹果在将来可以将它们一致化。

2. 视图中 @FetchRequest 定义不完整导致的崩溃

另一个隐蔽的问题涉及到 CoreData 为 SwiftUI 视图添加的 @FetchRequest 属性包装器。

swift 复制代码
@available(iOS 17, *)
struct WorriesView: View {    
    @FetchRequest(sortDescriptors: [.init(keyPath: \Worry.occurrenceTime, ascending: false)]) var worries
        
    var body: some View {
        NavigationStack {
            List {
                Section("最近担忧") {}
                
                Section("严重担忧") {}
                                
                Section("其它担忧") {}
            }
            .navigationTitle("担忧终结者")
        }
    }
}

上面的代码貌似人畜无害,而且在 Xcode 预览中的显示也是无懈可击:

不过,如果我们编译并胆敢在模拟器或真机上运行上述代码,App 就会立即崩溃:

而且从提示来看,很难发现究竟是哪里出了问题。如果不是我们已将问题局限在 @FetchRequest 那行代码上,大家恐怕很难轻易找出罪魁祸首,更何况如果它匿影藏形隐身在海量视图中了。

经常在 SwiftUI 中使用 @FetchRequest 属性修饰器来获取 CoreData 数据的小伙伴们,应该能一眼看出上面代码中的问题,实际上它缺少了 FetchedResults 后半部分的定义,是不完整的:

swift 复制代码
@FetchRequest(sortDescriptors: 
[.init(keyPath: \V3_Worry.occurrenceTime, ascending: false)]) 
var worries: FetchedResults<V3_Worry>

只要将其补全即可。

该问题的另一个特点是它在 Xcode 预览中讳莫如深、深藏不露,只为在实际运行时给秃头码农们"当头一棒",实属可恨!

不过,通过上面条分缕析的介绍,现在小伙伴们对它们一定能够火眼金睛、无所畏惧,棒棒哒!💯


更多 Xcode 预览调试中的技巧和陷阱,请小伙伴们移步如下链接观赏精彩的内容:


总结

在本篇博文中,我们讨论了 Xcode 16.1(iOS 18.1)中仍然存在 SwiftUI 的两个"鸱张鼠伏"、较难发现缘由小问题的"症状"和解决之道,希望可以帮助到大家。

感谢观赏,再会啦!😎

相关推荐
江鸟19984 天前
AI日报 · 2025年5月03日|Perplexity 集成 WhatsApp,苹果传与 Anthropic 合作开发 Xcode
人工智能·gpt·macos·大模型·agent·xcode·智能体
桌角的眼镜5 天前
模拟开发授权平台
macos·ios·xcode
BianHuanShiZhe7 天前
升级xcode15 报错Error (Xcode): Cycle inside Runner
ide·macos·xcode
明似水9 天前
如何解决 Xcode 签名证书和 Provisioning Profile 过期问题
macos·xcode
言之。9 天前
Go 语言中的 `os.Truncate` 函数详解
ios·golang·xcode
桃花仙丶9 天前
iOS/Flutter混合开发之PlatformView配置与使用
flutter·ios·xcode·swift·dart
长沙火山10 天前
SwiftUI 8.List介绍和使用
ios·list·swiftui
薛瑄11 天前
FFmpeg之三 录制音频并保存, API编解码从理论到实战
ffmpeg·音视频·xcode
东坡肘子11 天前
Chrome 会成为 OpenAI 的下一个目标?| 肘子的 Swift 周报 #081
人工智能·swiftui·swift
littleplayer14 天前
iOS 中的 @MainActor 详解
前端·swiftui·swift