【WWDC 2023】Xcode 15 更新内容
WWDC 2023 这几天陆续放出各个主题的视频,挑了几个我认为值得看看的,学习一下并做个笔记。当然目前大部分系统、软件都是 Beta 版本,正式版本可能还会更改,但整体更新内容是不会大变的。
我挑的第一个视频是 What’s new in Xcode 15 (opens in a new window),工欲善其事,必先利其器。
下面将根据视频的播放顺序,分析并实践各个段落部分。
2023 年 6 月 9 日,测试 Xcode 15 Beta 版本。
1. 根据文件名,当你想输入相同名字的 struct 或 class 时,会根据你输入的首字母匹配文件名。
比如文件名为”ThisIsACodeCompleteDemo.swift”,当你在文件里输入”struct t”时,自动弹出”ThisIsACodeCompleteDemo”为首选。

2. 会根据上下文分析推断你需要设置的属性或参数。
对 Text 设置了.font(.title)之后,再次输入”.”,会提示.bold() 、 .fontWeight(_ weight:)等字体相关的 modifier。

如果是图片,首选推荐的是.resizable。

1. 在 Assets.xcassets 里面添加资源文件会自动生成对应的常量名称
这个功能居然内置了,激动啊,泪流满面!
支持的资源文件包括以下几种:
- Color Set: 颜色
- Image Set: 图片
- Symbol Image: 符号图片,比较特别的 svg 格式
注意 Data Set 不在支持列表,依然使用 NSDataAsset 。
下面通过几个实例来了解一下具体的操作,在 Assets.xcassets 分别添加一个 “About” 的 Symbol Image,“baseline_bookmarks_black_48pt”的 Image,带 “Home” 目录的”tab-bookmarks” Image,“MainColor”的 Color Set,“mock-login-response” 的 JSON 文件。

用法如下:
import SwiftUI
struct ThisIsACodeCompleteDemo: View { var body: some View { VStack { Text("This is a code complete demo") .font(.title) .bold() // "About" 的 Symbol Image Image(.about) // "baseline_bookmarks_black_48pt"的 Image Image(.baselineBookmarksBlack48Pt) // 带 "Home" 目录的"tab-bookmarks" Image Image(.tabBookmarks) .foregroundColor(.main) // "MainColor"的 Color Set
Text(loadDataSet() ?? "No data") } }
func loadDataSet() -> String? { guard let data = NSDataAsset(name: "mock-login-response")?.data else { return nil }
return String(data: data, encoding: .utf8) }}这是怎么做到的呢?其实就是 Xcode 编译时自动生成了一个 GeneratedAssetSymbols.swift,根据 Assets 类型 生成了 2 个 struct:ColorResource, ImageResource 。两者的代码是一样的,我们就看 ImageResource :
/// An image resource.struct ImageResource: Hashable {
/// An asset catalog image resource name. fileprivate let name: String
/// An asset catalog image resource bundle. fileprivate let bundle: Bundle
/// Initialize an `ImageResource` with `name` and `bundle`. init(name: String, bundle: Bundle) { self.name = name self.bundle = bundle }
}然后对 UIKit 的 UIImage 和 SwiftUI 的 Image 各写了一个初始化 extension :
@available(iOS 11.0, tvOS 11.0, *)@available(watchOS, unavailable)extension UIKit.UIImage {
/// Initialize a `UIImage` with an image resource. convenience init(resource: ImageResource) {#if !os(watchOS) self.init(named: resource.name, in: resource.bundle, compatibleWith: nil)!#else self.init()#endif }
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)extension SwiftUI.Image {
/// Initialize an `Image` with an image resource. init(_ resource: ImageResource) { self.init(resource.name, bundle: resource.bundle) }
}上面这些是基本的工具方法,具体的 Assets 名称是存在于 ImageResource 的,绑定常量和名称。
// MARK: - Image Symbols -
@available(iOS 11.0, macOS 10.7, tvOS 11.0, *)extension ImageResource {
/// The "baseline_bookmarks_black_48pt" asset catalog image resource. static let baselineBookmarksBlack48Pt = ImageResource(name: "baseline_bookmarks_black_48pt", bundle: resourceBundle)
/// The "tab-bookmarks" asset catalog image resource. static let tabBookmarks = ImageResource(name: "tab-bookmarks", bundle: resourceBundle)
/// The "About" asset catalog image resource. static let about = ImageResource(name: "About", bundle: resourceBundle)
}注意上面的名称转化为常量的规则,我自己观察下来有点类似 Codable JSON 转 model 的规则:
-
驼峰命名
-
下划线和横线会自动忽略掉
-
Color Set 的名称中带有”Color”的会忽略掉其中的”Color”
@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)extension ColorResource {/// The "MainColor" asset catalog color resource.static let main = ColorResource(name: "MainColor", bundle: resourceBundle)}
这有什么好处?
- 这些自动生成的常量能够利用 Code Complete 的提醒功能,不需要切换文件复制名称等操作
- 当我们做一些资源清理时,很快就能知道哪些资源是没有用到的,即自动生成的常量没有被引用
- 减少字符串硬编码,在编译期可以检测名称是否正确。比如,当你改了图片名字时,再次编译会自动给你生成一个新名称的常量,这样可以在编译期报错了,如果是字符串,就必须全局搜索。
这个功能也可以用于 UIKit ,并且是 iOS 11.0 以上就支持的,点赞!

唯一让我觉得遗憾的地方是没有对有目录的 Image 增加命名空间的概念,比如上面提到的带 “Home” 目录的”tab-bookmarks” Image,Xcode 是直接取的 “tab-bookmarks” 来解析,忽略了”Home”层级。实际上大部分情况,我们会对图片资源也进行分级,比如这样:

在 Xcode 14 包括以前的版本,我习惯于用 SwiftGen (opens in a new window) 来自动生成一些常量,功能比 Xcode 15 更强大,支持以下类型:
- Assets Catalogs (opens in a new window)
- Colors (opens in a new window)
- Core Data (opens in a new window)
- Files (opens in a new window)
- Fonts (opens in a new window)
- Interface Builder files (opens in a new window)
- JSON and YAML files (opens in a new window)
- Plists (opens in a new window)
- Localizable strings (opens in a new window)
SwiftGen 既支持 Xcode 15 这种扁平化的方式,也支持目录层级的方式。层级模式生成的代码大概如下:
internal enum Asset { internal enum Home { internal static let alarmRing = ImageAsset(name: "alarm_ring") internal static let battery1 = ImageAsset(name: "battery1") }}
internal struct ImageAsset { internal fileprivate(set) var name: String internal typealias Image = UIImage
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) internal var image: Image { let bundle = BundleToken.bundle let image = Image(named: name, in: bundle, compatibleWith: nil) guard let result = image else { fatalError("Unable to load image asset named \(name).") } return result }}使用起来也很简单:
let imageView = UIImageView(image: Asset.Home.alarmRing.image)这是一个新的管理翻译字符串的方式,采用新的文件格式 .xcstrings。相比 Xcode 15 以前使用不同语言的目录的方式,更加集中统一了,这是一个比较好的改进。另外,这个功能是向后兼容的,iOS 17 以前也可以使用,👍🏻!
.xcstrings文件实际上是 JSON 文件,终于不是 Xcode 祖传的 XML 格式,Git 版本化就方便多了,感激涕零。
新建文件通过 “File” -> “New” -> “File” ,然后输入 string 过滤,下面图示。我们可以看到默认就是 String Catalog 了,Strings File 和 StringsDict 都变成了 Legacy ,Next 之后生成的默认文件名依然是”Localizable”,这个就最好不改了。

接下来,我们先来学习一下新的界面操作,首先依然会有一个 Base 的概念,就是其他语言根据 Base 来翻译,默认是 English。
在 Xcode 15 以前,这个 Base 基本上不改,但是 Xcode 15 多了一个默认语言的修改,你可以通过”Set Default” 来改成简体中文为默认。

下面这两个是 选中Localizable.xcstrings 的界面,相对来说还是比较明了的。

这里选择 English 为默认语言,当我们切换到中文简体的时候,列表项多了一个 “Default Localization(en)“,这样我们可以对比一下当前语言的翻译和默认语言的比对。

翻译状态
每一个翻译字符串都有一个 State 字段,表示当前的翻译状态,具体的有以下几种:

新加的 Key 肯定是 New 了,当你对某个 Key 改动了就变成NEEDS REVIEW ,当左右语言都翻译完了就是 ✅。
这里特别说明一下 STALE 状态的,表示你添加了 Key,但是并没有在代码里调用,有点废弃(Deprecated)的意思。当我们想要清理一下废弃资源的时候,这个状态就很好用。
在使用 Xcode 15 的过程中,我删除了某些 Key,但是没有在代码里删除对应的代码,因为是字符串,编译不会报错,但是 Xcode 15 自动给我在 String Catalogs 里面添加了对应的 Key,也就是说你也可以在代码里先写 Key,Build 之后在 String Catalogs 里面编辑。哇塞,还有一个反向的操作,疯狂点赞!
这有点类似于单元测试的状态。
复数问题
当 Key 里面包含数量的参数时,要注意处理英语国家的复数问题,比如 one day 和 two days,如果你的项目要求比较严格的话,按照 Xcode 15 以前的话,strings 格式的需要添加两个一样 Key 来处理复数问题,或者使用 stringsdict 这么来处理:

这个功能在 Xcode 15 就非常简单了,对需要处理复数的 Key,右键选择”Vary By Plural”

Xcode 会帮你自动细分成两种:One,Other。

还有一个情况是在以前 .strings 基本上没法处理的问题,一句话里面包含 2 个或者 2 个以上的数量参数,这时候要处理复数就要抓头了,方法也是非常啰嗦,不够优雅,只能使用.stringsdict。好在 Xcode 15 也帮我们考虑了这种情况,它可以 Vary By Plural 两个参数以上。
假如我们有一句话是 There are %lld boys and %lld girls in this class,通过下面图示的方法两次 Vary 了。

结果就会有 @arg1 和 @arg2 两个参数,然后我们将要处理复数的 “boys” 和 “girls” 挪到下面的参数,并处理对应的 复数 s问题。

Xcode 15 还支持自定义 @arg1 和 @arg2 的名称,比如你可以改成更好阅读的 @boys 和 @girls 。但是这个最好不要尝试,因为我遇到了几次崩溃,这一块的还有待完善。
老项目迁移
老的项目可以通过点击”Edit”-> “Convert” -> “To String Catalogs” ,界面出来之后会让你选择 Target,然后选择下一步选择需要转换的文件,包括Info.plist 和 Localizable.strings。
假如目前有一个老项目,支持简体、繁体、英语这 3 种语言,通过转换之后会生成两个文件:InfoPlist.xcstrings 和 Localizable.xcstrings,在”Base.lproj”、“zh-Hans.lproj”、“zh-Hant.lproj”等旧的”.strings”文件会被删除。
其他功能
- 可以在搜索框输入关键词过滤,这个就比
.strings在文件内搜索好用太多了,尤其是在你有几千个翻译的时候。 - 可以手动管理或者自动管理 State
- 除了添加会同步到所有语言,删除某一个 Key,也会自动同步到其他语言上,这个也比
.strings挨个去删除方便 - 可以按照 State 的状态排序,比如没有 Review 的排在前面。
- 所有 Key 的总体翻译进度,可以按语言显示百分比
Comment字段可以添加相关说明注释,这个也是好评,可以解释一下这个 Key 用在什么场景下- 导入导出功能,文件格式是
.xliff,专业的翻译格式 Vary By Device分设备类型的翻译,比如 Apple Watch 需要更短的翻译,但这种场景用的不多
更多详细内容请参考: Discover String Catalogs (opens in a new window)
遗憾
总体来说,我是对 String Catalogs 是非常满意的,稍微遗憾的就是没有像 Assets 一样可以 Generate Code,自动生成一些没有参数的常量,或者生成带参数的函数。
如果是 Xcode 14,我建议使用 Localization Editor (opens in a new window) 来编辑翻译字符串,主界面如下图,将所有语言平铺在一起,也有注释功能,所有编辑操作实时同步到 .strings文件。

注意一下,看我的 Key 是设置成”auth.btn.login”这样的,因为我借助上面提到过的 SwiftGen (opens in a new window) ,会帮我自动生成代码,还可以自定义函数名称,这样可以做到动态加载不同语言的翻译效果。实际调用时的代码如下:
// MARK: - SwiftGen// 缩写简化typealias L = L10n.Localizable// 自定义查询函数func customLookup(_ key: String, _ table: String, _ fallback: String) -> String { L10nManager.shared.get(key) ?? ""}
// 下面是实际调用的代码signUpButton.setTitle(L.Auth.Login.registerNewAccount, for: .normal)forgetPasswordButton.setTitle(L.Auth.Btn.forgetPsd, for: .normal)passwordTextField.placeholder = L.Auth.Password.placeholderSwiftGen 对没有参数的会生成一个 enum 的 static var,对于中间的点分割部分,会依次生成一个内部 enum,达到命名空间的效果。
/// 请输入密码internal static var placeholder: String { return L10n.tr("Localizable", "auth.password.placeholder", fallback: "Enter password") }主要是更新了一个实时预览的功能,作用不大。

这个宏就强大了,值得单开一篇文章来详细说一说,比如有以下一些好用到爆炸的内置宏
Observable: 着实简化了@Published变量#Predicate: 查询过滤有类型检查了,不需要通过 NSPredicate 字符串的形式Model: 用于 SwiftData,CoreData 的 Swift 版本
但是没啥用,因为不向后兼容,只支持 iOS 17😭。
预览在 SwiftUI 是很方便的,一边写代码一边预览效果,DSL 的 UI 就是这么便捷。 但是想要让 UIView 和 UIViewController 也有预览功能,以前只能使用 storyboard 或者 xib 了。但是对那些代码党来说,这就很难受了,不运行项目是看不到效果的。所以,InjectionIII (opens in a new window) 这个工具诞生了,通过注入 framework 的形式,修改运行期代码。但这个工具需要侵入代码,不是特别好,稳定性也不够。
Xcode 15 借助 Swift macro 功能,新增了一个 #Preview 宏,可以支持 UIKit 的预览了
#Preview { // LoginViewController 包含 2 个 UITextField 和 1 个 Button // 三个子View都是距View左右边界 30 pt。 let controller = LoginViewController() return controller}预览结果不理想,约束不生效。另外这个功能只支持 iOS 17,因为是 macro 实现的。

但是我们可以借助 SwiftUI 的预览功能,将 UIView 或者 UIViewController 用 SwiftUI 的 View 封装一层。下面简单写一个方法:
import SwiftUIimport UIKit
// 创建一个SwiftUI的容器View,将UIViewControler包含在内struct PreviewContainer<T: UIViewController>: UIViewControllerRepresentable { let viewController: T
init(_ viewControllerBuilder: @escaping () -> T) { viewController = viewControllerBuilder() }
// MARK: - UIViewControllerRepresentable func makeUIViewController(context: Context) -> T { return viewController }
func updateUIViewController(_ uiViewController: T, context: Context) {}}然后就可以像用 SwiftUI 的预览功能一样了:
struct ViewController_Previews: PreviewProvider { static var previews: some View { PreviewContainer { let controller = LoginViewController() return controller } }}
采用这种方式,就不会有约束失效的问题了,而且还可以支持到 iOS 13,完美!
Xcode 15 新增的书签功能就是给代码做个标记,可以快速定位你经常需要访问的代码行、源文件,或者特别标记的东西,比如 TODO、FIXME 等。
书签界面位于左侧导航视图的第三个位置,这里的位置可用快捷键Commmand + 数字 切换。

具体的功能有以下这些:
- 对代码行添加书签,右键选择具体的代码行
- 对文件添加书签,右键选择文件即可
- 可对书签添加描述信息,选中后 Enter 健,或者在 Editor 点击代码行右侧的书签图标
- 可将书签分组并重命名
- 可将书签”Mark as Complete” 或者 “Mark as Incomplete”,这是当做任务使用了,但是用的不多吧,因为没有在 Editor 上显示”Complete”或着”Incomplete”状态
- 可将搜索结果添加到书签,这样可以避免重复搜索内容,只要点击一下右边的刷新按钮,新增的结果自动出现在这里。
要把书签功能用好,一定要学习下面这几个标记符号,因为这算是行业共识了,无论用 VS Code 或者 Android Studio,都能识别到这些符号,并进行统计。
// MARK:标记// TODO:标识将来要完成的内容// FIXME:标识以后要修正或完善的内容// ???:疑问的地方// !!!:需要注意的地方
如果是 XCode 14,除了上面这些符号标记,我一般还会配合用断点来做一些书签功能。
书签功能是用户层级的,有一个bookmarks.plist文件来记录书签,文件位于下面这个地址,但最好不要加入到版本管理里面。类似的还有断点文件 Breakpoints_v2.xcbkptlist,可以直接将xcuserdata/加入到 .gitignore 里面过滤掉
# xxx 为你的系统用户名/Users/xxx/Xcode15Demo/Xcode15Demo.xcodeproj/project.xcworkspace/xcuserdata/xxx.xcuserdatad/Bookmarks/bookmarks.plistXcode 15 内置的版本管理工具终于可用了,真的不是很喜欢在 Source Tree 或者 Terminal 之间来回切换界面了。
下面就是源码控制导航界面,快捷键 Command + 2:

“Changes” 视图真的翻天覆地的变化了,简单来说就是跟 Source Tree 差不多了,只渲染改动代码行的上下文,多余的隐藏。像 Xcode 14 一样点击文件,居然把整个文件内容渲染出来,真是太傻吊了。而且还可以拖动代码行数上下两个小图标,显示更多内容,或者点击右上角的扩展图标,显示全部内容。
另外,Xcode 14 的 Source Control 菜单升级为 Xcode 15 的 Integrate 菜单,如下图所示:

大概有 2 个主要变化:
- Stage 常用操作
- 冲突解决操作
- Xcode Cloud 整合进这里了
主要变化如下:
- 比 Xcode 14 多 45% 内容的测试报告
- 左下角可以过滤测试项目
- “Performance Metrics” 性能指标标签
- Test Plan 的改进(没啥用)
- UI Test 的改进(没啥用)
本章节重点介绍 OSLog ,但是 OSLog 是 iOS 15+ 的 API,所以有点鸡肋。
Logger 是 iOS 14 + 的 API,相对好一点。
关于 Swift Logging 的整个历史变化,可以看 Peter Steinberger 文章 Logging in Swift (opens in a new window) 。
下面我们简单过一下 OSLog 在 Xcode 15 的变化,先看官方的例子:
import OSLog
let logger = Logger(subsystem: "BackyardBirdsData", category: "Account")
func login(password: String) -> Error? { var error: Error? = nil logger.info("Logging in user '\(username)'...")
// ...
if let error { logger.error("User '\(username)' failed to log in. Error: \(error)") } else { loggedIn = true logger.notice("User '\(username)' logged in successfully.") } return error}整个变化都体现在 Console 的 UI 上,增加了 Category 分类、日志类型的过滤、背景色、跳转到代码行、隐私数据处理。

SwiftLog
Apple 有在 github 开源一个 SwiftLog (opens in a new window) ,这是一个前端框架,提供了日常开发中常用的上层接口,但是需要自己依据 LogHandler协议实现后端,比如打印在 Console ,保存在文件,还是发送到日志服务器等。
有人以 OSLog 作为后端,实现了一个 LoggingOSLog (opens in a new window) 开源库,底层日志打印使用 os_log ,但是官方并不是很推荐这样使用,因为失去了静态字符串的性能,全是动态的字符串,以及隐私数据方面完成暴露了。
CocoaLumberjack
我相信大部分 iOS 开发者的日志工具还是 CocoaLumberjack (opens in a new window),没错,用它就够了。
Xcode Cloud 只能用于外网的 git 地址,对于我们公司这种局域网部署的 gitlab 无效,所以公司的项目都没有尝试过。
但是我在一些 Side Projects 上使用了一下,感觉还不是太给力,遂放弃。
本章节主要讨论 XCFramework 的签名有效性和隐私清单。
XCFramework 的签名有两种,一种是使用自己的证书重新签名,叫做”self-signed”,Xcode 15 会比较 XCFramework 的 SHA-256 值,跟刚加入到工程的时候是否一致。另一种是 XCFramework 开发者自己的证书签名,苹果会校验证书的有效性,比如对方的 Apple Developer Program 过期了,或者他的证书被 revoke 了。
Privacy Manifest 隐私清单,就是把 App Store Connect 网页上的隐私设置项搬到 Xcode 工程里面,有一个 Plist 文件格式的 PrivacyInfo.xcprivacy 配置文件,里面需要列出各个隐私标签的使用情况,以及是否允许某些域名访问隐私 API。

到 2024 的 4 月份左右,App 开发者 和 SDK 开发者都要适配这个功能,无论是更新的 App 还是新上架的 App 都要添加隐私清单文件。

参考资料:
- Verify app dependencies with digital signatures (opens in a new window)
- Get started with privacy manifests (opens in a new window)
Archive 之后多了几个新的选项:
-
TestFlight & App Store:上传到 TestFlight 和 App Store,使用 TestFlight 的内部测试和外部测试,准备好之后提交到 App Store
-
TestFlight internal only: 只上传到 TestFlight 的内部测试,分享给团队,App Store 提交里面不可选,在开发分支使用。
目前 Beta 版本没看到这个选项 。
-
Release Testing:导出一个优化过的 Release 版本,使用发布证书签名,只能安装在已注册的设备上
-
Enterprise:企业版证书发布
-
Debugging:导出一个优化过的 Release 版本,使用开发证书签名,只能安装在已注册的设备上
-
Custom :自定义 Xcode 14 的界面

所有这些发布选项都是流水线操作的,无需先过去一样手动选择证书,勾选,下一步等手动档。点击”Distribute”按钮,就是自动档的 D 档,自动签名、包含符号表、自动增加 Build 版本号、去除 Swift 的符号全包了。
Organizer 界面增加了一个 Feedback 选项,其实就是 TestFlight 网页的内容搬到这里来了,测试团队截图的 APP 内容就显示在这里。

Xcode 15 还有些小改进,限于时长因素在视频里没有体现。
新增了一个类似 VS Code 的 Command Palette 命令,快捷键 Command + Shift + A,会打开一个搜索框,可以搜索任何菜单操作。这个功能的好处是菜单快捷键太多了,实在记不过来。不过我会改掉这个快捷键,变成和 VS Code 一样的 F11,这样按一下就可以了,更快捷。

这个功能可以把多参数的函数自动格式化为每行一个参数,提高可读性。

比如,UIKit 的一些函数回调真的名称又臭又长:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { //}格式化之后变成下面这样:
func collectionView( _ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { //}