这篇文章是我从应用来理解R.Swift的笔记,主要想法是通过从R.Swift的引入方式,应用方式等来反向地了解R.Swift的几个方面
1.R.Swift做了什么使得我们能够以对象的方式调用资源库的数据。
2.R.generated是如何让我们通过对象来调用到真实数据的呢?
3.从1和2中我们可以了解到我们是通过R.generated来调用到对应的对象化的数据,那么,R.Swift又是如何在编辑阶段动态地根据项目中的资源库来生成以及更新R.generated文件的。
4.我们在知道R.generated的生成与更新的原理后需要更加深入地理解,R.Swift的整体结构
1.R.Swift做了什么使得我们能够以对象的方式调用资源库的数据
我们知道,当我们引入R.Swift框架时通常有数个步骤要执行。
以cocospod引入为例,第一步,将R.Swift记录在pod文件中并install
。
第二步,在Targers
中选中Build Phases
目录并通过左上方的加号按钮选择创建一个编译时运行脚本。
第三步,将编译时运行脚本拖曳到Compile Sources
的上方并在Check Pods Manifest.lock
的下方,然后加入相应的脚本运行命令。
第四步,将$TEMP_DIR/rswift-lastrun
命令加入到Input Files
中,并将$SRCROOT/R.generated.swift
命令加入到Output Files
中,从上面两句命令我们大致可以判断出来这是指定输入文件及输出文件的目录,分别对应第三步中的相关脚本。
第五步,拖曳生成的R.generated.swift
到项目中,并且,记得不要修改其实际物理地址。
尔后,当这五步操作完成时,我们可以发现,当我们将R.Swift支持的资源或是创建或是拖曳进项目时,经过一次编译就可以正常地使用对象去调用到实际资源。
这实际上就表明了R.Swift通过每次的编译阶段运行第三步指定的脚本工具获取项目下的资源信息,将其根据自身的源码进行编译,最后输出能让我们调用对象化资源的代码到R.generated.swift
中,由此,我们就可以在每次编译完成后获取R.Swift
更新的对象化资源。
这也从一个侧面描述了R.Swift
的作者在仓库页面所说的R.Swift
倡导的对象化资源的安全性,因为,根据逻辑而言,我们的代码在运行前都要经过编译阶段,而如果在使用对象化的资源时对象化资源所指定的真实资源发生了改变,那么在编译过程中R.generated.swift
产生了变化,此时Xcode本身的特性便会让编译阶段终止并报告错误,由此防止了运行阶段资源引用错误导致的不稳定状况。
2.R.generated是如何让我们通过对象来调用到真实数据的呢?
我们用两个常见的资源来看
R.image.sexygirl()
R.storyboard.main.second()
资源一是我拖曳进初始项目的一张漂亮妹子的图片,资源二是在MainStroyboard
中加入的一个UIViewController
,我设置其StoryboardID
为second
。
R.Swift
正是通过StoryboardID
来分类Stroybaord
中的资源数据的。
打开R.generated
文件,你可以看到如下代码:
/// This `R.image` struct is generated, and contains static references to 1 images.
struct image {
//R.image.sexygirl图像资源
static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
/// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
}
fileprivate init() {}
}
//R.storyboard.main对象
struct main: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable {
typealias InitialController = ViewController
let bundle = R.hostingBundle
let name = "Main"
let second = StoryboardViewControllerResource(identifier: "Second")
func second(_: Void = ()) -> UIKit.UIViewController? {
return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: second)
}
static func validate() throws {
if #available(iOS 11.0, *) {
}
if _R.storyboard.main().second() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'second' could not be loaded from storyboard 'Main' as 'UIKit.UIViewController'.") }
}
fileprivate init() {}
}
我们先来看sexygirl
资源,这个图像资源被分配image
结构体中,其实现了一个属性及一个函数
/// Image `sexygirl`.
static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
/// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
}
我们可以看到其在函数中调用了UIImage函数,入参是上面构造的属性R.image.sexygirl
,而点击这个关于UIImage
的初始化函数我们会跳转到R.swift.library
的相关文件中,表明这是一个UIImage
的扩展,在扩展中实现的初始化函数内部调用了UIImage
的真实初始化函数,通过imageName
和Boundle
获取到指定的资源。
而对应的imageName
和Boundle
是怎么来的呢?我们可以看到sexygirl
属性,其本身在定义时就指定了imageName
和Boundle
,也就是说,真实资源的指向信息在编译阶段由脚本向R.generated
完成插入。
而再看R.storyboard.main.second()
我们可以发现,R.storyboard
结构体中只实现了指向真实main
资源的函数,而其用来获取真实资源的R.storyboard.main
实际上是一个_R.storyboard.main
的对象,而当我们跳转到_R.storyboard.main
时才能看到关于second
的实现部分。
而同样的,我们可以发现,在second
的实现函数中同样是使用了预先获取好的identifier
调用UIStoryboard
获取对应标记控制器的系统接口来获取到真实资源。
所以到这一步,我们就明白了,``R.generated的工作主要就是在编译脚本运行完成后提供自动生成的对象化资源的相关接口,通过将接口以及早已在编译阶段通过脚本获取到的关于真实资源的信息相结合,形成了我们所看到的R.Swift的表象。而
R.swift.library的作用正是预先为
R.generated提供所需的扩展,在
R.generated新建或更新完成时使用。 这就是
R.Swift的表象,之所以说是表象,是因为我们真正想见到的关于
R.Swift的如何利用脚本去获取一个项目下的所有资源信息以及脚本是如何编写整个
R.generated的代码并最终生成
R.generated的文件的,这些
R.Swift的内核都藏在
R.Swift`的脚本中。
3.从1和2中我们可以了解到我们是通过R.generated来调用到对应的对象化的数据,那么,R.Swift又是如何在编辑阶段动态地根据项目中的资源库来生成以及更新R.generated文件的。
我们可以从R.Swift的git仓库直接下载zip包来获取源码,需要注意的是,git上的源码需要使用Swift Package 包管理系统进行配置,而配置完成后我们可以通过生成xocdeproj文件来查看代码,关于如何通过Swift Page Manager进行配置,git仓库有如下说明:
Building from source
R.swift is built using Swift Package Manager (SPM).
Check out the code
Run swift build -c release from the root directory
Follow the manual installation steps with the binary you now have
For developing on R.swift in Xcode, run swift package generate-xcodeproj --xcconfig-overrides RswiftConfig.xcconfig.
在配置完成后我们也可以通过在Build Phases
加入编译时脚本来使用源码生成R.generated.swift
文件,我试了几次,成功生成了文件,其中需要注意的有两点:
一、脚本不能直接拷贝git仓库的脚本命令,比如"$SRCROOT/rswift" generate "$SRCROOT/R.generated.swift"
,因为我们下载下来的源码的rswift不在根目录下,所以要修改地址到对应的目录下。
二、源码包中没有Project Bundle Identifier,需要在Build Setting中配置。
其他的我在生成R.generated.swift
中没有碰到什么困难。
在源码中生成R.generated.swift
仅是让人理解一下R.Swift使用源码生成的过程,实际要对源码有更多更深的理解我们还是直接看main.swift文件吧。
在进入main.swit文件我们会看到百多行的代码,可能直接从上往下看会让人不清不楚,所以我们先对头部的import
文件和尾部的运行代码先进行粗浅的理解。
...head
import Foundation
import Commander
import RswiftCore
import XcodeEdit
...
...end
let group = Group()
group.addCommand("generate", "Generates R.generated.swift file", generate)
group.run(Rswift.version)
...
我们从头部引用文件里可以看到有两个特别的引用文件,一个是Commander
,另一个是XcodeEdit
,这两个框架便是R.Swift
源码通过SPM引入的框架,简而言之,Commander
的作用是让我们在Swift中以其提供的相关接口快速方便地构造命令行接口。
而XcodeEdit
的功能对Xcode project.pbxproj
进行读写,而该框架在R.Swift
中的使用绝大多数的应用便是获取一个项目下的资源信息为R.Swift
自动化构造提供相应的助力。
接着看尾部代码。
Group
是Commander
中的类,其主要功能就是作为命令行接口的容器。
第二行代码,它往group中插入了一个命令,命令的key是generate
,备注是Generates R.generated.swift file
,实际上塞入了一个对象generate
。
我们可以拿R.swift
的编译脚本进行对比:
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"
所以我们大约能猜出编译脚本中的generate
实际上估摸就是在执行这个源码的相关命令。
我们接着往下看,第三行代码的run
函数很明显就是在执行第二行的代码。
这个时候我们可以回头去找第二行的代码中的generate
对象了。
往回回滚代码找到generate
。
最外壳的代码大致就是这个样子的:
let generate = command(
CommanderOptions.uiTest,
CommanderOptions.importModules,
CommanderOptions.accessLevel,
CommanderOptions.rswiftIgnore,
CommanderOptions.inputOutputFilesValidation,
CommanderArguments.outputPath
) { uiTestOutputPath, importModules, accessLevel, rswiftIgnore, inputOutputFilesValidation, outputPath in
......
}
最外壳的代码没什么深究的意义,如果你对Commander
这个框架有所理解,实际上CommanderOptions.uiTest
都是包装的命令可选参数,而在R.Swift
中这里使用到的参数都带有默认值,在我们仅使用generate
命令而不带可选参数时默认实现。
具体的这些参数的意义因为个人认为不牵扯到核心代码所以暂且略过。
在略过外壳代码后我们看到的是generate
对象的内部回调,在这里我们首先看到的是诸如
let processInfo = ProcessInfo()
// Touch last run file
do {
let tempDirPath = try ProcessInfo().environmentVariable(name: EnvironmentKeys.tempDir)
let lastRunFile = URL(fileURLWithPath: tempDirPath).appendingPathComponent(Rswift.lastRunFile)
try Date().description.write(to: lastRunFile, atomically: true, encoding: .utf8)
} catch {
warn("Failed to write out to '\(Rswift.lastRunFile)', this might cause Xcode to not run the R.swift build phase: \(error)")
}
let xcodeprojPath = try processInfo.environmentVariable(name: EnvironmentKeys.xcodeproj)
let targetName = try processInfo.environmentVariable(name: EnvironmentKeys.target)
let bundleIdentifier = try processInfo.environmentVariable(name: EnvironmentKeys.bundleIdentifier)
let productModuleName = try processInfo.environmentVariable(name: EnvironmentKeys.productModuleName)
这样的代码,直到try RswiftCore(callInformation).run()
之前都属于同一性质的代码,即预先为R.Swift
的核心代码配置前提数据。
我们同样可以忽略这部分代码,只要知道几个核心要素就行了,
其一是,这部分代码我们可以看到有相当数量的局部不可变对象都是通过ProcessInfo
这个获取当前进程相关信息的对象进行配置的,因此ProcessInfo
可以说是R.Swift
获取资源信息列表的前提。
其二是,另一部分预先配置对象都是通过我们之前看到的外壳入参的相关对象进行配置,这也说明在我们直接调用generate
命令时,外壳入参的可选参数的默认配置对内部的逻辑也起到一定作用。
但也正如我们之前所说的,这部分代码在为R.Swift
需求的资源列表信息提供预先准备的前提信息配置,就逻辑而言也是十分重要的,毕竟没有这部分信息,R.Swift
真实需求的资源列表信息可能就无从谈起了,但其可以说都没有涉及到R.Swift
的核心要素,因此,我们继续往下看。
try RswiftCore(callInformation).run()
callInformation
对象正是R.Swift
在进行自动化生产时需要的最终关于所有前提信息的封装容器。
进入到RswiftCore
的内部代码我们可以看到,在初始化时RswiftCore
并没有再对callInformation
进行数据再重构,所有的逻辑代码都存在于run
函数中。
在研究run
函数的内部结构时我是这么对其进行分割的,首先,我注意到了fileContents
这个对象以及紧随其中的writeIfChanged(contents: fileContents, toURL: callInformation.outputURL)
,这段代码很明显是在向一个URL地址里写入fileContents
对象,而通过options
查看fileContents
,我们可以发现这就是一个字符串,在看到这个字符串的时候我们就明白了,fileContents
已经代表了R.Swift
的最终成果,它将所有的资源信息都转译成我们在R.generate.swift
中看到的结构体,并将结构体转译成了带有格式的字符串,然后将字符串直接写入到命名为R.generate.swift
的文件中,使其按照字符串的格式排列。
那么用来生成fileContents
的generateRegularFileContents
函数自然就带有将所有资源信息生成的结构体进行拼装的职责。
先暂且不查看generateRegularFileContents
的内部结构,我们继续往上看,既然fileContents
已经是最终结果了,那上面寥寥数个局部不可变对象具有什么样的职责呢?
第一个xcodeproj
是通过url地址进行初始化,查看内部的初始化代码,我们很简单就可以理解其实际功能是利用XcodeEdit
框架获取到XCProjectFile
的实例对象。
接着看ignoreFile
,忽略掉它,顾名思义,就是忽略文件,在熟悉.gitnore
的我们眼里,它不过是另一种规则文件让R.Swift
在编译过程中忽略掉一些资源。
然后是resourceURLs
,文件名已经很明确地告诉我们了,这个对象数组就是我们需要的资源信息列表,而通过option
查看,我们也很直观就能看到这是一个URL
类型的数组。
那这个数组是如何生成的呢,我们首先进入xcodeproj.resourcePathsForTarget
中,它的第一部分代码通过传入的targetName
来确定一个projectFile
下哪个是最终所需要的target
。而这个targetName
正是我们在往一个项目中的target
的Build phases
时确定下来的。
接着找到target
的buildPhases
,通过buildPhases
来获取资源信息列表,因为这部分的逻辑都是存在于XcodeEdit
这个框架中,如果要深入了解的话必须对XcodeEdit
的源码做出分析,因此这里只是简单的说明一下,一个项目中的Build Phases
本身就存在资源信息列表的展示,你可以打开手头上随意一个项目进行,在Build Phases
下的Copy Bundle Resources
下就是项目的资源列表,我们可以简单地认为XcodeEdit
就是获取到了这个条目下的资源列表转化为相应的信息交给我们,当然,关于如何获取到资源信息实际上牵扯到很多东西,要深入了解这块东西就必须要对XcodeEdit
的源码有所了解了。
总而言之,这一步都是在调用XcodeEdit
的函数,最终获取到所需的资源信息列表,返传[Path]
对象,并最终重组为[URL]
。
现在,我们持有所有资源的URL地址了,但是只有URL地址是不够的,R.Swift
所支持的文件类型有nib、storyboard、图片、字体等等,我们需要从URL地址中识别该URL地址是哪一类的资源并将其分类到正确的资源数组中,有时,例如nib和storyboard类型的资源文件,我们往往还要分离出更多所需信息,比如nib中存在多少个view的nib文件,storyboard中有多少个控制器的nib,以及它们指向真实控制器的唯一标示符等等等等。
要想完成这一步,就要看resources
实例对象的初始化了。
Resources
的初始化正是将前一行代码获得的resourceURLs
进行系统化地分类。
进入到Resources
的初始化函数,我们可以看到其核心代码是遍历URL地址,并通过try特性尝试性地将url地址塞入对应的结构体初始化函数中,塞入成功则将其纳入对应的资源数组中。
这么说就太简单了,我们实际地来看使用URL地址对对应的结构体进行初始化都做了什么吧。
因为R.Swift
解析的类型数据有近十种,限于篇幅,这里主要是归纳了Nib、Storybaord、AssetFolder
这三种我们平时使用R.Swift
时最长接触的类型数据。
首先是Nib
结构体的尝试初始化:
init(url: URL) throws {
try Nib.throwIfUnsupportedExtension(url.pathExtension)
guard let filename = url.filename else {
throw ResourceParsingError.parsingFailed("Couldn't extract filename from URL: \(url)")
}
name = filename
guard let parser = XMLParser(contentsOf: url) else {
throw ResourceParsingError.parsingFailed("Couldn't load file at: '\(url)'")
}
let parserDelegate = NibParserDelegate()
parser.delegate = parserDelegate
guard parser.parse() else {
throw ResourceParsingError.parsingFailed("Invalid XML in file at: '\(url)'")
}
rootViews = parserDelegate.rootViews
reusables = parserDelegate.reusables
usedImageIdentifiers = parserDelegate.usedImageIdentifiers
usedColorResources = parserDelegate.usedColorReferences
usedAccessibilityIdentifiers = parserDelegate.usedAccessibilityIdentifiers
}
这部分的结构体初始化函数的前几行的代码逻辑都大致相同,查看是否能获取到文件名,如果是需要进行结构解析的对象的话尝试性根据URL地址初始化XML解析器。
而Nib
结构体在解析器初始化后将解析器的代理交给NibParserDelegate
对象开始解析,然后NibParserDelegate
在解析成功后再将相应的解析完成数据交给Nib
。
我们具体来看下NibParserDelegate
如何完成XML解析的。
要仔细了解R.Swift对Nib
文件的解析,我们自然需要创建一个Nib
文件来对照参考,你可以随意地在一个新项目中创建一个Nib
文件,设置其中的View
以及更多的视图并右键Nib
文件选择Open as Source Code
来查看其真实的XML格式数据。
例如:
因为一个XML文件的代码过于繁多,我这里只截取了其中的Objects
元素中的相关代码,该XML代码描述了一个属于TestView
类的视图,其中,有一个居中的子View。
我们再来看看NibParserDelegate
实现的XML解析代理中做了什么。
我们可以直接阅览NibParserDelegate
关于XMLParserDelegate
的@objc func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String])
的相关实现。
//第一部分
if isObjectsTagOpened {
levelSinceObjectsTagOpened += 1
}
if elementName == "objects" {
isObjectsTagOpened = true
}
若已经检测到元素名为Objects
的元素,则设置isObjectsTagOpened
为true
,代表需要解析的核心内容已经出现了,而当isObjectsTagOpened
为true
时,每次解析到元素都将levelSinceObjectsTagOpened
进行自增,这个值代表目前解析到的元素的深度,也可称作层级。
而与didStartElement
代理回调相对应的didEndElement
回调,会对上面的相关布尔值和层级值进行反置代表Objects
元素解析完成。
接着,对每一个元素名进行解析,如果是image、color、accessibility、userDefinedRuntimeAttribute
这数种类型,则直接从元素的内容字典中获取需要的value填入归纳数组中。
case "image":
if let imageIdentifier = attributeDict["name"] {
usedImageIdentifiers.append(imageIdentifier)
}
case "color":
if let colorName = attributeDict["name"] {
usedColorReferences.append(colorName)
}
case "accessibility":
if let accessibilityIdentifier = attributeDict["identifier"] {
usedAccessibilityIdentifiers.append(accessibilityIdentifier)
}
case "userDefinedRuntimeAttribute":
if let accessibilityIdentifier = attributeDict["value"], "accessibilityIdentifier" == attributeDict["keyPath"] && "string" == attributeDict["type"] {
usedAccessibilityIdentifiers.append(accessibilityIdentifier)
}
而除此之外的元素都通过viewWithAttributes
函数尝试性地进行解析,以找到Nib
文件中的rootView
。
而viewWithAttributes
内部实现了对attributeDict
的应用并尝试性获取customModuleProvider、customModule、customClass
三个字符串,若能获取到则根据规则将customClass
转化为SwiftIdentifier
对象,该对象负责将customClass
字符串转为为规定格式的字符并存储。之后SwiftIdentifier
作为参数被纳入Type
对象中,与之相同的还有被存储到Module
中的customModule
字符串,最终Type
对象作为最终存储单元输出。
这一步主要的工作就是找到一个当我们在调用R.nib.xxx
时应该被调到的对象的原始文本形态并将其转化为R.Swift
在输出R.generated.Swift
时需要的数据格式。
在看完Nib
的解析后,我们接下来看同样重要的Storyboard
。
跳过解析前与Nib
解析相类似的步骤,我们直接来看didStartElement
回调函数。
case "document":
if let initialViewController = attributeDict["initialViewController"] {
initialViewControllerIdentifier = initialViewController
}
首先,当元素为document
时记录初始控制器的标记符,这个属性会在之后获取到控制器时逐一对照来找到初始控制器。
接着,我们先跳过segue
属性,先看控制器的序列化。
控制器的序列化同样在swicth
的default
中进行,控制器通过viewControllerFromAttributes
函数进行序列化,大部分逻辑都与Nib
相同,但控制器的XML比Nib
要复杂的多,这里在进行序列化的时候也多了几个关键参数。
其一是id
,id
参数是这个控制器在Storyboard中的唯一标识符,我们上面有提到,在解析到document
时会记录一个初始控制器标识符的参数initialViewControllerIdentifier
,这个参数就是解析控制器XML时获得的id
。
其二是storyboardIdentifier
,回想一下使用R.Swift
的过程,我们是不是需要在Storyboard
中设置控制器的Storyboard ID
属性才能通过R.Swift
调到该对象,storyboardIdentifier
正是Storyboard ID
,记录这个属性至关重要。
最后,连带上述两个参数和Type
属性,初始化Storyboard.ViewController
并返传,一个控制器的解析也就完成了。
但这里的任务还未结束。
回到解析的代理回调,我们可以看到Storyboard
的解析与Nib
不同的地方是,没有直接将解析完成的数据传入全局数组中保存,而是用一个全局属性currentViewController
引用它。
因为,我们还需要这个解析完成的结构体去容纳其他数据。
现在,我们回到Segue
的解析。
在查看Segue
的解析代码,我们很容易就发现除了例行的Type
转化外还提取了segueIdentifier、destination、kind
三个属性,即使不常用Segue
也能理解这三个参数分别代表Segue
的唯一标示符、跳转目标的唯一标示符(即上面解析的id
)和跳转效果。
这三个参数会和Type
一起作为参数初始化Storyboard.Segue
,并将初始化的对象加入到currentViewController
的segue
数组中存储。
这样就完成了一个控制器XML的全部解析,并将XML中本来就位于控制器的XML代码内部的Segue
代码正确地插入到控制器结构体中。
最终,所有归纳完成的控制器都存储到Storyboard
结构体的属性数组中,返还Storyboard
结构体。
最后,我们来看一下存储了一般项目大部分资源的AssetFolder
的序列化。
首先回忆一下我们日常对Assets
的使用,我们通常会存储大量的图片资金到该资源夹下,iOS11增加了一些特性使得我们也可以存储颜色资源到资源夹下。并且,为了防止中、大型项目图片资源过多而Assets
资源夹难以阅读的问题,我们同样可以在Assets
中创建子文件夹。
而R.swift
的序列化同样也对标了三种URL
数组
var imageAssetURLs = [URL]()
var colorAssetURLs = [URL]()
var namespaces = [URL]()
在通过FileManager
对Assets
进行解析时首先将数据进行分类。
但需要注意的是,R.Swift
需求的数据可不是URL类型,即使是将URL转化为字符串也无法满足,因为,我们仅仅需要图片的名字,它前面一长串的路径地址是不需要的,就如我们调用UIImage.name()
时也不需要路径一样。
在对URL地址进行归纳时我们可以注意一下:
if imageExtensions.contains(pathExtension) {
imageAssetURLs.append(fileURL)
}
if colorExtensions.contains(pathExtension) {
colorAssetURLs.append(fileURL)
}
这几行代码。
通过查看imageExtensions
我们可以发现它是存储字符串的Set
,但Set
存储的却不是我们下意识以为的png、jpg
,因为实际上我们放入资源文件夹的资源都会被创建一个imageExtensions
中的后缀名的文件夹,例如图片资源的2x、3x图会被存储到同一名字的.imageset
文件夹下方便管理。
在完成URL分类后开始进行资源夹解析。
首先,进行子资源夹的解析,将URL类型的路径数组转化为NamespacedAssetSubfolder
对象数组,遍历调用populateSubfolders
函数。
该函数的(似乎)实际功能是为了将URL的路径层级转化成数据的层级,并且,对资源文件夹path
和resourcePath
,利用NamespacedAssetSubfolder
初始化时通过URL获取的相关数据进行字符串组合。(这段代码我不是看的很清楚,主要原因是这里的代码看似是在进行层级划分,但之后的生成Swift
代码文本环节没有看到有根据层级划分做什么,也可能是我看漏了。)
在完成资源夹的层级划分,使用同样的手段对图片资源和颜色资源进行解析,就如我们调用UIImage.name()
函数时无需后缀名一样,在解析图片资源时也不需要深入到.imageset
内部对2x、3x图片进行解析,只要直接将.imageset
去掉后缀名就可以了。解析完成后将资源全部存储到统一的资源数组中。
Assets
的解析工作就完成了。
其他的类型也通过类似的手段进行解析。
当所有的URL都解析完成时,接下来就是如何将所有的解析数据转化为Swift
代码格式的字符串了。
let fileContents = generateRegularFileContents(resources: resources, generators: [
ImageStructGenerator(assetFolders: resources.assetFolders, images: resources.images),
ColorStructGenerator(assetFolders: resources.assetFolders),
FontStructGenerator(fonts: resources.fonts),
SegueStructGenerator(storyboards: resources.storyboards),
StoryboardStructGenerator(storyboards: resources.storyboards),
NibStructGenerator(nibs: resources.nibs),
ReuseIdentifierStructGenerator(reusables: resources.reusables),
ResourceFileStructGenerator(resourceFiles: resources.resourceFiles),
StringsStructGenerator(localizableStrings: resources.localizableStrings),
AccessibilityIdentifierStructGenerator(nibs: resources.nibs, storyboards: resources.storyboards),
])
我们首先来看看generateRegularFileContents
函数的入参,其第一入参是上一步解析完成的总resources
,这个参数基本是备用性质,函数内部实现我都没见哪里用到……
接着是一个[StructGenerator]
类型的数组,StructGenerator
是一个协议,只要实现这个协议的对象都可以插入数组,所以我们看到了上面一长串的类似ImageStructGenerator
初始化函数的大量StructGenerator
初始化函数,其统一特征都是将上一步解析完成,填入Resources
内部属性数组的数据分别利用,就像ImageStructGenerator
的初始化就是利用resources.assetFolders
和resources.images
,前者通过解析Assets
获得,后者则是通过解析分散在项目各处的没有被添加入Assets
资源夹中的图片文件得到。
ImageStructGenerator
内部结构相对清晰明了,其在初始化的时候没有仅将入参作为全局属性保存,另一个至关重要的函数是generatedStruct
,该函数作为协议声明的函数,在generateRegularFileContents
函数被统一调用。
在阅览每个遵循StructGenerator
协议的结构体的generatedStruct
函数内部实现时,我建议利用R.generated.swift
达到更好理解的作用,例如,在查看ImageStructGenerator
的相关实现时,拉几张图片到项目中,编译后查看R.generated.swift
的Image
结构体相关代码,基本上就能很清楚ImageStructGenerator
的每一步做了什么了。
当ImageStructGenerator
的该函数被调用时,首先设置了文本名为image
,这个文本名对应了R.generated.swift
的image
结构体,接着,从初始化时存储的AssetFolder
和Image
数组中获取到所有的图片名并重组成同一的字符串数组。
let structName: SwiftIdentifier = "image"
let qualifiedName = prefix + structName
let assetFolderImageNames = assetFolders
.flatMap { $0.imageAssets }
let imagesNames = images
.grouped { $0.name }
.values
.compactMap { $0.first?.name }
let allFunctions = assetFolderImageNames + imagesNames
接着,浏览R.generated.swift
,我们可以发现一个image
结构内部组成总的来说有两部分,一部分是真实资源的静态全局变量
static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
另一部分是我们拿到真实数据的静态函数,通过遍历项目获得的图片名来调用对应的真实图片资源。
/// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
}
以此推论,ImageStructGenerator
内部应该有负责将这两块内容转化成Swift
代码文本的相关模块。
这两块的主体实现就是imageLets
和groupedFunctions.uniques.map { imageFunction(for: $0, at: externalAccessLevel, prefix: qualifiedName)
两块数据,前者生成了一个[Let]
结构体数组,后者生成了一个Function
结构体数组。
两者的数据来源都来自于groupedFunctions.unique
这个[String]
数组,而groupedFunctions
则是通过第一步我们获取的所有图片资源的字符串数组allFunctions
组装而来。
Let、Function、Struct
三种结构体的定义区别其实不大,前两者更为简单一些,而后者更复杂,因为通常情况后者是作为前两者的容器存在。
三种结构体都遵循并实现了SwiftCodeConverible
协议,该协议仅提供一个swiftCode
属性,结构体在实现该属性时即将自身所有的属性都转化为文本格式作为当外界调用swiftCode
时的输出字符。
我们可以对照来看一个Let
结构体的swiftCode
实现及其在R.generated.swift
中的代码。
Let(
comments: ["Image `\(name)`."],
accessModifier: externalAccessLevel,
isStatic: true,
name: SwiftIdentifier(name: name),
typeDefinition: .inferred(Type.ImageResource),
value: "Rswift.ImageResource(bundle: R.hostingBundle, name: \"\(name)\")"
)
-————————————————————————————————————————————————————
var swiftCode: String {
let commentsString = comments.map { "/// \($0)\n" }.joined(separator: "")
let accessModifierString = accessModifier.swiftCode
let staticString = isStatic ? "static " : ""
let typeString: String
switch typeDefinition {
case let .specified(type): typeString = ": \(type)"
case .inferred: typeString = ""
}
return "\(commentsString)\(accessModifierString)\(staticString)let \(name)\(typeString) = \(value)"
}
/// Image `sexygirl`.
static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
注释对应comments
,static
对应isStatic
布尔值,sexygirl
对应name
,调用Rswift.ImageResource
的代码对应了value
,这就是一个Let
结构体的全貌了。
而Function
和Struct
也是大致如此,只不过更加复杂一些。
现在我们知道ImageStructGenerator
通过所有的图片资源最终输出了一个已经包含R.image
所有实现文本的Struct
结构体了,接下来我们来看看generateRegularFileContents
如何利用各个结构体输出文本。
进入generateRegularFileContents
函数内部,第一行就是
let aggregatedResult = AggregatedStructGenerator(subgenerators: generators)
.generatedStructs(at: callInformation.accessLevel, prefix: "")
aggregatedResult
是一个包装过的元组,其一称作externalStruct
,其二称作internalStruct
,暂且记下名称的意义,我们进入AggregatedStructGenerator
内部。
这个类的意义是输出包裹所有我们之前看到被传入函数内部的结构体的结构体,generatedStructs
函数开头三行的R
和_R
两个静态文本很清晰地向我们揭示了这一点。
let structName: SwiftIdentifier = "R"
let qualifiedName = structName
let internalStructName: SwiftIdentifier = "_R"
而这两个字符分别定义了外部结构体和内部结构体的名称,即externalStruct
和internalStruct
。
那么如何划分内部结构体和外部结构体呢?
let collectedResult = subgenerators
.compactMap {
let result = $0.generatedStructs(at: externalAccessLevel, prefix: qualifiedName)
if result.externalStruct.isEmpty { return nil }
if let internalStruct = result.internalStruct, internalStruct.isEmpty { return nil }
return result
}
.reduce(StructGeneratorResultCollector()) { collector, result in collector.appending(result) }
首先,我们对subgenerators
进行遍历重组,在compactMap
高阶函数中调用每个StructGenerator
的generatedStructs
函数,通过该函数获得每个子类的Struct
输出,而我们之前浏览的ImageStructGenerator
中并没有该函数的实现,那是因为ImageStructGenerator
声明的协议是ExternalOnlyStructGenerator
,代表其不存在内部结构体,而该协议重实现了StructGenerator
协议的generatedStructs
函数,使其调用遵循该协议的对象的generatedStruct
函数(注意,没有s)。
这个时候,如果是StoryBoard
类型的StructGenerator
则会输出内外两个结构体,而Image
类型的则只输出外结构体。
在拿到所有的子类的Struct
输出时,再对元组result
使用reduce
高阶函数,将所有结构体存储到StructGeneratorResultCollector
结构体中,由此,完成整体的分类。最终,再定义内外两个结构体,分别容纳StructGeneratorResultCollector
中的相关数据并输出。(这一步为什么要再使用StructGeneratorResultCollector
对元组数据进行分类我并不是很理解)
let codeConvertibles: [SwiftCodeConverible?] = [
HeaderPrinter(),
ImportPrinter(
modules: callInformation.imports,
extractFrom: [externalStruct, internalStruct],
exclude: [Module.custom(name: callInformation.productModuleName)]
),
externalStruct,
internalStruct
]
最后,将所有数据转为[SwiftCodeConverible]
数组(该数组如果还有印象的话就知道SwiftCodeConverible
声明了swiftCode
属性,而所有的Let、Function、Struct
都实现了该属性输出文本),拼装成唯一的字符串,输出成为R.generated.swift
文件。
由此,整个R.swift
的脚本任务就完成。