移动端架构体系(四):View层的组织与调用方案

一、为什么 View 层架构值得单独谈

View 层(在 iOS 里常以 UIViewController + UIView 为核心,在 Android 里是 Activity / Fragment + View,在跨端里是 Page/Screen + Widget)是离产品形态最近、和业务耦合最深的一层。它往往具备三个特点:

  • 定型后难改:发版后大规模调整 View 结构,牵动的业务面最大。
  • 直接影响迭代效率:同样的需求,若 View 层杂乱、依赖横切、缺少工具与规范,业务开发会大量时间耗在"找代码、解冲突、改连锁反应"上。
  • 是其它分层能否落地的"门面":网络、持久化、组件化做得再好,若页面层一团糟,整体架构仍会显得不可维护。

因此,View 层的目标可以概括为:在符合平台范式的前提下,让业务同学少写样板代码、少踩协作坑,并让后续拆分与演进成本可控。

下面用一张总览图概括**"数据与界面"**在典型移动应用中的流向(可以理解为数据管理者 / 加工者 / 展示者 三分法):

二、常见 View 层问题(与迭代速度的关系)

结合工程经验,下面几类问题最常拖慢迭代:

问题 表现 后果
代码混乱、无固定分区 delegate、事件、私有方法散落 阅读成本高,改动易漏
过度继承 深层 VC/View 基类树 集成 Demo、单测、组件复用都变难
模块化不足 大文件、万能 VC 无法并行开发,复用差
横向依赖 A 业务直接 import B 的页面 编译耦合、联调阻塞、改名雪崩
缺少传承与规范 每人一种写法 架构被"腐蚀",无法演进

结论:View 层要同时解决怎么写(规范)、怎么拆(模块与模式)、怎么连(跨业务调用)三件事。

三、View 代码如何组织:文件内分区与职责

3.1 核心原则

  • viewDidLoad(或等价入口):以组装视图树为主(addSubview / addArrangedSubview 等),避免堆一大坨初始化逻辑。
  • 布局:见下文

分区顺序建议:

  • Lifecycle
  • 各 protocol 的 delegate/dataSource 实现(每个 #pragma mark / // MARK: 带协议名)
  • 事件响应(target-action、手势、按钮等)
  • Private helpers 尽量少;能外提则外提
  • Getter / 懒加载属性 放文件后部(避免挡在主逻辑前)

3.2 Objective-C 示例(懒加载 + 分区)

objectivec 复制代码
#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    [self.view addSubview:self.titleLabel];
    [self.view addSubview:self.actionButton];
    [self applyLayout];
}

#pragma mark - UIButton actions
- (void)onActionTapped {
    // ...
}

#pragma mark - Private
- (void)applyLayout {
    // 使用 Auto Layout / Masonry / 布局工具
}

#pragma mark - Getters
- (UILabel *)titleLabel {
    if (!_titleLabel) {
        _titleLabel = [[UILabel alloc] init];
        _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle2];
    }
    return _titleLabel;
}

3.3 Swift 等价写法(lazy var + MARK)

swift 复制代码
// MARK: - Lifecycle
override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
    view.addSubview(titleLabel)
    view.addSubview(actionButton)
    applyLayout()
}

// MARK: - Actions
@objc private func onActionTapped() { }

// MARK: - Private
private func applyLayout() { }

// MARK: - Subviews (lazy)
private lazy var titleLabel: UILabel = {
    let v = UILabel()
    v.font = .preferredFont(forTextStyle: .title2)
    return v
}()

要点:把"生产子视图"与"挂载到层级"在视觉上分开,长列表属性时文件仍可读;业务逻辑尽量不要和"创建 UILabel 属性"揉在 viewDidLoad 里。

3.4 关于布局写在哪

在 Auto Layout 场景下,在 viewWillAppear 里反复改 frame / 加约束容易和系统布局周期打架。更稳妥的做法是:约束在 viewDidLoad 里集中添加一次(或封装为 layoutPageSubviews()),或在 viewDidLayoutSubviews / updateConstraints 等系统约定的更新点做增量更新(注意避免重复添加相同约束)。
SwiftUI / Compose:声明式布局由运行时根据状态重算,传统"在哪一帧改 frame"的问题被框架吸收,但状态拆分与副作用边界仍然等价重要。

四、布局工具:可读性即生产力

裸 CGRectMake 可读性差;原生约束代码冗长。工程上常见做法:

  • iOS:SnapKit / Masonry;或自研 UIView 布局扩展(链式、相对关系命名清晰)。
  • Android:ConstraintLayout + ViewBinding;或 Compose 的 Modifier。

原则:布局 API 应能表达相对关系(相对父视图、相对兄弟视图),而不是魔法数字堆砌。

五、Storyboard / XIB / 代码 / 声明式 UI:怎么选

从协作冲突、需求变更、复杂动画三点来看倾向代码构建 UI:

方式 适合 风险
Storyboard 原型、单人小项目 合并冲突、多页混杂难协作
XIB 单块复杂控件、设计稿强绑定 Outlet 易与重构不同步
代码 UIKit 中大型团队、高频迭代 样板代码多,靠工具缓解
SwiftUI / Compose 新模块、状态驱动界面 与 UIKit/AndroidView 互操作需规范

团队规则示例:业务页面默认代码(或声明式);仅对稳定、复用度高的组件保留 XIB/SB;跨业务模块禁止在 Storyboard 里堆多页流程。

六、是否强制统一继承 BaseViewController?

能用组合与切面解决的,不必用继承堆环境依赖。

6.1 继承的问题

  • Demo / 独立流程要接主工程时,被迫改基类、拉全量依赖。
  • 新人默认写 UIViewController 会与规范冲突。
  • 基类膨胀后,成为第二个"上帝类"。

6.2 替代:AOP + Category / Extension

目标 :业务代码使用平台原生类型;进入主工程后框架自动生效。

Method Swizzling(或 Aspects 类库)钩住 viewDidLoad / viewWillAppear 等,做埋点、统一导航栏、权限检查等。

Extension 提供工具方法,而非在基类塞功能。

objectivec 复制代码
// 极简示意:在 +load 中对 UIViewController 做 swizzle(真实项目需防重复、防子类误伤)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [UIViewController class];
        // swap viewDidLoad -> xxx_viewDidLoad 等
    });
}

Swift 侧更推荐显式协议默认实现 + 在特定基类模块外仍保持可选,或依赖中间件/容器(如统一导航协调器),减少黑魔法。

七、模式梳理:MVC、MVCS、MVVM、VIPER

7.1 iOS 里 MVC 的再理解

服务端 MVC 里,"View"常常是描述视图的字符串;浏览器才是真正的 View。

在客户端,容器 + 子视图 + 事件回传必须由应用自己完成,苹果把容器放在 UIViewController.view 里是合理默认。

可记:C 管容器与调度;V 表达与弱反馈;M 提供数据与持久化接口。

7.2 MVCS

把 Store(数据存取) 从臃肿的 C 中抽出,适合瘦 Model + 集中存储路线。

7.3 MVVM(关键在 ViewModel,不在某个响应式库)

  • ViewModel:把 Raw Data 加工成可直接驱动 UI 的状态(格式化文案、Section 模型、MKAnnotation 列表等)。
  • Controller 仍存在:常见关系是 View ↔ C ↔ ViewModel ↔ Model(有人戏称 MVCVM)。C 负责绑定关系与导航,而不是"消失"。
  • Reactive:RxSwift /Combine / 响应式库是绑定手段,不是 MVVM 的定义;无响应式也可用 delegate、closure、Observation 完成单向 / 双向数据流。

Swift 简例(结构示意)

swift 复制代码
final class ProfileViewModel {
    struct State: Equatable {
        var title: String
        var avatarURL: URL?
    }
    private(set) var state: State
    init(user: User) {
        self.state = State(title: user.displayName, avatarURL: user.avatar)
    }
}

final class ProfileViewController: UIViewController {
    private let vm: ProfileViewModel
    init(vm: ProfileViewModel) {
        self.vm = vm
        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder: NSCoder) { fatalError() }
    override func viewDidLoad() {
        super.viewDidLoad()
        render(vm.state)
    }
    private func render(_ state: ProfileViewModel.State) {
        title = state.title
        // 加载头像等
    }
}

7.4 VIPER

把交互、展示、路由再细分,适合超复杂模块;成本高,需团队纪律配合,否则容易过度设计。

7.5 横向对比

八、"拆分"心法

  • 保留协调 V 与 M 的职责在 C(或 Presenter),其余能拆则拆。
  • 拆出的模块提高复用与抽象:重复出现的 tableView 数据源、表单校验、地图标注组装等,独立成类型。

抽象粒度:对外参数少、语义清晰;若业务分支多,用策略/插件收拢调度,避免 VC 里长 if-else。

8.1 策略模式:消化"小粒度模块组合"

swift 复制代码
enum MessageSendStrategy { case text, image, voice }

protocol SendStrategy {
    func send(_ message: BaseMessage, completion: (Result<Void, Error>) -> Void)
}

final class MessageSender {
    private let strategies: [MessageSendStrategy: SendStrategy]
    init(strategies: [MessageSendStrategy: SendStrategy]) {
        self.strategies = strategies
    }
    func send(_ message: BaseMessage, strategy: MessageSendStrategy,
              completion: @escaping (Result<Void, Error>) -> Void) {
        guard let s = strategies[strategy] else {
            completion(.failure(NSError(domain: "msg", code: -1)))
            return
        }
        s.send(message, completion: completion)
    }
}

Controller 只保留一行调度:messageSender.send(msg, strategy: image),复杂链路下沉到策略内部。

九、跨业务页面调用:依赖下沉与 Mediator / Router

多业务 App 里,禁止业务模块间直接引用对方 VC;应通过路由/中介统一解析 URL 或注册表,返回所需对象或协调导航。

能力要求

请求协议与业务无关(如 open("app://order/detail?id=1") 或类型安全的路由枚举)。

可扩展为跨 App 与 Universal Link 同一套解析(注意返回值:URL 路由常不返回对象,内部模块路由可以)。

十、面向 Android / 跨端的一句对齐

  • Android:Activity / Fragment 对应 VC 角色;ViewModel + UI State 对应 MVVM;导航用 Jetpack Navigation;模块间用 Deep Link / 接口下沉到 core。
  • Flutter:Widget 树 + ChangeNotifier / Bloc / Riverpod 等,同样是状态与视图分离、路由中心化。
  • 心法(少继承、多组合、协调层瘦身、跨模块不直连)平台无关。

十一、总结清单(Checklist)

  • 规范:文件分区、命名、懒加载/子视图创建方式、布局 API 统一。
  • 模式:以 MVC 为底色,按复杂度叠 MVCS / MVVM;VIPER 慎用但要知道适用边界。
  • 工具:布局库、路由、埋点/AOP、设计系统组件。
  • 协作:大中团队优先代码或声明式 UI;SB 谨慎;跨业务必走 Mediator。
  • 哲学:架构服务业务,而不是让业务给架构打工;接口越"傻瓜"、越稳。

十二、参考

实践时请结合你们栈(UIKit / SwiftUI / Compose / Flutter)把**"协调层 + 状态 + 导航 + 模块边界"**四条线画清楚,比死记缩写更重要。

相关推荐
2301_822703202 小时前
光影进度条:鸿蒙Flutter实现动态光影效果的进度条
算法·flutter·华为·信息可视化·开源·harmonyos
独特的螺狮粉2 小时前
城市空气质量简易指数查询卡片:鸿蒙Flutter框架 实现的空气质量查询应用
开发语言·flutter·华为·架构·harmonyos
架构师老Y3 小时前
011、消息队列应用:RabbitMQ、Kafka与Celery
python·架构·kafka·rabbitmq·ruby
沃尔威武3 小时前
微服务架构下:如何用gRPC实现跨语言高效通信
微服务·云原生·架构
Rick19933 小时前
LangChain 核心解析:底层架构、原理
架构·langchain
架构谨制@涛哥3 小时前
《哥谭神话-Palantir故事篇》Palantir 产品战略与架构全景
后端·系统架构·软件构建
heimeiyingwang3 小时前
【架构实战】数据加密架构:传输加密+存储加密
架构
2501_948114244 小时前
Claude Sonnet 4.6 深度评测:性能逼近 Opus、成本打骨折,附接入方案与选型指南
大数据·网络·人工智能·安全·架构
李李李勃谦4 小时前
Flutter 框架跨平台鸿蒙开发 - 鲜花礼品配送
flutter·华为·harmonyos