22 min read

【WWDC 2023】Xcode 15 更新内容

【WWDC 2023】Xcode 15 更新内容

WWDC 2023 这几天陆续放出各个主题的视频,挑了几个我认为值得看看的,学习一下并做个笔记。当然目前大部分系统、软件都是 Beta 版本,正式版本可能还会更改,但整体更新内容是不会大变的。

我挑的第一个视频是 What's new in Xcode 15,工欲善其事,必先利其器。

下面将根据视频的播放顺序,分析并实践各个段落部分。

2023 年 6 月 9 日,测试 Xcode 15 Beta 版本。

Code completion updates

1. 根据文件名,当你想输入相同名字的 struct 或 class 时,会根据你输入的首字母匹配文件名。

比如文件名为"ThisIsACodeCompleteDemo.swift",当你在文件里输入"struct t"时,自动弹出"ThisIsACodeCompleteDemo"为首选。

mIi3A7

2. 会根据上下文分析推断你需要设置的属性或参数。

对 Text 设置了.font(.title)之后,再次输入".",会提示.bold().fontWeight(_ weight:)等字体相关的 modifier。

VYwvFA

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

Ggo2GW

What's new in asset catalogs

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 文件。

image-20230610141026780

用法如下:

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-20230610134336645

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

image-20230610190157193

在 Xcode 14 包括以前的版本,我习惯于用 SwiftGen 来自动生成一些常量,功能比 Xcode 15 更强大,支持以下类型:

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)

Meet string catalogs

这是一个新的管理翻译字符串的方式,采用新的文件格式 .xcstrings。相比 Xcode 15 以前使用不同语言的目录的方式,更加集中统一了,这是一个比较好的改进。另外,这个功能是向后兼容的,iOS 17 以前也可以使用,👍🏻!

.xcstrings文件实际上是 JSON 文件,终于不是 Xcode 祖传的 XML 格式,Git 版本化就方便多了,感激涕零。

新建文件通过 "File" -> "New" -> "File" ,然后输入 string 过滤,下面图示。我们可以看到默认就是 String Catalog 了,Strings File 和 StringsDict 都变成了 Legacy ,Next 之后生成的默认文件名依然是"Localizable",这个就最好不改了。

image-20230611085009588

接下来,我们先来学习一下新的界面操作,首先依然会有一个 Base 的概念,就是其他语言根据 Base 来翻译,默认是 English。

在 Xcode 15 以前,这个 Base 基本上不改,但是 Xcode 15 多了一个默认语言的修改,你可以通过"Set Default" 来改成简体中文为默认。

image-20230610233540449

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

image-20230610195409197

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

image-20230610195440560

翻译状态

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

image-20230610213906598

新加的 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 这么来处理:

image-20230610214258044

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

image-20230610215043187

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

image-20230610222417189

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

假如我们有一句话是 There are %lld boys and %lld girls in this class,通过下面图示的方法两次 Vary 了。

image-20230610222030301

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

image-20230610230026013

Xcode 15 还支持自定义 @arg1@arg2 的名称,比如你可以改成更好阅读的 @boys@girls 。但是这个最好不要尝试,因为我遇到了几次崩溃,这一块的还有待完善。

老项目迁移

老的项目可以通过点击"Edit"-> "Convert" -> "To String Catalogs" ,界面出来之后会让你选择 Target,然后选择下一步选择需要转换的文件,包括Info.plistLocalizable.strings

假如目前有一个老项目,支持简体、繁体、英语这 3 种语言,通过转换之后会生成两个文件:InfoPlist.xcstringsLocalizable.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

遗憾

总体来说,我是对 String Catalogs 是非常满意的,稍微遗憾的就是没有像 Assets 一样可以 Generate Code,自动生成一些没有参数的常量,或者生成带参数的函数。

如果是 Xcode 14,我建议使用 Localization Editor 来编辑翻译字符串,主界面如下图,将所有语言平铺在一起,也有注释功能,所有编辑操作实时同步到 .strings文件。

image-20230611093039982

注意一下,看我的 Key 是设置成"auth.btn.login"这样的,因为我借助上面提到过的 SwiftGen ,会帮我自动生成代码,还可以自定义函数名称,这样可以做到动态加载不同语言的翻译效果。实际调用时的代码如下:

// 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.placeholder

SwiftGen 对没有参数的会生成一个 enum 的 static var,对于中间的点分割部分,会依次生成一个内部 enum,达到命名空间的效果。

/// 请输入密码
internal static var placeholder: String { return L10n.tr("Localizable", "auth.password.placeholder", fallback: "Enter password") }

What's new in Swift-DocC

主要是更新了一个实时预览的功能,作用不大。

image-20230611100355093

Meet Swift macros

这个宏就强大了,值得单开一篇文章来详细说一说,比如有以下一些好用到爆炸的内置宏

  • Observable : 着实简化了 @Published 变量
  • #Predicate : 查询过滤有类型检查了,不需要通过 NSPredicate 字符串的形式
  • Model : 用于 SwiftData,CoreData 的 Swift 版本

但是没啥用,因为不向后兼容,只支持 iOS 17😭。

What's new in Previews

预览在 SwiftUI 是很方便的,一边写代码一边预览效果,DSL 的 UI 就是这么便捷。
但是想要让 UIView 和 UIViewController 也有预览功能,以前只能使用 storyboard 或者 xib 了。但是对那些代码党来说,这就很难受了,不运行项目是看不到效果的。所以,InjectionIII 这个工具诞生了,通过注入 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 实现的。

image-20230611110429982

但是我们可以借助 SwiftUI 的预览功能,将 UIView 或者 UIViewController 用 SwiftUI 的 View 封装一层。下面简单写一个方法:

import SwiftUI
import 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
        }
    }
}
image-20230611111104617

采用这种方式,就不会有约束失效的问题了,而且还可以支持到 iOS 13,完美!

The bookmark navigator

Xcode 15 新增的书签功能就是给代码做个标记,可以快速定位你经常需要访问的代码行、源文件,或者特别标记的东西,比如 TODO、FIXME 等。

书签界面位于左侧导航视图的第三个位置,这里的位置可用快捷键Commmand + 数字 切换。

image-20230611212326905

具体的功能有以下这些:

  • 对代码行添加书签,右键选择具体的代码行
  • 对文件添加书签,右键选择文件即可
  • 可对书签添加描述信息,选中后 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.plist

What's new in source control

Xcode 15 内置的版本管理工具终于可用了,真的不是很喜欢在 Source Tree 或者 Terminal 之间来回切换界面了。

下面就是源码控制导航界面,快捷键 Command + 2

image-20230611222744998

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

另外,Xcode 14 的 Source Control 菜单升级为 Xcode 15 的 Integrate 菜单,如下图所示:

image-20230611223152164

大概有 2 个主要变化:

  • Stage 常用操作
  • 冲突解决操作
  • Xcode Cloud 整合进这里了

What's new in testing

主要变化如下:

  • 比 Xcode 14 多 45% 内容的测试报告
  • 左下角可以过滤测试项目
  • "Performance Metrics" 性能指标标签
  • Test Plan 的改进(没啥用)
  • UI Test 的改进(没啥用)

Updates in the debug console

本章节重点介绍 OSLog ,但是 OSLog 是 iOS 15+ 的 API,所以有点鸡肋。

Logger 是 iOS 14 + 的 API,相对好一点。

关于 Swift Logging 的整个历史变化,可以看 Peter Steinberger
文章 Logging in Swift

下面我们简单过一下 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 分类、日志类型的过滤、背景色、跳转到代码行、隐私数据处理。

image-20230612114724021

SwiftLog

Apple 有在 github 开源一个 SwiftLog ,这是一个前端框架,提供了日常开发中常用的上层接口,但是需要自己依据 LogHandler协议实现后端,比如打印在 Console ,保存在文件,还是发送到日志服务器等。

有人以 OSLog 作为后端,实现了一个 LoggingOSLog 开源库,底层日志打印使用 os_log ,但是官方并不是很推荐这样使用,因为失去了静态字符串的性能,全是动态的字符串,以及隐私数据方面完成暴露了。

CocoaLumberjack

我相信大部分 iOS 开发者的日志工具还是 CocoaLumberjack,没错,用它就够了。

Updates in Xcode Cloud distribution

Xcode Cloud 只能用于外网的 git 地址,对于我们公司这种局域网部署的 gitlab 无效,所以公司的项目都没有尝试过。

但是我在一些 Side Projects 上使用了一下,感觉还不是太给力,遂放弃。

Signature verification and privacy manifests

本章节主要讨论 XCFramework 的签名有效性和隐私清单。

XCFramework 的签名有两种,一种是使用自己的证书重新签名,叫做"self-signed",Xcode 15 会比较 XCFramework 的 SHA-256 值,跟刚加入到工程的时候是否一致。另一种是 XCFramework 开发者自己的证书签名,苹果会校验证书的有效性,比如对方的 Apple Developer Program 过期了,或者他的证书被 revoke 了。

Privacy Manifest 隐私清单,就是把 App Store Connect 网页上的隐私设置项搬到 Xcode 工程里面,有一个 Plist 文件格式的 PrivacyInfo.xcprivacy 配置文件,里面需要列出各个隐私标签的使用情况,以及是否允许某些域名访问隐私 API。

image-20230612165538066

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

image-20230612163744549

参考资料:

What's new in Xcode distribution

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 的界面

image-20230612160215182

所有这些发布选项都是流水线操作的,无需先过去一样手动选择证书,勾选,下一步等手动档。点击"Distribute"按钮,就是自动档的 D 档,自动签名、包含符号表、自动增加 Build 版本号、去除 Swift 的符号全包了。

Organizer 界面增加了一个 Feedback 选项,其实就是 TestFlight 网页的内容搬到这里来了,测试团队截图的 APP 内容就显示在这里。

image-20230612151752291

Others

Xcode 15 还有些小改进,限于时长因素在视频里没有体现。

Quick Action

新增了一个类似 VS Code 的 Command Palette 命令,快捷键 Command + Shift + A,会打开一个搜索框,可以搜索任何菜单操作。这个功能的好处是菜单快捷键太多了,实在记不过来。不过我会改掉这个快捷键,变成和 VS Code 一样的 F11,这样按一下就可以了,更快捷。

image-20230611225943093

Format to multiple lines

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

fKwaub

比如,UIKit 的一些函数回调真的名称又臭又长:

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    //
}

格式化之后变成下面这样:

func collectionView(
    _ collectionView: UICollectionView,
    willDisplay cell: UICollectionViewCell,
    forItemAt indexPath: IndexPath
) {
    //
}