大家好!
我叫德米特里。 碰巧的是,在过去两年中,我是由13个iOS开发人员组成的团队的负责人。 我们一起致力于Tinkoff Business应用程序。
我想与您分享我们的经验,即如何在不期望的时候以最大的功能集或错误修复集发布应用程序,并且仍然不会灰显。
我将向您介绍一些实践和方法,这些方法和方法可帮助团队显着加快开发和测试速度,并显着减少计划外或紧急发布带来的压力,错误和问题。 #MakeReleaseWithoutStress 。
走吧
问题描述
想象以下情况。
还有另一个版本。 在进行回归测试之前,测试人员再次找到了显示行ID的位置,而不是应用程序中的文本。

这是我们遇到的最常见的问题之一。
如果您没有将应用程序本地化为另一种语言,或者没有使用Localizable.strings文件,所有本地化都直接在代码行中编写,则可能不会遇到此问题。
但是您可能还会遇到其他问题,我们将为您解决:
原因→效果
为什么这一切都发生了?
有可以编译的程序代码。 如果您写错了一些东西(语法上或调用时函数名不正确),则您的项目将根本无法汇编。 这是可以理解,显而易见和合乎逻辑的。
但是资源之类的东西呢?
它们没有被编译,只是在编译代码后将它们添加到包中。 在这方面,在运行时可能会发生大量问题,例如,上述情况-字符串本地化。
寻找解决方案
我们考虑了一般如何解决此类问题以及如何解决此问题。 我回想起mail.ru上的一次Cocoaheads会议。 有人谈论比较代码生成工具。
在再次看到这些工具(库/框架)的全部含义之后,我们终于找到了所需的工具。
同时,多年来,Android的开发人员一直在使用类似的方法。 Google对它们进行了思考,使它们成为开箱即用的工具。 但是,即使是稳定的Xcode,苹果也无法做到...
剩下的就是只找出要选择的乐器: Natalie , SwiftGen或R.swift ?
Natalie没有本地化支持,因此决定立即放弃。 SwiftGen和R.swift具有非常相似的功能。 我们选择R.swift只是根据星星数,它知道我们随时可以更改为SwiftGen。
R.swift如何工作
启动预编译的构建阶段脚本,该脚本在项目结构中运行并生成一个名为R.generated.swift
的文件,该文件需要添加到项目中(我们将在最后告诉您更多有关此操作的信息)。
该文件具有以下结构:
import Foundation import Rswift import UIKit /// This `R` struct is generated and contains references to static resources. struct R: Rswift.Validatable { fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current fileprivate static let hostingBundle = Bundle(for: R.Class.self) static func validate() throws { try intern.validate() } // ... /// This `R.string` struct is generated, and contains static references to 2 localization tables. struct string { /// This `R.string.localizable` struct is generated, and contains static references to 1196 localization keys. struct localizable { /// en translation: Apple Pay /// /// Locales: en, ru static let card_actions_activate_apple_pay = Rswift.StringResource(key: "card_actions_activate_apple_pay", tableName: "Localizable", bundle: R.hostingBundle, locales: ["en", "ru"], comment: nil) // ... /// en translation: Apple Pay /// /// Locales: en, ru static func card_actions_activate_apple_pay(_: Void = ()) -> String { return NSLocalizedString("card_actions_activate_apple_pay", bundle: R.hostingBundle, comment: "") } } } }
用法:
let str = R.string.localizable.card_actions_activate_apple_pay() print(str) > Apple Pay
您问:“为什么Rswift.StringResource
需要Rswift.StringResource
?”。 我自己不明白为什么要生成它,但是,正如作者解释的那样,以下内容是必需的: link 。
实际应用
以下内容的简要说明:
*是-他们使用该方法一段时间后,最终离开了
*它已成为-编写新代码时使用的方法
*并非如此,但是您可以拥有-一种在我们的应用程序中从未存在过的方法,但是当我那时还没有在Tinkoff.ru工作时,我在各种项目中都遇到了它。
本地化
我们开始使用R.swift
进行本地化,这使我们摆脱了最初写的问题。 现在,如果本地化中的id已更改,则将不会组装项目。
*仅在将所有本地化的ID更改为另一个ID时,此方法才有效。 如果字符串保留在任何本地化版本中,则在编译时会出现一个警告,提示此ID不会以所有语言本地化。

不在那儿,但是您可能有: final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } }
那是: extension String { public func localized(in bundle: Bundle = .main, value: String = "", comment: String = "") -> String { return NSLocalizedString(self, tableName: nil, bundle: bundle, value: value, comment: comment) } } final class NewsViewController: UIViewController { private enum Localized { static let newsTitle = "news_title".localized() } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = Localized.newsTitle } }
它变成了: titleLabel.text = R.string.localizable.newsTitle()
图片
现在,如果我们在* .xcassets中重命名了某些内容,并且未更改代码,则该项目将完全无法汇编。
那是: imageView.image = UIImage(named: "NotExist") // imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash
它变成了: imageView.image = R.image.tinkoffLogo() //
故事板
那是: let someStoryboardName = "SomeStoryboard" // Change to something else (eg: "somestoryboard") - get nil or crash in else let someVCIdentifier = "SomeViewController" // Change to something else (eg: "someviewcontroller") - get nil or crash in else let storyboard = UIStoryboard(name: someStoryboardName, bundle: .main) let _vc = storyboard.instantiateViewController(withIdentifier: someVCIdentifier) guard let vc = _vc as? SomeViewController else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯}
它变成了: guard let vc = R.storyboard.someStoryboard.someViewController() else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯ }
依此类推。
验证情节提要
如果您在情节提要或xib文件中做错了什么, R.validate()是一个很棒的工具,可以动动手(或者,只是将错误抛出到catch块中)。
例如:
- 指示图片名称,该名称不在项目中
- 他们指出了字体,然后停止使用并将其从项目中删除(来自info.plist)
用法:
final class AppDelegate: UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { #if DEBUG do { try R.validate() } catch { // fatalError // debug // - , production fatalError(error.localizedDescription) } #endif return true } }
现在您准备购买两个!

如何实施?
*基于组件的系统-Wiki ,一种代码开发概念,其中,组件(一组连接在一起的屏幕/模块)在封闭的环境中(在我们的情况下是在本地pod中)开发,以减少代码库的一致性。 许多人都知道后端的方法,该方法基于此概念-微服务。
* Monolith- Wiki ,即代码开发的概念,其中整个代码库都位于一个存储库中,并且代码紧密相关。 此概念适用于功能有限的小型项目。
如果您正在开发单一应用程序或仅使用第三方依赖项,那么您很幸运(但这并不准确)。 接受本教程并严格执行所有操作。
这不是我们的情况。 我们参与其中。 因为我们使用基于组件的系统,所以
将R.swift嵌入主应用程序之外,我们还决定将其嵌入本地pod(即组件)中。
由于本地化,图片和影响R.Generated.swift文件的所有元素的不断更新,合并到common分支时,生成的文件中存在许多冲突。 为了避免这种情况,您应该从git存储库中删除R.Generated.swift。 作者还建议这样做 。
.gitignore
添加到.gitignore
。
# R.Swift generated files *.generated.swift
另外,如果您不想为某些资源生成代码,则可以始终使用忽略单个文件或整个文件夹的方法:
"${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore"
.rswiftignore的描述
与在主项目中一样,对我们来说重要的是不要将R.Generated.swift文件从本地Pod添加到git存储库。 我们开始考虑如何实现此目的的选项:
- R.Generated.swift上的别名,以便将文件(别名,例如:R.swift)添加到项目中,然后在通过引用进行编译时,可以使用真实文件。 但是可可足类很聪明,不被允许这样做
- 在预编译阶段的podspec中,使用脚本将R.Generated.swift文件添加到项目本身中,但是之后它将简单地作为文件添加到文件系统中,并且该文件不会出现在项目中
- 其他或多或少的整洁选择
Podfile中的魔术
魔力
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods'
- 和另一个选项...仍然将R.Generated.swift添加到git
我们暂时选择了以下选项:“ Podfile中的魔术”,尽管它存在许多缺点:
- 它只能从项目根目录启动(尽管cocoapods可以从项目中的几乎任何文件夹启动)
- 所有Pod都应有一个名为Sources的文件夹(尽管如果Pod顺序正确,这并不重要)
- 他很陌生,难以理解,但迟早他得支持(这仍然是拐杖)
- 如果某个第三方库位于其路径中带有“ LocalPods”的文件夹中,则它将尝试在该文件夹中添加R.Generated.swift文件,否则它将因错误而崩溃
prepare_command
我生活了一段脚本和痛苦的时光,我决定更广泛地研究这个话题,并找到了另一种选择。
在Podspec中,有prepare_command ,仅用于创建和修改源,然后将其添加到项目中。
*新闻-吊舱的名称,必须替换为您本地吊舱的名称
* touch-命令创建文件。 参数是文件的相对路径(包括带有扩展名的文件的名称)
接下来,我们将使用News.podspec进行欺诈
该脚本称为第一次运行pod install
,并将我们需要的文件添加到炉膛中的源文件夹中。
Pod::Spec.new do |s|
接下来是另一个“假耳朵” –我们需要调用脚本R.swift来获取本地壁炉。
Pod::Spec.new do |s|
确实,有一个“但是”。 prepare_command
不适用于本地吊舱,或者说可以使用,但是在某些特殊情况下。 Github上对此主题进行了讨论 。
死亡人数
*死亡-Wiki,真人快打中的最终热门。
经过更多研究后,我找到了另一个解决方案-方法c prepare_command
和pre_install
的混合。
Podfile中的魔术的小修改:
pre_install do |installer|
和没有在本地壁炉运行的相同脚本
Pod::Spec.new do |s|
最后,它按预期工作。
终于!
PS:
我尝试制作另一个自定义命令,而不是prepare_command
,但pod lib lint
(用于验证podspec内容和炉膛本身的命令)发誓会产生额外的变量,并且不会通过。
非本地壁炉
在远程吊舱(每个吊舱都在各自的存储库中)中,您不需要所有上面描述的脚本魔术,因为在那里代码库严格依赖于依赖版本。
只需将Example(pod lib create <Name>命令之后生成的项目)嵌入R.swift脚本本身并将R.Generated.swift添加到库包中(底部)就足够了。 如果项目中没有Example,那么您将必须编写与我引用的脚本相似的脚本。
PS:
有一点澄清:
R.swift + Xcode 10 +新构建系统+增量构建!= <3
有关该问题的更多信息,请参见库的主页或此处
R.swift v4.0.0不适用于cocoapods 1.6.0 :(
我认为所有问题很快都会得到纠正。
结论
您始终需要保持尽可能高的质量标准。 这对于使用财务的应用程序尤其重要。
在这种情况下,您不需要过载测试并尽早发现错误。 在我们的情况下,这要么是在开发人员编译代码时,要么是在测试运行请求请求时。 因此,我们发现本地化的不足不是通过测试人员的细心观察或自动测试,而是通过构建应用程序的通常过程进行的。
您还需要考虑以下事实:这是与项目结构相关联并分析其内容的第三方工具。 如果项目文件的结构发生更改,则必须更改该工具。
我们冒了这个风险,在这种情况下,总是可以随时将此工具更改为任何其他工具或自行编写。
R.swift带来的收益是大量的工时,团队可以将其花费在更重要的事情上:新功能,新技术解决方案,质量改进等。 R.swift完全归还了整合所花费的时间,甚至考虑到将来可能用另一种类似的解决方案替换它。
斯威夫特
红利
您可以尝试使用一个示例,以立即亲眼看到为资源生成代码所带来的收益。 “可以玩”的项目的源代码: GitHub 。
非常感谢您阅读本文或只是浏览到这个地方,无论如何我都很高兴)
仅此而已。