利用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格式和操作的理解,从而写出需要的代码来

comments powered by Disqus