利用xcodeproj给主工程添加子工程

现在,ccocoapods已经成为iOS工程的标配,在这个工具的开发过程中,开源了一个专门用来操作工程的.xcodeproj文件的ruby库Xcodeproj,利用它,我们自己也可以用ruby脚本来添加和删除工程中的文件等,做到自动化操作

问题的提出

在我们的组件化过程中,是通过子工程的方式来建立业务组件的.可能有人会问,为什么不用pod来建立业务组件呢?其实当时也有考虑过,pod更适合已经比较成熟的组件,而我们现在的业务变动还很大,并且pod在开发的过程中,新增文件什么的,还要运行下pod install才能运行,综合考虑,在业务早期,还是使用子工程的方式更便捷,能取得各方面的权衡

当我们采用子工程来建立业务组件,那么通常建立了一个模板化的组件工程(可以通过多种方式建立,此处不述了)后,还要做4件事,才能添加到主工程中,如下图所示:


image.png
  1. 拖动工程到主工程中
  2. 设置Target Dependencies,因为每个组件工程有个资源bundle的target,如果不设置依赖,当他们改动时候,主工程并不会去编译它们.
  3. 设置Link Binary With Libraries
  4. 拷贝资源

虽然事情不到,总归还是觉得建立组件和将组件添加到工程,是割裂的,难免有遗憾.
我们每天都使用的cocoapods,就是一个脚本就建立好了工程和设置完成了依赖,于是就想着用ruby借助xcodeproj库来将建立工程和设置结合起来.

xcodeproj介绍

Xcodeproj是cocoapods团队在写cocoapods过程中开源出来的库,它的工程代码赏心悦目,结构化程度很高.并且还提供了很多单元测试.不过遗憾的是,它没有个详细的使用文档,加上使用了在国内比较小众的ruby语言编写的,所以使用起来,还是颇费一番周折的.

网上有不少中文的使用教程,它们都是简单的添加.h或者.m文件等等,相对来说比较简单.对于怎么给工程添加子工程,倒是没有人叙述过.无奈只能自己各种尝试,还是不得要领,又想到,cocoapods怎么是怎么做到的呢?
于是为了解决我的这个问题,我将cocoapods源码也下载下来进行阅读分析,其实最理想还是能调试就好了,但无奈,对ruby的熟悉度有限,再加上这个工程着实庞大,还是没办法.不过在阅读源码的过程中还是有很大的收获的.

各种尝试

从cocoapods源码的阅读过程中,发现了xcodeproj库竟然有个file_references_factory.rb文件,在其中

def new_reference(group, path, source_tree)
            ref = case File.extname(path).downcase
                  when '.xcdatamodeld'
                    new_xcdatamodeld(group, path, source_tree)
                  when '.xcodeproj'
                    new_subproject(group, path, source_tree)
                  else
                    new_file_reference(group, path, source_tree)
                  end

            configure_defaults_for_file_reference(ref)
            ref
 end

然后在group.rb中

def new_reference(path, source_tree = :group)
          FileReferencesFactory.new_reference(self, path, source_tree)
        end

难道问题这么简单,直接就可以使用啊
赶紧写段代码试试,命名为createProjectDependcy.rb

require 'xcodeproj'

def addSubProj
    projectPath = "DemoMain.xcodeproj"
    project = Xcodeproj::Project.open(projectPath)
    project.main_group.new_reference("Modules/YLBusiness1/YLBusiness1.xcodeproj",:group)
    project.save
end

addSubProj

在终端执行

ruby ./createProjectDependcy.rb

再看看工程


image.png

成功了!!

看来问题其实很简单啊,就按普通文件的方式来添加就好了,这方法内部,已经针对是.xcodeproj处理了

不过当我们这个时候手动添加 Target Dependencies或者Link Binary的时候,xcode会crash掉!!

image.png

这还没完,这种方式添加的子工程,当我们在xcode中删除的时候,会导致工程中的products这个group中的文件消失了!!

删除前


image.png

删除后


image.png

xcodeproj竟然有这么严重的问题,一直没有人反馈过........

怎么办?

难道这个问题解决不了了??

解决方式

既然xcodeproj这段代码是有问题的,那么要解决问题,只能我们自己修改了.

首先,既然上面的代码能够添加成功,那么说明总体上应该是没啥问题的,只是代码中有些问题,至于问题出在哪,目前还不清楚

我们首先用手动拖动的形式,来给主工程添加子工程,然后将project.pbxproj文件保存下来,再通过上面代码的方式来添加,也把project.pbxproj文件保存下来,两个进行对比,看看有什么不同的地方

具体的对比过程是乏味冗长的,通过对比发现,手动拖动生成的,多了一个不在xcode工程可视化中出现的group

        4C5117FE2255AD3500914224 /* Products */ = {
            isa = PBXGroup;
            children = (
                4C5118042255AD3500914224 /* libBusiness1.a */,
                4C5118062255AD3500914224 /* Business1Tests.xctest */,
                4C5118082255AD3500914224 /* Business1Bundle.bundle */,
            );
            name = Products;
            sourceTree = "<group>";
        };
在projectReferences的ProductGroup中使用的是上面建立的group
projectReferences = (
                {
                    ProductGroup = 4C5117FE2255AD3500914224 /* Products */;
                    ProjectRef = 4C5117FD2255AD3500914224 /* YLBusiness1.xcodeproj */;
                },

而通过xcodeproj这段代码生成的是在原来的products这个group下添加了引用,以下 4CFBA7F42099B5BC00E39A19这个Products group是原来存在的!

4CFBA7F42099B5BC00E39A19 /* Products */ = {
            isa = PBXGroup;
            children = (
                4C51181C2255AEF600914224 /* DemoMain.app */,
                4C51181D2255AEF600914224 /* DemoMainTests.xctest */,
                4C51181E2255AEF600914224 /* DemoMainUITests.xctest */,
                D5EEDB2A75FE09CBA854F57C /* libBusiness1.a */,
                73D1A8603F14BDF13804CD30 /* Business1Tests.xctest */,
                A5CD1082BFBEC674BC72C28A /* Business1Bundle.bundle */,
            );
            name = Products;
            sourceTree = "<group>";
        };
使用的时候
{
                    ProductGroup = 4CFBA7F42099B5BC00E39A19 /* Products */;
                    ProjectRef = A06BA4184D5F5B2B56B8D071 /* YLBusiness1.xcodeproj */;
                },

问题就是出在这里了,添加子工程,不应该重用工程原来的products group,重用了,导致删除的时候,会清掉这个group.

至于为什么添加依赖会导致xcode crash,从这里的分析看,应该也是和这个projectReferences有关.

既然知道问题所在,那么我么就可以想办法解决了
从源码中可看到,添加子工程的方法是FileReferencesFactory中的 new_subproject方法

         def new_subproject(group, path, source_tree)
            ref = new_file_reference(group, path, source_tree)
            ref.include_in_index = nil

            product_group_ref = find_products_group_ref(group, true)

            subproj = Project.open(path)
            subproj.products_group.files.each do |product_reference|
              container_proxy = group.project.new(PBXContainerItemProxy)
              container_proxy.container_portal = ref.uuid
              container_proxy.proxy_type = Constants::PROXY_TYPES[:reference]
              container_proxy.remote_global_id_string = product_reference.uuid
              container_proxy.remote_info = 'Subproject'

              reference_proxy = group.project.new(PBXReferenceProxy)
              extension = File.extname(product_reference.path)[1..-1]
              reference_proxy.file_type = Constants::FILE_TYPES_BY_EXTENSION[extension]
              reference_proxy.path = product_reference.path
              reference_proxy.remote_ref = container_proxy
              reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'

              product_group_ref << reference_proxy
            end

            attribute = PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
            project_reference = ObjectDictionary.new(attribute, group.project.root_object)
            project_reference[:project_ref] = ref
            project_reference[:product_group] = product_group_ref
            group.project.root_object.project_references << project_reference

            ref
          end

这段代码中的

product_group_ref = find_products_group_ref(group, true)

是获取工程中原来的Products group,这个造成了上面的问题,所以,我们要修改它

稍微修改下此代码,当然,我们这里,因为并不是在原来代码的类上写,需要把一些类的前缀都加上

def add_new_subProj(group, path, source_tree)
    ref = Xcodeproj::Project::FileReferencesFactory.send(:new_file_reference, group, path, source_tree)
    ref.include_in_index = nil
    ref.name = Pathname(path).basename.to_s

    #product_group_ref = group.new_group("Products") 这种方式创建的group会挂载在main_group下,这会导致删除的时候,出现一个空的group,而手动拖动的就不会,所以改为group.project.new(Xcodeproj::Project::PBXGroup)
    #从xcode手动添加子工程来看,它要创建一个包含子工程的group
    product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup) #find_products_group_ref(group, true)
    product_group_ref.name = "Products" #手动拖动创建的group名字是Products,所以我们这里新创建的名字也赋值为products

    subproj = Xcodeproj::Project.open(path)
    subproj.products_group.files.each do |product_reference|
      container_proxy = group.project.new(Xcodeproj::Project::PBXContainerItemProxy)
      container_proxy.container_portal = ref.uuid
      container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
      container_proxy.remote_global_id_string = product_reference.uuid
      container_proxy.remote_info = 'Subproject'

      reference_proxy = group.project.new(Xcodeproj::Project::PBXReferenceProxy)
      extension = File.extname(product_reference.path)[1..-1]
      reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
      reference_proxy.path = product_reference.path
      reference_proxy.remote_ref = container_proxy
      reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'

      product_group_ref << reference_proxy
    end

    attribute = Xcodeproj::Project::PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
    project_reference = Xcodeproj::Project::ObjectDictionary.new(attribute, group.project.root_object)
    project_reference[:project_ref] = ref
    project_reference[:product_group] = product_group_ref
    group.project.root_object.project_references << project_reference

    ref

    
end


def addSubProjTest
    projectPath = "DemoMain.xcodeproj"
    project = Xcodeproj::Project.open(projectPath)
    add_new_subProj(project.main_group,"Modules/YLBusiness1/YLBusiness1.xcodeproj",:group)
    project.save
end

addSubProjTest

测试一下,完成了!!
删除,添加dependcy等操作都完全可以了!!

到这里,其实已经可以添加了
当然了,上面的代码依然不完美,


image.png

我们看到,手动拖动进来的 ,建立的PBXContainerItemProxy对象的remote_info赋值为的是子工程的target的名字,而我们上面代码创建的是

container_proxy.remote_info = 'Subproject'

虽然不改,不会出错什么的,以后一旦在xcode中添加link等等,xcode会自动修正这个值,但,我们在建立的时候,就做到和xcode的默认行为一致会更好.

增加一个方法

   #根据productReference 找到其对应的target
    def get_target_with_productReference(productReference,project)
        project.native_targets.each { |target|
            if target.product_reference == productReference
                puts "target = #{target}"
                return target
            end
        }
    end
然后修改上面代码中的
#container_proxy.remote_info = 'Subproject'
      subproj_native_target = get_target_with_productReference(product_reference,subproj)
      container_proxy.remote_info = subproj_native_target.name

完美!

添加Link Binary With Libraries

核心是调用

native_target.frameworks_build_phase.add_file_reference(reference_proxy)

添加依赖

native_target.dependencies << target_dependency

添加资源

native_target.resources_build_phase.files << build_file

我将上面的综合起来,生成一个类

class SubProjectDispose
    attr_reader :mainproj_path, :subproj_path, :main_project ,:sub_project,:subproj_ref_in_mainproj,:subproj_product_group_ref
    def initialize(mainproj_path,subproj_path)
        @mainproj_path = mainproj_path
        @subproj_path = subproj_path
        @main_project = Xcodeproj::Project.open(mainproj_path)
    end
    #根据productReference 找到其对应的target
    def get_target_with_productReference(productReference,project)
        project.native_targets.each { |target|
            if target.product_reference == productReference
                puts "target = #{target}"
                return target
            end
        }
    end

    def add_new_subProj(group, path, source_tree)
        @subproj_ref_in_mainproj = Xcodeproj::Project::FileReferencesFactory.send(:new_file_reference, group, path, :group)
        @subproj_ref_in_mainproj.include_in_index = nil
        @subproj_ref_in_mainproj.name = Pathname(subproj_path).basename.to_s
        #product_group_ref = group.new_group("Products") 这种方式创建的group会挂载在main_group下,这会导致删除的时候,出现一个空的group,而手动拖动的就不会,所以改为group.project.new(Xcodeproj::Project::PBXGroup)
        #从xcode手动添加子工程来看,它要创建一个包含子工程的group
        product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup)
        product_group_ref.name = "Products" #手动拖动创建的group名字就是Products
        @sub_project = Xcodeproj::Project.open(path) #打开子工程
        @sub_project.products_group.files.each do |product_reference|
            puts "product_reference = #{product_reference},name = #{product_reference.name},path = #{product_reference.path}"#product_reference = FileReference,name = ,path = ChencheMaBundle.bundle reference_proxy.file_type = wrapper.plug-in
            container_proxy = group.project.new(Xcodeproj::Project::PBXContainerItemProxy)
            container_proxy.container_portal = @subproj_ref_in_mainproj.uuid
            container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
            container_proxy.remote_global_id_string = product_reference.uuid
            #container_proxy.remote_info = 'Subproject' #这里和手动添加的是不一致的,手动的,这里是targets的名字
            subproj_native_target = get_target_with_productReference(product_reference,@sub_project)
            container_proxy.remote_info = subproj_native_target.name
            reference_proxy = group.project.new(Xcodeproj::Project::PBXReferenceProxy)
            extension = File.extname(product_reference.path)[1..-1]
            puts("product_reference.path = #{product_reference.path}")
            if extension == "bundle"
                #xcodeproj的定义中,后缀为bundle的对应的是'bundle'       => 'wrapper.plug-in',但是我们手动拖动添加的是 'wrapper.cfbundle'
                reference_proxy.file_type = 'wrapper.cfbundle'
            elsif
            reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
            end

            reference_proxy.path = product_reference.path
            reference_proxy.remote_ref = container_proxy
            reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
            product_group_ref << reference_proxy
        end
        @subproj_product_group_ref = product_group_ref
        attribute = Xcodeproj::Project::PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
        project_reference = Xcodeproj::Project::ObjectDictionary.new(attribute, group.project.root_object)
        project_reference[:project_ref] = @subproj_ref_in_mainproj
        project_reference[:product_group] = product_group_ref
        group.project.root_object.project_references << project_reference
        product_group_ref
    end


    def add_subproject()
        add_new_subProj(self.main_project.main_group,self.subproj_path,:group)
        add_frameworks_build_phase()
        add_dependencies()
        add_copy_bundle_resource()
    end


    def add_frameworks_build_phase()
        puts("self.subproj_product_group_ref = #{self.subproj_product_group_ref}")
        reference_proxys = self.subproj_product_group_ref.children.grep(Xcodeproj::Project::PBXReferenceProxy)
        reference_proxys.each do |reference_proxy|
            if (reference_proxy.file_type == Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION["a"]) || (reference_proxy.file_type == Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION["bundle"]) then
                puts("reference_proxy = #{reference_proxy}")
                native_target = self.main_project.native_targets.first
                native_target.frameworks_build_phase.add_file_reference(reference_proxy)
            end
        end
    end


    def add_dependencies()
        #添加target的dependencies,需要的是子工程的target
        # @main_project = Xcodeproj::Project.open(self.mainproj_path)
        # @sub_project = Xcodeproj::Project.open(self.subproj_path) #打开子工程
        # @subproj_ref_in_mainproj = @main_project.objects_by_uuid['0CC9D5720EABC826EE0ECB3B'] #使用uuid可以获取任何一个对象
        native_target = self.main_project.native_targets.first
        @sub_project.native_targets.each do |nativeTarget|
            if (nativeTarget.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:static_library]) || (nativeTarget.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:bundle]) then
                puts("nativeTarget.productType = #{nativeTarget.product_type}")
                container_proxy = self.main_project.new(Xcodeproj::Project::PBXContainerItemProxy)
                container_proxy.container_portal = @subproj_ref_in_mainproj.uuid
                container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:native_target] #1
                container_proxy.remote_global_id_string = nativeTarget.uuid
                container_proxy.remote_info = nativeTarget.product_name

                target_dependency = @main_project.new(Xcodeproj::Project::PBXTargetDependency)
                target_dependency.name = nativeTarget.name
                target_dependency.target_proxy = container_proxy
                native_target.dependencies << target_dependency
            end
        end


    end

    def add_copy_bundle_resource()
        puts("add_copy_bundle_resource")
        # @main_project = Xcodeproj::Project.open(self.mainproj_path)
        # @sub_project = Xcodeproj::Project.open(self.subproj_path) #打开子工程
        # @subproj_product_group_ref = @main_project.objects_by_uuid['37A142A1F74B773563256D88'] #使用uuid可以获取任何一个对象

        native_target = self.main_project.native_targets.first
        build_file = @main_project.new(Xcodeproj::Project::PBXBuildFile)
        reference_proxys = self.subproj_product_group_ref.children.grep(Xcodeproj::Project::PBXReferenceProxy)
        reference_proxys.each do |reference_proxy|
            puts("reference_proxy.file_type = #{reference_proxy.file_type}")
            if reference_proxy.file_type == 'wrapper.cfbundle' && reference_proxy.path.include?(".bundle") then
                puts("reference_proxy = #{reference_proxy}")
                build_file.file_ref = reference_proxy;
                native_target.resources_build_phase.files << build_file
            end
        end


    end


    def close()
        self.main_project.save()
    end

end

使用的时候

dispose =SubProjectDispose.new("DemoMain.xcodeproj","Modules/YLBusiness1/YLBusiness1.xcodeproj")
dispose.add_subproject()
dispose.close()

后记

实在没想到xcodeproj竟然存在这么严重的一个bug,不过好在它们的代码可读性非常好,虽然不能单点调试,不过阅读起来,也基本上大差不差了
在使用的时候,多用Dash查看文档,多用git的文件修改对比来进行分析,将会大大的增加对xcodeproj格式和操作的理解,从而写出需要的代码来

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 项目组件化、平台化是技术公司的共同目标,越来越多的技术公司推崇使用pod管理第三方库以及私有组件,一方面使项目架构...
    swu_luo阅读 21,483评论 0 39
  • CocoaPods 是什么? CocoaPods 是一个负责管理 iOS 项目中第三方开源库的工具。CocoaPo...
    朝洋阅读 25,637评论 3 51
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 稍有 iOS 开发经验的人应该都是用过 CocoaPods,而对于 CI、CD 有了解的同学也都知道 Fastla...
    Draveness阅读 6,691评论 9 77
  • 哈哈哈〜 首先说一下关于pod的命令语句 1. 在命令行中进入到当前的工程的文件夹 简单说下命令行的几个用法: c...
    Amanda_Lhy阅读 1,009评论 0 0