更新时间:2022-6-22
增加了参数回调的说明,并列举可以通过字典方式传递闭包然后进行参数的回调。更新时间:2022-9-1
补充子模块的.podspec文件内容。更新时间:2022-10-9
目前正在独立开发一款APP,准备使用这套组件化方案,预计明年上线,上线后我会更新实际项目使用中遇到的问题以及对组件化新的理解。更新时间:2024-3-14
独立设计开发的APP,万事口袋1.0.0第一次提交审核。更新时间:2024-3-29
万事口袋App 1.0.0 版本已上线全球,手机 App Store。更新时间:2024-5-13
万事口袋App 1.1.0 已更新,支持了 iCloud 同步以及英文
接下来准备编写关于swift组件化实际项目的总结。
前言
文字内容较多,内容较为枯燥,请做好准备
最近利用空闲时间搞了一下 Swift 的组件化/模块化。
该方案是学习和设计的组件化方案雏形,包含完整的Demo和文档。
如果你对该方案有什么问题或见解,欢迎评论或私信。
- 使用 Cocoapods 通过路由的方式做的组件化。
- 实现了页面跳转,复杂对象/图片等的传参与回调,错误监听等功能。
- 使用起来也比较方便,下边已放上组件化的Demo。
- 支持第三方 OC/Swift 库等的使用。
如果发现有哪些没有考虑到的地方,还请指出。
希望能给想要了解组件化的同学提供一下思路。
文章较长,请备好瓜子,可乐。
我个人对组件化的理解
- 首先,组件化也可以理解为模块化。
- 我们通过私有库的方式,将项目中的页面,功能等拆分出来制作成组件。
- 之后我们再将多个组件进行拼装,实现一个模块
- 最后将多个模块组装后变成一个完成的App。
组件化后每个模块都是独立开的,通过路由的方式跳转到其他页面,不会出现相互直接使用的方式。
我认为做组件化的目的:
- 在多人开发时可以更加方便,每个人只需要在自己的模块上进行代码的编写即可,合并代码时不容易造成冲突。
- 页面跳转时不需要引入其他的库或页面,降低耦合度。
- 封装过后的模块与组件可以更方便的复用。
我认为关于复用的部分,我的理解是做了组件化以后才做一部分功能的复用,而不是为了复用而做的组件化。
1.可以假设一个场景,例如你正负责直播模块的业务。
2.这时有了一个新的需求,需要在直播模块增加一个新的礼品页面,但是礼品的详情页使用另一个模块的详情页(详情页正在商品模块进行开发)。
3.正常情况下,如果你要开发,就需要先把自己的页面写好并且等待同事将详情页写好,然后合并代码后,将跳转详情页的地方改成对应的类名。
4.在使用了组件化后,你写完自己的模块,跳转的页面直接使用 path。如果没有bug后续就不需要管了。虽然详情页正在开发,但是对方只要在文档中提供了路径和参数,你就可以实现跳转。
5.而在主项目中,直接将直播模块进行升级即可,也不需要进行合并,降低了合并冲突的概率。
6.并且,正常情况下,如果详情页出现bug,那么对方将bug修复完成后,你也需要将代码拉取,合并等。如果跟你有关系的话你可能也需要改动。但是使用了组件化后,对方出现什么bug,也只是对方模块上的问题。只要不需要改参数,不需要改 path,自己的模块都不需要改动,并且改动完只需要将模块升级即可。
7.所以说降低了包括合并,交流,bug修改,职责分配等一系列的问题。
8.然后在组件化的基础上,对复用较多的业务,组件,逻辑等进行抽离。从而形成公共组件(所以组件复用率的高低并不是评判组件化是否必要的方式)。
当然组件化也是有一些缺点的
- 需要有详细的文档,需要标明页面名称,功能,参数,甚至跳转方式与动画。
- 需要检查相同模块是否有相同的路径,避免页面冲突。
- 会增加团队间的交流等(这里的交流指的是在需求研讨期间,对参数,路径,跳转方式等的交流。当进入到开发阶段,基本上就不需要交流了)。
- 还有一个缺点就是组件库会比较多,有时可能一个文件也需要搞一个组件库,目的是为了将他们的类别分清楚,同时也会间接的增加一些工作量。
废话不多说直接进入正题
在使用组件化时,需要学习 Cocoapods 私有库的搭建与使用,可以看我另一篇文章 私有库的搭建。
没有学会私有库搭建话,也可以先往下看了解下组件化思路。
点击进入 Demo 页面进行下载【2022-3-8 更新对 OC 库的使用示例】
该项目是可以运行的,运行项目可以更清晰的了解页面跳转传参的逻辑。注意:请使用iOS12.4及以上的真机或模拟器运行。
如果出现找不到第三方库的情况,先执行pod repo add https://gitee.com/fa_dou_miao/private-podspec.git
然后在【Podfile】文件中注释掉两个 pod,执行 pod install,然后再去掉注释执行 pod install。(相当于重新安装)
项目无法进行断点调试是因为关闭了
Debug executable
,可以在Edit Scheme...
中打开。
打开已经下载好的项目,该项目是 App 的主项目,可以理解为多个模块的壳。
结构并不复杂,代码也尽量精简过了,阅读起来不会很难,下边也会进行细致的讲解。
首先看一下主项目的 Podfile 文件
source "https://gitee.com/fa_dou_miao/private-podspec.git" source 'https://github.com/CocoaPods/Specs.git' # platform :ios, '9.0' target 'AppDemo' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for AppDemo pod 'A_Moudle' pod 'B_Moudle' end
两个 Source
一个是私有组件的索引地址,另一个是 Cocopods 的索引地址。
接着就是对两个私有模块的引入
这里我使用 A, B 两个模块进行举例。如果不方便理解,可以将 A 想象为登录模块,其中登录模块包括登录页,注册页,忘记密码页等多个页面。
B 模块可以想象为设置模块,包括设置页,关于页,退出登录页。这样可以方便理解
MyRouter
然后我们看一下最主要的路由模块 MyRouter (名字可以在编写时起适合的名字)
MyRouter 中包含两个文件
先看 【MyRouter.swift】 文件
import Foundation //MARK: - 模块协议 public protocol RouterMoudleProtocol { /// 模块名称 var moudle: String { get } /// 标识 var scheme: String { get } /// 路由列表 [path: className] var pathDic: [String: String] { get } } public extension RouterMoudleProtocol { /** 默认注册方法 */ func registerPages() { 通过该方法,将自定义模块中的 pathDic 注册(保存)到 MyRouter 单例中, 之后才可以通过路径查找对应的页面进行跳转 MyRouter.shared.registerMoudle(moudle, scheme: scheme, pathDic: pathDic) } }
首先是一个模块协议,在创建其他模块时只需要实现该协议即可,可以对照下边的例子阅读
以下是实现模块的方法,以 A 模块的 【A_Moudle.swift】 文件举例
该文件可以理解为是该模块的模块服务部分,主要职责是为路由和子页面建立索引路径,可以想象为一个模块中间件或模块目录这样子
import Foundation import MyRouter import OtherMoudle //MARK: - 模块A public class A_Moudle: RouterMoudleProtocol { 1.模块命名空间的名称:必须与当前模块名称相同,也就是与 import 时的名称相同 (这里实际上是该库的命名空间名称,因为没找到私有库中如何获取当前库的命名空间, 所以选择手写。如果你知道更好的方法,请在评论区指出,非常感谢!) public var moudle: String { "A_Moudle" } 2.模块标识:可以随意填写,只要不与其他模块冲突即可 public var scheme: String { "apps" } 3.路径字典:存储着路径与页面类名的对应关系, 每次增加新页面都需要在 pathDic 中添加对应的 path 与 className public var pathDic: [String: String] { ["pathA":"A_Controller", "pathA_Detail":"A_DetailController"] } 4.可以理解为在使用时,通过 url 拿到对应类的字符串名称,再将该名称转换为对应的类。 5.使用举例 "apps://pathA" 或 "apps://pathA_Detail" 将会跳转到对应页面或拿到对应的控制器 6.路径注册的类方法 public class func registerPages() { 因为在 Swift 中是不允许重写 load() 方法的,所以必须在主项目中导入该模块手动注册。 封装该方法也是为了在注册时更方便一些,可以直接通过类方法注册。 当然也可以直接使用下面的方法在主项目中注册。 查看【AppDelegate.swift】文件了解注册方式。 A_Moudle().registerPages() } }
继续查看 MyRouter类
属性部分//MARK: - 路由 public class MyRouter: NSObject { 单例方法 public static let shared = MyRouter() 错误通知:在跳转页面并且找不到对应的页面时会进行通知,监听该通知可以进行一些自定义操作, 例如弹出错误页面。查看【AppDelegate.swift】文件了解错误监听。 public static let routerErrorNotificaiton = "RouterErrorNotificaiton" // [scheme: [path: className]] 标识字典:通过模块标识获取对应的模块路径字典 private lazy var schemeDic = [String: [String: String]]() // [scheme: moudleName] 模块字典:通过模块标识获取对应的模块命名空间名称 private lazy var moudleDic = [String: String]() }
这里通过 schemeDic 存储对应模块的路径,而不是将所有的路径存在一起。
因为考虑到一个项目中可能有多个模块,大的项目甚至有六七十甚至上百个模块。而字典的本质是哈希表,随着内容的增多,可能会降低查找效率。所以将不同的模块分开存储,降低查找压力。
并且分开存储可能对后期路由功能的扩展产生一些帮助。
继续查看 MyRouter类
公共方法部分//MARK: - Public Action public extension MyRouter { /** 模块注册 - 模块注册调用该方法 - parameter moudle: 模块名称 - parameter scheme: 标识 - parameter pageClassName: 页面名称 */ 这里是模块注册部分,将对应模块的路径,标识进行存储 func registerMoudle(_ moudle: String, scheme: String, pathDic: [String: String]) { if moudleDic[scheme] == nil { moudleDic[scheme] = moudle } if schemeDic[scheme] == nil { schemeDic[scheme] = [String: String]() } schemeDic[scheme] = pathDic } /** 获取控制器 - parameter url: 路由 - parameter parameters: 传参 - parameter callBackParameters: 目标参数回调 - returns: 返回一个 UIViewController 控制器 */ 这里是获取控制器的部分,通过传入 url 配合参数和回调获取对应的控制器 func viewController(_ url: String, parameters: [String : Any]? = nil, callBackParameters: (([String: Any]) -> Void)? = nil) -> UIViewController? { guard let decoude = decoudeUrl(url), let moudle = moudleDic[decoude.scheme], let className = schemeDic[decoude.scheme]?[decoude.path] else { decoudeUrl() 方法先对 url 进行解码, 如果解码失败,拿不到对应的模块名称或路径对应的类名,则直接返回 nil return nil } 如果拿到了对应的类名,则转换为 UIViewController if let pageClass = MyRouter.moudleAnyClass(moudle, className: className), let pageType = pageClass as? UIViewController.Type { 然后通过对 UIViewController 的扩展,拿到对应的控制器 return pageType.routerController(parameters, callBackParameters: callBackParameters) }else { return nil } } /** 发送路由跳转错误通知 */ 当跳转页面时找不到对应页面,则会发送通知 func postRouterErrorNotification() { NotificationCenter.default.post(name: .init(MyRouter.routerErrorNotificaiton), object: nil, userInfo: nil) } }
继续查看 MyRouter类
私有方法部分//MARK: - Private Action private extension MyRouter { /** 对 url 进行解码 - parameter url: url - returns: (scheme: 标识, path: 路径)? */ 对 url 进行解码,我这里就分割了一下字符串,验证方式比较简单,实际可以做的更复杂些 func decoudeUrl(_ url: String) -> (scheme: String, path: String)? { let urlAry = url.components(separatedBy: "://") guard urlAry.count >= 2 else { return nil } return (urlAry[0], urlAry[1]) } }
继续查看 MyRouter类
其他方法部分//MARK: - OtherAction public extension MyRouter { /** 通过类名获取一个类 - parameter moudleName: 模块名称 - parameter className: 类名称 */ 这里就是通过命名空间与类名的拼接,将字符串转换为对应的类 AnyClass class func moudleAnyClass(_ moudleName: String, className: String) -> AnyClass? { var frameworksUrl = Bundle.main.url(forResource: "Frameworks", withExtension: nil) frameworksUrl = frameworksUrl?.appendingPathComponent(moudleName) frameworksUrl = frameworksUrl?.appendingPathExtension("framework") guard let bundleUrl = frameworksUrl else { return nil } guard let bundleName = Bundle(url: bundleUrl)?.infoDictionary?["CFBundleName"] as? String else { return nil } return NSClassFromString(bundleName + "." + className) } }
以上就是整个 MyRouter 文件的代码,主要功能是路径的存储和控制器的获取。
这时候可能会发现并没有实现页面跳转的方法。
因为如果在 MyRouter 模块 中实现了跳转方法,那么在使用的时候,其他模块的子页面就需要引入 MyRouter 模块 来进行跳转。就算通过模块服务文件【即 A_Moude.swift文件】进行一层封装,在使用时也需要通过【模块服务类】进行使用。
为了更加的方便,我选择使用扩展的方式对路由跳转功能进行封装。
继续阅读 MyRouter
控制器扩展 【ExtensionController.swift】 文件首先是页面跳转部分
import Foundation //MARK: - 跳转页面 extension UIViewController { /** 返回到上一个页面 */ 返回到上一个页面,由于模块是分割开的,有些页面可能即支持 push 跳转,又支持 present 跳转, 所以需要进行判断跳转方式再进行返回, 在返回上一页时控制器直接调用该方法即可 【self.dismissRouterController(animated: true)】 @objc open func dismissRouterController(animated: Bool) { let children = self.navigationController?.children if children?.count ?? 0 > 1 && children?.last == self { self.navigationController?.popViewController(animated: animated) }else { self.dismiss(animated: animated, completion: nil) } } /** 通过 url 的方式 present 一个控制器 - 可重写:重写需要在最后调用 super 方法实现跳转 - parameter url: 路由 - parameter parameters: 可选参数 - parameter animated: 是否执行动画 - parameter callBackParameters: 目标参数回调 */ 通过 url 的方式 present 到下一个页面 @objc open func presentRouterControllerWithUrl(_ url: String, parameters: [String: Any]? = nil, animated: Bool = true, callBackParameters: (([String: Any]) -> Void)? = nil) { presentRouterController(MyRouter.shared.viewController(url, parameters: parameters, callBackParameters: callBackParameters), animated: animated) } /** 通过 viewController 的方式 present 一个控制器 - 可重写:重写需要在最后调用 super 方法实现跳转 - parameter viewController: target viewController - parameter animated: 是否执行动画 */ 通过 viewController 的方式 present 到下一个页面 (考虑到有些情况可能会先拿到控制器进行一些操作再跳转,所以可以使用该方法进行跳转) @objc open func presentRouterController(_ viewController: UIViewController?, animated: Bool = true) { guard let vc = viewController else { // 找不到控制器,发送错误通知 MyRouter.shared.postRouterErrorNotification() return } present(vc, animated: animated, completion: nil) } /** 通过 url 的方式 push 一个控制器, 需要带有 navigationController - 可重写:重写需要在最后调用 super 方法实现跳转 - parameter url: 路由 - parameter parameters: 可选参数 - parameter animated: 是否执行动画 - parameter callBackParameters: 目标参数回调 */ 通过 url 的方式 push 一个控制器 @objc open func pushRouterControllerWithUrl(_ url: String, parameters: [String: Any]? = nil, animated: Bool = true, callBackParameters: (([String: Any]) -> Void)? = nil) { pushRouterController(MyRouter.shared.viewController(url, parameters: parameters, callBackParameters: callBackParameters), animated: animated) } /** 通过 viewController 的方式 push 一个控制器, 需要带有 navigationController - 可重写:重写需要在最后调用 super 方法实现跳转 - parameter viewController: target viewController - parameter animated: 是否执行动画 */ 通过 viewController 的方式 push 一个控制器 @objc open func pushRouterController(_ viewController: UIViewController?, animated: Bool = true) { guard let navigationController = self.navigationController else { // 找不到 navigationController 发送错误通知 MyRouter.shared.postRouterErrorNotification() return } guard let vc = viewController else { // 找不到控制器,发送错误通知 MyRouter.shared.postRouterErrorNotification() return } navigationController.pushViewController(vc, animated: animated) } }
以上是页面跳转的部分,因为在【模块服务文件】必须引入MyRouter,所以模块内的子页面都可以通过扩展的方法进行页面跳转,不需要在子页面中再次引用【模块服务类】或 【路由类】
继续阅读 MyRouter
控制器扩展 【ExtensionController.swift】 文件接下来是控制器获取部分
//MARK: - 获取控制器 extension UIViewController { /** 返回当前的控制器 可通过重写该方法,对传入的参数进行初始化,赋值等操作 - 可重写:重写不需要调用 super 方法 - parameter parameters: 可选参数 - returns: 返回一个 UIViewController 控制器 */ 首先是返回当前的页面。 当其他模块通过 url 获取当前页面时会调用该方法。 默认情况是不需要传参直接创建一个自己页面的对象进行返回。 如果当前页面需要传参才可以使用的话,可以重写该方法, 然后对 parameters (传过来的参数进行操作),然后再判断是否应该返回当前控制器 @objc open class func routerController(_ parameters: [String: Any]? = nil, callBackParameters: (([String: Any]) -> Void)? = nil) -> UIViewController? { // 返回一个控制器 return self.init() } /** 通过 url 获取目标控制器 - parameter url: 路由 - parameter parameters: 传参 - parameter callBackParameters: 目标参数回调 - returns: 返回一个 UIViewController 控制器 */ 有时会需要拿到一个其他模块的控制器,但是不需要跳转。 可以通过该方法进行目标控制器的获取 @objc open func viewController(_ url: String, parameters: [String : Any]? = nil, callBackParameters: (([String: Any]) -> Void)? = nil) -> UIViewController? { MyRouter.shared.viewController(url, parameters: parameters, callBackParameters: callBackParameters) } }
举例:页面传参以及参数回调的使用
以下代码来自【A_Controller.swift】文件//MARK: - Action extension A_Controller { /** 点击跳转按钮 实际上,可以不使用演示中这种参数回调的方法,因为闭包是可以通过字典传递的。 例如, let callBack: ((Int) -> Void) = { value in print("闭包传递返回:\(value)") } let datailParameters: [String: Any] = ["block": callBack],通过这种方式将闭包传过去,然后对方再调用闭包也可以实现参数的回调,代码中只是其中一种方式。 */ @objc private func clickButton() { let datailParameters: [String: Any] = ["id": "id123", "name": "name123", "image": UIImage()] self.pushRouterControllerWithUrl("apps://pathA_Detail", parameters: datailParameters, animated: true) { parameters in // 页面参数回调 print("==========") print("页面参数回调") print("当前页面: A_Controller") print("参数来自: apps://pathA_Detail") print("参数内容: \(parameters)") print("==========") } } }
举例:传参获取以及参数回调的使用
以下代码来自【A_DetailController.swift】文件//MARK: - Action extension A_DetailController { /** 重写该方法进行参数获取 - parameter parameters: 传入的参数 - parameter callBackParameters: 数据回调 */ public override class func routerController(_ parameters: [String : Any]? = nil, callBackParameters: (([String : Any]) -> Void)? = nil) -> UIViewController? { if let id = parameters?["id"] as? String, let name = parameters?["name"] as? String{ // 拿取参数 // 可以自定义初始化传参的方式 // 不建议使用 init 传参,如果通过 init 传参一定要重写该方法,否则跳转时会崩溃 let vc = A_DetailController(id: id, name: name) // 可以将需要的参数通过属性的方式传参 vc.image = parameters?["image"] as? UIImage // 在需要的地方通过 callBackParameters 进行参数回调 vc.callBackParameters = callBackParameters return vc }else { // 如果该页面必须拿到参数才可以跳转,拿不到必要参数则返回空页面 // 当 Router 收到空页面时,会在 present 或 push 时发送错误通知并中断跳转 // 如果 app 监听了错误通知,可以手动弹出一个错误页面或进行其他操作 return nil } } }
路由模块总结
以上就是整个路由模块部分了,该模块主要功能包含路径存储,页面获取与跳转,错误通知。
其实还有很多值得优化的地方,例如路由解码部分,可以封装一个 web 解析页面,当检测到传入的路径为 webUrl 的情况时,自动跳转到 web 页面。
也可以对 url 解析做的更复杂一些,根据实际的情况对路由模块进行补充与扩展。
关于第三方库的使用
那么一个项目,除了自己写的页面外,还会使用第三方工具。
一般在项目中我们会直接在主工程中引入第三方库,然后对该库进行封装然后使用。
那么在组件化中如何保持解耦的情况下还能使用第三方库呢
我想到的方法是二次封装。
如何理解二次封装
我们以 Alamofire 举例:
- 先创建一个公共库模块,也就是项目中的【OtherMoudle(名字为了演示用,请根据实际功能起名)】
- 通过【OtherMoudle】引入 Alamofire ,并对其进行封装。
【以下是 OtherMoudle 中的 NetworkManager.swift 文件】
// // NetworkManager.swift // OtherMoudle // // Created by 发抖喵 on 2022/1/30. // // 为网络请求进行第一次封装,仅供演示 // 在使用前需要模块服务导入该组件,并对组件再次进行继承,也就是二次封装 import Foundation import Alamofire open class NetworkManager: NSObject { open func request(_ url: URLConvertible) { AF.request(url) // 演示代码 } }
- 我们对网络请求的功能进行了第一次封装。
也就是说,所有的业务模块,都需要引入该模块,才能使用网络请求的功能。
然后我们到业务模块中
业务模块想使用网络请求功能怎么办呢?
【以下是 A_Moudle 中的 A_Moudle.swift 文件】//MARK: - 演示用途, 可进行二次封装, 单独为该模块进行封装 class A_ModuleNetWorkManager: NetworkManager { static let shared = A_ModuleNetWorkManager() }
我们不能直接使用 NetworkManager,而是对 NetworkManager 再封装一层,也就是第二次封装
- 于是我们就可以在自己模块中使用属于自己的网络请求功能,而不需要再次引入 【OtherMoudle】模块。
- 并且,我们可以根据模块的需求,在父类的基础上自定义属于自己的网络请求方式。
- 这种方式用起来比较复杂,因为需要多写一部分代码,但他的优点是可以根据模块不同的需求进行自定义的封装与使用,并且,如果基础库更换,那么也只需要修改封装部分的代码,业务部分代码不需要改动。
- 其他的库也是如此,在第三方库模块中进行第一次封装。在需要使用的【模块服务】文件中进行第二次封装。
- 如果不需要自定义,直接继承即可。如果需要自定义,继承后再根据需求进行重写等。
基础模块也是如此,
通过对基础模块(常量值等)进行一次封装,
再在需要使用的【模块服务】文件中进行二次封装即可使用,
有些基础配置模块可能不需要改动,可以不进行二次封装,具体看使用情况。
大体功能与逻辑解释的差不多了,接下来是实际项目中的使用流程
创建一个新的项目(或对已有项目的功能进行拆分)
- 创建基础模块(常量,key等)
创建基础工具模块(网络请求库,弹窗库等)
- 创建业务模块,必须引入路由模块并实现协议,其他需要啥引入啥(例如A_Moudle 或 B_Moudle)【补充:这里的 pathDic 类型也可以使用 [string: AnyClass],好处是在不需要通过字符串转换类,也就不需要手写 moudle 属性了,缺点是占用内存比字符串更多,因为这是需要注册常驻在内存中,类占用的内存是大于字符串的】
- 在主项目中 pod 业务模块,注意添加 source
在主项目中的 AppDelegate 文件中,引入每一个业务模块并进行注册(未注册的模块是无法找到对应路径的)
监听路由错误的通知,并进行自定义操作。
可以通过 url 获取对应的控制器创建 rootViewController
// // AppDelegate.swift // AppDemo // // Created by 发抖喵 on 2022/1/27. // import UIKit import MyRouter import A_Moudle import B_Moudle @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // register A_Moudle.registerPages() B_Moudle.registerPages() // 监听路由错误通知 NotificationCenter.default.addObserver(self, selector: #selector(routerErrorNotification), name: .init(MyRouter.routerErrorNotificaiton), object: nil) // rootVC window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = rootController() window?.makeKeyAndVisible() return true } /** 演示用 */ func rootController() -> UIViewController { if let a_VC = MyRouter.shared.viewController("apps://pathA"), let b_VC = MyRouter.shared.viewController("bpps://path/b") { let tabVC = UITabBarController() tabVC.addChild(UINavigationController(rootViewController: a_VC)) tabVC.addChild(UINavigationController(rootViewController: b_VC)) tabVC.tabBar.tintColor = .orange tabVC.tabBar.unselectedItemTintColor = .gray return tabVC } return ErrorViewController() } @objc func routerErrorNotification() { print("收到错误信息, 弹出一个错误页面") window?.rootViewController?.present(ErrorViewController(), animated: true, completion: nil) } }
总结
好了,以上就是我对组件化的总结了,希望能对想学组件化的同学有帮助
方案可能会有考虑不周或不是最优方法的情况,如果你有疑问或更好的方式,都可以提出来,非常感谢!
🥺🥺🥺🥺🥺如果觉得有用就点个赞吧,你的赞是我最大的动力🥺🥺🥺🥺🥺
关于图片资源的补充
- 日常开发中经常会用到图片,图标等资源。
- 在使用组件化之前我们的大部分图片都是存放在【Assets.xcassets】中。
- 在使用组件化之后,可以将图片放到每个模块的资源文件中。(但是这样会造成不同模块使用了相同图片的问题,从而导致打包后文件过大)
我个人认为可以将图片抽成一个基础组件,并增加一个和页面文档相同的图标文档。
该文档由设计负责分类,命名。
- 每次在设计的时候,先到图标文档查找是否有合适的图标。
- 如果有的话在文档中标出已有图标名称,如果没有的话再设计新图标添加到文档中
- 并告知负责该模块的开发人员添加新图标并更新库(命名与文档中命名相同)。
- 在使用的时候,其他模块只需要引用该图标模块即可,不会造成图片重复等问题(封装库时需要注意图片库的 Bundle 名称问题),以及图片在私有库中可以直接使用文件的方式加载但需要注意区分@2x与@3x的文件,也可以使用【xcassets】资源文件的方式加载,但是需要在【podspec】文件中设置【resource_bundles】参数)。
关于其他组件与 OC 库的引用
一个项目中除了正常的业务组件,还有有用户信息,产品配置文件等。
对于基本配置文件,如果该文件或者说是该模块不会在业务模块中用到,怕麻烦的话是可以直接放到主工程中的。这类文件一般会在 APP 启动时使用,之后很少改动也不需要在其他业务中使用。
对于用户本地信息这类部分,我个人是实现一个单例类,将其定义为用户配置组件。因为这类组件在业务中会使用到。例如展示用户名,修改用户信息等。
私有库除了 Swift 文件一定还需要引用一些 OC 的库或文件,对于 pod OC 的库,直接引用并使用 Swift 类对主要使用的 OC 类进行继承即可,对于自己创建的 OC 文件,因为可能无法继承,所以创建一个新的类将 OC 类作为一个属性存储使用即可(目前是这样的,找到更好的方法话会更新文档)
我的理解就是在组件化中,万物皆组件。制作组件的目的是以提高开发效率为主,其次是模块的复用。
获取自定义 View 的方式
除了 UIViewController 可以通过扩展的方式获取以外,自定义的 View 也可以通过相同的方式获取,代码都是相同的我就不再写例子了,大家举一反三一下就可以。
补充子模块的.podspec文件内容
A_Moudle'
# # Be sure to run `pod lib lint A_Moudle.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = 'A_Moudle' s.version = '0.3.7' s.summary = '组件化模块A' # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? # * Try to keep it short, snappy and to the point. # * Write the description between the DESC delimiters below. # * Finally, don't worry about the indent, CocoaPods strips it! s.description = <<-DESC TODO: Add long description of the pod here. DESC s.homepage = 'https://gitee.com/xxxxxx/a_moudle' # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'trembleCat' => 'xxxxxx@163.com' } s.source = { :git => 'https://gitee.com/xxxxxx/a_moudle.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>' s.ios.deployment_target = '10.0' s.swift_version = '4.2' s.source_files = 'A_Moudle/Classes/**/*' s.dependency 'MyRouter' s.dependency 'OtherMoudle' # s.resource_bundles = { # 'A_Moudle' => ['A_Moudle/Assets/*.png'] # } # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' end
B_Moudle
# # Be sure to run `pod lib lint B_Moudle.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = 'B_Moudle' s.version = '0.3.5' s.summary = 'A short description of B_Moudle.' # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? # * Try to keep it short, snappy and to the point. # * Write the description between the DESC delimiters below. # * Finally, don't worry about the indent, CocoaPods strips it! s.description = <<-DESC TODO: Add long description of the pod here. DESC s.homepage = 'https://gitee.com/xxxxxx/b_moudle' # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'trembleCat' => 'xxxxxx@163.com' } s.source = { :git => 'https://gitee.com/xxxxxx/b_moudle.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>' s.ios.deployment_target = '10.0' s.swift_version = '4.2' s.source_files = 'B_Moudle/Classes/**/*' s.dependency 'MyRouter' s.dependency 'OtherMoudle' # s.resource_bundles = { # 'B_Moudle' => ['B_Moudle/Assets/*.png'] # } # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' # s.dependency 'AFNetworking', '~> 2.3' end
OtherMoudle
# # Be sure to run `pod lib lint OtherMoudle.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = 'OtherMoudle' s.version = '0.1.5' s.summary = '对其他组件库的封装Demo' # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? # * Try to keep it short, snappy and to the point. # * Write the description between the DESC delimiters below. # * Finally, don't worry about the indent, CocoaPods strips it! s.description = <<-DESC TODO: Add long description of the pod here. DESC s.homepage = 'https://gitee.com/xxxxxxx/other-moudle' # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'trembleCat' => 'xxxxxxx@163.com' } s.source = { :git => 'https://gitee.com/xxxxxx/other-moudle.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>' s.public_header_files = "Pod/Classes/*.h" s.ios.deployment_target = '10.0' s.swift_version = '4.2' s.source_files = 'OtherMoudle/Classes/**/*' s.dependency 'SnapKit' s.dependency 'Alamofire' s.dependency 'Kingfisher', '~> 5.15.8' s.dependency 'YYText' # s.resource_bundles = { # 'OtherMoudle' => ['OtherMoudle/Assets/*.png'] # } # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' # s.dependency 'AFNetworking', '~> 2.3' end