从0到1:我在iOS项目中的一次技术探索与实践

优雅_探险家
2025-06-18 14:43
阅读 537

大家好,我是一名有五年工作经验的iOS开发工程师。今天想跟大家分享一次让我印象特别深刻的技术探索经历——如何在一个实际项目中引入并落地一个原本我们团队都比较陌生的新技术栈。

这段经历发生在我去年参与的一个电商类App重构项目中。当时我们的需求是把原有以MVC为主的代码结构逐步过渡到更现代化的状态管理方案,并尝试引入一套新的UI组件库来统一界面风格、提升开发效率。这听上去挺合理,但在真正实施过程中,却遇到了不少“真·技术难题”和“人祸型问题”。

背景介绍:为什么要做这次技术调整?

背景介绍:为什么要做这次技术调整?

这个App已经上线几年了,随着功能越来越多、业务逻辑越来越复杂,原本基于MVC模式的架构逐渐暴露出一些问题:

  • View和Controller耦合度高,维护困难;
  • 状态管理混乱,多个页面之间数据同步容易出错;
  • UI一致性差,不同人写的界面风格差异大;
  • 单元测试覆盖率低,回归测试耗时长。

我们在评估了一段时间之后,决定开始尝试转向以MVVM为核心架构,同时结合Combine(因为Swift 5.3支持得更成熟了)进行响应式编程,并引入了一个内部孵化但尚未完全成熟的UI组件框架。

听起来是不是有点像“既要改房子的地基,还要装修”?没错,这就是当时摆在我们面前的挑战。


遇到的问题:理想很丰满,现实很骨感

遇到的问题:理想很丰满,现实很骨感

技术层面的挑战

刚开始推进的时候,我们主要遇到了以下几类问题:

  1. 异步状态处理混乱:旧代码里很多网络请求直接写在View Controller里,各种completion block嵌套调用,导致状态追踪和错误处理非常麻烦。
  2. 状态共享机制缺失:登录信息、购物车状态等需要多页面共用的数据,只能通过NotificationCenter或者单例来传递,极易出错且难以调试。
  3. 新UI组件不兼容老界面:我们选择的新UI组件库虽然统一了样式风格,但某些交互行为与旧页面存在冲突,比如导航栏样式、按钮点击反馈等。
  4. 性能瓶颈明显:部分页面使用了大量CollectionView和自定义动画,切换页面时卡顿严重。

团队协作方面的挑战

除了技术上的问题,我们还面临了不少来自团队协作方面的阻力:

  • 有人对新架构理解不够,写出“伪MVVM”代码,ViewModel中混入了太多View相关的逻辑;
  • 新技术的学习成本高,新人上手慢;
  • 旧代码改造节奏难把控,前期进度慢影响整体排期;
  • 没有明确的“边界”划分,有时候一个页面多人修改引发Merge Conflict频繁。

面对这些问题,我们一开始也犯了些错误,比如太急于全面推广新技术,结果导致线上出现了一些偶现的Crash。后来经过几次“踩坑”+总结,才慢慢摸索出一条适合我们项目的改进路径。


解决思路与技术选型:走对第一步很关键

解决思路与技术选型:走对第一步很关键

在整个项目初期阶段,我们花了整整两周时间做了技术选型调研,并最终锁定了以下几个关键技术方向:

方向 技术选型 说明
架构 MVVM + Combine 统一状态管理和事件流处理
网络 URLSession封装 + 自定义中间件 可扩展性强,利于后续集成认证拦截等逻辑
状态管理 基于ViewModel + Coordinator模式 ViewModel负责状态,Coordinator处理路由
UI 内部封装的UIKit组件库 提供统一的Button、Card、Input组件
测试 XCTest + Snapshot Testing 接口测试+UI截图对比

这些选型背后其实也有过争议。比如是否采用ReactiveSwift还是Combine?我们最后选Combine是因为它已经内建在Swift标准库中(iOS 13+),学习成本相对较低,而且Apple官方也在大力推广。

另外一个重要决定是:不是一次性全量替换架构,而是采用渐进式重构策略。这样既能保证现有功能稳定运行,也能为后期全面迁移打好基础。


代码实战:真实项目中的代码片段解析

代码实战:真实项目中的代码片段解析

接下来我会分享几个关键模块的具体实现方式,以及它们是如何解决我们的问题的。

1. 网络层封装(Network Layer)

我们将原有的散落在各个VC中的网络请求全部收敛到Service层统一处理,并用Combine实现异步链式调用:

class APIService {
    func fetchProducts() -> AnyPublisher<[Product], APIError> {
        let url = URL(string: "https://api.example.com/products")!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .mapError { _ in .network }
            .flatMap { data, response -> AnyPublisher<[Product], APIError> in
                guard let httpResponse = response as? HTTPURLResponse,
                      (200...299).contains(httpResponse.statusCode) else {
                    return Fail(error: .serverError(statusCode: -1)).eraseToAnyPublisher()
                }

                do {
                    let decoder = JSONDecoder()
                    let result = try decoder.decode([Product].self, from: data)
                    return Just(result).setFailureType(to: APIError.self).eraseToAnyPublisher()
                } catch {
                    return Fail(error: .decodingFailed).eraseToAnyPublisher()
                }
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

通过这套机制,不仅实现了统一异常处理,还能灵活地组合多个网络请求:

let productsRequest = apiService.fetchProducts()
let cartRequest = cartService.fetchCartItems()

productsRequest
    .flatMap { products in
        cartRequest.map { (products, $0) }
    }
    .sink(receiveCompletion: { _ in }, receiveValue: { products, cartItems in
        // 更新界面逻辑...
    })
    .store(in: &cancellables)

2. ViewModel 的典型结构

class ProductListViewModel: ObservableObject {
    @Published var products: [Product] = []
    @Published var isLoading = false
    private let apiService: APIService

    init(apiService: APIService = APIService()) {
        self.apiService = apiService
    }

    func loadProducts() {
        isLoading = true
        apiService.fetchProducts()
            .sink(receiveCompletion: { [weak self] _ in
                self?.isLoading = false
            }) { [weak self] fetchedProducts in
                self?.products = fetchedProducts
            }
            .store(in: &cancellables)
    }
}

这样的结构让VC变薄了,View通过绑定属性来响应变化,代码更易读、也方便测试。


3. 页面跳转与协调器模式(Coordinator Pattern)

我们没有使用传统的AppDelegate或NavigationController直接驱动跳转逻辑,而是引入了Coordinator模式来管理页面生命周期:

protocol Coordinator {
    func start()
}

class AppCoordinator: Coordinator {
    private let navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let viewModel = ProductListViewModel()
        let vc = ProductListViewController(viewModel: viewModel)
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: true)
    }

    func showProductDetail(product: Product) {
        let viewModel = ProductDetailViewModel(product: product)
        let vc = ProductDetailViewController(viewModel: viewModel)
        navigationController.pushViewController(vc, animated: true)
    }
}

这样做的好处是:每个页面之间的跳转逻辑被解耦出来,便于复用和集中控制。


实战中踩过的那些“坑”

在这个过程里,我也踩过不少坑,有些教训至今记忆犹新:

坑一:Combine内存泄漏问题

刚开始使用Combine的时候,很多人不太注意AnyCancellable的释放时机,导致页面退出后回调仍然执行。比如我们曾经有个列表页退出后仍在更新状态,导致崩溃。

✅ 解决办法:所有订阅都要绑定一个Set<AnyCancellable>,并在页面销毁时调用.cancel()

var cancellables = Set<AnyCancellable>()

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    cancellables.forEach { $0.cancel() }
    cancellables.removeAll()
}

坑二:ViewModel滥用@Published

很多人误以为只要是变量都应该标记为@Published,结果导致界面上不必要的刷新。比如有个页面的Model对象包含几十个字段,每个都用了@Published

✅ 解决办法:拆分状态颗粒度,按需监听;对于不需要绑定的属性不要加修饰符。


坑三:组件库命名冲突

由于是我们团队自己封装的组件库,刚开始并没有做严格的命名规范,导致和第三方库如SnapKit、Kingfisher偶尔出现符号冲突。

✅ 解决办法:给所有自定义组件前缀加上统一命名空间,例如MyAppButtonMyAppCardView等。


坑四:单元测试没写,上线翻车

某个ViewModel里的逻辑判断比较多,我们只写了接口返回成功的测试用例,忽略了失败场景,导致正式环境遇到特殊错误码时报空指针。

✅ 解决办法:使用XCTest实现完整的测试覆盖,特别是边缘条件(error handling, nil值处理等)。


实践成果与收益

经过三个月左右的时间,我们终于完成了整个项目的架构调整和技术迁移工作。回头看,最大的几个收益包括:

  • 代码可维护性显著提升:VC代码平均减少约40%,逻辑清晰了很多;
  • Bug数量下降:状态管理统一之后,因异步操作混乱引起的Crash减少了60%以上;
  • 新人上手速度加快:标准化的组件库和架构模板降低了认知门槛;
  • 测试覆盖率提高:从不到10%上升至50%左右,自动化测试可以有效覆盖核心流程;
  • 页面加载速度优化:通过对UICollectionView的优化及懒加载机制,主Tab页启动速度提升了30%。

我的经验建议:给正在探索的你

如果你正处在类似的技术转型期,或是刚刚准备开始某种架构/技术实践的尝试,我有一些建议希望能帮到你:

1. 不要追求完美架构,先跑通再迭代

初期不必追求所谓的“终极架构”,哪怕是局部MVVM+简单的Combine封装,能跑起来验证可行性就足够了。先跑通再说优化。

2. 技术决策要有业务支撑

任何架构升级都应该服务于业务目标。比如说我们要支持夜间模式,那就要考虑Theme管理该怎么设计;要做国际化,也要提前规划语言切换的机制。

3. 制定技术落地路线图很重要

制定阶段性目标,比如第一周完成网络层抽象、第二周完成一个完整页面的MVVM重构。避免“边写边改”的无序状态。

4. 多做一些小工具辅助开发

我们团队为了辅助Transition,开发了几个小插件:

  • 一个轻量级的UI快照测试工具;
  • 一个帮助自动生成ViewModel模板的小脚本;
  • 一份详细的技术文档Wiki,记录每一步的注意事项。

这些工具节省了大量时间,尤其是重复性的劳动部分。

5. 定期做Code Review & Pair Programming

每周固定安排一次小组Review,大家一起看改动的代码。既提高了代码质量,也让成员之间互相学习进步。


最后想说的几句心里话

这几年的iOS开发经历告诉我:技术探索永远不是一个人的战斗,而是一个团队的共同努力。每一次架构的演进、每一项技术的落地,背后都有无数沟通、妥协、反复打磨的过程。

这篇文章讲的是我经历过的一次具体实践,但它代表的不只是某一项技术的选择,而是一种持续成长和不断适应的心态。

无论你现在是初级开发者,还是经验丰富的老司机,希望你能在这个行业中始终保持好奇心和探索欲。因为真正的进步,往往发生在你以为已经掌握了某个领域的那一刻之后。

如果你对这篇内容有任何疑问,或者你也有关于iOS架构演进的经历想要交流,欢迎在评论区留言。我是真心希望咱们都能在这条路上越走越远!

祝 coding 快乐,bug越来越少,头发也越来越浓密 😄

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝