开篇词 iOS 架构与开发工程化,让你成为 10x 程序员
你好,我叫林永坚,在澳洲互联网公司 REA Group 担任 Mobile Tech Lead 一职。作为技术负责人,我主要负责移动端的架构与研发,同时也负责移动团队的建设和推动公司的工程化实践。
在我所负责的 App 中, realestate.com.au App 是澳洲本土企业流量排名前几的 App,几乎每个在澳洲买房、卖房、租房的人都会使它。在 App Store 上,这款 App 的评分也一直保持在 4.7 星以上。要知道,在 2020 年澳洲排名前 20 的免费 App 只有 8 个能到达 4.7 星。
现在看这款 App 已经是一个非常成功的产品了,但其实在几年前,我们也面临着重重困境。
工程化实践的重要性
那是五六年前,我刚加入 REA Group 公司,当时我们的 App 版本发布周期长达 8 个月到一年。
为什么会这么长呢?因为我们没有好的开发流程和工程化标准。
举例来说,仅仅在准备发布阶段,由于没有代码管理规范,也缺乏自动化工具的支持,我们都需要专门停止手头工作,用一两个月的时间来准备。比如合并代码,需要手工操作,要花 2周 ~ 4 周;部署测试也是手动,需要 1 周时间;最后再进行回归测试,又要 2 周 ~ 3 周。
当时因为发布流程十分复杂,人工操作繁重还容易出错,有的同事为了逃避麻烦甚至请了病假。
虽然我们花那么长时间来准备,但 App 在 App Store 上的评分只有 2.5 星。主要原因是用户体验非常不好,经常崩溃,差不多每 5 个中就有一个用户的 App 会闪退。
为什么会这样?
其中一个原因就是没有统一的代码管理流程,开发者想要 Push 什么风格的代码都可以。比如,当时就有一名开发者为了学习 Core Data,在一个周末期间把学习的 Core Data 的代码直接 push 到了主分支。
结果,他的这个 Push 操作让 App 的崩溃率上涨了 10%。事后虽然我们想要补救,但因为这部分代码没有任何架构设计,花了两三年才逐渐把相关代码移除完毕!
另一个原因就是,代码没有统一而灵活的系统架构设计与开发模式。就像前面的那次事故,之所以要花两三年才移除干净,就是因为缺乏面向接口的编码模式,Core Data 的数据对象污染了整个代码库。同时,又因为 App 缺乏分层的架构设计,导致所有逻辑都编写在臃肿的 View Controller 里,单元测试覆盖率几乎为零。可以说,代码库进入几乎没法维护的状态,很多崩溃情况无法修复。
为了避免同类的事情再次发生,也为了提高效率,我们开始了漫长而十分崎岖的工程化实践之路。最终,App 的崩溃率从 20% 以上下降到 0.01% 以下,App Store 的评分也从 2.5 拉升到 4.7 星。
为什么有这门课程?
虽然我们下定决心改变现状,但在开始实施工程化实践的时候还是面临许多困难。
当时我们想要规范代码管理流程,提高开发、迭代、打包和发布效率,不再纯手动操作,但实际做下来事情千头万绪,既没有明确的工程化目标和方向,市面也没有相关的资料可供参考,只能通过不断试错来推进。
而且,既然是要进行 iOS 工程化开发,就需要工具支持。但当时不仅相关的工程化、自动化工具稀少,苹果公司所提供的工具链也非常不友好,市面上的 CI(Continuous Integration,持续集成)工具并不支持脚本化和版本管理,很多时候需要人手操作来搭建,且每次 MacOS 或者 Xcode 更新,都需要重新搭建。
如今四五年过去了, 适用于 iOS 开发的 Swift 语言逐渐成熟,可重用组件的编写以及可扩展架构的设计变得越来越方便;而苹果公司的工具链,特别是支持自动化和工程化的开源工具和在线服务(如 Fastlane、 TravisCI )也越来越丰富。
此外,像 GitHub 、GitLab 等代码管理平台的出现,也使我们的开发流程更容易统一和规范。
但我发现,还有些 iOS 开发者并没有意识到技术的变化,仍在用纯手工的形式开发和发布 App。
另外,虽然过去了很长时间,但市面上有关 iOS 开发的工程化实践资料仍然很少,有的只是介绍如何使用系统 API 来开发一款 iOS App,还有的是介绍各种自动化和工程化工具的使用,并没有连贯起来形成体系。
为了让你不再像我以前那样一直掉坑填坑,也为了帮助更多的开发者,我结合多年的工程化实践经验,做了这样一门课程。我希望以一个类似朋友圈的 App 为案例,来向你介绍如何在架构和开发 App 的时候进行工程化实践,降低开发门槛和维护成本,同时还能快速迭代与发布。
之所以选择以一个开发类似朋友圈的 App 为案例,是因为该 App 的架构非常通用,它包含了网络访问、 JSON 数据解析、数据存储,以及响应式数据流处理等功能模块。通过它,我们可以把这一套架构应用到各种场景中,例如电商类 App、生活类的房产App、卖车 App,以及社交类短视频 App,等等。
课程设计与收获
根据实际项目开发流程,我把本课程分为五大部分。
第一部分:配置与规范
在这部分,我主要介绍如何定义和配置一套统一的规范,具体包括:搭建统一的开发环境,使用 CocoaPods 统一管理第三方的依赖库,统一 Project 和 Target 的配置,使用 Swiftlint 统一编码规范,以及使用 Git 和 GitHub 来规范源代码管理流程。
我希望通过这部分的学习,能让你在开发所有的 iOS 项目时更规范。如果你的团队有新成员,它也可以帮助你更好地让新成员接手项目,同时也可以极大减轻团队成员之间的沟通成本。
第二部分:基础组件设计
因为基础组件能够帮助功能模块之间解藕,提高可重用性,还能支撑业务功能的快速开发。所以,我将会介绍如何使用极少的工作量来构建一些非常实用的基础组件。这些组件包括:设计规范组件、路由组件、多语言支持组件、动态字体和深色模式的支持组件。除此之外, 我还会介绍如何使用隐藏功能菜单来分离生产环境与研发环境。
第三部分:架构与实现
随着 iOS 和 Android 成为移动端的两大霸主,为了兼顾两者,有些公司使用 React Native 或者 Fluter 等非官方方案。而我们则致力于使用官方平台所提供的原生技术,通过一套架构设计,使得开发者可以很方便地在 iOS 和 Android 之间进行业务代码的开发。
在这一部分,我会为你详细介绍如何使用 BFF 和 MVVM 来设计一套跨平台的架构。
具体来说,我会详细介绍这套框架中的 iOS 的部分,包括 MVVM 模式中每一层的功能、责任,以及具体的代码实现。同时我还会演示如何通过 TDD 方法来对 MVVM 中每一个功能模块进行单元测试,从而提高代码的质量和有效地降低 App 的崩溃率。
通过这部分的介绍,我希望你能学会如何从零开始设计一个符合你团队具体情况的 App,同时也可以尝试在原有的 App 里面,循序渐进地引入 MVVM 架构来提高代码的灵活性、扩展性和可维护性。
第四部分:上架与优化
在这部分我会介绍自动化上架以及上线后优化的一些技巧。比如,如何统一管理证书与描述文件,如何快速交付,如何灵活支持统计分析,如何借助崩溃报告解决线上的 Bug,如何设置远程开关,如何进行A/B 测试,等等。
我希望你可以在这部分学会搭建一套自动化的流程,来实现无人操作打包、签名、分发与上架,有效提升 App 的交付速度。
第五部分:加餐
缺乏架构设计的 App 都会遇到一些问题,每当一个新的功能,或者更好的技术栈出现后,想要引进来会带来很多麻烦。
比如想支持动态字体或者深色模式,由于最初开发时没有考虑到,后来想要加进这些功能时就变得十分麻烦,几乎需要修改整个 App。所以架构设计的可扩展性、灵活性对产品不断迭代尤为重要。
为了测试架构的灵活性,我就做了一个大胆设想,在不改动任何原有代码的基础上把 UI 层从 UIKit 替换成苹果公司最新的 UI 框架 SwiftUI(下面是 SwiftUI 的实现效果)。在专栏中,我会详细告诉你我是一步步怎么做的。
讲师寄语
如果你已经在 iOS 开发上有 2~3 年的经验,能读懂系统 API 和使用三方库开发绝大部分的功能,想要自己从头到尾架构设计一个 App;如果你平常需要花很长时间做大量的重复劳动,例如管理证书、打包、签名、部署测试设备和上架,迫切想从中解放出来;如果你想提升自己,彰显技术实力,帮助团队规范开发流程,甚至成为团队的技术负责人……
那么,这门课非常适合你。
通过它,你可以掌握一套工程级的 iOS 开发流程。它能让你编写出更高效、易读,易于维护和扩展的代码,同时还能减少大量重复劳动,提高技术工作效率,助你以一当十,成为10x 程序员。
在学习上有一个 721 法则,说的是,一个人的能力习得,70%来自实践,20%来自他人,10%来自培训学习。我希望通过这次实战课程,能让你更上一层楼。
01 开发环境:如何使用 Ruby 工具链统一开发环境?
在 iOS 开发过程中,你是不是会经常遇到这些情况:
每次打开一个新项目,都需要手动搭建开发环境;有时候在安装第三方工具时使用到 sudo 权限,导致以后安装工具都需要手工输入密码而无法实施自动化。还有,每当启动一台新 CI 时,就需要手工登录并配置一遍,更可怕的是,原先搭建好的 CI 会随着 Xcode 版本更新需要重新配置。
为什么会这么麻烦呢?就是因为你在项目开始之初没有做好统一配置。
所谓统一配置,就是所有的配置信息都以文本的格式存放在 Git 里面,我们可以随时查看修改记录,以此来帮助我们比较不同配置之间的差异性,然后在这个基础上不断更新迭代。
可以说,有了统一配置,任何工程师都可以搭建出一模一样的开发环境,构建出功能一致的 App;有了统一配置,还可以让我们按需延展 CI 服务,而不用任何手工操作。更重要的是,它还可以应用到各个类似的 iOS 项目中,极大地减轻了项目前期的搭建成本。
既然统一的配置那么重要,那么我们怎样搭建统一配置的开发环境呢?
Ruby 工具链
我们可以通过 Ruby 工具链为整个项目搭建一致的开发和构建环境。为什么选择 Ruby 而不是其他语言环境呢?因为在 iOS 开发方面,目前流行的第三方工具 CocoaPods 和 fastlane 都是使用 Ruby 来开发的。特别是 Ruby 有非常成熟的依赖库管理工具 RubyGems 和 Bundler,其中 Bundler 可以帮我们有效地管理 CocoaPods 和 fastlane 的版本。
下面一起来看看怎样搭建一个统一的开发环境吧。
开发环境统一配置图
通常,统一的开发环境应该从操作系统开始。对于 iOS 开发来说,MacOS 是目前 iOS 开发唯一支持的操作系统。在公司,MacOS 的版本一般由 IT 部门统一管理和更新。要注意,当公司统一更新了我们开发环境的 MacOS 版本以后,需要同时更新 CI 上 MacOS 的版本,以保持一致。
Xcode
位于 MacOS 上层的是 Xcode 和 rbenv。其中,Xcode 是 iOS 开发和构建工具,在同一个项目里,最好使用同一个版本的 Xcode 进行开发和构建,我们可以在项目的 README.md 文件标注 Xcode 的版本。
像我们将要开发的这款类似朋友圈的 Moments App 项目,我就在对应的 README.md 文件里标明了需要使用 Xcode Version 12.2 (12B45b)。具体内容你也可以在代码仓库找到。
那我们怎样才能保证每个人都安装同一个版本号的 Xcode 呢?技巧就是我们不要到有自动更新功能的 Mac App Store 中下载 Xcode,而是到苹果的开发者网站搜索并下载。
有时候我们会同时开发多个项目,这样有可能要安装多个不同版本的 Xcode。如果你的机器有多于一个版本的 Xcode,此时需要特别注意,为了保证所使用的编译器版本一致,在每次执行自动化命令之前(如执行bundle exec fastlane test
),要先使用xcode-select -s
来选择该项目所对应版本的 Xcode。
比如说我的电脑上有多个 Xcode 版本,在开发 Moments App 时,每次执行自动化命令之前都会执行这样一条命令xcode-select -s /Applications/Xcode12.2.app/Contents/Developer
来选择 Moments App 项目所使用的 Xcode。这里的Xcode12.2.app
就是我安装的 Xcode 12.2 版所在的位置。
rbenv
有了版本一致的 Xcode 以后,因为后期我们会用到 CocoaPods 等第三方 Ruby 工具,为了自动化安装和管理这些工具,整个项目团队所使用的 Ruby 版本也必须保持一致。为此,我们就需要用到 Ruby 环境管理工具。
目前流行的 Ruby 环境管理工具有 RVM 和 rbenv。我推荐使用的是 rbenv,因为它使用 shims 文件夹来分离各个 Ruby 版本,相对于 RVM 更加轻装而方便使用。千万注意,团队内部不要同时使用不同的 Ruby 环境管理工具,否则项目编译会出错。
rbenv 是 Ruby 环境管理工具,能够安装、管理、隔离以及在多个 Ruby 版本之间切换。要使用 rbenv,我们可以通过 Homebrew 来安装它,下面是安装 Homebrew 和 rbenv 的脚本。
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install rbenv ruby-build rbenv-vars
一旦安装 rbenv 完毕,我们需要把以下的设置信息放到你的 Shell 配置文件里面,例如 ~/.bash_profile 或者 ~/.zshrc 等文件,这样能保证每次打开终端的时候都会初始化 rbenv。
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
接着我们就可以安装和设置项目的 Ruby 环境了。
$ cd $(PROJECT_DIR)
$ rbenv install 2.7.1
$ rbenv local 2.7.1
此处是把项目的 Ruby 环境配置为 2.7.1 版本。rbenv 会帮我们建立 一个叫作.ruby-version 的文件,该文件里面只保存一个版本号(例如2.7.1
)的字符串。这个包含了版本号的文件可以用 Git 进行管理。如果要更新版本,可以通过rbenv local
命令进行,每次更新也由 Git 统一管理,这样就能让其他开发者使用同一版本的 Ruby 开发环境了。
RubyGems 和 Bundler
RubyGems 和 Bundler 主要是用来安装和管理 CocoaPods 和 fastlane 等第三方工具。
具体来说,RubyGems 是 Ruby 依赖包管理工具。在 Ruby 的世界,包叫作 Gem,我们可以通过gem install
命令来安装。但是 RubyGems 在管理 Gem 版本的时候有些缺陷,就有人开发了 Bundler,用它来检查和安装 Gem 的特定版本,以此为 Ruby 项目提供一致性的环境。
要安装 Bundler,我们可执行gem install bundler
命令进行,之后,再执行bundle init
就可以生成一个 Gemfile 文件,像 CocoaPods 和 fastlane 等依赖包,我们就可以添加到这个文件里面。
具体代码如下:
source "https://rubygems.org"
gem "cocoapods", "1.10.0"
gem "fastlane", "2.166.0"
注意我们在gem
命令里面都指定了依赖包的特定版本号。例如,在我们的 Moment App 就使用了1.10.0
版的 CocoaPods,然后执行bundle install
来安装各个 Gem。 Bundler 会自动生成一个 Gemfile.lock 文件来锁定所安装的 Gem 的版本,例如:
DEPENDENCIES
cocoapods (= 1.10.0)
fastlane (= 2.166.0)
为了保证团队其他成员都可以使用版本号一致的 Gem,我们需要把 Gemfile 和 Gemfile.lock 一同保存到 Git 里面统一管理起来。
到此为止,我们已经知道怎样使用 Ruby 工具链配置一个统一的开发环境。但在真实的开发环境中,搭建环境只需要一个人来完成即可,其他成员执行以下脚本就能完成整套开发环境的搭建。
$ ./scripts/setup.sh
我们一起看看这个脚本做了些什么?
# Install ruby using rbenv
ruby_version=`cat .ruby-version`
if [[ ! -d "$HOME/.rbenv/versions/$ruby_version" ]]; then
rbenv install $ruby_version;
fi
# Install bunlder
gem install bundler
# Install all gems
bundle install
# Install all pods
bundle exec pod install
该脚本主要做了四件事情,第一步是在 rbenv 下安装特定版本的 Ruby 开发环境,然后通过 RubyGems 安装 Bunlder,接着使用 Bundler 安装 CocoaPods 和 fastlane 等依赖包,最后安装各个 Pod。这样,一个统一的项目环境就搭建完成了,接下来开发者就可以打开 Moments.xcworkspace进行开发了。
说完 Ruby 环境搭建以后,最后我们一起聊聊保证项目文件一致性的 .gitignore 文件。
.gitignore 文件
.gitignore 文件是一个配置文件,用来指定让 Git 需要忽略的文件或者目录。如果没有 .gitignore 文件,项目成员可能会不小心把一些自动生成等无关重要的文件或者具有个人信息(例如 xcuserdata)的文件保存到 Git 里面。这就大大增加了查看 Git 修改历史的难度。因此,在项目初期就配置一个合适的 .gitignore 文件,能减轻后续的管理工作。
如何创建 .gitignore 文件呢?
我一般会在 gitignore.io 里面输入关键字,例如 Xcode,Swift 等,然后该网站会帮我们生成一个默认的 .gitignore 文件。咱们项目 Moments App 的.gitignore 文件你可以到拉勾教育的仓库中查看。
总结
以上,我们通过 Xcode、rbenv、RubyGems 和 Bundler 搭建一个统一的 iOS 开发和构建环境。
再次强调下,为了让各个开发和构建环境能保持一致,我们要把 .ruby-version、 Gemfile 和 Gemfile.lock 文件通过 Git 统一管理起来,并共享给整个项目团队使用。
而且,由于我们的开发环境已经通过 Bundler 管理起来,今后,当使用各个 Gem 工具的时候,也需要使用 Bundler。例如在使用 CocoaPods 时要执行bundle exec pod
,以保证我们使用的是项目级别而不是操作系统级别的 Gem 工具。
思考题:
请问如果我们不使用 rbenv ,那我们使用的 Ruby 来自哪里?使用 CocoaPods 等工具又来自哪里?不同项目能使用不同版本的 CocoaPods 吗?
你可以把回答写到下面的留言区哦,下一讲我将介绍如何使用 CocoaPods 统一依赖库的管理。
源码地址:
README.md
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/README.md
Moments App 的.gitignore 文件
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/.gitignore
02 依赖管理:如何使用 CocoaPod 统一依赖库的管理?
在 iOS App 开发方面,几乎所有的 App 都需要使用到第三方依赖库。依赖库不仅能为我们提供丰富的功能,还能避免我们从头开发,在节省时间的同时也减少许多 Bug 。
但伴随着软件功能越来越丰富,依赖库数量越来越多,由此也出现了“依赖地狱”,比如依赖库循环依赖,底层依赖库版本冲突等。为了解决此类问题,于是,依赖库管理工具也就出现了。
目前流行的依赖库管理工具主要有:Git Submodules、Carthage、 Swift Package Manager 和 CocoaPods。在这里我们选择 CocoaPods。为什么呢?原因有三:
-
CocoaPods 非常成熟,十分稳定,并且简单易用,学习成本低,效果明显;
-
CocoaPods 会自动整合 Xcode 项目,使得其他项目成员在使用第三方库时无须任何额外的手工操作;
-
CocoaPods 已经成为 iOS 业界标准,支持几乎所有的开源库和商业库,即便是 Objective-C 的依赖库以及二进制文件(binary)依赖库,CocoaPods 也提供支持。
那么,怎样使用 CocoaPods 来管理第三方依赖库呢?接下来我会从语义化版本管理、Pod 版本管理、Pod 版本更新三个方面展开介绍。
语义化版本管理
开发软件,免不了要更新迭代,所以每一次更新的版本号管理变得很重要。并且,一旦版本号混乱,就会导致一系列问题,比如很难查找和修改线上崩溃,没办法支持多团队并行开发,等等。为了避免此类问题,我们可以使用语义化版本管理(Semantic Versioning)来统一版本号的定义规范。
语义化版本号是一种通用的版本号格式规范,目前绝大部分优秀的第三方依赖库都遵循这一规范来发布版本。
具体来说,语义化版本号的版本号一般包括四部分:MAJOR、MINOR、PATCH、BUILD。每一部分都由递增的数值组成,例如 1.2.3.4,其中 1 是MAJOR, 2 是 MINOR。如果我们更新 MINOR 版本号,那么下一个版本就是 1.3.0.0。接下来我详细介绍下这四部分。
-
MAJOR 是指主版本号,通常在重大更新的时候才会需要更新主版本号。例如 iOS 每年都会更新一个主版本号。而对于第三方库来说,主版本号的更新,表示该库的 API 新增了重大功能,或者引入了不可兼容的更新 (breaking changes)。
-
MINOR 是指副版本号,用于小功能的改善。例如 iOS 14 在发布主版本后,在一年内可能发布多个副版本如 14.1、 14.2 来完善其系统功能。一般对于第三方库来说,副版本的更新就是新增一些 API,但不包含不可兼容的更新。
-
PATCH 是指补丁版本号,一般用于 bug fix 以及修复安全性问题等。对于第三方库来说,补丁版本号的更新也不应该有不可兼容的更新。虽然实际操作中这会有些困难,但我们可以通过把原有 API 标记为 deprecated,或者为新 API 参数提供默认值等办法来解决。
-
BUILD 是指构建版本号,通常在内部测试时使用。一般当我们使用 CI 服务器进行自动构建时,构建版本号会自动更新。
Pod 版本管理
要使用 CocoaPods 管理第三方依赖库,首先要新建一个 Podfile 文件,然后执行bundle exec pod install
命令来安装所有依赖库。这时候 CocoaPods 会自动帮我们建立一个 Podfile.lock 文件和一个 Workspace文档。
注意,在第一讲我们说过,由于是通过 Bundler 来安装 CocoaPods,每次执行pod
命令前,都需要加上bundle exec
。不过为了简洁,后面涉及pod
命令时,我会省略bundle exec
部分。
接下来,我详细介绍下 Podfile 文件、 Podfile.lock 和 Workspace 文档到底是什么,以及如何使用。
Podfile 文件
Podfile 文件是一个配置文件,它主要是用来描述 Xcode 项目里各个 target 的依赖库。我们项目的 Podfile 文件可以在仓库中找到。在这里,我主要和你介绍一下 Podfile 文件中的几个重要配置。
source 配置
source
用于指向 PodSpec(Pod 规范)文件的 Repo,从而使得 CocoaPods 能查询到相应的 PodSpec 文件。
具体来说,当使用公共依赖库的时候,source
需要指向 CocoaPods Master Repo,这个主仓库集中存放所有公共依赖库的 PodSpec 文件。 由于 CocoaPods 经常被开发者吐槽 Pod 下载很慢,因此 CocoaPods 使用了 CDN (Content Delivery Network,内容分发网络)来缓存整个 CocoaPods Master Repo, 方便开发者快速下载。具体的配置方法就是使source
指向 CND 的地址,代码示例如下:
source 'https://cdn.cocoapods.org/'
如果使用的是私有依赖库,我们也需要把source
指向私有库的 PodSpec Repo,以使得 CocoaPods 能找到相应的 PodSpec 文件。 代码示例如下:
source 'https://my-git-server.com/internal-podspecs'
注意,当我们使用私有库时,执行pod install
命令的机器必须能访问到source
所指向的 Repo。
project 和 workspace
project
用于指定我们的主项目文档。该项目文档会使用到 CocoaPods 管理的所有第三方依赖库。
workspace
用于指定要生成和更新的 Workspace 文档。和其他依赖库管理工具不一样,CocoaPods 会自动生成一个 Workspace 文档,然后我们只能使用该文档而不是 Xcode 项目文档来进行后续开发。
代码示例如下:
project './Moments/Moments.xcodeproj'
workspace './Moments.xcworkspace'
这其中 Moments.xcodeproj 就是我们的主项目文档,它一般放在和项目名字相同的下一层目录下。
而 Moments.xcworkspace 是 CocoaPods 为我们生成的 Workspace文档,为了统一,我建议名字也是和主项目相同。
platform 和 use_frameworks
先看示例,它表示什么呢?
platform :ios, '14.0'
use_frameworks!
为了保证所有依赖库与主项目在编译和运行时兼容,我们指定的系统版本号需要和主项目所支持的系统版本号保持一致。而platform
就是用于指定操作系统以及所支持系统的最低版本号。比如,例子中的platform :ios, '14.0'
就表示支持 iOS 14.0 以上的所有 iOS 版本。
另外一行的use_frameworks!
这一配置会让 CocoaPods 把所有第三方依赖库打包生成一个动态加载库,而不是静态库。因为动态库是我们经常用到的,它能有效地加快编译和链接的速度。
组织同类型的第三方依赖库
def dev_pods
pod 'SwiftLint', '0.40.3', configurations: ['Debug']
pod 'SwiftGen', '6.4.0', configurations: ['Debug']
end
其中configurations: ['Debug']
用于指定该依赖库只是使用到Debug
构建目标(target)里面,而不在其他(如Release
)构建目标里面,这样做能有效减少 App Store 发布版本的体积。
def dev_pods end
代码块是“复用同一类依赖库方式”的意思,我们可以把同类型的依赖库都放进这个代码块里面。比如,我们的 Moments 项目中就分别有dev_pods
(开发相关的库),core_pods
(核心库)以及thirdparty_pods
(第三方库)等代码块定义。
target 配置
有了这些复用库定义以后,怎样使用到项目的构建目标(target)里面呢?下面就是一个例子。
target 'Moments' do
dev_pods
core_pods
# other pods...
end
我们可以把构建目标所使用的所有依赖库放进target
代码块中间,上面中的Moments
就是我们的 App 构建目标。该构建目标依赖了dev_pods
和core_pods
等各组依赖库。执行pod install
的时候,CocoaPods 会把dev_pods
代码块自动展开为SwiftLint
和SwiftGen
,那么Moments
构建目标能使用SwiftLint
和SwiftGen
依赖库了。
依赖库的版本
pod 'RxSwift', '= 5.1.1'
pod 'RxRelay', '= 5.1.1'
在 CocoaPods 里面,每一个依赖库称为一个 Pod (注意这里首字母大写,Pod 指一个库),指定一个 Pod 的命令是pod
(注意这里是小写,表示一条命令)。在 Podfile 里面我们可以通过这样的格式pod 'RxSwift', '= 5.1.1'
来配置依赖库的版本号。其中,RxSwift
或者RxRelay
是依赖库的名字,5.1.1
为版本号。这些库的名字以及版本号都可以在 CocoaPods 官网上找到。
为了统一管理第三方依赖库的版本,我建议统一使用 = 来锁定该依赖库的版本,这样就能保证每次执行pod install
的时候都可以为同一个库下载同一个版本。
除了 = 操作符以外,CocoaPods 还支持其他操作符来指定版本:
-
> 0.1
表示大于 0.1 的任何版本,这样可以包含 0.2 或者 1.0; -
>= 0.1
表示大于或等于 0.1 的任何版本; -
< 0.1
表示少于 0.1 的任何版本; -
<= 0.1
表示少于或等于 0.1 的任何版本; -
~> 0.1.2
表示大于 0.1.2 而且最高支持 0.1.* 的版本,但不包含 0.2 版本。
这几个操作符相里面,~>
(Squiggy arrow)操作符更为常用,它是以最后一个部分的版本号(例子中 0.1.2 的最后一个部分是补丁版本号 ..2)来计算可以支持的最高版本号。
例如~> 0.1.2
表示 >= 0.1.2 并且 < 0.2.0,但不能等于 0.2.0, 因为 0.2.0 已经更新了副版本号而不仅仅是补丁版本号了。另外一个例子是~> 0.1
,表示 >= 0.1 并且 < 1.0,举例来说,我们可以更新到 0.9 但不能更新到 1.0。
前面我介绍的是引用外部的第三方依赖库,如果我们的项目有自己的内部依赖库,要怎样在 CocoaPods 引用它呢?其实很简单,我们可以执行以下命令:
pod 'DesignKit', :path => './Frameworks/DesignKit', :inhibit_warnings => false
和其他外部依赖库不一样,我们需要使用:path
来指定该内部库的路径。
Podfile.lock 文件
Podfile.lock 文件是由 CocoaPods 自动生成和更新的,该文件会详细列举所有依赖库具体的版本号。比如,
DEPENDENCIES:
- Alamofire (= 5.2.0)
- Firebase/Analytics (= 7.0.0)
PODFILE CHECKSUM: 400d19dbc4f5050f438797c5c6459ca0ef74a777
当执行pod install
后,CocoaPods 会根据 Podfile 文件解释出各依赖库的特定版本号,然后一一列举在 DEPENDENCIES 下面。在上述的例子中,我们的 App 在构建过程中使用了5.2.0 的 Alamofire 库以及 7.0.0 的 Firebase Analytics 库。
PODFILE CHECKSUM 用于记录 Podfile 的验证码,任何库的版本号的更改,都会改变该验证码。这样能帮助我们在不同的机器上,快速检测依赖库的版本号是否一致。
我建议要把 Podfile 和 Podfile.lock 文件一同 commit 并 push 到 Git 代码管理服务器里面。特别是在团队开发的环境下,这样能帮助我们保证各个依赖库版本号的一致性。
在实践操作中,无论我们在哪台机器上执行pod install
, PODFILE CHECKSUM 都不应该发生任何改变。因为我们在 Git 保存了 Podfile.lock,一旦我们发现老版本 App 的 Bug ,就可以根据该文件为各个依赖库重新安装同一版本号,来重现和定位问题,从而帮助我们快速修改这些 Bug。
Workspace 文档
Workspace 文档是 Xcode 管理子项目的方式。通过 Workspace,我们可以把相关联的多个 Xcode 子项目组合起来方便开发。
前面说过,当我们执行pod install
的时候,CocoaPods 会自动创建或者更新一个叫作 Pods 的项目文档(Pods.xcodeproj )以及一个 Workspace 文档(在我们项目中叫作 Moments.xcworkspace)。
其中,Pods 项目文档负责统一管理各个依赖库,当我们在 Podfile 里面指定构建成动态库的时候,该项目会自动生成一个名叫Pods_<项目名称>.framework
的动态库供我们项目使用。
而 Workspace 文档则统一管理了我们原有的主项目 (Moments.xcodeproj)以及那个 Pods 项目。
与此同时,CocoaPods 还会修改 Xcode 项目中的 Build Phases 以此来检测 Podfile.lock 和 Manifest.lock 文件的一致性,并把Pods_<项目名称>.framework
动态库嵌入我们的主项目中去。
以上所有操作都是由 CocoaPods 自动帮我们完成。以后的开发,我们都可以打开 Workspace 文档而不是原有的 Xcode 项目文档来进行。
Pod 版本更新
使用 CocoaPods 管理第三方依赖库的操作非常简单,可是一旦使用不当,特别是在 Pod 更新的时候,很容易引起依赖库版本不一致,从而出现各种问题。
比如,在编译程序的时候,有些开发者可以顺利进行,而另外一些开发者编译时候就会出错;或者程序在本地编译时运行良好,一旦在 CI 上构建,就会出现 App 崩溃,等等。
那么,怎么保证更新 Pod 的时候都能保证版本一致呢?
下面结合我的实践经验,以第三方网络库 Alamofire 为例子和你介绍下。
第一步,CocoaPods 已经为我们提供了pod outdated
命令,我们可以用它一次查看所有 Pod 的最新版本,而无须到 GitHub 上逐一寻找。下面是执行pod outdated
命令的其中一条结果:
The following pod updates are available:
- Alamofire 5.2.0 -> 5.2.0 (latest version 5.4.0)
这表示当前我们使用了版本为 5.2.0 的 Alamofire ,其最新版本为 5.4.0。如果我们决定更新到版本 5.4.0,那么可以继续下一步。
第二步,在更新依赖库版本之前,为了避免在新版本中不小心引入 Bug,我们需要了解新的版本到底提供了哪些新功能,修改了哪些 Bug,与老版本是否兼容等事项。具体我们可以到 CocoaPods 官网上查找需要更新的第三方依赖库,然后在 GitHub 等平台上找到,并仔细阅读该库的版本说明(release note)。
请注意,我们要阅读当前使用版本到要更新的版本之间的所有版本说明。 在这个例子中,我们要阅读 5.2.1,5.2.2,5.3.0 和 5.4.0 的所有版本说明。这些版本说明会列出新增功能,更新的 API,修改的 Bug,有没有不可兼容的更新 。
第三步,在 Podfile 文件里把要更新的 Pod 的版本号进行修改。例如把pod 'Alamofire', '= 5.2.0'
改成pod 'Alamofire', '= 5.4.0'
。 然后执行pod install
来重新生成 Podfile.lock 文件。
此时特别注意的是,我们要使用pod install
而不是pod update
。因为执行pod update
会自动更新所有 Pod 的版本,这可能会更新了一些我们目前还不想更新的 Pod,从而会引入一些难以觉察的问题。
第四步,如果所更新的版本包含了不可兼容的更新,我们需要修改代码来保证代码能顺利完成编译。
第五步,很多第三方依赖库都是一些通用的基础组件,一旦发生问题会影响到整个 App 的功能,因此我们需要根据所更新的库进行回归测试。例如当更新了 Alamofire 库的时候,我们需要把每个网络请求都执行一遍,避免所更新的版本引入新的 Bug。
第六步,为了把更新的版本共享给所有开发者和 CI 服务器,我们需要把 Podfile 和 Podfile.lock 文件一同 commit 并 push 到 Git 代码管理服务器,并通过 Pull Request 流程并入主分支。
第七步,一旦更新的代码并入主分支后,要通过 Slack 等内部通信软件告诉所有开发者 pull 或者 rebase 主分支的代码,并执行pod install
来更新他们开发环境的所有依赖库。
特别注意,千万不要使用pod update
,因为pod update
会自动把开发者机器上所有 Pod 的版本自动更新了。这种更新往往不是我们想要的结果,我们希望统一更新各个 Pod 的版本,并通过 Git 进行集中管理。
如果开发者在编译新代码前没有执行pod install
命令,会出现以下的错误。
The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
这错误可以有效提醒所有开发者,需要再次执行pod install
来更新他们本地的依赖库,从而保证所有开发者使用的依赖库的版本都是一致的。
另外,如果更新了基础组件的依赖库(如网络库),在测试阶段,我们还需要进行全面的回归测试。因为这些基础组件库的新版本如果有 Bug 很可能导致我们的 App 会发生大比例的崩溃,严重影响用户的体验。
有了上面的一流程,我们就可以有效地保证每个开发者使用的依赖库版本都是一致的,同时也能保证 CI 在自动构建 App 的时候所使用的依赖库版本也是统一的。
总结
这一讲我介绍了如何使用 CocoaPods 来统一管理依赖库的版本。特别是根据我自己的经验总结了一套更新 Pod 版本的流程,希望你灵活使用这些步骤,从而少走弯路。
这里我再特别强调一下,为了保证依赖库版本都能保持一致,尽量不要执行pod update
,而是使用通过修改 Podfile 文件里的版本号并执行pod install
来更新 Pod 的版本,然后把 Podfile 和 Podfile.lock 文件一同并入 Git 主分支中进行统一管理。
思考题:
CocoaPods 非常简单易用,它可以同时管理依赖库的依赖项,例如我们的 App 依赖 A 库, 而 A 库又依赖 B 库,同时 B 库依赖 C 库,CocoaPods 可以帮我们自动找出所有依赖项并按顺序安装所有依赖库。 那你知道 CocoaPods 是如何管理依赖库的依赖呢?
下一讲,我将为你介绍如何统一构建配置。
源码地址:
Podfile 文件地址:
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Podfile
### 03 配置准备:如何搭建多环境支持,为 App 开发作准备
在开始之前,我先问你几个问题,在测试的时候,App 一般需要连接测试服务器,那么在上架后,还需要连生产服务器吗?在发布前,你的 App 需要通过 Ad-hoc 分发给内部测试组吗?在发布到 App Store 的时候,你的 App 需要同时支持免费版和收费版吗?
如果你的回答是“是”,那么你的 App 就需要搭建多环境支持,优化开发的工作流程。多环境提供很多好处,比如能基于同一套源代码自动构建出有差异功能的 App;能支持多个团队并行开发,也能分离测试和生产环境,提高产品的迭代速度,保证上架的 App 通过严格测试和功能验证。
在 Moments App 项目中,我们就使用了三个不同的环境,分别是开发环境,测试环境和生产环境。它们到底有什么区别呢?
-
开发环境, 用于日常的开发,一般有未完成的功能模块。编译时,也不进行任何优化,可以打印更多的日志,帮助开发者快速定位问题。
-
测试环境, 主要是用于测试,以及为产品经理进行功能验证,包括部分完成的功能模块,也提供一些隐藏功能,方便我们进行开发和迭代,例如快速切换用户,清理 Cache,连接到不同后台服务器等等。
-
生产环境, 只包含通过了测试并验证过的功能模块,它是最终提交到 App Store 供终端用户使用的版本。
多环境支持需要用到 Xcode 的构建配置,这一讲,我就结合 Moments App 项目来聊聊这个问题。
Xcode 构建基础概念
一般在构建一个 iOS App 的时候,需要用到 Xcode Project,Xcode Target,Build Settings,Build Configuration 和 Xcode Scheme 等构建配置。它们各有什么用呢?
Xcode Project
Xcode Project用于组织源代码文件和资源文件。一个 Project 可以包含多个 Target,例如当我们新建一个 Xcode Project 的时候,它会自动生成 App 的主 Target,Unit Test Target 和 UI Test Target。
在 Moments App 项目中,主 Target 就是 Moments,Unit Test Target 是 MomentsTests, UI Test Target 就是 MomentsUITests。
Xcode Target
Xcode Target用来定义如何构建出一个产品(例如 App, Extension 或者 Framework),Target 可以指定需要编译的源代码文件和需要打包的资源文件,以及构建过程中的步骤。
例如在我们的 Moments App 项目中,负责单元测试的MomentsTestsTarget 就指定了 14 个测试文件需要构建(见下图的 Compile Sources),并且该 Target 依赖了主 App TargetMoments(见下图的 Dependencies)。
有了 Target 的定义,构建系统就可以读取相关的源代码文件进行编译,然后把相关的资源文件进行打包,并严格按照 Target 所指定的设置和步骤执行。那么 Target 所指定的设置哪里来的呢?来自 Build Settings。
Build Settings
Build Setting保存了构建过程中需要用到的信息,它以一个变量的形式而存在,例如所支持的设备平台,或者支持操作系统的最低版本等。
通常,一条 Build Setting 信息由两部分组成:名字和值。比如下面是一条 Setting 信息,iOS Development Target
是名字,而iOS 14.0
是值。
有了这些基础知识以后,接下来我就结合 Moments App 来和你介绍下如何进行多环境配置,从而生成不同环境版本的 App。
Moments App 构建配置
一般用 Xcode 编译出不同环境版本的 App 有多种办法,例如拷贝复制所有源代码,建立多个 Target 来包含不同的源码文件等等。不过,在这里我推荐使用 Build Configuration 和 Xcode Scheme 来管理多环境,进而构建出不同环境版本的 App。为什么?因为这两个是目前管理成本最低的办法。接下来我一一介绍下。
Build Configuration
当我们在 Xcode 上新建一个项目的时候,Xcode 会自动生成两个 Configuration:Debug和Release。Debug 用于日常的本地开发,Release 用于构建和分发 App。而在我们的 Moments App 项目中,有三个 configuration:Debug,Internal 和 AppStore。它们分别用于构建开发环境、测试环境和生产环境。 其中 Internal 和 AppStore 是从自动生成的 Release 拷贝而来的。
那什么是 Build Configuration 呢?
Build Configuration就是一组 Build Setting。 我们可以通过 Build Configuration 来分组和管理不同组合的 Build Setting 集合,然后传递给 Xcode 构建系统进行编译。
有了 Build Configuration 以后,我们就能为同一个 Build Setting 设置不同的值。例如Build Active Architecture Only
在 Debug configuration 是Yes
,而在 Internal 和 AppStore configuration 则是No
。这样就能做到同一份源代码通过使用不同的 Build Configuration 来构建出功能不一样的 App 了。
那么,在构建过程中怎样才能选择不同的 Build Configuration 呢?答案是使用 Xcode Scheme。
Xcode Scheme
Xcode Scheme用于定义一个完整的构建过程,其包括指定哪些 Target 需要进行构建,构建过程中使用了哪个 Build Configuration ,以及需要执行哪些测试案例等等。在项目新建的时候只有一个 Scheme,但可以为同一个项目建立多个 Scheme。不过这么多 Scheme 中,同一时刻只能有一个 Scheme 生效。
我们一起看一下 Moments App 项目的 Scheme 吧。 Moments App 项目有三个 Scheme 来分别代表三个环境,Moments Scheme 用于开发环境,Moments-Internal Scheme 用于测试环境,而 Moments-AppStore Scheme 用于生产环境。
下面是MomentsScheme 的配置。
左边是该 Scheme 的各个操作,如当前选择了 Build 操作;右边是对应该操作的配置,比如 Build 对应的 Scheme 可以构建三个不同的 Targets。不同的 Scheme 所构建的 Target 数量可以不一样,例如下面是Moments-InternalScheme 的配置。
该 Scheme 只构建主 App TargetMoments,而不能构建其他两个测试 Target。
当我们选择 Run、Test、Profile、 Analyze 和 Archive 等操作时,在右栏有一个很关键的配置是叫作 Build Configuration,我们可以通过下拉框来选择 Moments App 项目里面三个 Configuration (Debug,Internal 和 AppStore) 中的其中一个。
为了方便管理,我们通常的做法是,一个 Scheme 对应一个 Configuration。有了这三个 Scheme 以后,我们就可以很方便地构建出 Moments α(开发环境),Moments β(测试环境)和 Moments(生产环境)三个功能差异的 App。

你可能已经注意到这三个 App 的名字都不一样,怎么做到的呢?实际上是我们为不同的 Configuration 设置了不一样的 Build Setting。其中决定 App 名字的 Build Setting 叫作PRODUCT_BUNDLE_NAME
,然后在 Info.plist 文件里面为 Bundle name 赋值,就能构建出名字不一样的 App。
为了构建出不同环境版本的 App,我们需要经常为各个 Build Configuration 下的 Build Setting 设置不一样的值。 在这其中,使用好 xcconfig 配置文件就显得非常重要。
xcconfig 配置文件
xcconfig 会起到什么作用呢?
一般修改 Build Setting 的办法是在 Xcode 的 Build Settings 界面上进行。 例如下面的例子中修改 Suppress Warnings。
这样做有一些不好的地方,首先是手工修改很容易出错,例如有时候很难看出来修改的 Setting 到底是 Project 级别的还是 Target 级别的。其次,最关键的是每次修改完毕以后都会修改了 xcodeproj 项目文档 (如下图所示),导致 Git 历史很难查看和对比。
幸运的是,Xcode 为我们提供了一个统一管理这些 Build Setting 的便利方法,那就是使用 xcconfig 配置文件来管理。
xcconfig 概念及其作用
xcconfig也叫作 Build configuration file(构建配置文件),我们可以使用它来为 Project 或 Target 定义一组 Build Setting。由于它是一个纯文本文件,我们可以使用 Xcode 以外的其他文本编辑器来修改,而且可以保存到 Git 进行统一管理。 这样远比我们在 Xcode 的 Build Settings 界面上手工修改要方便很多,而且还不容易出错。
在 xcconfig 文件里面的每一条 Setting 都是下面的格式:
BUILD_SETTING_NAME = value
其中,BUILD_SETTING_NAME
表示 Build Setting 的名字,而value
是该 Setting 的值。下面是一个例子。
SWIFT_VERSION = 5.0
SWIFT_VERSION
是用于定义 Swift 语言版本的 Build Setting,其值是5.0
。Setting 的名字都是由大写字母,数值和下划线组成。这种命名法我们一般成为蛇型命名法,例如SNAKE_CASE_NAME
。
当我们使用 xcconfig 时,Xcode 构建系统会按照下面的优先级来计算出 Build Setting 的最后生效值:
-
Platform Defaults (平台默认值)
-
Xcode Project xcconfig File(Project 级别的 xcconfig 文件)
-
Xcode Project File Build Settings(Project 级别的手工配置的 Build Setting)
-
Target xcconfig File (Target 级别的 xcconfig 文件)
-
Target Build Settings(Target 级别的手工配置的 Build Setting)
Xcode 构建系统会按照上述列表从上而下读取 Build Setting,如果发现同样的 Setting ,就会把下面的 Setting 覆盖掉上面的,越往下优先级别越高。
例如我们在 Project 级别的 xcconfig 文件配置了SWIFT_VERSION = 5.0
而在Target 级别的 xcconfig 文件配置了SWIFT_VERSION = 5.1
,那么Target 级别的 Build Setting 会覆盖 Project 级别的SWIFT_VERSION
设置,最终SWIFT_VERSION
生效的值是5.1
。
那么,要怎样做才能做到不覆盖原有的 Build Setting 呢?我们可以使用下面例子中的$(inherited)
来实现。
BUILD_SETTING_NAME = $(inherited) additional value
可以保留原先的 Setting,然后把新的值添加到后面去。比如:
FRAMEWORK_SEARCH_PATHS = $(inherited) ./Moments/Pods
其中的FRAMEWORK_SEARCH_PATHS
会保留原有的值,然后加上./Moments/Pods
作为新值。
在配置 Build Setting 时,还可以引用其他已定义的 Build Setting。
例如下面的例子中,FRAMEWORK_SEARCH_PATHS
使用了另外一个 Build SettingPROJECT_DIR
。
FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)
为了重用,我们可以通过#include
引入其他 xcconfig 文件。
#include "path/to/OtherFile.xcconfig"
Moments App xcconfig 配置文件
下面我们一起来看看 Moments App 项目是怎样管理 xcconfig 配置文件吧。
我们把所有 xcconfig 文件分成三大类:Shared、 Project 和 Targets。
其中 Shared 文件夹用于保存分享到整个 App 的 Build Setting,例如 Swift 的版本号、App 所支持的 iOS 版本号等各种共享的基础信息。 下面是 SDKAndDeviceSupport.xcconfig 文件里面所包含的信息:
TARGETED_DEVICE_FAMILY = 1
IPHONEOS_DEPLOYMENT_TARGET = 14.0
TARGETED_DEVICE_FAMILY
表示支持的设备,1
表示 iPhone。而IPHONEOS_DEPLOYMENT_TARGET
表示支持 iOS 的最低版本,我们的 Moments App 所支持的最低版本是 iOS 14.0。
Project 文件夹用于保存 Xcode Project 级别的 Build Setting,其中 BaseProject.xcconfig 会引入 Shared 文件夹下所有的 xcconfig 配置文件,如下所示:
#include "CompilerAndLanguage.xcconfig"
#include "SDKAndDeviceSupport.xcconfig"
#include "BaseConfigurations.xcconfig"
然后我们会根据三个不同的环境分别建了三个xcconfig 配置文件,如下:
-
DebugProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG
-
InternalProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) INTERNAL
-
AppStoreProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PRODUCTION
它们的共同点是都引入了用于共享的 BaseProject.xcconfig 文件,然后分别定义了 Swift 编译条件配置SWIFT_ACTIVE_COMPILATION_CONDITIONS
。其中$(inherited)
表示继承原有的配置,$(inherited)
后面的DEBUG
或者INTERNAL
表示在原有配置的基础上后面添加了一个新条件。有了这些编译条件,我们就可以在代码中这样使用:
#if DEBUG
print("Debug Environment")
#endif
该段代码只在开发环境执行,因为只有开发环境的SWIFT_ACTIVE_COMPILATION_CONDITIONS
才有DEBUG
的定义。这样做能有效分离各个环境,保证同一份代码构建出对应不同环境的 App。
Targets 文件夹用于保存 Xcode Target 级别的 Build Setting,也是由一个 BaseTarget.xcconfig 文件来共享所有 Target 都需要使用的信息。
PRODUCT_BUNDLE_NAME = Moments
这里的PRODUCT_BUNDLE_NAME
是 App 的名字。
下面是三个不同环境的 Target xcconfig 文件。
-
DebugTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.debug.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) α
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.development
-
InternalTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.internal.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) β
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.internal
-
AppStoreTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.appstore.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited)
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments
它们都需要引入 CocoaPods 所生成的 xcconfig 和共享的 BaseTarget.xcconfig 文件,然后根据需要改写 App 的名字。例如DebugTarget 覆盖了PRODUCT_BUNDLE_NAME
的值为Moments α*
, 其所构建的 App 叫作Moments α。
一般在 App Store 上所有 App 的标识符都必须是唯一的。如果你的项目通过 Configuration 和 Scheme 来生成免费版和收费版的 App,那么,你必须在两个 Configuration 中分别为PRODUCT_BUNDLE_IDENTIFIER
配置对应的标识符,例如com.lagou.free
和com.lagou.paid
。
在 Moments App 中,我们也为各个环境下的 App 使用了不同的标识符,以方便我们通过 CI 自动构建,并分发到内部测试组或者 App Store。同时,这也能为各个环境版本的 App 分离用户行为数据,方便统计分析。
一旦有了这些 xcconfig 配置文件,今后我们就可以在 Xcode 的 Project Info 页面里的 Configurations 上引用它们。
下面是所有 Configurations 所引用的 xcconfig 文件。
在配置好所有 xcconfig 文件的引用以后,可以在 Build Settings 页面查看某个 Build Setting 的生效值。我们以IPHONEOS_DEPLOYMENT_TARGET
为例,一起看看。
当我们选择All和Levels时,可以看到所有配置信息分成了不同的列。这些列分别代表前面的 Build Settng 优先级:
-
平台默认值
-
Project 级别的 xcconfig 文件
-
Xcode 项目文件中的 Project 级别配置
-
Target 级别的 xcconfig 文件
-
Xcode 项目文件中的 Target 级别配置
Build Settng 的优先级是从左到右排序的。越是左边优先级就越高。例如,我们在 Project 级别的 xcconfig 文件里面定义了IPHONEOS_DEPLOYMENT_TARGET
的值为14.0
,那么Project 级别的 xcconfig 文件(Project Config File) 一列上就会显示iOS 14.0
,它覆盖了系统的默认值 (iOS Default)iOS 14.2
。这就是因为 Project 级别的 xcconfig 文件,它的优先级高于系统默认值,因此最后生效的值是iOS 14.0
。
总结
本讲我介绍了如何通过 Build Configuration、 Xcode Scheme 以及 xcconfig 配置文件来统一项目的构建配置,从而搭建出多个不同环境,为后期构建出对应环境的 App 做准备。
在使用 xcconfig 配置时,还是需要注意以下两点:
首先,我们必须把所有 Build Setting 都配置在 xcconfig 文件里面,并通过 Git 进行统一管理;
其次,我们千万不要在 Xcode 的 Build Settings 页面修改任何 Setting,否则该配置会覆盖 xcconfig 文件里面的配置。如果你不小心修改了,可以通过点击删除键把页面是的配置删掉。
思考题:
请问我们 Moments App 项目的主 App 为什么只使用了一个 Target 吗?如果使用多个 Target,例如 Debug Target,Internal Target 和 Release Target 会有什么问题?
你可以把回答写到下面的留言区哦,下一讲我将介绍如何使用 Swiftlint 统一编码规范。
源码地址:
https://github.com/lagoueduCol/iOS-linyongjian/tree/main/Moments/Moments/Configurations
04 编码规范:如何使用 SwiftLint 统一编码规范?
在软件开发领域有很多有趣且重要的话题,比如使用什么样的系统架构来让代码更容易维护,使用哪些第三方库能提高开发效率,等等。但也有一些话题不仅无趣,还很难得出结论,比如像下面这行变量定义,里面的空格哪个正确?
let name: String = "Jake"
let name : String = "Jake"
let name :String = "Jake"
let name: String= "Jake"
let name: String="Jake"
还有代码缩减,到底是用 2 个空格还是 4 个?这就像豆浆到底是喝甜的还是喝咸的一样,并没有标准答案。也因此,出现了许多永无休止的讨论。特别是当新成员所提交的代码风格,与团队其他成员有很大的区别时,往往会出现沟通与协作问题,甚至发生争执而影响工作。此时,团队如果有一套统一的编码规范,那么这样的问题就很容易解决。
除了能促进沟通协作,一套统一的编码规范还能降低代码维护的成本和减少 Bug 的数量。此外,由于规范往往由团队资深开发者指定并不断完善,也有助于其他团队成员快速成长。
既然统一的编码规范由那么多优点,那么我们如何在团队中实施统一编码规范呢?在 iOS 开发领域,使用 SwiftLint 能有效地建立和改进 Swift 项目的编码规范。接下来我就和你聊聊这方面的内容。
安装 SwiftLint
安装 SwiftLint 的方式有很多种,例如使用 Homebrew,Mint,下载 SwiftLint.pkg 安装包等等。但我只推荐 CocoaPods 这一种方法,因为通过 CocoaPods 可以有效地管理 SwiftLint 的版本,从而保证团队内各个成员都能使用一模一样的 SwiftLint 及其编码规范。
通过 CocoaPods 来安装 SwiftLint 非常简单。在 Moments App 项目中,我们在Podfile
文件中添加SwiftLint
Pod 即可。
pod 'SwiftLint', '= 0.41.0', configurations: ['Debug']
由于我们只在开发环境下使用 SwiftLint,因此配置了只有Debug
的 Build Configuration 才生效。
为了每次编译完都使用 SwiftLint 来检查代码,我们需要在主 App TargetMoments的 Build Phases 里面添加Run SwiftLint步骤。然后配置它执行"${PODS_ROOT}/SwiftLint/swiftlint"
命令。
这里要注意,由于 SwiftLint 的设计是检查有效的 Swift 代码(编译通过的代码就是有效的代码),我们需要把Run SwiftLint步骤放在Compile Source步骤之后。否则 SwiftLint 可能会反馈一些错误的结果。
有了上面的配置以后,每次编译程序, SwiftLint 都会自动执行检查,我们可以在 Xcode 上修正这些警告信息来保证编码规范的统一。

例如上面的截图所示,SwiftLint 告诉我们空格的使用不正确。
那么,这些警告信息到底怎样来的呢?我们一起看看.swiftlint.yml
文件吧。
.swiftlint.yml 文件
当我们执行 SwiftLint 命令时,它会自动帮我们启动一堆编码规则,并扫描和检查我们的项目。这些规则有comma
(逗号前后的空格处理),private_over_fileprivate
(优先使用 priviate),force_cast
(避免强制转型)等等 。详细规则列表你也可以在SwiftLint 官网找到。
但正如 SwiftLint 的作者所说: “规则存在,但并不意味着你必须用它”。我们需要根据团队自身的情况和成员的统一意见,来决定需要启动和关闭哪些规则。此时,就需要用到 .swiftlint.yml 文件了。
.swiftlint.yml主要用于启动和关闭 SwiftLint 所提供的规则,以及自定义配置与规则。一旦我们有了 .swiftlint.yml 文件以后,SwiftLint 在执行过程中会严格按照该文件的定义来扫描和检查代码。由于 .swiftlint.yml 是一个纯文本文件,我们可以通过 Git 统一管理,这样能保证整个团队在执行 SwiftLint 的时候都会得到一模一样的效果,从而保证了整个团队代码规范的一致性。
规则设置
SwiftLint 提供了disabled_rules
,opt_in_rules
和only_rules
三种规则设置方法。其中,disabled_rules
能帮我们关闭默认生效的规则,而opt_in_rules
可以启动默认关闭的规则。
另外,SwiftLint 所提供的每条规则都有一个叫作Enabled by default的属性来表示该规则是否默认启动。例如class_delegate_protocol
规则是默认启动的,而array_init
规则是默认关闭的。
disabled_rules:
- class_delegate_protocol
opt_in_rules:
- array_init
上面的配置表示,关闭默认生效的class_delegate_protocol
,并同时启动array_init
。
虽然使用disabled_rules
和opt_in_rules
能够完成配置,但我不推荐你使用它们 ,而是用only_rules
来定义每条生效的规则。
我们在 Moments App 项目中也使用了only_rules
。你可以在拉勾教育的代码仓库找到该 .swiftlint.yml 文件来查看项目启动的所有规则。由于only_rules
是 SwiftLint 0.41.0 引入的,如果你需要以前版本,可以使用whitelist_rules
来替代。下面是 .swiftlint.yml 文件中的部分规则。
only_rules:
- array_init
- attributes
- block_based_kvo
- class_delegate_protocol
- closing_brace
通过only_rules
,我们可以把每一条规则明确添加到 SwiftLint 里面。这样能保证我们整个团队都使用一致的规则,而不会像使用disabled_rules
和opt_in_rules
那样,随着 SwiftLint 默认规则的改变,导致最终启动的规则不一样。
自定义配置
在我们配置一条规则的时候,为了符合团队自身的情况,可以修改其默认配置。例如line_length
的默认配置是当一行代码多于 120 个字符的时候会报告编译警告,而多于 200 个字符的时候报告编译错误。
来源:SwiftLintFramework Docs
我们可以在 .swiftlint.yml 文件中修改这些配置。
line_length: 110
file_length:
warning: 500
error: 1200
上述的配置表示我们修改了line_length
的配置,当一行代码多于 110 个字符(而不是默认的 120 个字符)时就会报告编译警告。我们也可以同时覆盖编译警告和编译错误的配置,例如把file_length
的编译警告改成 500,而编译错误改成 1200。
自定义规则
除了 SwiftLint 所提供的默认规则以外,我们还可以自定义规则。例如在 Moments App 项目中,我就自定义了“不能硬编码字符串”的规则,具体如下:
custom_rules:
no_hardcoded_strings:
regex: "([A-Za-z]+)"
match_kinds: string
message: "Please do not hardcode strings and add them to the appropriate `Localizable.strings` file; a build script compiles all strings into strongly typed resources available through `Generated/Strings.swift`, e.g. `L10n.accessCamera"
severity: warning
该规则no_hardcoded_strings
会通过正则表达式来检查字符串是否进行了硬编码。如果是SwiftLint 会根据我们的自定义规则显示警告信息,如下图所示。
排除扫描文件
默认情况下 SwiftLint 会扫描和检查整个项目的所有代码。因为一些第三方依赖库的源码风格可能和我们团队的风格不一致,为了方便使用第三方依赖库,我们可以用excluded
来把它排除在外,避免扫描和检查。示例如下:
excluded:
- Pods
现在我们已经通过配置 .swiftlint.yml 文件来帮助我们统一编码规范了。
总结
在这一讲,我介绍了如何使用 SwiftLint 来统一编码规范。特别是其中的only_rules
,我们要使用它来定义需要生效的规则。
此外,在制定编码规范时,我们还需要注意以下几点。
首先,所制定的规范要和业界标准同步,这能让新成员接手代码时,更容易接受而不是反驳。一个建议是,你可以从 SwiftLint 所提供的默认规则开始,毕竟这些规则都是通过许多人沟通和完善下来的,比你独自一人想出来要靠谱得多。
其次,在制定规范时,重点是提高代码的可读性,而不是为了高大上去使用黑魔法或者选择那些不常用功能等。这样可以让团队绝大部分成员更容易理解和遵循这些规范。
最后要强调的是,一套编码规范是需要不断迭代和完善的,我建议团队要定时 Retro(Retrospective,敏捷回顾),讨论和优化这些规范,让得大家都有机会贡献到规范中,增加他的认同感。这种做法在多团队平行开发的环境下特别有效。
思考题:
你所做的团队除了使用 SwiftLint 等工具检查以外,还使用了哪些手段来保证编码规范的统一呢?
请把回答写到下面的留言区哦,下一讲我将介绍如何使用 Fastlane 执行自动化操作。
源码地址:
swiftlint.yml 文件
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Moments/.swiftlint.yml