iOS UI 开发完全指南:UIKit 与 SwiftUI
📖 目录
🎯 概览
iOS 界面开发主要分为两大阵营:
| 维度 | UIKit | SwiftUI |
|---|---|---|
| 推出时间 | iOS 2.0 (2008) | iOS 13.0 (2019) |
| 编程范式 | 命令式 (Imperative) | 声明式 (Declarative) |
| 当前状态 | 成熟稳定,维护模式 | 快速发展,Apple 主推 |
| 最低支持 | iOS 2.0+ | iOS 13.0+ |
🧱 UIKit 框架详解
1. 核心概念与架构
📐 UIKit 是什么?
UIKit 是 iOS 开发的基础 UI 框架,提供了一套完整的命令式编程接口来构建用户界面。它是 iOS 应用界面开发的基石。
🔗 UIKit、Storyboard、UIViewController 的关系
提供基础组件
提供控制器基类
加载/管理
描述布局
通过连线
运行时创建
渲染为
UIKit 框架
UIView/UIButton/UILabel...
UIViewController
Storyboard 文件
XML 配置
用户界面
三者关系总结:
| 组件 | 角色 | 本质 | 类比 |
|---|---|---|---|
| UIKit | 基础设施 | 框架 (Framework) | 建筑材料库 |
| UIViewController | 控制器 | 类 (Class) | 施工监理 + 骨架 |
| Storyboard | 布局文件 | XML 文件 | 可视化设计图纸 |
协同工作流程:
swift
// 1. 创建 UIViewController 子类
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel! // 从 Storyboard 连线
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Hello UIKit"
}
@IBAction func buttonTapped(_ sender: UIButton) {
label.text = "按钮被点击了"
}
}
// 2. 在 Storyboard 中拖拽 UI 元素并连线到上述代码
// 3. App 运行时,UIViewController 加载 Storyboard,用 UIKit 创建真实视图
2. UIKit 主要组件分类
📦 基础视图组件
| 组件 | 用途 | 常用场景 |
|---|---|---|
UIView |
所有视图的基类 | 容器、背景 |
UIButton |
按钮 | 用户点击操作 |
UILabel |
文本标签 | 显示静态文字 |
UITextField |
单行文本输入 | 表单、登录框 |
UITextView |
多行文本输入 | 备注、评论 |
UIImageView |
图片展示 | 头像、配图 |
UIScrollView |
滚动视图 | 内容超出屏幕 |
UITableView |
列表视图 | 消息列表、设置页 |
UICollectionView |
集合视图 | 相册、瀑布流 |
UISwitch |
开关 | 设置开关 |
UISlider |
滑块 | 音量、进度调节 |
UIProgressView |
进度条 | 加载进度 |
🎯 容器控制器
| 容器 | 用途 | 使用场景 |
|---|---|---|
UINavigationController |
导航栈管理 | 层级页面跳转 |
UITabBarController |
Tab 标签页 | 底部导航栏 |
UISplitViewController |
分屏视图 | iPad 左右布局 |
UIPageViewController |
翻页效果 | 电子书、引导页 |
3. UIViewController 生命周期
swift
class LifecycleViewController: UIViewController {
// 视图加载完成(只调用一次)
override func viewDidLoad() {
super.viewDidLoad()
print("1. 视图已加载")
// 初始化数据、注册通知、设置 delegates
}
// 视图即将出现
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("2. 视图即将出现")
// 每次页面显示前都会调用,适合刷新数据
}
// 视图已经出现
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("3. 视图已出现")
// 启动动画、开启定时器
}
// 视图即将消失
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("4. 视图即将消失")
// 保存数据、停止动画
}
// 视图已经消失
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("5. 视图已消失")
// 停止网络请求、移除通知
}
}
4. UIKit 的三种 UI 构建方式
方式一:Storyboard(推荐新手/原型)
swift
// ViewController.swift
class LoginViewController: UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
loginButton.layer.cornerRadius = 8
}
@IBAction func loginButtonTapped(_ sender: UIButton) {
let username = usernameTextField.text ?? ""
let password = passwordTextField.text ?? ""
print("登录: \(username)/\(password)")
}
}
方式二:XIB(单个视图复用)
swift
// CustomCardView.swift
class CustomCardView: UIView {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var contentLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
loadFromNib()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
loadFromNib()
}
private func loadFromNib() {
let nib = UINib(nibName: "CustomCardView", bundle: nil)
let view = nib.instantiate(withOwner: self).first as! UIView
view.frame = bounds
addSubview(view)
}
}
方式三:纯代码(最高灵活性)
swift
class CodeOnlyViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "纯代码界面"
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let actionButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("点击我", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews()
setupConstraints()
setupActions()
}
private func setupViews() {
view.addSubview(titleLabel)
view.addSubview(actionButton)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
actionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 50),
actionButton.widthAnchor.constraint(equalToConstant: 200),
actionButton.heightAnchor.constraint(equalToConstant: 44)
])
}
private func setupActions() {
actionButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc private func buttonTapped() {
titleLabel.text = "按钮被点击了"
}
}
5. UIKit 使用场景
| 场景 | 推荐程度 | 理由 |
|---|---|---|
| 维护 5 年以上老项目 | ⭐⭐⭐⭐⭐ | 历史代码量大,迁移成本高 |
| 支持 iOS 13 以下版本 | ⭐⭐⭐⭐⭐ | SwiftUI 最低要求 iOS 13 |
| 复杂自定义动画 | ⭐⭐⭐⭐⭐ | Core Animation 成熟稳定 |
| 超长列表(1000+ 条) | ⭐⭐⭐⭐ | UITableView 复用机制优秀 |
| 地图/视频编辑等高性能场景 | ⭐⭐⭐⭐⭐ | 底层 Metal 集成完善 |
| 高度定制 UI 控件 | ⭐⭐⭐⭐⭐ | 完全控制绘制过程 |
| 新项目(无历史包袱) | ⭐⭐ | Apple 主推 SwiftUI |
6. UIKit 注意事项 ⚠️
-
Auto Layout 调试困难:约束错误信息不直观
swift// 调试约束冲突 view.window?.constraints.forEach { print($0) } -
Storyboard 合并冲突:XML 格式难以手动合并
- 建议:大型团队分模块使用多个 Storyboard
-
内存管理:循环引用常见
swift// 错误示例 button.addTarget(self, action: #selector(method), for: .touchUpInside) // 正确示例 button.addTarget(self, action: #selector(method), for: .touchUpInside) // 需要在 deinit 中移除或使用 weak -
主线程 UI 更新:必须在主线程
swiftDispatchQueue.main.async { self.label.text = "更新" }
🚀 SwiftUI 框架详解
1. 核心概念与架构
📐 SwiftUI 是什么?
SwiftUI 是 Apple 于 2019 年推出的声明式UI 框架,允许开发者用简洁的代码描述界面应该呈现的样子,框架自动处理状态变化时的视图更新。
🔗 SwiftUI 的核心组件
渲染错误: Mermaid 渲染失败: Parse error on line 10: ...ack] C --> H[@State] C --> ---------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'
与 UIKit 的根本差异:
swift
// UIKit - 命令式:告诉系统"怎么做"
var count = 0
label.text = "\(count)" // 手动设置
func increment() {
count += 1
label.text = "\(count)" // 又要手动更新
}
// SwiftUI - 声明式:告诉系统"是什么"
@State var count = 0
var body: some View {
Text("\(count)") // 描述关系
Button("增加") { count += 1 } // 修改状态,UI 自动更新
}
2. 状态管理
| 属性包装器 | 用途 | 作用域 | 示例 |
|---|---|---|---|
@State |
视图本地状态 | 当前 View | 点击计数、开关状态 |
@Binding |
双向绑定 | 父子视图传递 | 表单输入 |
@ObservedObject |
外部可观察对象 | 跟随视图生命周期 | ViewModel |
@StateObject |
创建并持有 ObservableObject | 视图生命周期 | 数据模型 |
@EnvironmentObject |
全局共享状态 | 整个视图树 | 用户登录状态 |
@Environment |
系统环境值 | 当前视图 | 颜色模式、大小类 |
示例演示:
swift
// 1. 定义数据模型
class UserViewModel: ObservableObject {
@Published var username = ""
@Published var isLoggedIn = false
}
// 2. 主视图
struct ContentView: View {
@StateObject private var viewModel = UserViewModel()
@State private var showSettings = false
@Environment(\.colorScheme) var colorScheme // 系统环境
var body: some View {
NavigationStack {
VStack {
if viewModel.isLoggedIn {
ProfileView()
.environmentObject(viewModel) // 传递到子视图
} else {
LoginView(username: $viewModel.username) // 双向绑定
}
Text("当前主题: \(colorScheme == .dark ? "深色" : "浅色")")
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
}
}
}
// 3. 子视图 - 使用 @Binding
struct LoginView: View {
@Binding var username: String
@State private var password = ""
var body: some View {
TextField("用户名", text: $username)
SecureField("密码", text: $password)
}
}
// 4. 子视图 - 使用 @EnvironmentObject
struct ProfileView: View {
@EnvironmentObject var viewModel: UserViewModel
var body: some View {
Text("欢迎, \(viewModel.username)!")
Button("退出") {
viewModel.isLoggedIn = false
}
}
}
3. 布局系统
SwiftUI 使用 Stack 布局 替代 Auto Layout:
| 容器 | 作用 | 类比 UIKit |
|---|---|---|
VStack |
垂直排列 | UIStackView axis=.vertical |
HStack |
水平排列 | UIStackView axis=.horizontal |
ZStack |
层叠排列 | 多个视图叠加 |
LazyVStack |
懒加载垂直列表 | 优化的垂直列表 |
LazyHStack |
懒加载水平列表 | 优化的水平列表 |
Grid |
网格布局 | UICollectionView |
Form |
表单布局 | 分组 TableView |
布局示例:
swift
struct LayoutDemoView: View {
var body: some View {
ScrollView {
// 垂直布局
VStack(alignment: .leading, spacing: 16) {
// 水平布局
HStack {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
Text("用户信息")
.font(.title2)
.bold()
Spacer() // 弹性空间
Button("编辑") { }
}
.padding()
// ZStack 层叠
ZStack(alignment: .topTrailing) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.2))
.frame(height: 200)
Text("新")
.font(.caption)
.padding(4)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.offset(x: -8, y: 8)
}
// 网格布局
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 8) {
ForEach(0..<9) { index in
Rectangle()
.fill(Color.blue.opacity(0.3))
.aspectRatio(1, contentMode: .fit)
.overlay(Text("\(index)"))
}
}
}
.padding()
}
}
}
4. 常用组件示例
基础组件
swift
import SwiftUI
struct ComponentDemoView: View {
@State private var text = ""
@State private var isOn = false
@State private var sliderValue = 0.5
var body: some View {
List {
// 文本
Text("普通文本")
Text("自定义样式")
.font(.title)
.foregroundColor(.blue)
.bold()
// 图片
Image(systemName: "heart.fill")
.foregroundColor(.red)
.font(.largeTitle)
// 按钮
Button("普通按钮") { print("点击") }
Button(action: { print("自定义按钮") }) {
Label("自定义", systemImage: "star.fill")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
// 输入框
TextField("请输入用户名", text: $text)
.textFieldStyle(.roundedBorder)
// 开关
Toggle("开启通知", isOn: $isOn)
// 滑块
Slider(value: $sliderValue, in: 0...1)
Text("进度: \(Int(sliderValue * 100))%")
// 进度条
ProgressView(value: sliderValue)
}
}
}
导航与列表
swift
struct NavigationListDemoView: View {
let items = ["消息", "通讯录", "发现", "我的"]
var body: some View {
NavigationStack {
List(items, id: \.self) { item in
NavigationLink(destination: DetailView(title: item)) {
HStack {
Image(systemName: "folder")
Text(item)
}
}
}
.navigationTitle("首页")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("设置") { }
}
}
}
}
}
struct DetailView: View {
let title: String
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Text("这是 \(title) 页面")
.font(.largeTitle)
Button("返回") {
dismiss()
}
}
}
}
5. 动画系统
swift
struct AnimationDemoView: View {
@State private var isExpanded = false
@State private var rotation = 0.0
var body: some View {
VStack(spacing: 30) {
// 隐式动画
RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
.fill(Color.blue)
.frame(width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100)
.animation(.easeInOut(duration: 0.5), value: isExpanded)
// 显式动画
Image(systemName: "arrow.clockwise")
.font(.largeTitle)
.rotationEffect(.degrees(rotation))
.onTapGesture {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
rotation += 360
}
}
// 过渡动画
if isExpanded {
Text("出现/消失动画")
.transition(.scale.combined(with: .opacity))
}
Button("切换动画") {
withAnimation {
isExpanded.toggle()
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
6. 与 UIKit 混合使用
swift
// 在 SwiftUI 中嵌入 UIKit 组件
struct MapView: UIViewRepresentable {
let coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
// 在 UIKit 中嵌入 SwiftUI 视图
class HostingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = Text("来自 SwiftUI")
.font(.largeTitle)
.padding()
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
📊 对比分析
1. 代码量对比
相同功能登录页面:
| 框架 | 代码行数 | Builder 文件 | 总复杂度 |
|---|---|---|---|
| UIKit + Storyboard | ~60 行 + Storyboard | 1 个 XML | 中等 |
| UIKit + 纯代码 | ~120 行 | 1 个 Swift | 较高 |
| SwiftUI | ~30 行 | 1 个 Swift | 低 |
2. 性能对比
| 场景 | UIKit | SwiftUI | 说明 |
|---|---|---|---|
| 列表滚动 (1000 行) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | SwiftUI 需要 Lazy 容器 |
| 复杂动画 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 复杂动画需用 UIKit 兜底 |
| 启动速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | SwiftUI 首次渲染稍慢 |
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | SwiftUI 状态管理有开销 |
| 渲染帧率 | 60/120fps | 60/120fps | 两者可达流畅体验 |
3. 使用量统计(全球 iOS 应用)
| 指标 | UIKit | SwiftUI |
|---|---|---|
| 存量项目占比 | ~85% | ~15% |
| 2024 新项目占比 | ~40% | ~60% |
| 大型企业项目 | ~70% | ~30% |
| 个人/小团队项目 | ~30% | ~70% |
4. 学习曲线
text
学习难度
↑
高 │ UIKit纯代码 ──────────┐
│ │
中 │ UIKit+SB SwiftUI
│ │
低 │ ↓
└──────────────────────────────→ 时间
1周 1月 3月 1年
🎯 选型建议
选择 UIKit 的场景
markdown
✅ 适合 UIKit 的情况:
1. 维护 3 年以上的老项目
2. App 需要支持 iOS 13 以下版本
3. 团队 UIKit 经验丰富,SwiftUI 经验为 0
4. 应用有大量自定义复杂动画
5. 使用大量未适配 SwiftUI 的第三方库
6. 超长列表(如聊天记录、相册)需要极致性能
7. 深度集成 Core Graphics/Core Animation
8. 使用 SceneKit/ARKit 等 3D 框架
选择 SwiftUI 的场景
markdown
✅ 适合 SwiftUI 的情况:
1. 全新项目,无历史包袱
2. App 最低支持 iOS 15+(最佳体验 iOS 17+)
3. 团队有声明式 UI 经验(React/Flutter/Compose)
4. 快速原型开发或 MVP 产品
5. 跨 Apple 平台应用(iOS + macOS + watchOS)
6. UI 以标准组件为主,定制程度低
7. 希望减少代码量,提高开发效率
8. 需要实时预览快速迭代
混合开发策略(推荐)
swift
// 最佳实践:渐进式迁移
// 第一阶段:新页面用 SwiftUI
struct NewSettingsView: View {
var body: some View {
// SwiftUI 实现
}
}
// 在 UIKit 中展示
let hostingVC = UIHostingController(rootView: NewSettingsView())
navigationController?.pushViewController(hostingVC, animated: true)
// 第二阶段:复杂组件保留 UIKit
struct CustomMapView: UIViewRepresentable {
// 高性能地图组件继续用 UIKit
}
// 第三阶段:逐步将老页面重写
📝 总结
UIKit 核心要点
- ✅ 成熟稳定:10+ 年验证,坑都已踩平
- ✅ 高性能:列表滚动、动画极致优化
- ✅ 生态丰富:第三方库最多
- ❌ 代码量大:需要编写大量模板代码
- ❌ 状态管理复杂:容易出 Bug
- ❌ 未来发展有限:Apple 不再加入新特性
SwiftUI 核心要点
- ✅ 代码简洁:减少 50-70% 代码量
- ✅ 状态驱动:自动更新 UI,减少 Bug
- ✅ 跨平台:一套代码多端运行
- ✅ Apple 主推:未来所有新特性只在 SwiftUI 发布
- ❌ 需 iOS 13+:无法支持老设备
- ❌ 部分场景未成熟:复杂动画、长列表仍有坑
- ❌ 生态积累不足:部分三方库未适配
最终建议
对于 2024 年及以后的新项目,优先选择 SwiftUI。遇到 SwiftUI 无法满足的需求时,通过
UIViewRepresentable和UIViewControllerRepresentable嵌入 UIKit 组件。这种混合模式既能享受 SwiftUI 的开发效率,又能充分利用 UIKit 的成熟生态。
📚 参考资源
文档版本:1.0
最后更新:2026 年 5 月