iOS(Swift) 组件化从本地pod开始

9,091 阅读11分钟

PS

因本人文笔有限,在写完这篇文章后,自觉读起来十分拗口,所以通篇让gpt帮我润色了一下,敬请见谅

引子

  • 在上古洪荒的iOS修炼界,修炼者众多而杂乱,大多数人只注重将代码敲定,功能实现,而往往忽略了修为的真正提升——那是需要在不断的实践中,对基础知识的夯实与技能的锤炼。而当提到组件化与模块化这种高阶功法时,多数修炼者无疑感到步履艰难,甚至望而却步。这些技艺不仅看似缓慢、费力,而且难以立即见效,因此并不受欢迎。

  • 尤其是一些初入练气期的新手,他们在讨论组件化和模块化时,常常觉得这些概念虽然知晓,却极为抽象,且实际操作起来颇为繁琐。在小门派的iOS圈中,修炼者寥寥无几,手头的项目也是屈指可数,很多时候,直接复制文件夹似乎比深入探索组件化要来得简单快捷。因此,尽管拥有不少工具集,但大多数人还是选择了简单粗暴的复制方式,慢慢地,组件化的修炼法门便从许多小公司的iOS开发者的视线中渐渐淡出。

  • 这是一个需要改变的局面。真正的修炼,不仅仅是表面功夫的堆砌,而是对内功的深度挖掘和精练。只有真正理解并实践组件化与模块化,iOS修炼者们才能在繁杂的开发海洋中游刃有余,将各种功能、模块灵活运用,最终达到代码的高境界,从而让开发之路更加顺畅,效率更高,维护更简单。在未来的修炼路上,这将是区分高下的关键一步。

接下来,我将以一个小公司iOS修炼者的视角,浅谈我自己在组件化道路上的探索和实践。希望我的经验能为同样在小公司修行的iOS开发者们提供一些启示和帮助。

原始项目

image.png

展示的项目架构较为清晰:Module目录存放所有业务模块,如登录(login)、直播(live)等;而CommonModule用于存放公共工具及业务基础模块,例如网络封装和各类组件。然而,这种架构存在不少问题。通常因为框架同步更新不及时,开发人员往往不会仔细阅读源码,而是专注于自己的业务开发,甚至在系统类中随意扩展业务方法或属性,逐渐导致框架的侵蚀。

我们的项目中使用的这一常见框架虽然修改方便,但也暴露了多个问题,这些问题可能随时间逐步显现:

  • 三方的集成本就是重复性无意义的工作,如果起多个项目,每次都要阅读文档,手动集成一遍,浪费时间,还有可能出错.比如内购,推送,facebook登录,谷歌登录,苹果登录,aws文件上传,firebase埋点,bug分析等等.
  • 业务组件与公共组件混合,如GradientView这样的公共组件在不同项目中可以广泛使用,而UserBadge(用户标签)则是根据本项目用户信息定制的组件,其样式在其他项目中需要改动,这种组件并不适合随项目转移。
  • 开发人员可以轻易修改主package中的公共组件,这种开放的修改权限极易导致公共组件功能的侵蚀,使其逐渐失去公共组件的特性。
  • 在Swift项目中,由于不需要显式import,公共组件可能不经意间掺杂了业务数据。例如,为了方便特定业务,开发人员可能将业务逻辑直接写入网络请求的底层代码中,这种做法将破坏框架的健康,理想情况下,网络请求应保持抽象,通过注册拦截器来处理加密、解密等操作,而不是在网络层直接加入业务代码。
  • 公共模块和组件的不统一会导致开发人员各自为战,大量重复的功能组件或扩展因未统一而泛滥,从而增加了项目的维护难度。
  • 对于跨多个项目的管理,如果不是集中维护公共组件,开发人员因不熟悉现有的代码结构可能会随意添加第三方依赖,使项目变得更加混乱。
  • 启动新项目时,复制CommonModule需要进行大量的修改,这无疑增加了项目启动的难度和复杂性。

总的来说,虽然当前的框架在一定程度上简化了开发流程,但为了长远的健康发展,我们需要对架构进行审视和优化,以应对上述问题。

所以在这一基础上,我做了公共模块与业务模块的拆分.

2.0公共模块拆分后项目

2.0的公共模块拆分中,我将框架做成了一个项目内的本地pod库

  • 这一步非常的重要!!!
  • 这一步非常的重要!!!
  • 这一步非常的重要!!!

为什么很重要,因为这一步拆分好了,剩下做子模块管理就十分十分简单了.

我们需要隔离环境,慢慢将抽象的,或者与业务无关的组件抽离开来,单独抽象到一个Pod里,作为一个基础pod,并且子模块管理其他公共组件pod.这里大致介绍一下我前期的一些封装

  • Base(基础子模块)
    • Component组件,布局相关常用的view封装
      • 渐变,雷达,扫描
      • 轮播,定时器,window,segment,pager,pageControl,textfield等等等等
      • 环形进度,渐变扇形进度等等
      • collection各类布局,左右布局,瀑布流等等等等等
    • Factory工厂方法
    • Util
      • LLoger日志打印
      • DateUtil时间相关
      • CryptUtil加密相关
      • KeyChain与KVStore存储相关
      • 以及一些其他零零碎碎内容
      • Router(抽象路由),提供注册路由能力
    • Protocol
      • 常用协议相关
    • Extension拓展类
      • Foundation+
      • UIKit+
    • Value
      • 数值相关的,比如别名,固定数据数据等
  • Network(网络请求模块)
    • moya网络请求抽象
    • APIError,APITarget,APIProvider,APIResultProtocol,APIPlugin
    • 将加密plugin与result抽象出去
  • IAP(内购模块)
    • 内购抽象
  • IM(Socket模块)
    • 公司内部IM链接抽象层
  • ImagePicker
    • 图片选择,依赖了一个三方TZ做成组件,这里抽象出来抽象出来
  • Facebook
    • 登录抽象
  • Firebase
    • 崩溃集成
    • 埋点分析
  • AWS
    • AWS文件上传抽象
  • Permission权限控制相关,每个权限都需要申请相应权限,做成了子模块
    • Location定位
    • Camera
    • Photo
    • Microphone
    • 权限抽象后续都可以按需添加

在这篇文章中,我没有展示所有的细节,因为内容太多会显得冗长。以下是关于本地Pod的基本情况。由于公司时间资源的限制,很多部分本应进一步细化和拆分,例如UI组件。但在当时,除了需要草草完成本地Pod之外,我还负责完全重构整个直播模块并编写示例框架,因此只能尽力而为。 image.png

下面是子模块的依赖关系,项目可以根据需要选择依赖。

image.png

image.png

image.png

image.png

image.png

子模块的依赖如下,项目可以按需依赖即可. image.png

最初,我设想将所有功能整合到一个大的Pod中,让各个项目根据需求进行依赖。虽然这个设想在理论上很美好,实际操作中我也投入了大量工作,但新的项目并不多,基本上是我一个人在维护这些基础库。然而,在实际开发过程中,大Pod的一些问题逐渐显现:

  • 当处理公司内部的IM依赖时,我被迫将整个podspec修改为使用静态框架(s.static_framework = true),这让我感到不满。
  • Podspec文件由于子库众多,看起来非常杂乱无章。

基于这些问题,完成直播项目后,我开始逐步将一些模块拆分成独立的Pods,如我们的内部本地埋点库等。我还计划将之前整合的一些库慢慢分离出来,以期实现更好的模块管理和维护效率。

3.0子模块管理项目

在2.0时代的项目管理中,本地项目会随主项目的提交一起被提交,使用同一个Git仓库。这样做有其明显的优势:

  • 实时更新:更改立即可见,并直接编译。
  • 效率提升:特别是在创建pod库的初期,这种做法可以快速开发多个库,无需搭建单独的Git仓库。

然而,这种做法在需要保证使用同一个公共库的场景下显得不够理想。项目中的库随项目变动,尽管这种变动不需要剔除业务中的公共组件,但实质上仍是一种复制操作。

为了解决这个问题,可以考虑将公共库做成一个独立的Git提交。但这又引入了新的问题:项目初期公共pod库并非一成不变,独立的Git仓库需要维护。这意味着我们的操作流程变为:

  • 将代码打包成pod库项目,进行修改和测试。
  • 提交pod库的代码,并打上新的标签(tag)。
  • 回到主项目,执行pod install来更新。

对于初期一个人维护多个库的情况,这种跨文件夹的操作可能会变得相当繁琐——毕竟除了写业务代码外,还需抽出时间来维护pod库。

为了简化这种管理过程,我选择使用Git子模块来管理。通过这种方式,项目的结构可以优化为使用SourceTree的子模块目录,从而提高效率并减轻维护负担。这不仅保持了代码的模块化和解耦,还提供了更灵活的版本控制和依赖管理。

image.png

image.png

子模块项目在本地项目位置,修改完项目直接提交即可

image.png

现在,我们已经成功地将原先的庞大pod库拆分为多个小型pod,并采用submodule方式进行管理。现在,每个小pod的依赖结构都变得非常清晰。修改代码时,可以直接通过SourceTree或者直接在某个主项目中调整基础库代码,提交更新后,这些更改会自动反映到使用该库的所有其他项目中,无需手动复制公共pod库。如果其他项目也采用submodule来管理,并且具备修改权限,团队成员甚至可以参与到公共库的维护中,这极大地方便了协作和管理。

当然,这个过程中有许多值得注意的细节,这里就不详细展开了。我希望大家能够亲自尝试,体验这种方法

聊聊CocoaPods与SPM的优劣

我个人对SPM的偏爱

  • 简洁高效.package 文件的操作简单快捷,将已有的CocoaPods库转换支持SPM也非常方便。
  • 官方支持:SPM作为官方支持的依赖管理工具,代表了未来的大趋势。许多国际上的第三方库已逐渐转向仅支持SPM,尽管国内对此的反应相对滞后。

然而,SPM的局限性让我犹豫

  • 更新延迟问题:SPM在每次打开项目时都会尝试更新依赖,如果依赖库数量较多,这将非常繁琐,频繁出现加载界面。此外,对于网络连接不佳的同事来说,这种更新机制尤其成问题。
  • 依赖冲突:据我所知(如有误请指正),如果SPM和CocoaPods同时依赖同一个库(如Alamofire),它们会在不同的文件夹中生成两份代码,这种重复我认为是不理想的。理想情况下,我希望能够合并为一份代码。
  • 项目责任:作为项目和团队的负责人,综合评估后我发现SPM目前还不适合我们的项目环境。

不过,不论是SPM还是CocoaPods,git子模块都是一个通用的解决方案。作为框架的维护者,子模块管理使我能够轻松修改各个子库的代码,而无需一一寻找原始代码库。更重要的是,git子模块作为git的功能,并不局限于iOS开发;无论是Android、JS还是Java,都可以从中受益。我鼓励大家深入了解并利用这一强大的功能。