Tuist 注意事项
最近在学习使用 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_frameworks 和 ios_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 项目。