iOS组件化实践
参考资料:
http://wereadteam.github.io/2016/03/19/iOS-Component/#more
https://casatwy.com/iOS-Modulization.html
https://casatwy.com/modulization_in_action.html#
https://www.jianshu.com/p/b1c6d070c92b
这篇文章只是一个学习过程的记录,因为我发现相关文章都没有比较详细的操作记录,而这个过程必定不可能像他们博文中写的那样顺利,因此这篇文章主要是两点,一点是关于组件化的主要过程和结构梳理,另外就是使用pod过程中会遇到的各种问题.至于对组件化的使用,我直接引用其中一篇文章的总结:”实际上我没有组件化相关的实践,这里仅从 limboy 和 casa 提供的这几个方案对比分析,我还对组件化带来的收益是否大于组件化增加的成本这点存疑,相信真正实践起来还会碰到很多坑,继续探索中。”
这篇使用方案一,直接把这句话拿过来就是”总结起来就是,组件通过中间件通信,中间件通过 runtime 接口解耦,通过 target-action 简化写法,通过 category 感官上分离组件接口代码”.
1.总流程
1.新建主项目仓库,也就是需要组件化的项目,并将主项目push到远端仓库.
2.新建组件A项目及远端私有仓库,并同步.
3.新建组件A与外界联系媒介A_Category项目及远端仓库,并同步.
4.新建组件B项目及远端仓库,并同步.
5.新建组件B与外界联系媒介B_Category项目及远端仓库,并同步.
6.主项目Podfile本地引用组件A和组件B,并使项目编译通过.
7.将本地引用改为远程引用,运行项目并编译成功,组件化完成.
2.开始操作
1.新建主项目仓库,也就是需要组件化的项目,并将主项目push到远端仓库
直接把这个项目(https://github.com/ModulizationDemo/MainProject)拿过来用就可以了,自己写也可以,很简单的push两个页面,需要先pod install,因为使用了作者的一个简单的布局库.
运行无误之后就可以push到自己的主项目仓库了.
在码云新建仓库MainProject,clone到本地,把刚才的项目拖进去,然后push到远程仓库.
2.新建组件A项目及远端私有仓库,并同步.
先在码云新建仓库A_Section,然后clone到本地,然后在本地目录新建Xcode项目,也就是组件A项目,这里直接使用pod命令:
pod lib create A_Section
按提示输入选项,项目便创建好了,把项目文件中git相关的文件删掉,然后放到刚才clone到本地的路径,.
(用这条命令直接生成模板在模板中编辑不容易出错,如果自己用Xcode创建新项目,用
pod spec create A_Section https://gitee.com/xxxxxxx/A_Section.git
这条命令去创建.podspec文件,再去编辑的话,很容易遇到一些奇奇怪怪的错误,比如死活报错找不到source_files,而花费时间解决这些奇怪的麻烦并不明智.)
接下来可以删掉本地引用库里面的ReplaceMe.m文件,不删也不会报错.然后在项目中新建Classes文件夹
接下来就可以把MainProject中属于A组件的代码移动到这里了.
也就是
运行项目,编译报错,可以看到AViewController中使用了HandyFrame库,因此需要在Podfile中加入HandyFrame:
第一行是引用本地库,也就是ReplaceMe.m所在路径,不需要管,注释掉,以免产生不必要的影响,再次执行
pod install
重新打开项目,运行可以发现仍然报错,因为AViewController里现在直接引用了BViewController:
而组件化的目的就是消除组件之间的直接引用,所以删掉BViewController头文件引用,并将代码中的BViewController代码注释掉消除报错.
把主界面设置成AViewController
再次运行成功,并显示AViewController界面.
接下来编辑A_Section.podspec文件
接下来添加Target_A:
代码如下:
// Target_A.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Target_A : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end NS_ASSUME_NONNULL_END // Target_A.m #import "Target_A.h" #import "AViewController.h" @implementation Target_A - (UIViewController *)Action_viewController:(NSDictionary *)params { AViewController *viewController = [[AViewController alloc] init]; return viewController; } @end
至此执行下一步,push到远端仓库后面再做,因为代码还要改动.
3.新建组件A与外界联系媒介A_Category项目及远端仓库,并同步.
同上,先在码云新建一个A_Category私有仓库,然后clone到本地,然后在本地目录新建Xcode项目,直接使用pod命令:
pod lib create A_Category
按提示输入选项,项目便创建好了,把项目文件中git相关的文件删掉,然后放到刚才clone到本地的路径.
打开项目Podfile,添加
pod \'CTMediator\'
注释掉本地引用,执行
pod install
新建分类
编写代码:
// CTMediator+A.h #import <CTMediator/CTMediator.h> @interface CTMediator (A) - (UIViewController *)A_aViewController; @end // CTMediator+A.m #import "CTMediator+A.h" @implementation CTMediator (A) - (UIViewController *)A_aViewController { return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO]; } @end
至此执行下一步,同样push到远端仓库留到后面验证过代码没有问题再和其他组件一起做.
别忘了编辑A_Category.podspec文件,以及添加依赖
s.dependency \'CTMediator\'
参照前面.
4.新建组件B项目及远端仓库,并同步.(参考2)
先在码云新建仓库B_Section,然后clone到本地,然后在本地目录新建Xcode项目,也就是组件B项目,这里直接使用pod命令:
pod lib create B_Section
按提示输入选项,项目便创建好了,把项目文件中git相关的文件删掉,然后放到刚才clone到本地的路径,.
在项目中新建Classes文件夹
接下来就可以把MainProject中属于B组件的代码移动到这里了.
也就是
运行项目,编译报错,可以看到BViewController中使用了HandyFrame库,因此需要在Podfile中加入HandyFrame:
第一行是引用本地库,也就是ReplaceMe.m所在路径,不需要管,注释掉,以免产生不必要的影响,再次执行
pod install
重新打开项目,运行成功:
把主界面设置成BViewController
再次运行成功,并显示BViewController界面.
接下来编辑B_Section.podspec文件
接下来添加Target_B:
代码如下:
// Target_B.h #import <Foundation/Foundation.h> @interface Target_B : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end // Target_B.m #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
至此执行下一步,push到远端仓库后面再做,因为代码还要改动.
5.新建组件B与外界联系媒介B_Category项目及远端仓库,并同步.(参考3)
先在码云新建一个B_Category私有仓库,然后clone到本地,然后在本地目录新建Xcode项目,直接使用pod命令:
pod lib create B_Category
按提示输入选项,项目便创建好了,把项目文件中git相关的文件删掉,然后放到刚才clone到本地的路径.
打开项目Podfile,添加
pod \'CTMediator\'
注释掉本地引用,执行
pod install
新建分类
编写代码:
// CTMediator+B.h #import <CTMediator/CTMediator.h> @interface CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText; @end // CTMediator+B.m #import "CTMediator+B.h" @implementation CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"contentText"] = contentText; return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO]; } @end
至此执行下一步,同样push到远端仓库留到后面验证过代码没有问题再和其他组件一起做.
别忘了编辑B_Category.podspec文件,以及添加依赖
s.dependency \'CTMediator\'
参照前面.
6.主项目Podfile本地引用组件A和组件B,并使项目编译通过.
把前面的四个项目文件copy到MainProject文件夹,并编辑MainProject项目的Podfile文件
由于A_Section和B_Section中都依赖了HandyFrame,所以会自动pod \’HandyFrame\’,因此这里不写pod \’HandyFrame\’也可以.
执行
pod install
运行MainProject,成功.
此时在ViewController中还是直接引用的AViewController,组件化之后调用AViewController应该使用A_Category.
因此在ViewController.m文件中做如下修改:
#import "ViewController.h" #import <HandyFrame/UIView+LayoutMethods.h> #import "CTMediator+A.h" ... #pragma mark - event response - (void)didTappedPushAViewControllerButton:(UIButton *)button { UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController]; [self.navigationController pushViewController:viewController animated:YES]; }
编译通过之后,进行下一步,现在已经可以从ViewController访问AViewController,接下来就是从AViewController访问BViewController,所以接下来编辑AViewController.m文件:
#import "AViewController.h" #import <HandyFrame/UIView+LayoutMethods.h> #import "CTMediator+B.h" ... #pragma mark - event response - (void)didTappedPushBViewControllerButton:(UIButton *)button { UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"夏帆帆"]; [self.navigationController pushViewController:viewController animated:YES]; }
再次运行成功,至此本地引用测试通过.接下来就是改成远程私有库引用了.
7.将本地引用改为远程引用,运行项目并编译成功,组件化完成.
将B_Category运行编译通过之后,push到远端私有库,并打上tag:
git tag 0.0.1 git push --tags
本地验证.podspec文件
pod lib lint --allow-warnings
远程验证.podspec文件
pod spec lint --allow-warnings
将B_Category.podspec文件提交到之前添加的本地私有索引库PrivatePodRepo
pod repo push PrivatePodRepo B_Category.podspec --verbose --allow-warnings
远程验证和提交的时候很容易出现这个错误,比如:The `source_files` pattern did not match any file.
但是检查发现source_files路径是没问题的,出现这种情况的时候,先查看一下远端仓库和本地代码是否同步,tag是否相同,如果一切都没有问题却还是报这个错误,
去这个文件夹
/Users/Library/Caches/CocoaPods/Pods/External/项目文件
把 s.source_files = \’Example/B_Category/Classes/**/*\’,对应的Example文件夹拷贝到项目文件位置,再次提交即可.
这个问题我觉得是工具的锅,报错信息莫名其妙,而且我也没有发现确切的流程上有什么明显问题,因为不是我每次做都会出现这个错误,所以我只找到了解决这个问题的办法,但是还不知道如何完全避免,如果有谁知道怎样避免这个问题,欢迎不吝赐教.
同样我认为它不管什么问题都报一样的错误,有很大的误导性,这是软件设计的问题,因此我觉得并没必要花太多时间去纠结这些使用工具上细枝末节的问题,解决使用上的问题即可.
作为程序员,做出来的软件使用上麻烦容易出错,不用多考虑一定是开发者的有问题,而不是使用者的问题.(pod添加私有库的过程相当容易出错,尤其是最后push到仓库,很可能修改多次,push多次才能成功,因为只要有一点本地修改,以及版本号的修改,和远端仓库不一致,就不能添加成功,就要再push,再打tag,然后再尝试再发现问题再修改,循环这个过程直到.podspec成功添加到PrivatePodRepo仓库为止)
下面A_Category和B_Category一样,都是这个流程,就不多说了.
接下来将第6步中的AViewController和BViewController修改同步到copy之前的项目中,会发现A_Section编译报错,是因为它引用B_Category了但是还没有pod \’B_Category\’,将B_Category项目copy到A_Section,在Podfile文件中本地引用.
pod install
运行通过.
然后将本地引用改为远端引用:
pod install
运行成功,别忘了到.podspec文件中添加B_Category依赖,
但是到了这一步之后,验证就出现各种问题,
- ERROR | [iOS] unknown: Encountered an unknown error (Unable to find a specification for `B_Category` depended upon by `A_Section`
先是pod lib lint报以上错误,更改命令为:
Pod lib lint A_Section.podspec --sources=https://gitee.com/alan12138/PrivatePodRepo.git,https://github.com/CocoaPods/Specs.git --allow-warnings
将私有源添加到命令,依然会报错
- ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information. - NOTE | xcodebuild: note: Using new build system - NOTE | [iOS] xcodebuild: note: Planning build - NOTE | [iOS] xcodebuild: note: Constructing build description - NOTE | xcodebuild: /Users/zhangwei/Desktop/A_section/A_Section/Example/A_Section/Classes/AViewController/AViewController.m:11:9: fatal error: \'CTMediator+B.h\' file not found
竟然说找不到B_Category库的头文件,这个问题我没找到合适的解决办法,网上相关的资料也很少,而且这些组件化过程中pod遇到的各种问题,在所有相关文章中都基本没有提到,我不知道他们是不是真的用的那么顺利.cocoaPods 的github有这个问题的反馈,但是并没有解决,让到Stack Overflow去问,一样没有解决办法,浪费了很多时间之后,这里我准备放弃,前面我说过了,没必要花太多时间去解决工具的糟糕设计上产生的问题,因此我将这里B_Category直接放到本地.如果有人知道解决办法,欢迎不吝赐教.
综上也就是,在一个私有库中引用另外一个私有库,不论是本地引用还是远程引用,都会出现这个莫名其妙的 file not found ,因此我直接将B_Category导入A_Section项目使用,规避这个问题.
然后
pod install pod lib lint pod repo push PrivatePodRepo A_Section.podspec --verbose --allow-warnings
最后B_Section,参考上面流程,不再多说.
接下来回到MainProject,将本地pod改为远程pod:
pod install之后发现,
没有代码文件被下载下来,因此一定是目录出了问题,查看Git仓库目录发现,索引库中的.podspec文件中的s.source_files是从每个Pod的Git仓库目录中去找的,而在我的A_Category库中,最外层不是Example,而是A_Category:
因此修改A_Category.podspec文件的s.source_files:
再次提交A_Category.podspec索引,然后MainProject移除pod \’A_Category\’,pod install,再添加pod \’A_Category\’,再pod install,即可.
其他都同样修改.
最后pod install,运行成功,组件化完成.
因为pod仓库之间文件是互相不可见的,因此这里并不会出问题.这个样子也算是对pod奇葩问题最后妥协的结果.当然这样做是有很大问题的,所以你可以选择到第六步为止,摆脱了pod的折磨,也并不影响组件化.
当然也是为了尽快脱坑这篇文章,因为在这篇文章里为了解决各种各样的pod校验问题浪费了大量时间,而个人认为在这种工具软件的使用问题上浪费大量时间是非常不划算的一件事,除非你非它不可,或者是它的开发者. 而我尽量去列出遇到过的所有问题并给出找到的解决方法,遗憾的是最终有一个问题没有找到答案,我依然觉得这是cocoaPods本身的问题,使用过程中容易出问题的地方太多太多.
所以,与其浪费大量时间跟cocoapods较劲,不如做些更有意义的事,毕竟我们需要的是组件化的思想和实践,而不是pod的使用大全,而这些到第六步已经完成了.
p.p1 { margin: 0; font: 18px Menlo; color: rgba(163, 21, 21, 1); background-color: rgba(239, 255, 236, 1) }
p.p1 { margin: 0; font: 18px Menlo; color: rgba(163, 21, 21, 1); background-color: rgba(239, 255, 236, 1) }
p.p1 { margin: 0; font: 18px Menlo; color: rgba(163, 21, 21, 1); background-color: rgba(239, 255, 236, 1) }
span.s1 { color: rgba(0, 0, 255, 1) }