使用代码为 Xcode 工程添加文件

2019 年 12 月 10 日

使用代码为 Xcode 工程添加文件

这几天在持续更新 DKNightVersion 这个仓库, 我对这个框架投入了很多的时间和精力.


我还是希望把这个框架的方方面面都做好的, 为框架的使用者提供最好的用户体验, 将开发者的使用成本和使用难度降到最低, 提升开发者的使用体验.


但是在我维护这个框架的过程中还是感觉到如何平衡框架的可扩展性和易用性实在是太难了. 为了减少开发者工作量以达到减少错误的目的, 我在这最近几乎每天 8 小时以上的与 xcodeproj 这个文件打交道, 把原来所需的工作尽可能的降低, 从原来的:


ruby generator.rb


拖拽 UIKit 文件到工作区的指定位置


选择指定的 Target


到现在的一个命令:


rake


我也付出了很大的努力, 虽然这个过程看起来非常的简单, 只是将原来的拖拽改变为使用代码来操作而已. 但是在实际的操作中, 还是极其麻烦的.


麻烦到什么程度呢? 在我每次觉得我要觉得我要成功的时候, 总是会遇到这样那样奇怪的错误或者错误给予我致命一击, 让一切努力都从零开始, 而这种情况在这几天处理这个问题的过程中遇到了 5,6 次.


我很多次都是想过要放弃实现这个功能, 不过到最后, 我还是完成了.


很大一部分原因是无法容忍作品中的不完美, 如果我没有想到它, 可能不会去做, 但是既然我想到了这个减少工作量的办法, 而且已经有先例(Cocoapods)完成了, 那么我也一定要实现这个功能.


而到现在, 我觉得需要写一篇博客记录一下这个过程, 既能够为后人带来一些启发也能少走一些弯路.


Xcode 做了什么


因为 DKNightVersion 使用了 Ruby 脚本来生成 Objective-C 代码, 而在代码生成之后, 文件虽然会出现在你文件夹之中, 但是并不会直接添加到你的工程文件中, 也就是编译器不知道你需要这些文件.


我们一般都是通过拖拽来把这些文件添加到工程中合适的位置, 让编译器 Xcode 知道: 我需要这些文件, 帮我”编译”它们. 这就是你在拖拽的过程中, Xcode 实际上做的事情.


在拖拽的过程中, 它实际上就是改变了一个神奇的文件 pbxproj, 这个文件在你的 xcodeproj 目录中, 你需要右键点击 xcodeproj 文件


然后会出现这样的菜单, 选中显示包内容


然后你就会看到一个 project.pbxproj 文件, 而你对文件进行的操作, 添加和删除实际上都是对这个文件的修改.


如果你长期使用命令行, 你就会非常自然的发现 *.xcodeproj *.xcodeworkspace 实际上都是文件夹, 但是后者并不是我们今天所要关心的问题.


我是怎么知道的呢, 我通过反复的尝试, 再删除一些文件的 refernece(没有将文件移到垃圾桶), 然后使用 git diff 查看所做的修改, 就会发现其他的文件并没有被修改, 唯一被修改的就是这个 project.pbxproj 文件.


在我们进行文件的增加和删除(不包括修改)的过程中, 实际上就是对 project.pbxproj 的修改.


向 Xcode 工程中添加或删除文件


如何在非 GUI 中完成对 Xcode 工程文件的操作呢? 我总共进行了三种不同的尝试.


手搓


作为一个闲的蛋疼专业的工程师, 我的第一想法是, 这需求肯定很多, 干脆造一个轮子好了, 于是我打开了 project.pbxproj 文件. 立刻被里面的数据所淹没了.


/* Begin PBXBuildFile section /0DFDC45E10D450EE9CA10394 / UITableViewCell+BackgroundColor.h in Headers / = {isa = PBXBuildFile; fileRef = A9EE3067770E740BB65F5B9D / UITableViewCell+BackgroundColor.h /; };186AC5221AF855410095ED95 / DKNightVersionManagerTest.m in Sources / = {isa = PBXBuildFile; fileRef = 186AC5211AF855410095ED95 / DKNightVersionManagerTest.m /; };18D2288D1AF8BA4F00872BF1 / UINavigationBar+Animation.m in Sources / = {isa = PBXBuildFile; fileRef = 18D2288C1AF8BA4F00872BF1 / UINavigationBar+Animation.m */; };


七百多行的文件, 我在这里只选取了三行. 因为这个文件中的数据实在是太多了. 你会看到每一行的前面大概都是一个 24 位的 16 进制数字 然后就是一个文件名 UITableViewCell+BackgroundColor.h 在最后有一个 isa 和一个 fileRef.


在大概浏览了一下这个文件的全部内容之后, 我对于手搓这个 script 有了一个初步的想法, 第一步就是生成这个 24 位的 16 进制数字. 当然我希望知道苹果是如何生成这一 16 进制数字的.


(注: 我实在无法对这个文件中的任何一部分进行详尽的解释, 不过之后我会介绍几个重要的相关概念)


然而我没有找到相关的资料, 而我自己也没有这个能力来推导出这个神奇的数字是如何生成的.


使用 Xcodeproj


Xcodeproj 是一个使用 Ruby 来创建和修改 Xcode 工程文件的工具. 我找到它的原因是 Cocoapods 也通过 Ruby 代码向 Xcode 工程中添加文件, 所以我在 Cocoapods 中找到了这一组件.


在最开始尝试使用这个工具的时候, 发现它的文档是极其糟糕. 根本无法直接在文档找到向 Xcode 工程中添加文件的方法, 这一简单的需求并没有明确的写在文档中, 这令我十分的不理解.


这个组件的主要功能不就是向工程中添加文件么? 为什么没有写在文档中, 而我所做的就是不断的阅读这个组件的源代码, 尝试理解 project.pbxproj 中的东西到底都是什么. 而在这期间, 有几个非常重要的问题需要我们理解.


Target


Group


FileRef


Target


Target 到底是什么, 我在以前的 iOS 开发的工作中并没有仔细地考虑这一个问题, 直到我遇到这一需求时, 我在尝试理解这背后的意义.


A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.


Target 指定了一个用于产品(product), 并且包含了从工程中的一些文件中构建产品的命令.


这些命令使用构建设置(build settings)和构建阶段(build phases)的方式来组织, 我们可以在 Xcode 编辑器中改变这些设置.


Group


Group 这个概念和我们平时经常说的 folder 文件夹有很大的差别, 文件夹在我们的日常使用时是一直所接触到的, 而对于 Group, 如果你不使用 Xcode 来编程(不是很清楚别的 IDE 是否有这个功能)的话, 这个概念距离你太远了.


Group 其实是 Xcode 中用来组织文件的一种方式, 它对文件系统没有任何影响, 无论你创建或者删除一个 Group, 都不会导致 folder 的增加或者移除. 当然如果在你删除时选择 Move to Trash 就是另外一说了.


在 Group 中的文件的关系, 不会与 folder 中的有什么冲突, 它只是 Xcode 为你提供的一种分离关注的方式而已. 但是, 我一般会在开发过程中将不同的模块分到不同的 Group 和 folder 中便于整理.


Group 之间的关系, 也是在 project.pbxproj 中定义的, 这个文件中包含了 Xcode 工程中所有 File 和 Group 的关系, 如果你大致浏览过这个文件的话, 你就会对我所说的有所了解.


Group 在我们的工程中就是黄色的文件夹, 而 Folder 是蓝色的文件夹(一般在 Xcode 工程中, 我们不会使用 Folder).


FileRef


FileRef 其实就是 File Reference 的缩写, 当你从 Xcode 中删除一个文件的时候, 它会弹出这样的提示框.


而其中的 Remove Reference 选项并不会将这个文件移除到垃圾桶, 而只是会将这个文件的引用从 Xcode 的工程文件中删除.


如果你曾经看过 Build Phases 中的内容, 你会发现


如果删除的是 .h 文件, 它会从 Build Phases 中的 Headers 部分删除


如果删除的是 .m 文件, 它会从 Build Phases 中的 Compile Source 部分删除


但是文件还是会在原来的地方, 因为 Xcode 中所加入到工程的只是文件的一个引用 — File Ref.


添加文件


我们已经基本了解了阅读这一篇博客所需要的全部知识, 接下来, 我们就需要来分析一下向 Xcode 工程中添加文件所需要的几个步骤.


当我们生成一堆 Objective-C 代码时, 我们的第一步是要将这些文件拖入 Xcode 工程中, 这时 Xcode 会弹出视图询问你是创建 Groups 还是 Folder references, 并询问你要加入到哪个 Target 中.


当选择创建 Groups 时, Xcode 就会把 .h 文件加入 Build Phases 中的 Header, 把 .m 文件加入 Compile Sources 中, 并创建一个黄色的文件夹.


当选择创建 Folder references 时, Xcode 会把所有的文件加入 Build Phases 中的 Copy Bundles Resources, 不进行任何的编译, 然后创建一个蓝色的文件夹.


我们现在就来使用代码来模拟将文件加入 Xcode 中, 选择 Create Groups 并且添加到指定 Target 的全过程.


在这里我们需要使用 Ruby 的开源框架 xcodeproj 这个框架是著名的开源框架 Cocoapods 的一个组件.


Create and modify Xcode projects from Ruby.


查找 *.xcodeproj 文件


lib/xcodeproj/project.rb


在使用 xcodeproj 时, 只需要查找 *.xcodeproj 文件而不是 *.pbxproj. 我们通过调用 Project 的类方法来打开文件.


project = Xcodeproj::Project.open(path)


获取 Target


lib/xcodeproj/project.rb


接下来要获取 Target, 模拟选择 Add to targets 这一操作, 在每一个 Project 中都有多个 Target.


一般在我们的项目中, 一般都有一个以 项目名 和一个以 项目名+Test 命名的 Target.


Tips:


在 Pod 项目中, 每一个 Pod 都会对应一个 Target, 然后会有一个 Pods-项目名 Target.


而在这里我想要获得的就是以项目名命名的 Target, 一般都是 targets 数组中的第一个.


target = project.targets.first


创建 Group


lib/xcodeproj/project/object/group.rb


在获取 Target 之后, 需要创建或者获取一个文件即将被添加进去的 group, 我一般使用 find_subpath 这个方法


def find_subpath(path, should_create = false)


它能比较快捷的根据路径名寻找 group, 如果当前的 group 不存在, 它还会递归地创建(可选).


group = project.main_group.find_subpath(File.join(‘DKNightVersion’, ‘Pod’, ‘Classes’, ‘UIKit’), true)


因为这个方法是一个 group 的实例方法, 所以先通过 main_group 获取主 group, 然后再调用这个方法, 最后会返回指定的 group. 在工程中创建这样一种的结构.


在成功获取之后还需要把 group 的 source_tree 设置成 ‘SOURCE_ROOT’, 这样在加入到 Build Phases 的时候, 它会从工程文件的根目录下开始寻找你所添加的文件, 不会出现一些非常奇怪的问题.


group.set_source_tree(‘SOURCE_ROOT’)


向 group 中添加文件


lib/xcodeproj/project/object/group.rb


我们在获取 group 之后就可以向其中添加文件了. 在这时使用 new_reference 方法, 为文件创建一个 FileRef 添加到 group 中.


file_ref = group.new_reference(file_path)


这样这个文件就添加到了 group 中, 会出现在工程中的导航栏中.


但是这个文件并没有被添加到 Build Phases 中, 无论你是编译还是作为资源来使用, Xcode 都会提示你无法找到这个文件. 我们还需要把这个文件加入到 Build Phases 中.


将文件加入 Build Phases


lib/xcodeproj/project/object/native_target.rb


在前面获取到的 Target 在这一步就开始起了作用, 我们需要获取 Target 的 Build Phase 并将在上面添加的文件添加到 Build Phase 中.


target.add_file_references([file_ref])


add_file_references 就负责把一组 FileRef 添加到对应的 Build Phases 中, source_build_phase headers_build_phase resource_build_phase framework_build_phase 在 GUI 中你可以找到这四者对应的 section.


而 add_file_references 方法自动为你把 FileRef 添加到合适的 phase 中.


但是从 Build Phase 中移除文件就需要手动获取这些 *_build_phase 然后从中调用 remove_reference 来删除文件或者资源.


target.source_build_phase.remove_file_reference(file_ref)


target.headers_build_phase.remove_file_refernece(file_ref)


保存 Project


在最后, 我们只需要调用 save 方法来保存整个工程就好了.


project.save


总结


project = Xcodeproj::Project.open(path)


target = project.targets.first


group = project.main_group.find_subpath(File.join(‘DKNightVersion’, ‘Pod’, ‘Classes’, ‘UIKit’), true)


group.set_source_tree(‘SOURCE_ROOT’)


file_ref = group.new_reference(file_path)


target.add_file_references([file_ref])


project.save


其实到现在为止, 我感觉到使用代码向 Xcodeproj 中添加文件是很简单的事情, 那是因为, 首先有 Xcodeproj 这样文档糟糕但是功能还是比较齐全的第三方框架, 而且这是我在几天不停地阅读源代码, 不停被坑, 一点一点尝试才摸索出来的结果, 都是泪啊…不想多说了…不过最后把这个问题解决之后, 自我感觉还是蛮好的还是挺高兴的. 嗯, 就这样.


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/bei-xcodeproj-keng-de-zhe-ji-tian


2019 年 12 月 10 日 17:5270

评论

发布
暂无评论
发现更多内容

技巧收藏|10个JavaScript常用数组操作方法

华为云开发者社区

Java 数组 开发

年轻人想详细了解做了十年Linux跟做了十年Windows的程序员差距有多大吗?听我慢慢道来!

ShenDu_Linux

Linux 程序员 windows

程序员的故事

Philips

敏捷开发 快速开发 原创小说 企业开发 企业应用

我在阿里巴巴做 Serverless 云研发平台

阿里巴巴云原生

Serverless 容器 开发者 云原生 CloudNative

腾讯T4Java大牛连续半个月每天熬夜到凌晨三四点,竟然就只是为了写一份MyBatis源码学习笔记

Java成神之路

Java 程序员 架构 面试 编程语言

赶紧收藏!Java大牛熬夜一周肝出的《Spring AOP/IOC源码笔记》

Java成神之路

Java 程序员 架构 面试 编程语言

值得学习!阿里P8架构师“墙裂”推荐:Java程序员必读的架构书籍

Java成神之路

Java 程序员 架构 面试 编程语言

性能压测

jorden wang

挑战赛 | 话题王者VS互动先锋(第一季)

InfoQ写作平台

话题讨论 活动专区

每周学点TARS——服务自定义命令

TARS基金会

c++ DevOps 后端 TARS

ONES 收购知名协作工具 Tower

万事ONES

团队协作 高效 研发管理工具 收购 资讯

给你一个亿的keys,Redis如何统计?

不才陈某

redis

即使不会node.js,拖拽就可完成数据的可视化展示

华为云开发者社区

node.js 数据 可视化

一个真正0基础小白学习前端开发的心路历程

华为云开发者社区

开发 开发小白 0基础

腾讯大牛整合Java+spring5系统学习架构,神乎其技

小Q

Java 学习 编程 面试 spring 5

已拿腾讯后台开发岗offer,简单说下自己的面试经历和学习路线

程序员小灰

c++ 后台开发 架构师 TCP/IP Linux服务器开发

讲述我在阿里六面的经历,幸好我掌握了这份“Java并发编程+面试题库”成功拿到20K的offer

比伯

Java 编程 架构 面试 计算机

「更高更快更稳」,看阿里巴巴如何修炼容器服务「内外功」

阿里巴巴云原生

容器 运维 云原生 双十一 CloudNative

ONES 收购 Tower,五源资本合伙人对话两位创始人

万事ONES

项目管理 团队协作 ONES Tower 收购

架构师训练营第二周框架设计课后练习

Geek_xq

京东T7架构师手写的10万字Spring Boot详细学习笔记+源码免费下载

Java成神之路

Java 程序员 架构 面试 编程语言

蘑菇街Java大牛纯手写熬夜肝出的《Spring MVC源码笔记》赶紧收藏

Java成神之路

Java 程序员 架构 面试 编程语言

训练营第七周作业

大脸猫

极客大学架构师训练营

《迅雷链精品课》第十课:共识算法理论基础

迅雷链

区块链

打造Django私有化缓存组件django-api-cache

pygodnet

django django-api-cache django缓存 私有化缓存 接口缓存

想了解任务型对话机器人,我们先从自然语言理解聊起

华为云开发者社区

人工智能 机器人 自然语言

接口测试怎么进行,如何做好接口测试

测试人生路

软件测试 接口测试

单机服务器模型,reactor的5种实现方式,只有这样才算了解了

linux亦有归途

c++ Linux reactor 后端开发

新闻|Babelfish使PostgreSQL直接兼容SQL Server应用程序

PostgreSQLChina

数据库 postgresql 开源

架构师训练营 1 期 -- 第十一周总结

曾彪彪

极客大学架构师训练营

《前端实战总结》之使用CSS3实现酷炫的3D旋转透视

徐小夕

css3 前端 前端工程 CSS小技巧

使用代码为 Xcode 工程添加文件-InfoQ