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

現(xiàn)在,ccocoapods已經(jīng)成為iOS工程的標(biāo)配,在這個(gè)工具的開發(fā)過程中,開源了一個(gè)專門用來操作工程的.xcodeproj文件的ruby庫Xcodeproj,利用它,我們自己也可以用ruby腳本來添加和刪除工程中的文件等,做到自動(dòng)化操作

問題的提出

在我們的組件化過程中,是通過子工程的方式來建立業(yè)務(wù)組件的.可能有人會(huì)問,為什么不用pod來建立業(yè)務(wù)組件呢?其實(shí)當(dāng)時(shí)也有考慮過,pod更適合已經(jīng)比較成熟的組件,而我們現(xiàn)在的業(yè)務(wù)變動(dòng)還很大,并且pod在開發(fā)的過程中,新增文件什么的,還要運(yùn)行下pod install才能運(yùn)行,綜合考慮,在業(yè)務(wù)早期,還是使用子工程的方式更便捷,能取得各方面的權(quán)衡

當(dāng)我們采用子工程來建立業(yè)務(wù)組件,那么通常建立了一個(gè)模板化的組件工程(可以通過多種方式建立,此處不述了)后,還要做4件事,才能添加到主工程中,如下圖所示:


image.png
  1. 拖動(dòng)工程到主工程中
  2. 設(shè)置Target Dependencies,因?yàn)槊總€(gè)組件工程有個(gè)資源bundle的target,如果不設(shè)置依賴,當(dāng)他們改動(dòng)時(shí)候,主工程并不會(huì)去編譯它們.
  3. 設(shè)置Link Binary With Libraries
  4. 拷貝資源

雖然事情不到,總歸還是覺得建立組件和將組件添加到工程,是割裂的,難免有遺憾.
我們每天都使用的cocoapods,就是一個(gè)腳本就建立好了工程和設(shè)置完成了依賴,于是就想著用ruby借助xcodeproj庫來將建立工程和設(shè)置結(jié)合起來.

xcodeproj介紹

Xcodeproj是cocoapods團(tuán)隊(duì)在寫cocoapods過程中開源出來的庫,它的工程代碼賞心悅目,結(jié)構(gòu)化程度很高.并且還提供了很多單元測(cè)試.不過遺憾的是,它沒有個(gè)詳細(xì)的使用文檔,加上使用了在國內(nèi)比較小眾的ruby語言編寫的,所以使用起來,還是頗費(fèi)一番周折的.

網(wǎng)上有不少中文的使用教程,它們都是簡單的添加.h或者.m文件等等,相對(duì)來說比較簡單.對(duì)于怎么給工程添加子工程,倒是沒有人敘述過.無奈只能自己各種嘗試,還是不得要領(lǐng),又想到,cocoapods怎么是怎么做到的呢?
于是為了解決我的這個(gè)問題,我將cocoapods源碼也下載下來進(jìn)行閱讀分析,其實(shí)最理想還是能調(diào)試就好了,但無奈,對(duì)ruby的熟悉度有限,再加上這個(gè)工程著實(shí)龐大,還是沒辦法.不過在閱讀源碼的過程中還是有很大的收獲的.

各種嘗試

從cocoapods源碼的閱讀過程中,發(fā)現(xiàn)了xcodeproj庫竟然有個(gè)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

在終端執(zhí)行

ruby ./createProjectDependcy.rb

再看看工程


image.png

成功了!!

看來問題其實(shí)很簡單啊,就按普通文件的方式來添加就好了,這方法內(nèi)部,已經(jīng)針對(duì)是.xcodeproj處理了

不過當(dāng)我們這個(gè)時(shí)候手動(dòng)添加 Target Dependencies或者Link Binary的時(shí)候,xcode會(huì)crash掉!!

image.png

這還沒完,這種方式添加的子工程,當(dāng)我們?cè)趚code中刪除的時(shí)候,會(huì)導(dǎo)致工程中的products這個(gè)group中的文件消失了!!

刪除前


image.png

刪除后


image.png

xcodeproj竟然有這么嚴(yán)重的問題,一直沒有人反饋過........

怎么辦?

難道這個(gè)問題解決不了了??

解決方式

既然xcodeproj這段代碼是有問題的,那么要解決問題,只能我們自己修改了.

首先,既然上面的代碼能夠添加成功,那么說明總體上應(yīng)該是沒啥問題的,只是代碼中有些問題,至于問題出在哪,目前還不清楚

我們首先用手動(dòng)拖動(dòng)的形式,來給主工程添加子工程,然后將project.pbxproj文件保存下來,再通過上面代碼的方式來添加,也把project.pbxproj文件保存下來,兩個(gè)進(jìn)行對(duì)比,看看有什么不同的地方

具體的對(duì)比過程是乏味冗長的,通過對(duì)比發(fā)現(xiàn),手動(dòng)拖動(dòng)生成的,多了一個(gè)不在xcode工程可視化中出現(xiàn)的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這個(gè)group下添加了引用,以下 4CFBA7F42099B5BC00E39A19這個(gè)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>";
        };
使用的時(shí)候
{
                    ProductGroup = 4CFBA7F42099B5BC00E39A19 /* Products */;
                    ProjectRef = A06BA4184D5F5B2B56B8D071 /* YLBusiness1.xcodeproj */;
                },

問題就是出在這里了,添加子工程,不應(yīng)該重用工程原來的products group,重用了,導(dǎo)致刪除的時(shí)候,會(huì)清掉這個(gè)group.

至于為什么添加依賴會(huì)導(dǎo)致xcode crash,從這里的分析看,應(yīng)該也是和這個(gè)projectReferences有關(guān).

既然知道問題所在,那么我么就可以想辦法解決了
從源碼中可看到,添加子工程的方法是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,這個(gè)造成了上面的問題,所以,我們要修改它

稍微修改下此代碼,當(dāng)然,我們這里,因?yàn)椴⒉皇窃谠瓉泶a的類上寫,需要把一些類的前綴都加上

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") 這種方式創(chuàng)建的group會(huì)掛載在main_group下,這會(huì)導(dǎo)致刪除的時(shí)候,出現(xiàn)一個(gè)空的group,而手動(dòng)拖動(dòng)的就不會(huì),所以改為group.project.new(Xcodeproj::Project::PBXGroup)
    #從xcode手動(dòng)添加子工程來看,它要?jiǎng)?chuàng)建一個(gè)包含子工程的group
    product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup) #find_products_group_ref(group, true)
    product_group_ref.name = "Products" #手動(dòng)拖動(dòng)創(chuàng)建的group名字是Products,所以我們這里新創(chuàng)建的名字也賦值為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

測(cè)試一下,完成了!!
刪除,添加dependcy等操作都完全可以了!!

到這里,其實(shí)已經(jīng)可以添加了
當(dāng)然了,上面的代碼依然不完美,


image.png

我們看到,手動(dòng)拖動(dòng)進(jìn)來的 ,建立的PBXContainerItemProxy對(duì)象的remote_info賦值為的是子工程的target的名字,而我們上面代碼創(chuàng)建的是

container_proxy.remote_info = 'Subproject'

雖然不改,不會(huì)出錯(cuò)什么的,以后一旦在xcode中添加link等等,xcode會(huì)自動(dòng)修正這個(gè)值,但,我們?cè)诮⒌臅r(shí)候,就做到和xcode的默認(rèn)行為一致會(huì)更好.

增加一個(gè)方法

   #根據(jù)productReference 找到其對(duì)應(yīng)的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

核心是調(diào)用

native_target.frameworks_build_phase.add_file_reference(reference_proxy)

添加依賴

native_target.dependencies << target_dependency

添加資源

native_target.resources_build_phase.files << build_file

我將上面的綜合起來,生成一個(gè)類

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
    #根據(jù)productReference 找到其對(duì)應(yīng)的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") 這種方式創(chuàng)建的group會(huì)掛載在main_group下,這會(huì)導(dǎo)致刪除的時(shí)候,出現(xiàn)一個(gè)空的group,而手動(dòng)拖動(dòng)的就不會(huì),所以改為group.project.new(Xcodeproj::Project::PBXGroup)
        #從xcode手動(dòng)添加子工程來看,它要?jiǎng)?chuàng)建一個(gè)包含子工程的group
        product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup)
        product_group_ref.name = "Products" #手動(dòng)拖動(dòng)創(chuàng)建的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' #這里和手動(dòng)添加的是不一致的,手動(dòng)的,這里是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的對(duì)應(yīng)的是'bundle'       => 'wrapper.plug-in',但是我們手動(dòng)拖動(dòng)添加的是 '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可以獲取任何一個(gè)對(duì)象
        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可以獲取任何一個(gè)對(duì)象

        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

使用的時(shí)候

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

后記

實(shí)在沒想到xcodeproj竟然存在這么嚴(yán)重的一個(gè)bug,不過好在它們的代碼可讀性非常好,雖然不能單點(diǎn)調(diào)試,不過閱讀起來,也基本上大差不差了
在使用的時(shí)候,多用Dash查看文檔,多用git的文件修改對(duì)比來進(jìn)行分析,將會(huì)大大的增加對(duì)xcodeproj格式和操作的理解,從而寫出需要的代碼來

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 項(xiàng)目組件化、平臺(tái)化是技術(shù)公司的共同目標(biāo),越來越多的技術(shù)公司推崇使用pod管理第三方庫以及私有組件,一方面使項(xiàng)目架構(gòu)...
    swu_luo閱讀 22,827評(píng)論 0 39
  • CocoaPods 是什么? CocoaPods 是一個(gè)負(fù)責(zé)管理 iOS 項(xiàng)目中第三方開源庫的工具。CocoaPo...
    朝洋閱讀 25,978評(píng)論 3 50
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,031評(píng)論 4 61
  • 稍有 iOS 開發(fā)經(jīng)驗(yàn)的人應(yīng)該都是用過 CocoaPods,而對(duì)于 CI、CD 有了解的同學(xué)也都知道 Fastla...
    Draveness閱讀 6,873評(píng)論 9 76
  • 哈哈哈? 首先說一下關(guān)于pod的命令語句 1. 在命令行中進(jìn)入到當(dāng)前的工程的文件夾 簡單說下命令行的幾個(gè)用法: c...
    Amanda_Lhy閱讀 1,141評(píng)論 0 0

友情鏈接更多精彩內(nèi)容