iOS组件化【Swift组件化方案】
寂静海岸(加载图).jpg
最近更新时间:2022-5-5
前言
最近利用空闲时间搞了一下 Swift 的组件化/模块化。
该方案是学习和设计的一个组件化方案雏形,包含完整的Demo和文档。
如果你对该方案有什么问题或见解,欢迎评论或私信。
- 使用 Cocoapods 通过路由的方式做的组件化。
- 实现了页面跳转,复杂对象/图片等的传参与回调,错误监听等功能。
- 使用起来也比较方便,下边已放上组件化的Demo。
- 支持第三方 OC/Swift 库等的使用。
如果发现有哪些没有考虑到的地方,还请指出。
希望能给想要了解组件化的同学提供一下思路。
文章较长,请准备好瓜子,可乐。
我个人对组件化的理解
- 首先,组件化也可以理解为模块化。
- 我们通过私有库的方式,将项目中的页面,功能等拆分出来制作成组件。
- 之后我们再将多个组件进行拼装,实现一个模块
- 最后将多个模块组装后变成一个完成的App。
这里我画了一个组件化前和组件化后项目的导入文件路径图(画的不准确请见谅)
组件化前
组件化前
组件化后
组件化后
组件化后每个模块都是独立开的,通过路由的方式跳转到其他页面,不会出现相互直接使用的方式。(这里有个小错误,第一个模块A服务,后边应该是B服务,C服务。以及主工程对配置组件库只有包含功能,没有引用功能。懒得改了问题不大)
我认为做组件化的目的:
- 在多人开发时可以更加方便,每个人只需要在自己的模块上进行代码的编写即可,合并代码时不容易造成冲突。
- 页面跳转时不需要引入其他的库或页面,降低耦合度。
- 封装过后的模块与组件可以更方便的复用。
我认为关于复用的部分,我的理解是做了组件化以后才做一部分功能的复用,而不是为了复用而做的组件化。
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。(相当于重新安装)
打开已经下载好的项目,该项目是 App 的主项目,可以理解为多个模块的壳。
结构并不复杂,代码也尽量精简过了,阅读起来不会很难,下边也会进行细致的讲解。
首先看一下主项目的 Podfile 文件
source "https://gitee.com/fa_dou_miao/private-podspec.git" source 'https://github.com/CocoaPods/Specs.git's # 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.png
先看 【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 { /** 点击跳转按钮 */ @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】模块。
- 并且,我们可以根据模块的需求,在父类的基础上自定义属于自己的网络请求方式。
- 这种方式用起来比较复杂,因为需要多写一部分代码,但他的优点是可以根据模块不同的需求进行自定义的封装与使用,并且,如果基础库更换,那么也只需要修改封装部分的代码,业务部分代码不需要改动。
- 其他的库也是如此,在第三方库模块中进行第一次封装。在需要使用的【模块服务】文件中进行第二次封装。
- 如果不需要自定义,直接继承即可。如果需要自定义,继承后再根据需求进行重写等。
基础模块也是如此,
通过对基础模块(常量值等)进行一次封装,
再在需要使用的【模块服务】文件中进行二次封装即可使用,
有些基础配置模块可能不需要改动,可以不进行二次封装,具体看使用情况。
大体功能与逻辑解释的差不多了,接下来是实际项目中的使用流程
创建一个新的项目(或对已有项目的功能进行拆分)
image.png
- 创建基础模块(常量,key等)
创建基础工具模块(网络请求库,弹窗库等)
image.png
- 创建业务模块,必须引入路由模块并实现协议,其他需要啥引入啥(例如A_Moudle 或 B_Moudle)【补充:这里的 pathDic 类型也可以使用 [string: AnyClass],好处是在不需要通过字符串转换类,也就不需要手写 moudle 属性了,缺点是占用内存比字符串更多,因为这是需要注册常驻在内存中,类占用的内存是大于字符串的】
image.png
- 在主项目中 pod 业务模块,注意添加 source
image.png
在主项目中的 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 也可以通过相同的方式获取,代码都是相同的我就不再写例子了,大家举一反三一下就可以。
寂静海岸(加载图).jpg
组件化前
组件化后
MyRouter.png
image.png
image.png
image.png
image.png