这篇文章参考 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
然后继续编译,说找不到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
@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
#import
@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
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
@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];
}