最近在学习使用 Tuist 生成项目,摆脱烦人的 .xcodeproj。但是 Tuist 好用虽然好用,但是因为最近文档正在迁移,加之一些东西只能从示例中发掘,整个学习过程有点费劲。所以开一篇文章记录一下。

UIKit 模版

在有的地方你可以看到 “通过 tuist init --template swiftui 创建 SwiftUI 项目”。也就是说默认是创建 UIKit 项目。

但是!UIKit 的模板已经在这个 PR 中被删除精简掉,并跟随 4.0.0 版本一起发布了。所以如果你使用的是 4.0+ 版本的 Tuist,那么执行 init 操作后默认生成的就是 SwiftUI 项目了。

官方认为大多数人已经迁移到SwiftUI了...

所以如果你想创建 UIKit 项目,那么你有两种(或者更多)选择:

  • 手动把 UIKit 模板从该 PR 中找回来。
  • 每创建一个项目都手动修改一下 Info.plist 以及添加 AppDelegate 等文件。

因为我是第一次使用,所以是手动添加的相关文件。

如果采用这种方法,那么有可能会遇到这个问题:解决从SwiftUI迁回UIKit时,SceneDelegate不执行的问题。之前已经为这个问题单独发过一片文章了,这里再重复提一下。

资源生成

Tuist 集成了 SwiftGen 来实现资源生成,所以 SwiftGen 的模板可以直接拿到 Tuist 里使用。

Tuist 的默认模块在这里:Templates

以文件资源的生成为例,这个默认模板 和 SwiftGen 的 structured-swift5.stencil 模板几乎一样,只是多了一些 SwiftFormat 和 SwiftLint 的内容。

不包含目录层级

SwiftGen 对于一种资源有多个默认模版,比如文件资源还有 flat-swift5.stencil 这个不包含文件夹层级的模版。单独使用 SwiftGen 的话我们可以通过 --templateName 参数来使用该模板,但是 Tuist 里每种类型的资源只有一种模板。

所以如果我们想生成的资源不包含文件夹目录层级,只能参考Tuist官方文档的内容自定义模板:

If you want to provide your own templates to synthesize accessors to other resource types, which must be supported by SwiftGen, you can create them at Tuist/ResourceSynthesizers/{name}.stencil, where the name is the camel-case version of the resource.

Resource Template name
strings Strings.stencil
assets Assets.stencil
plists Plists.stencil
fonts Fonts.stencil
coreData CoreData.stencil
interfaceBuilder InterfaceBuilder.stencil
json JSON.stencil
yaml YAML.stencil
files Files.stencil

注意,官方的这个方法其实是替换了默认实现,相关代码应该是这里

如果你不想覆盖默认实现,想要自定义一个模板,可以参考app_with_plugins这个示例。

自定义解析类型

文档最后的部分有提到,可以通过 Project.resourceSynthesizers 属性来设置本项目自动为哪些类型的资源生成相应的代码。

这个属性是有默认值的,其默认值定义在这里

extension [ResourceSynthesizer] {
    public static var `default`: Self {
        [
            .strings(),
            .assets(),
            .plists(),
            .fonts(),
        ]
    }
}

可见使用的是默认模板,而且不包含 .files。如果有需要的话则需要手动添加。

禁用资源生成

虽然你大概率不需要,不过还是提一嘴。文档中也有说明,那就是可以通过设置 Project.Options.disableSynthesizedResourceAccessors 属性,来禁用资源的自动生成。

组件化

Tuist 应该是实施组件化的好手。

目录结构

首先让我们来讨论一下目录结构,这关系到我们如何组织主项目和各个组件。

官方文档里我们能看到如下目录结构:

Tuist/
  Config.swift
  Package.swift
  ProjectDescriptionHelpers/
Projects/
  App/
    Project.swift
  Feature/
    Project.swift
Workspace.swift

Tuist 文件夹是 Tuist 的一些配置,Workspace.swift 用来包含各个 Projects。

但是如果你仔细翻看过他们的示例的话,比如 ios_app_with_static_frameworksios_app_with_framework_and_resources,就会发现其实根目录下的内容是多变的。

如果你跟我一样好奇 “什么是最佳实践”,可以看这个讨论。然后结论就是:没有什么最佳实践,完全取决于你的个人用法和习惯。

Tuist 真的非常灵活,提供了很多便捷的写法,不同人用 Tuist 会写出不同的配置文件,也会有不同的用法。

Tuist vs SPM

对目录结构有了一些概念之后,就该考虑如何组织各个组件了。

官方文档里有一篇文章:Migrate local Swift Packages 讲的是如何将本地的 SPM 组件迁移到 Tuist,改为使用 Tuist 管理。

国外应该是已经没有什么人用 CocoaPods 做组件化管理了?...

这里我也推荐使用 Tuist + Multiple Projects 来做子组件。首先 SPM 要比 CocoaPods 更现代更官方,但是 SPM 本身有性能问题,再加上国内访问问题,所以真要实际用起来很麻烦。另外使用 Tuist 来管理项目,可以使用自带的 SwiftGen,不用再在 SPM 里进行相关配置。最后不得不提的是,用 Tuist 加载依赖是真的快...

单独运行组件

我个人对组件化的标准是每个子组件应该都能独立编译,进一步能独立运行。基于这个标准的话,会有一些问题。

善用软链接

项目根目录下的 Tuist/ProjectDescriptionHelpers 是不能被子项目读取的,比如前文提到的目录结构中:

Tuist/
  Config.swift
  Package.swift
  ProjectDescriptionHelpers/
Projects/
  App/
    Project.swift
  Feature/
    Project.swift
    Tuist/
        Config.swift
        Package.swift
Workspace.swift

Projects/Feature 中执行 tuist generate 命令时,是读取不到根目录中 Tuist/ProjectDescriptionHelpers 里的公共方法的。

一个可行的方法是使用软链接,将 ProjectDescriptionHelpers 文件夹软链到 Projects/Feature/Tuist 里。

同理 Tuist/.swiftpm/configuration/mirrors.json 文件也可以这么做。

是否需要多个 Config.swift

经过进一步实践,以及相关 讨论,发现其实不建议 有多个 Config.swift 文件。所以 Feature/Tuist/Config.swift 文件应该是没有必要的。

我现在使用的目录结构如下所示:

Tuist/
  Config.swift
  Package.swift
  ProjectDescriptionHelpers/
Projects/
  App/
    Project.swift
  Components/
    FeatureA/  
        Project.swift
    FeatureB/  
        Project.swift
Workspace.swift

也就是说各个组件内完全没有 Tuist 文件夹(依赖部分在 Tuist/ProjectDescriptionHelpers 进行了封装)

这时假如我们在 Projects/Components/FeatureA 目录下执行 tuist generate,其实读取的是根目录中 Tuist 的配置,生成的是 App 项目而不是组件项目。

这个时候肯定是不能满足 “组件独立运行” 的,因为现在这个组件算是一个 lib,没有前端 AppDelegate 等相应入口去运行 App。

那么为什么还要改成这种方式呢?一是基于官方推荐的不要使用多个 Config.swift 的建议;二是和群友讨论后,发现一般测试一个 lib 时会单独建一个测试项目,比如 FeatureATest,该测试项目不在 FeatureA 下;三是理想很丰满现实很骨干,话是这么说,但是实际上几乎不会单独运行某个 lib。

所以基于以上三个理由,如果未来需要单独测试某个 lib,那么最佳实践应该是新建一个相关的 Test 项目。