05 | MVC 模式:iOS 中的架构基石
5.1 什么是 MVC
MVC 是 Model-View-Controller 的缩写,是 iOS 开发中最基础、最经典的架构模式。它将程序分为三个角色:
| 角色 | 职责 | 典型实现 |
|---|---|---|
| Model(模型) | 管理数据和业务逻辑 | 数据类、网络请求、数据库操作 |
| View(视图) | 负责界面展示和用户交互 | UIView 及其子类(UILabel、UIButton 等) |
| Controller(控制器) | 协调 Model 和 View,处理业务逻辑 | UIViewController 及其子类 |
5.2 三者的关系与数据流
用户操作 → View → Controller → Model(更新数据)
↓
View ← Controller ← Model(数据变化通知)
核心原则:
- View 不直接访问 Model:视图只负责展示,不知道数据从哪来
- Model 不依赖 View 和 Controller:模型是独立的,可以脱离界面使用
- Controller 是桥梁:负责从 Model 获取数据,更新 View;接收 View 的用户事件,操作 Model
5.3 iOS 中 MVC 的特殊性
iOS 的 MVC 有时被称为 "Massive View Controller"(臃肿的控制器),因为 Controller 往往承担过多职责:
- 页面跳转
- 数据请求
- 视图布局
- 事件处理
Apple 的 MVC 中,View 和 Model 之间是严格隔离的,所有交互都必须经过 Controller。这和传统 Web MVC 有所不同。
5.4 实际代码示例
swift
// ===== Model =====
class UserModel {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
// ===== View =====
class ProfileView: UIView {
let nameLabel = UILabel()
let ageLabel = UILabel()
func configure(with user: UserModel) {
nameLabel.text = user.name
ageLabel.text = "\(user.age) 岁"
}
}
// ===== Controller =====
class ProfileViewController: UIViewController {
private let profileView = ProfileView()
private var user = UserModel(name: "zzb", age: 25)
override func viewDidLoad() {
super.viewDidLoad()
profileView.configure(with: user) // Controller 从 Model 取数据,传给 View
}
}
5.5 MVC 的优缺点
优点:
- 职责清晰,便于团队协作
- iOS 框架原生支持
- 入门门槛低
缺点:
- Controller 容易变得臃肿
- 不利于复杂项目的维护和测试
- 后续可考虑 MVVM、VIPER 等进阶架构
5.6 复习要点
- 能说出 M、V、C 各自的职责
- 理解数据流方向(View ↔ Controller ↔ Model)
- 知道为什么 iOS 的 Controller 容易"膨胀"
- 能用简单的代码实现一个 MVC 结构
06 | iOS 中的视图 UIView
6.1 UIView 是什么
UIView 是所有 iOS 界面元素的基类。你在屏幕上看到的一切------按钮、标签、图片、文本框------都是 UIView 的子类。
UIView
├── UILabel(文本标签)
├── UIButton(按钮)
├── UIImageView(图片视图)
├── UITextField(文本输入框)
├── UITextView(多行文本)
├── UIScrollView(滚动视图)
├── UITableView(表格视图)
├── UICollectionView(集合视图)
└── ... 等等
6.2 UIView 的核心属性
位置和尺寸
swift
// frame:相对于父视图的位置和大小(绝对坐标)
view.frame = CGRect(x: 20, y: 100, width: 200, height: 50)
// bounds:相对于自身坐标系的位置和大小(通常 origin 为 0,0)
view.bounds = CGRect(x: 0, y: 0, width: 200, height: 50)
// center:视图中心点坐标
view.center = CGPoint(x: 187.5, y: 406)
frame vs bounds 的区别:
frame= 在父视图坐标系中的位置 + 大小bounds= 在自身坐标系中的大小(用于内部绘制)- 旋转/缩放时,frame 会变,bounds 不变
外观属性
swift
view.backgroundColor = .systemBlue // 背景色
view.alpha = 0.8 // 透明度(0.0~1.0)
view.isHidden = false // 是否隐藏
view.layer.cornerRadius = 10 // 圆角
view.layer.borderWidth = 1.0 // 边框宽度
view.layer.borderColor = UIColor.gray.cgColor // 边框颜色
view.clipsToBounds = true // 裁剪超出部分
view.tag = 100 // 标签(可通过 viewWithTag 查找)
用户交互
swift
view.isUserInteractionEnabled = true // 是否允许交互
view.isMultipleTouchEnabled = false // 是否支持多点触控
6.3 视图层级(View Hierarchy)
swift
// 添加子视图
parentView.addSubview(childView)
// 从父视图移除
childView.removeFromSuperview()
// 插入到指定层级
parentView.insertSubview(childView, at: 0) // 最底层
parentView.insertSubview(childView, belowSubview: otherView)
parentView.insertSubview(childView, aboveSubview: otherView)
// 调整层级
parentView.bringSubviewToFront(childView) // 移到最前面
parentView.sendSubviewToBack(childView) // 移到最后面
// 获取关系
let parent = childView.superview // 父视图
let children = parentView.subviews // 所有子视图
let found = parentView.viewWithTag(100) // 通过 tag 查找
6.4 坐标系
iOS 的坐标系原点在左上角:
-
X 轴向右增大
-
Y 轴向下增大(和数学坐标系相反)
(0,0) ──────────────→ X
│
│ (x, y) ●
│
↓
Y
6.5 使用代码创建视图
swift
override func viewDidLoad() {
super.viewDidLoad()
// 1. 创建视图
let redView = UIView()
redView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)
redView.backgroundColor = .systemRed
redView.layer.cornerRadius = 16
redView.clipsToBounds = true
// 2. 添加到视图层级
view.addSubview(redView)
// 3. 添加子标签
let label = UILabel()
label.text = "Hello, iOS!"
label.frame = CGRect(x: 0, y: 80, width: 200, height: 40)
label.textAlignment = .center
redView.addSubview(label)
}
6.6 Auto Layout 基础
除了 frame 布局,现代 iOS 开发更常用 Auto Layout(约束布局):
swift
let label = UILabel()
label.text = "Auto Layout"
label.translatesAutoresizingMaskIntoConstraints = false // 关键!关闭自动约束转换
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
])
6.7 复习要点
- 理解 UIView 是所有 UI 元素的基类
- 掌握 frame 和 bounds 的区别
- 会用代码创建视图并添加到层级
- 理解 iOS 坐标系(原点在左上角)
- 掌握子视图的增删和层级调整方法
07 | 了解 UIView 的生命周期
7.1 生命周期总览
UIView 从创建到销毁,经历以下关键阶段:
init → layoutSubviews → draw → willMoveToSuperview
→ didMoveToSuperview → willMoveToWindow → didMoveToWindow
→ ... 运行中 ...
→ removeFromSuperview → dealloc
7.2 关键生命周期方法
初始化阶段
swift
// 代码初始化(最常用)
override init(frame: CGRect) {
super.init(frame: frame)
// 自定义初始化逻辑
setupUI()
}
// 从 Storyboard/XIB 加载时调用
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
布局阶段
swift
// 布局子视图 ------ 非常重要的方法
override func layoutSubviews() {
super.layoutSubviews()
// 每次视图需要重新布局子视图时调用
// 场景:旋转屏幕、约束变化、子视图 frame 需要动态计算
// ⚠️ 不要在 layoutSubviews 中调用 setNeedsLayout(),会导致死循环
}
// 返回合适的尺寸
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: 44)
}
// 自动调整大小以适应内容
override func sizeToFit() {
// 调用 sizeThatFits 后自动设置 frame
}
绘制阶段
swift
// 自定义绘制 ------ 在 draw 方法里用 Core Graphics 绘图
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 画一条线
context.move(to: CGPoint(x: 0, y: 0))
context.addLine(to: CGPoint(x: rect.width, y: rect.height))
context.setStrokeColor(UIColor.red.cgColor)
context.strokePath()
}
触发绘制的时机:
- 视图首次出现
- 调用
setNeedsDisplay()标记需要重绘 - 调用
setNeedsDisplay(_:)指定区域重绘
视图层级变化
swift
// 即将被添加到父视图
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
// newSuperview 为 nil 表示即将从父视图移除
}
// 已经被添加到父视图
override func didMoveToSuperview() {
super.didMoveToSuperview()
// 此时 superview 已更新
}
// 即将被添加到窗口
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
}
// 已经被添加到窗口
override func didMoveToWindow() {
super.didMoveToWindow()
// window 不为 nil = 视图正在显示
// window 为 nil = 视图已从显示中移除
}
7.3 刷新机制
swift
// 标记需要重新布局(下一个 runloop 周期执行 layoutSubviews)
view.setNeedsLayout()
// 立即触发布局更新(同步执行 layoutSubviews)
view.layoutIfNeeded()
// 标记需要重新绘制(下一个 runloop 周期执行 draw)
view.setNeedsDisplay()
常用组合 --- 动画:
swift
view.setNeedsLayout()
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded() // 在动画块中触发,产生平滑动画
}
7.4 生命周期调用顺序(完整版)
1. init(frame:) / init(coder:) --- 创建
2. willMove(toSuperview:) --- 即将加入父视图
3. didMoveToSuperview() --- 已加入父视图
4. willMove(toWindow:) --- 即将加入 window
5. didMoveToWindow() --- 已加入 window
6. layoutSubviews() --- 布局子视图
7. draw(_:) --- 绘制
8. [运行中,可能多次调用 6 和 7]
9. removeFromSuperview() --- 移除
10. willMove(toSuperview: nil) --- 即将离开父视图
11. didMoveToSuperview() --- 已离开父视图
12. willMove(toWindow: nil) --- 即将离开 window
13. didMoveToWindow() --- 已离开 window
14. dealloc --- 释放
7.5 复习要点
- 掌握 init(frame:) 和 init(coder:) 的区别
- 理解 layoutSubviews 的调用时机和注意事项
- 知道 setNeedsLayout 和 layoutIfNeeded 的区别
- 了解 willMove/didMove 系列方法的用途
- 能说出完整的生命周期调用顺序