WWDC Quick Look 💓 By SwiftGGTeam
Xcode essentials

Xcode essentials

观看原视频

Highlight

Xcode 团队从”编辑-调试-测试-提交”这个日常开发循环出发,系统梳理了导航器过滤、Find Navigator 高级搜索、断点条件与自动继续、Swift Testing 标签过滤、Code Coverage 可视化、以及 TestFlight 分发等完整工作流中的效率提升技巧。


核心内容

项目越来越大,团队越来越多,“找到要改的代码”本身就成了问题的一半。你可能在一两百个文件里搜索一个符号,可能在一堆搜索结果里手动翻找,也可能在调试时反复重启应用只为了重现一次崩溃。这个 Session 的目标就是帮你缩短这些环节的耗时。

上半场由设计师 Cheech 讲编辑和导航。核心思路是:Xcode 的每个导航器底部都有过滤栏,Find Navigator 支持搜索范围保存和符号索引模式,Tab 分为永久和隐式两种,Jump Bar 和 Open Quickly(Command-Shift-O)是最快的文件跳转方式。这些功能零散分布在界面各处,组合起来能大幅减少”找代码”的时间。

下半场由项目经理 Myke 讲调试、测试和分发。调试部分的重点是断点的高级用法:条件断点、自动继续的日志断点、Swift Error Breakpoint、以及拖拽绿色程序计数器回退执行。测试部分介绍了 Swift Testing 的标签过滤、Test Plans 的跨 scheme 复用、Code Coverage 的行级可视化。分发部分则覆盖了 Archive 流程、TestFlight 内部/外部测试、以及 Xcode Organizer 里的反馈和崩溃数据。


详细内容

导航与搜索

Project Navigator 底部过滤栏支持按文件名搜索、按 target 名称过滤、按 git status 过滤(只显示已修改文件)。

Find Navigator(Command-Shift-F)的高级用法:

  • 底部过滤栏可在搜索结果中做二次筛选,按文件名或匹配行中的额外关键词过滤
  • Command-点击搜索结果中文件名旁的展开箭头,折叠所有同级文件(适用于所有 outline 视图)
  • 选中不需要的结果按 Delete 键,从当前查询中隐藏(不会删除代码)
  • 搜索范围(Scope)可限定到特定目录,常用范围可保存
  • 点击搜索框上方的”Text”切换到符号索引模式,“Descendant Types”可查看类层级(05:10

选中文字后按 Command-E,文字进入搜索框但不覆盖剪贴板,这在 macOS 全局有效(05:27)。

文件间移动与编辑

Xcode 的 Tab 有两种类型:永久 Tab(你主动编辑的文件)和隐式 Tab(只浏览过、斜体标题标识、离开即消失)。双击隐式 Tab 或右键选”Keep Open”可转为永久 Tab。Option-点击关闭按钮可关闭除当前外的所有 Tab(06:32)。

Open Quickly(Command-Shift-O)是 Xcode 里最快的跳转方式:输入文件名或符号的部分关键词即可跳转,查询中加斜杠匹配路径,加冒号和行号跳到指定行。按住 Option 再按 Return 在新分栏中打开(11:22)。

Quick Actions(Command-Shift-A)用自然语言搜索 Xcode 命令,适合记不住快捷键的场景。

Xcode 16 的 Predictive Code Completion 会根据上下文预测整行代码,按 Tab 接受可见部分,按 Option-Tab 接受完整预测。注释和变量名越具描述性,预测越准确(14:24)。

断点高级用法

对于高频命中的代码,可以使用双断点配合:在按钮处理器设断点,命中后手动启用忙碌函数中的断点再继续,这样就只在特定场景下停下(17:39)。

Swift Error Breakpoint 在 Breakpoint Navigator 中添加,应用会在任何 Swift 错误抛出处停下,而不是只停在 catch(18:16):

let url = try! getVideoResourceFilePath()

关键点:

  • try! 强制解包,如果失败会触发运行时错误
  • Swift Error Breakpoint 能在 throw 发生时立即暂停,而不是等到崩溃

条件断点:双击断点编辑,添加条件表达式,只在条件为真时暂停。也可以添加打印表达式并设置”自动继续”,这样就不用重新编译就能获得临时日志(18:59):

cloudURL.scheme == "https"

关键点:

  • 这是一个布尔表达式,只有当 URL 的 scheme 为 “https” 时断点才会暂停
  • 适合在高流量代码中只关注特定条件的场景

在断点中添加日志表达式并设置自动继续:

p "Username is \(cloudURL.user())"

关键点:

  • p 是 LLDB 的 print 命令,在控制台输出表达式的值
  • 搭配”Automatically continue”选项,无需暂停程序即可记录信息

调试器回溯与回退

在调试器中,可以用 p 命令回溯分析 guard 语句为什么提前返回(19:44):

guard cloudURLs.allSatisfy({ $0. scheme == "https" }),
    session.configuration.networkServiceType == .video else {
    return
}

分别用 p cloudURLs.allSatisfy({ $0.scheme == "https" })p session.configuration.networkServiceType == .video 检查每个条件的值,找出哪个条件为假。

绿色程序计数器可以向后拖拽,重新执行之前的表达式。副作用不会回退,但比完全重启调试会话更快(20:11)。

Command-Control-R “Run Without Building” 跳过编译直接运行上次的构建产物,适合需要反复调试但代码未改的场景(21:02)。

Xcode 16 支持在编辑器中查看完整回溯(Full Backtraces),从 Debug Bar 激活,把来自不同文件的调用栈集中在同一编辑器视图中(21:20)。

从 print 迁移到 os_log

Session 对比了 print 和 os_log 两种调试日志方式。print 语句的输出难以过滤和定位来源:

var releaseDate: Date {
    print("🎬 Entering func \(#function) in \(#fileID)...")
    let currentDate = Date()
    let gregorianCal = Calendar(identifier: .gregorian)
    var components = DateComponents()
    components.year = releaseYear
    print("\(#fileID)@\(#line) \(#function): 📅 releaseYear is \(releaseYear)")
    if releaseYear == gregorianCal.component(.year, from: currentDate) {
        components.month = Int(releaseMonth)
        isNewRelease = true
        print("\(#fileID)@\(#line) \(#function): 🆕 this is a new release!")
    }
    if releaseYear < 2000 {
        isClassicMovie = true
        print("\(#fileID)@\(#line) \(#function): 🎻 this one is a classic!")
    }
    let calendar = Calendar(identifier: .gregorian)
    return calendar.date(from: components)!
}

用 os_log 替代后,每条日志带有级别(debug/info/error),可以按文本、级别、来源库过滤,点击箭头直接跳转到源码行(22:09):

var releaseDate: Date {
    os_log(.debug, "🎬 Entering func \(#function) in \(#file)...")
    let currentDate = Date()
    let gregorianCal = Calendar(identifier: .gregorian)
    var components = DateComponents()
    components.year = releaseYear
    os_log(.info, "📅 releaseYear is \(releaseYear)")
    if releaseYear == gregorianCal.component(.year, from: currentDate) {
        components.month = Int(releaseMonth)
        isNewRelease = true
        os_log(.info, "🆕 this is a new release!")
    }
    if releaseYear < 2000 {
        isClassicMovie = true
        os_log(.info, "🎻 this one is a classic!")
    }
    let calendar = Calendar(identifier: .gregorian)
    return calendar.date(from: components)!
}

关键点:

  • os_log(.debug, ...)os_log(.info, ...) 为每条日志设定级别
  • 控制台可按级别、文本、来源库过滤,不需要手动加 #fileID 等宏
  • 日志条目带跳转箭头,点击直接定位到源码行

Swift Testing 与标签过滤

Swift Testing 用 @Test 标记测试函数,#expect 替代 XCTAssert。标签(Tag)可以对测试分组过滤(24:19):

@Test(.tags(.stars)) func testStarRating() async throws {
    for video in library.videos {
        #expect(video.info.starRating > 0)
        #expect(video.info.starRating <= 5)
    }
}

@Test(.tags(.library)) func testLibraryLoaded() async throws {
  #expect(library.videos.count > 1)
}

extension Tag {
  @Tag static var stars: Tag
  @Tag static var library: Tag
}

关键点:

  • .tags(.stars) 给测试打标签,在 Test Navigator 中可按标签过滤
  • #expect 是 Swift Testing 的断言宏,比 XCTAssert 语法更简洁
  • Tag 定义放在 extension Tag 中作为静态属性

命令行运行测试时可以指定 scheme、test plan、甚至单个测试(26:35):

xcodebuild test -scheme DestinationVideo
xcodebuild test -scheme DestinationVideo -testPlan TestAllTheThings
xcodebuild test -scheme DestinationVideo -testPlan TestAllTheThings -only-testing "Destination VideoUITests/testABeach"

关键点:

  • 第一个命令运行 scheme 下所有测试
  • -testPlan 指定测试计划
  • -only-testing 只运行特定测试用例,适合配合 git bisect 定位回归

Code Coverage

在 Editor 菜单启用 Code Coverage 后运行测试,编辑器右侧显示每行代码被执行的次数。0 表示未覆盖,可能意味着测试有缺口或代码从未被调用(29:03)。

Report Navigator(Command-9)提供文件级和函数级的覆盖率概览,帮助定位改进目标。

Test Report 中点击失败的测试,可以看到执行事件序列与屏幕录制并排显示,时间线底部标注失败发生的时间点(29:57)。

分发与 Organizer

Archive 是编译后应用的快照,包含 release 构建和 debug symbols。分发选项包括:App Store Connect(上传到 TestFlight 或 App Store Connect)、TestFlight Internal Only(跳过 App Review、仅限组织内部测试者)、Release Testing / Enterprise / Debugging(生成给注册设备安装的构建)(31:54)。

Xcode Organizer(Command-Option-Shift-O)汇总 TestFlight 反馈、启动耗时、终止率等数据,数据来自同意共享的用户,隐私安全。反馈标签页显示测试者的文字反馈,启动和终止标签页帮助定位性能问题和崩溃(33:36)。


核心启发

  • 做什么:在 Find Navigator 中保存常用搜索范围。为什么值得做:大型项目中反复搜索同一组目录是常见操作,保存 scope 后一键切换,省去每次手动选择。怎么开始:搜索时点击搜索框下方的范围菜单,选 “Custom Scopes…”,选定目录后点保存。

  • 做什么:用条件断点 + 自动继续替代临时 print 语句。为什么值得做:不需要修改代码、不需要重新编译,断点日志直接输出到控制台,调试完删除断点即可。怎么开始:双击断点打开编辑器,添加 p 表达式,勾选 “Automatically continue after evaluating actions”。

  • 做什么:把项目中的 print 调试日志迁移到 os_log。为什么值得做:os_log 支持按级别过滤、按来源库过滤、点击跳转到源码行,团队协作时控制台输出更可控。怎么开始:将 print("...") 替换为 os_log(.debug, "..."),删掉手动的 #fileID#function 宏。

  • 做什么:给 Swift Testing 测试加上 Tag 标签。为什么值得做:测试数量增长后,按标签过滤比按名称搜索更高效,Test Navigator 中一键筛选特定模块的测试。怎么开始:在测试函数的 @Test 属性中添加 .tags(.yourTag),在 extension Tag 中定义静态标签。

  • 做什么:用 Command-Control-R(Run Without Building)加速反复调试。为什么值得做:调试崩溃时经常需要重启应用,如果代码没改就无需重新编译,省下等待构建的时间。怎么开始:下次调试循环中直接按 Command-Control-R 代替 Command-R。


关联 Session

评论

GitHub Issues · utterances