這篇文章參考 casa 大神的組件化實踐和使用Cocoapods創建私有podspec,不過因為之前對 iOS 組件化方面了解的比較少,所以在跟著 casa 的步驟一步一步組件化工程的時候遇到了不少問題,這里當做一個自己組件化的時候遇到問題的總結吧,寫得比較繁瑣,因為我盡量把每一步都講得清楚點,基礎差點的人一步一步按照本文來操作也可以感受組件化帶來的快感!
那么,首先你要懂得組件化的整體思想,如下圖,就是通過一個中間者傳遞信息,用來降低模塊間的耦合度。
好了,本文主要并不探討組件化的思想和解決方案的優劣,主要目的是讓你先成功的組件化一個小 demo,然后再回過頭分析這個 demo被組件化后是怎么達到通過中間者調度信息的。
每一個組件都是一個獨立的 pod,我們把組件化的代碼放到 git 上托管,制作成私有 pod,然后通過 cocoapods 就可以連接各個組件,最后把他們合到一起,變成一個完整的項目。
本文借用 casa 文章中的 demo,一個主工程 MainProject,主工程中有兩個業務,A部分,和 B 部分,我們要把 A 部分組件化,那么我們需要創建A 的私有 Pod 源(以 coding.net為例), 這里注意區分私有倉庫Repo,和私有 Pod的關系,可以參考下圖,在完成所有組件化步驟之后再回頭這個圖可能會更加容易理解,所以暫時不明白不要急,慢慢一步一步來。
組件化操作流程:
第一:添加私有 Pods源
第二:創建 MainProject Xcode工程
第三:創建 A_section 的 Xcode 工程和對應的私有 Repo
第四:創建 A_Category 的 Xode 工程和對應的私有 Repo
第五:解決主工程編譯不通過的問題
第六:為 A_section 工程創建 Target(一個組件對應一個 Target-Action)
第七:解決A_section工程編譯不通過的問題
第八:準備發版 pod
一 、添加私有 Pods源
我們在coding.net上,先創建一個 MainProject 的私有倉庫作為私有 pods 源倉庫。然后添加到本地,添加成功后會在本地Users/xxx/.cocoapods/repos 文件夾下看到自己添加的私有pod源
pod repo add [私有Pod源倉庫名字] [私有Pod源的repo地址]
二、創建 MainProject Xcode工程
在桌面新建文件夾 Project,新建Xcode項目工程 Project/MainProject,作為我們的主工程,并且實現 casa文章里的demo 中未組件化之前的 MainProject 里的功能,就是很簡單的 push 一個 A界面,然后在 A 界面 push 到 B 界面。直接復制casa的代碼就好,也可以自己寫。這個時候我們的 Project 文件下的目錄應該是這樣的
此時的MainProject是完全沒有組件化之前的工程,也沒有 cocoapods,當然 casa 的這個 MainProject 中用到了pod 'HandyFrame', 就是個布局 UI 的小分類,和組件化工程沒有什么關系,只是為了代碼寫的方便而已,所以下一步我們在 MainProject 工程中先把它pod install 下。
現在你應該保證 MainProject 已經實現了 push 兩個界面的功能了。
三、創建A_section Xcode 工程
1.新建A_section Xcode 工程
我們要把 A 業務組件化出來,也就是casa 文章中的 A,MainProject 中的 A界面。因為 coding 不能創建單個字母的私有倉庫,為了保證 Xocde 工程和私有 Repo 的名字一一對應,所以就把名字改成了 A_section,本文中的 A 和 A_section 是等同的。
2.新建私有的 A_section Repo
在 coding 上創建一個私有的 Repo,起名 A_section,用來存放 A_section 代碼。
把 A_section 的私有倉庫 clone 下來(剛創建,所以是空倉庫),然后把 A_section 的 Xcode 工程文件全部放進去,然后 push 到倉庫,這樣你的 A_section 私有倉庫上就托管了你的 A_section Xcode 工程了,或者你使用 git remote add origin 命令,最終都是達到一個目的 。
3.分離 A_section業務代碼
然后我們在MainProject中,把屬于A_section業務的代碼移出來,然后拖放到A_section的Xcode工程中。原來MainProject里面A業務的代碼直接刪掉,此時MainProject和A_section工程編譯不過都是正常的。
這個時候A_section文件目錄應該是這樣的
4.把 A_section 配置成私有 Pod
cd到 A_section 文件夾下,命令
pod spec create A_section https://git.coding.net/xxxxxxx/A_section.git
之后會在 A_section 工程中生成A_section.podspec文件。
接下來就是編輯A_section.podspec文件。最好用編輯工具,如 Sublime, 會有語法高亮,方便編輯,如果用文本編輯的話,很可能你修改了內容,標點符號會自動更改成中文的,我就因為這個錯誤耽誤了半天,而且很難發現,所以不要用默認的文本編輯器。
打開之后,如果不知道該怎么填一方面可以參考官方文檔,查看每個屬性的意思,另外也可以重新開個文件夾然后執行
pod lib create A_section
然后會問你幾個問題,自己如實回答就好。 這個命令的作用是 cocoapods 提供一個 demo 給你,你可以對照這個 demo 里的.podspec 文件去填寫。
如果你也懶得去看得話,我給一些必須用到的參數都做了注釋,可以按照下面的代碼編輯自己的.podspec 文件,如果對配置私有 pod 方面不是很熟悉的朋友,一定要仔細看我的每個注釋,基本都說的通俗易懂。
Pod::Spec.new do |s|
s.name = "A_section"
#發版版本號,每更新一次代碼就改變一次版本號
s.version = "0.0.1"
#一個簡單的總結,隨便寫
s.summary = "A short description of A_section."
#一定要寫上,不寫的話,執行 pod lib lint 驗證項目的時候會報找不到 UIKIT 等框架錯誤
s.platform = :ios, "8.0"
#描述,隨便寫 但是要比 s.summary 長度長
s.description = <<-DESC
short description of A_section short description of A_section
DESC
#你的 git 倉庫首頁的網頁 url,注意并不是 https/ssh這種代碼倉庫地址
s.homepage = "https://coding.net/u/xxxx/p/A_section"
#直接寫 MIT
s.license = "MIT"
#你是誰
s.author = { "" => "" }
#這里就是你 git 倉庫的 https/ssh 地址了
s.source = { :git => "https://git.coding.net/xxxx/A_section.git", :tag => "#{s.version}" }
#這里的文件夾下的內容就是這個 pods 被pod install 的時候會被下載下來的文件,不在這個文件夾,將不會被引用
# Classes 目錄和.podspec 目錄是平級的。
#你可以隨便指定文件夾名稱,只要這個文件夾是真實存在的
#Classes/**/*.{h,m},表示 Classes 文件夾及其文件夾下的所有.h,.m 文件。
s.source_files = "A_section/Classes/**/*.{h,m}"
#資源文件地址,下面的所有.png資源都被打包成 s.name.bundle
s.resource = ['Images/*.png','Sounds/*']
#資源文件地址,和 resource 的區別是,這個屬性可以指定 bundle 的名字,下面的所有.png文件都會被打包成 ABC_section.bundle
s.resource_bundle = {
'ABC_section' => ['Classes/ABCImage/*png']
}
#指定公有頭文件,如果沒有寫,那么所有 pod 中的頭文件都默認公有,可以被 import。如果指定了某些頭文件,那么只有這些被指定的頭文件才可以被 import。
s.public_header_files = 'Classes/Public/*.h'
#這個 pods 還依賴于其他哪些 pods
s.dependency "B_Category"
s.dependency "HandyFrame"
編輯完A_section.podspec,把 A_section 的業務代碼,移動到 s.source_files 指定的文件夾中,以保證 pod 發版的時候,這些文件能被發布出去。
這是我自己對 A_section 的配置
根據我自己對 A_section 的配置,重新調整A_section 工程的文件結構這個時候因該是這樣的
這里針對 A_section 工程我們還要pod "HandyFrame",因為它也用到了里面的布局 UI 的方法。執行pod install 安裝HandyFrame。
此時編譯, A_section 編譯失敗,應該報錯找不到BViewController.h,這里暫時先放下,后面再解決。
四、創建 A_Category 的 Xode 工程和對應的私有 Repo
按照創建 A_section的流程,我們創建 A_Category 的 Xode 工程和對應的私有 Repo,同樣讓你的 A_Category工程托管到你的 A_Category 私有倉庫上。
然后去A_Category下,在Podfile中添加一行pod "CTMediator",然后執行pod install。
對照之前的格式編輯A_Category.podspec文件,然后在s.source_file對應的文件夾中新建基于CTMediator的Category:CTMediator+A。
CTMediator+A.h,在里面添加一個方法:
- (UIViewController *)A_aViewController;
再去CTMediator+A.m中,補上這個方法的實現,把MainProject中調用的語句作為注釋放進去,將來寫Target-Action要用
- (UIViewController *)A_aViewController
{
/*
AViewController *viewController = [[AViewController alloc] init];
*/
return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];
}
最后你的A_Category工程應該是這樣的:
和配置之后的文件結構截圖
五、解決主工程編譯不通過的問題
去主工程的Podfile下添加,然后執行 pod install
pod "CTMediator"
pod "A_Category", :path => "../A_Category
然后編譯一下,說找不到AViewController的頭文件。此時我們把頭文件引#import AViewController.h用改成#import <A_Category/CTMediator+A.h>。
然后繼續編譯,說找不到AViewController這個類型,然后我們把主工程調用AViewController的地方改為基于CTMediator Category A的實現
UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController];
[self.navigationController pushViewController:viewController animated:YES];
再編譯一下,編譯通過。
到此為止主工程就改完了,現在跑主工程點擊這個按鈕跳不到A頁面是正常的,因為我們還沒有在MainProject工程中引入 A_section 組件,也沒有在A_section工程中實現Target-Action。
此時主工程中關于A業務的改動就全部結束了,后面的組件化實施過程中,就不會再有針對A業務線對主工程的改動了。
六、為 A_section 工程創建 Target(一個組件對應一個 Target-Action)
此時我們關掉所有XCode窗口。然后打開兩個工程:A_Category工程和A_section工程。
我們在A_section工程中創建一個文件夾:Target,然后看到A_Category里面有performTarget:@"A",所以我們新建一個對象,叫做Target_A。
然后又看到對應的Action是viewController,于是在Target_A中新建一個方法:Action_viewController。這個Target對象是這樣的:
頭文件:
#import <UIKit/UIKit.h>
@interface Target_A : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
實現文件:
#import "Target_A.h"
#import "AViewController.h"
@implementation Target_A
- (UIViewController *)Action_viewController:(NSDictionary *)params
{
AViewController *viewController = [[AViewController alloc] init];
return viewController;
}
@end
最終你的 A_section 工程目錄應該是這樣
七、解決A_section工程編譯不通過的問題
然后我們再繼續編譯A_section工程,發現找不到BViewController。由于我們這次組件化實施的目的僅僅是將A_section業務線抽出來,BViewController是屬于B業務線的,所以我們沒必要把B業務也從主工程里面抽出來。但為了能夠讓A_section工程編譯通過,我們需要提供一個B_Category來使得A_section工程可以調度到B,同時也能夠編譯通過。
新建 B_Category Xcode 工程,和對應的私有倉庫 Repo,然后一樣托管到對應的遠程倉庫, 配置.podspec 文件,配置B_Category.podspec 文件的時候,在最后加上 s.dependency "CTMediator"(這樣是因為下一步 A_section 從本地 pod B_Category 的時候也可以pod 到CTMediator) ,pod "CTMediator", 創建CTMediator+B 分類代碼,
頭文件:
#import <CTMediator/CTMediator.h>
#import <UIKit/UIKit.h>
@interface CTMediator (B)
- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;
@end
實現文件:
#import "CTMediator+B.h"
@implementation CTMediator (B)
- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText
{
/*
BViewController *viewController = [[BViewController alloc] initWithContentText:@"hello, world!"];
*/
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"contentText"] = contentText;
return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
最終你的 B_Category ,應該可以直接編譯通過,像這樣
B_Category添加好后,我們在A_section工程的Podfile中本地指過去
pod "B_Category", :path => "../B_Category"
然后我們對應地在A_section工程中修改頭文件引用為#import <B_Category/CTMediator+B.h>,并且把調用的代碼改為:
UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"];
[self.navigationController pushViewController:viewController animated:YES];
此時再編譯一下,編譯通過了。注意哦,這里A業務線跟B業務線就已經完全解耦了,跟主工程就也已經完全解耦了。
此時還有一個收尾工作是我們給B業務線創建了Category,但沒有創建Target-Action。所以我們要去MainProject創建一個B業務線的Target-Action。創建的時候其實完全不需要動到B業務線的代碼,只需要新增Target_B對象即可:
Target_B頭文件:
#import <UIKit/UIKit.h>
@interface Target_B : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
Target_B實現文件:
#import "Target_B.h"
#import "BViewController.h"
@implementation Target_B
- (UIViewController *)Action_viewController:(NSDictionary *)params
{
NSString *contentText = params[@"contentText"];
BViewController *viewController = [[BViewController alloc] initWithContentText:contentText];
return viewController;
}
@end
這個時候我們MainProject 結構應該是
八、準備發版 pod
1.先在本地測試
接下來我們就要為發版 pod 做準備,我們先在本地實驗 pod 可用不可用,此時我們的 Project 文件夾中應該有四個文件,像這樣
我們可以把MainProject 中的 podfile 文件改成
target 'MainProject' do
pod "A_section", :path => "../A_section"
pod "A_Category", :path => "../A_Category"
pod "B_Category", :path => "../B_Category"
pod "CTMediator"
pod 'HandyFrame'
end
然后執行pod install,這里的目的是測試在本地情況下pod 是否能順利執行。如果順利的話在安裝完成之后,會在 Mainproject 工程的 development pods 文件夾下查看到本地的 pods。你運行 MainProject 達到的效果應該是和沒有組件化之前是完全一樣的。
2.發布組件
準備發布pod之前要檢查依賴是不是寫上了,否則會發版失敗,
A_section的 A_section.podspec 文件最后應該有這兩個依賴
s.dependency "B_Category"
s.dependency "HandyFrame"
A_Category 中 A_Category.podspec和B_Category.podspec 文件最后應該有
s.dependency "CTMediator"
B_Category 中 B_Category.podspec和B_Category.podspec 文件最后應該有
s.dependency "CTMediator"
然后我們就可以把把私有 Repo 發布到網上了,
1. 把代碼push 到 git 上
git add .
git commit -m "initial pod"
git push
2. 為這版的代碼打上 tag 號,tag 號一定要和.podspec 文件的 s.version 號一致
git tag 0.0.1
git push --tags
3. 發布 pod 到私有 Pods
pod repo push PrivatePodRepo A_section.podspec --verbose --allow-warnings
接著發布A_Category,B_Category的 pods,都是相同的步驟。如果在上傳代碼到 git 上的時候碰到403錯誤,或者沒有權限等問題,可以參考我的另一篇筆記。
然后到我們的 MainProject 主工程之下,把 Podfile 文件內容改成下面這個樣子,
添加上你的私有 Pods 的地址
source 'https://git.coding.net/xxxxx/PrivatePodRepo.git'
source 'https://github.com/CocoaPods/Specs.git'
target 'MainProject' do
pod "CTMediator"
pod "A_Category"
pod "A_section"
pod 'HandyFrame'
end
然后執行 pod install,組件化到此結束,現在你可以好好研究下,組件化的過程是如何通過中間者協調信息的。
如果組件化本例的時候遇到什么問題,可以到 casa 在 github 上開的orgnization上一一對照你的組件化前和組件化后的配置對不對。
3.補充
pod install的過程就是首先到本地
/Users/用戶名/.cocoapods/repos/PrivatePodRepo
文件夾中尋找對應的私有 pod,找到之后根據.podspec 文件的version 號,然后到 git 的地址上找到 tag 號和 version 號一致的那個版本的代碼 pull 下來。
所以,我們如果修改了組件的內容,想更新的話,記得要修改s.version的版本號,然后 打上 tag, tag 要和 version 號一樣。
如果在 push 到 Repo的時候
pod repo push PrivatePodRepo A_section.podspec --verbose --allow-warnings
報錯 [!] The repo at ../../../.cocoapods/repos/xxxx is not clean
解決方案: 把本地的私有 Pod 刪除,之后再重新添加
關于 xib 和圖片:這里是很簡單的組件化Demo,如果你的組件中用到了圖片,或者 xib 資源,要指定資源的文件路徑,否則不會把圖片打包到你的組件中,
s.resource_bundles = {
'A_section' => ['A_section/AImages/**/*.{png}', 'A_section/Classes/*.{xib}']
}
或者
s.resources = ['A_section/AImages/**/*.{png}', 'A_section/Classes/*.{xib}']
我實驗的結果是:如果使用 Xocde 工程自帶的 Assets 那個文件夾的話,圖片也無法打包到組件中,最好自己重新創建一個新的文件夾用來存放圖片資源。
然后代碼中獲取 xib, png 等 resource 時,bundle 重新設置,這樣就保證了無論在組件中,還是在 MainProject 工程中,都可以配置到正確的 Bundle,如果你使用 s.resource_bundles={},配置了自定義的 bundle名稱,那么 [bundle pathForResource:@"A_section" ofType:@"bundle"]中就要替換成相應的名稱。
//mainBundle
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *bundlePath = [bundle pathForResource:@"A_section" ofType:@"bundle"];
if (bundlePath)
{
//組件資源所在的 bundle
bundle = [NSBundle bundleWithPath:bundlePath];
}