欢迎光临
我们一直在努力

【SwiftUI娓娓道来】第1课:从一行文字开始,走进声明式UI的世界

开篇:为什么我们要学 SwiftUI?

如果你曾经想过开发 iPhone、iPad、Mac、Apple Watch 甚至 Apple TV 上的应用,那么你大概率听说过 UIKit、AppKit,或者近些年越来越火的 SwiftUI。

过去,苹果平台的界面开发主要依赖 UIKit 和 AppKit。它们非常强大,也非常成熟,但有一个共同点:命令式。什么意思呢?就像你请一位装修师傅装修房子,你不能只说“我想要一个温馨的客厅”,你得一步步告诉他:先把墙刷成米白色,再把沙发放在左边,茶几放中间,灯挂在上面,如果用户点了开关,就把灯打开,如果用户又点了开关,就把灯关掉。你得管理每一个步骤、每一个状态、每一个变化。

SwiftUI 的出现,改变了很多东西。它让界面开发变成了声明式。你只需要说:“我这里要一个客厅,里面有沙发、茶几和灯,灯的状态由 isLightOn 决定。”当 isLightOn 变成 true,SwiftUI 自己知道该把灯点亮;变成 false,它自己知道该把灯关掉。你不再需要手动去找到那盏灯、修改它的颜色、刷新界面。你描述“界面应该是什么样”,系统负责“怎么变成那样”。

这就是 SwiftUI 最核心的思想:状态驱动界面,界面是状态的函数。

不过,要真正理解 SwiftUI 的好,我们不能只盯着 SwiftUI 自己看。我们还要看看以前的 UIKit 是怎么写的。没有对比,就不容易体会声明式的轻松。所以这一课,我们会不断拿 SwiftUI 和 UIKit 做对比。每一个小功能,我都尽量给出两边代码。你看完就会明白:为什么 SwiftUI 会让界面代码变短、变清晰。

这一课会很长,但请你不要急着一次全部记住。你可以把每一节当成一个小故事,边看边在 Xcode 里敲一敲。学习 SwiftUI 最好的方式,不是背 API,而是动手改一改,看看界面怎么变。

好,我们开始。


一、SwiftUI 到底是什么?

1.1 一句话解释

SwiftUI 是苹果在 2019 年推出的一个用于构建用户界面的框架。它使用 Swift 语言,采用声明式语法,可以跨苹果平台使用。

你可以把它理解成一套“界面乐高”:

  • Text 是一块显示文字的积木;
  • Image 是一块显示图片的积木;
  • Button 是一块可以点击的积木;
  • VStack、HStack、ZStack 是把积木竖着、横着、叠着摆的积木;
  • List 是自动帮你排好一列内容的积木;
  • NavigationStack 是管理页面跳转的积木。

你把这些积木组合起来,就得到了一个界面。组合的方式不是写一堆“创建、添加、约束、更新”的命令,而是用代码描述出一棵树。这棵树叫 View 树。

1.2 声明式与命令式的区别:计数器例子

我们用一个例子感受一下。

假设我们要显示一个计数器:屏幕上有一个数字,下面有一个按钮,点一下数字加一。

SwiftUI 的写法:

import SwiftUI

struct CounterView: View {
@State private var count = 0

var body: some View {
VStack(spacing: 16) {
Text("你点了 \\(count) 次")
.font(.title2)
Button("点我") {
count += 1
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}

你只描述了:

  • 界面上有一个垂直排列;
  • 上面是文字,文字内容包含 count;
  • 下面是按钮,点击后 count 加一。

至于 count 变化后,文字怎么刷新、界面怎么重绘,SwiftUI 自己会处理。

UIKit 的写法:

import UIKit

class CounterViewController: UIViewController {
private var count = 0

private let label: UILabel = {
let label = UILabel()
label.text = "你点了 0 次"
label.font = .preferredFont(forTextStyle: .title2)
label.textAlignment = .center
return label
}()

private let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("点我", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
return button
}()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground

button.addTarget(self, action: #selector(increment), for: .touchUpInside)

let stack = UIStackView(arrangedSubviews: [label, button])
stack.axis = .vertical
stack.spacing = 16
stack.alignment = .center
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)

NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}

@objc private func increment() {
count += 1
label.text = "你点了 \\(count) 次"
}
}

你看,UIKit 里我们要:

  • 创建一个 UILabel;
  • 创建一个 UIButton;
  • 把 label 和 button 加到 stack;
  • 把 stack 加到 view;
  • 设置约束;
  • 给 button 添加点击事件;
  • 在点击事件里,把 count 加一;
  • 手动把 label 的文字改成新的 count。
  • 每一步都要你亲自做。SwiftUI 里,你只描述结果,系统负责过程。这就是声明式与命令式的最大区别。

    1.3 SwiftUI 能做什么?

    SwiftUI 能做的事情已经非常多:

    • iPhone、iPad App;
    • Mac App;
    • Apple Watch App;
    • Apple TV App;
    • 小组件 Widget;
    • 实时活动 Live Activity;
    • 部分 visionOS 应用。

    而且同一套代码,很多时候只需要少量调整,就能跑在不同设备上。当然,不同平台差异仍然存在,但 SwiftUI 大大降低了跨平台界面的成本。

    1.4 SwiftUI 和 UIKit 的关系

    你可能会问:学了 SwiftUI,还需要学 UIKit 吗?

    答案是:入门和大多数新项目,可以先用 SwiftUI。遇到 SwiftUI 暂时做不到或做不好的地方,再用 UIKit 桥接。

    SwiftUI 并不是要立刻消灭 UIKit。它们可以共存。比如你可以用 UIViewRepresentable 把 UIKit 的视图包装进 SwiftUI,也可以用 UIHostingController 把 SwiftUI 视图放进 UIKit。

    所以,不要有心理负担。你不需要先精通 UIKit 才能学 SwiftUI。相反,SwiftUI 更适合初学者建立界面开发的整体感觉。但了解 UIKit 的写法,能让你更懂 SwiftUI 为什么这样设计。


    二、第一个 SwiftUI 应用:从 App 到 View

    2.1 新建项目时发生了什么?

    在 Xcode 里新建一个 SwiftUI 项目,你会看到类似这样的代码:

    import SwiftUI

    @main
    struct MyFirstApp: App {
    var body: some Scene {
    WindowGroup {
    ContentView()
    }
    }
    }

    另一个文件是:

    import SwiftUI

    struct ContentView: View {
    var body: some View {
    VStack {
    Image(systemName: "globe")
    .imageScale(.large)
    .foregroundStyle(.tint)
    Text("Hello, world!")
    }
    .padding()
    }
    }

    我们一点点拆开看。

    2.2 @main 是什么?

    @main 表示这是程序的入口。以前在 UIKit 里,入口通常是 AppDelegate 和 SceneDelegate。在 SwiftUI 里,入口是一个遵循 App 协议的结构体。

    @main
    struct MyFirstApp: App {
    var body: some Scene {
    WindowGroup {
    ContentView()
    }
    }
    }

    • MyFirstApp 是整个 App 的类型;
    • body 返回一个 Scene;
    • WindowGroup 表示一个窗口组,在 iPhone 上通常就是一个全屏窗口;
    • ContentView() 是窗口里显示的第一个视图。

    你可以把 App 理解成“整个应用的外壳”,Scene 理解成“一个窗口或一组窗口”,View 理解成“窗口里的内容”。

    UIKit 的入口则通常长这样:

    import UIKit

    @main
    class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
    return true
    }

    func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
    }

    还有 SceneDelegate:

    import UIKit

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
    ) {
    guard let windowScene = scene as? UIWindowScene else { return }

    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = UINavigationController(
    rootViewController: CounterViewController()
    )
    self.window = window
    window.makeKeyAndVisible()
    }
    }

    对比一下:UIKit 要配置 AppDelegate、SceneDelegate、Window、rootViewController;SwiftUI 只要一个 WindowGroup { ContentView() }。这就是声明式入口的简洁。

    2.3 ContentView 是什么?

    struct ContentView: View {
    var body: some View {
    …
    }
    }

    • ContentView 是一个结构体;
    • 它遵循 View 协议;
    • 它必须实现 body 属性;
    • body 返回 some View,表示“某种具体的 View”,但我们不需要写出具体类型。

    这里的 some View 叫不透明返回类型。你不用管它背后到底是什么类型,编译器知道。你只需要知道:body 里写的就是界面。

    UIKit 里对应的概念通常是 UIViewController:

    class ContentViewController: UIViewController {
    override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground

    let label = UILabel()
    label.text = "Hello, world!"
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)

    NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])
    }
    }

    UIViewController 有生命周期:viewDidLoad、viewWillAppear、viewDidAppear。SwiftUI 的 View 没有这些传统生命周期,它更像一个描述,SwiftUI 会在需要时重新计算 body。

    2.4 body 里为什么不能写普通代码?

    body 是一个计算属性。它里面通常只写 View 的构建代码。你可以写 if、switch、ForEach,但不能随便写 var x = 1 这种语句,除非用 let 或某些允许的构建方式。

    这是因为 SwiftUI 使用了一个叫 ViewBuilder 的结果构建器。它会把你在 body 里写的一堆视图,自动组合成一个整体。

    例如:

    var body: some View {
    Text("第一行")
    Text("第二行")
    }

    这看起来像两个并列的 View。实际上,在 ViewBuilder 里,它们会被组合成一个元组视图。但更常见的是,我们会把它们放进 VStack 里,因为直接并列在 body 里,布局行为可能不是你想要的。


    三、View:SwiftUI 世界的积木

    3.1 View 不是 UIView

    在 UIKit 里,UIView 是一个类,是屏幕上的一块矩形区域。它有坐标、大小、背景色、子视图。

    在 SwiftUI 里,View 是一个协议。它更像一个“描述”。Text、Image、Button、VStack 都是 View。它们不是屏幕上的实体,而是对界面的描述。SwiftUI 会根据这些描述,在底层创建、更新、销毁真正的渲染对象。

    所以,请不要把 SwiftUI 的 View 直接等同于 UIKit 的 UIView。它更轻、更抽象。

    3.2 View 是结构体,不是类

    SwiftUI 里大多数 View 都是结构体。结构体是值类型。这意味着当你传递一个 View 时,传递的是一份描述,而不是一个共享的引用。

    这带来很多好处:

    • 状态管理更清晰;
    • 不容易意外修改;
    • 组合和复用更容易。

    但也要注意:结构体里不能像类那样随意修改属性。如果你想修改状态,需要用 @State 等属性包装器。

    UIKit 的 UIView 和 UIViewController 是类,是引用类型。你可以持有它们的指针,随时修改它们。比如:

    class MyView: UIView {
    var count = 0
    }

    你可以直接改 myView.count,然后手动刷新界面。SwiftUI 不鼓励这种方式,它希望你通过状态变化来驱动界面。

    3.3 组合优于继承

    UIKit 里经常用继承:创建一个 BaseViewController,然后子类化。SwiftUI 更推荐组合:把小的 View 组合成大的 View。

    例如 SwiftUI 的 UserCard:

    struct UserCard: View {
    var body: some View {
    HStack {
    Image(systemName: "person.circle.fill")
    .font(.largeTitle)
    VStack(alignment: .leading) {
    Text("张三")
    .font(.headline)
    Text("iOS 开发者")
    .font(.subheadline)
    .foregroundStyle(.secondary)
    }
    Spacer()
    }
    .padding()
    .background(Color(.systemGray6))
    .clipShape(RoundedRectangle(cornerRadius: 12))
    .padding(.horizontal)
    }
    }

    UIKit 里,你通常要自定义一个 UIView 子类:

    import UIKit

    class UserCardView: UIView {
    private let iconView = UIImageView()
    private let nameLabel = UILabel()
    private let jobLabel = UILabel()

    override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
    }

    required init?(coder: NSCoder) {
    super.init(coder: coder)
    setup()
    }

    private func setup() {
    backgroundColor = .systemGray6
    layer.cornerRadius = 12
    layer.masksToBounds = true

    iconView.image = UIImage(systemName: "person.circle.fill")
    iconView.tintColor = .label
    iconView.contentMode = .scaleAspectFit
    iconView.translatesAutoresizingMaskIntoConstraints = false

    nameLabel.text = "张三"
    nameLabel.font = .preferredFont(forTextStyle: .headline)

    jobLabel.text = "iOS 开发者"
    jobLabel.font = .preferredFont(forTextStyle: .subheadline)
    jobLabel.textColor = .secondaryLabel

    let textStack = UIStackView(arrangedSubviews: [nameLabel, jobLabel])
    textStack.axis = .vertical
    textStack.spacing = 4

    let stack = UIStackView(arrangedSubviews: [iconView, textStack])
    stack.axis = .horizontal
    stack.spacing = 12
    stack.alignment = .center
    stack.translatesAutoresizingMaskIntoConstraints = false
    addSubview(stack)

    NSLayoutConstraint.activate([
    iconView.widthAnchor.constraint(equalToConstant: 44),
    iconView.heightAnchor.constraint(equalToConstant: 44),
    stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
    stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: –16),
    stack.topAnchor.constraint(equalTo: topAnchor, constant: 16),
    stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: –16)
    ])
    }
    }

    这个 UserCardView 可以到处使用。它不是通过继承得到的,而是通过组合 UIImageView、UILabel、UIStackView 得到的。SwiftUI 把这种组合变得非常自然。

    3.4 自定义 View 的好处

    当界面复杂时,把一部分抽成自定义 View,会让代码更清晰。

    SwiftUI:

    struct TitleView: View {
    let title: String

    var body: some View {
    Text(title)
    .font(.title)
    .bold()
    .foregroundStyle(.blue)
    }
    }

    使用:

    VStack {
    TitleView(title: "欢迎")
    TitleView(title: "SwiftUI")
    }

    UIKit 里通常要自定义 UIView 或封装一个方法。SwiftUI 的自定义 View 更轻,组合更容易。


    四、Text 与 Image:最基础的两块积木

    4.1 Text 与 UILabel

    SwiftUI 显示文字:

    Text("Hello, SwiftUI")

    设置字体:

    Text("标题")
    .font(.title)

    颜色:

    Text("红色文字")
    .foregroundStyle(.red)

    UIKit 显示文字:

    let label = UILabel()
    label.text = "Hello, UIKit"
    label.font = .preferredFont(forTextStyle: .title1)
    label.textColor = .systemRed
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)

    对比一下:SwiftUI 的 Text 是一行描述,UIKit 要创建对象、设置属性、加入视图、设置约束。SwiftUI 的修饰符链可以不断叠加,UIKit 则要一行行改属性。

    4.2 Image 与 UIImageView

    SwiftUI 显示系统图标:

    Image(systemName: "star.fill")
    .foregroundStyle(.yellow)

    显示资源图片:

    Image("myPhoto")
    .resizable()
    .scaledToFit()

    UIKit 显示图片:

    let imageView = UIImageView()
    imageView.image = UIImage(systemName: "star.fill")
    imageView.tintColor = .systemYellow
    imageView.contentMode = .scaleAspectFit
    imageView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(imageView)

    如果要圆角裁剪:

    SwiftUI:

    Image("myPhoto")
    .resizable()
    .scaledToFill()
    .frame(width: 100, height: 100)
    .clipShape(Circle())

    UIKit:

    let imageView = UIImageView()
    imageView.image = UIImage(named: "myPhoto")
    imageView.contentMode = .scaleAspectFill
    imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    imageView.layer.cornerRadius = 50
    imageView.clipsToBounds = true

    4.3 SF Symbols:苹果的图标库

    SF Symbols 是苹果提供的图标库,数量很多,风格统一。SwiftUI 使用方式:

    Image(systemName: "heart.fill")

    UIKit 使用方式:

    let image = UIImage(systemName: "heart.fill")
    let imageView = UIImageView(image: image)

    4.4 文字与图标组合

    SwiftUI:

    Label("设置", systemImage: "gear")

    UIKit 里通常要组合 UIImageView 和 UILabel,或者用 UIButton 的 configuration:

    var config = UIButton.Configuration.plain()
    config.title = "设置"
    config.image = UIImage(systemName: "gear")
    config.imagePadding = 8
    let button = UIButton(configuration: config)

    SwiftUI 的 Label 更简单,而且会自动适配不同平台。


    五、布局三剑客:VStack、HStack、ZStack

    5.1 VStack 与 UIStackView 垂直排列

    SwiftUI:

    VStack(spacing: 20) {
    Text("第一行")
    Text("第二行")
    Text("第三行")
    }

    UIKit:

    let stack = UIStackView(arrangedSubviews: [
    UILabel(),
    UILabel(),
    UILabel()
    ])
    stack.axis = .vertical
    stack.spacing = 20
    stack.alignment = .center
    stack.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(stack)

    如果你不想用 UIStackView,那就要给每个 label 加约束,代码会更多。SwiftUI 的 VStack 就是布局容器,写起来非常直接。

    5.2 HStack 与 UIStackView 水平排列

    SwiftUI:

    HStack {
    Image(systemName: "person")
    Text("张三")
    }

    UIKit:

    let iconView = UIImageView(image: UIImage(systemName: "person"))
    let nameLabel = UILabel()
    nameLabel.text = "张三"

    let stack = UIStackView(arrangedSubviews: [iconView, nameLabel])
    stack.axis = .horizontal
    stack.spacing = 8
    stack.alignment = .center

    5.3 ZStack 与重叠视图

    SwiftUI:

    ZStack {
    Color.blue
    Text("我在上面")
    .foregroundStyle(.white)
    }

    UIKit 里要重叠视图,通常是把两个 view 加到同一个父视图,然后通过约束或 frame 让它们重叠:

    let backgroundView = UIView()
    backgroundView.backgroundColor = .systemBlue
    backgroundView.translatesAutoresizingMaskIntoConstraints = false

    let label = UILabel()
    label.text = "我在上面"
    label.textColor = .white
    label.translatesAutoresizingMaskIntoConstraints = false

    view.addSubview(backgroundView)
    view.addSubview(label)

    NSLayoutConstraint.activate([
    backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
    backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])

    SwiftUI 的 ZStack 把“叠放”变成了一个明确的容器,顺序就是层级,非常直观。

    5.4 Spacer 与 Auto Layout 弹簧

    SwiftUI:

    HStack {
    Text("左边")
    Spacer()
    Text("右边")
    }

    UIKit 里通常用约束:

    let leftLabel = UILabel()
    leftLabel.text = "左边"
    let rightLabel = UILabel()
    rightLabel.text = "右边"

    leftLabel.translatesAutoresizingMaskIntoConstraints = false
    rightLabel.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(leftLabel)
    view.addSubview(rightLabel)

    NSLayoutConstraint.activate([
    leftLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    leftLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),

    rightLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: –16),
    rightLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])

    SwiftUI 的 Spacer() 就像弹簧,自动占据剩余空间。

    5.5 padding 和 frame 与约束

    SwiftUI:

    Text("按钮文字")
    .padding()
    .frame(maxWidth: .infinity)
    .background(.blue)
    .foregroundStyle(.white)
    .clipShape(RoundedRectangle(cornerRadius: 12))

    UIKit 里要设置内边距、宽度、背景色、圆角:

    let label = UILabel()
    label.text = "按钮文字"
    label.textAlignment = .center
    label.textColor = .white
    label.backgroundColor = .systemBlue
    label.layer.cornerRadius = 12
    label.clipsToBounds = true
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)

    NSLayoutConstraint.activate([
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: –16),
    label.heightAnchor.constraint(equalToConstant: 44)
    ])

    SwiftUI 的修饰符链把样式和布局写在一起,阅读起来像一句话。UIKit 则是分散的属性设置和约束代码。

    5.6 alignment 和 spacing 的小结

    • VStack 控制垂直排列,alignment 控制水平方向对齐;
    • HStack 控制水平排列,alignment 控制垂直方向对齐;
    • ZStack 控制叠放,alignment 控制整体对齐。

    这些概念一开始容易混,多写几个界面就熟悉了。


    六、修饰符:给 View 穿衣服

    6.1 什么是修饰符?

    SwiftUI 里,你经常看到:

    Text("Hello")
    .font(.title)
    .foregroundStyle(.blue)
    .padding()

    这些 .font、.foregroundStyle、.padding 就是修饰符。它们返回一个新的 View。所以链式调用的顺序很重要。

    UIKit 里没有完全对应的“修饰符链”。UIKit 更多是直接改属性:

    let label = UILabel()
    label.text = "Hello"
    label.font = .preferredFont(forTextStyle: .title1)
    label.textColor = .systemBlue
    label.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)

    SwiftUI 把“设置属性”变成了“包装 View”。每次修饰都产生一个新 View,最终组合成完整的界面。

    6.2 顺序为什么重要?

    看两个例子:

    Text("Hello")
    .padding()
    .background(.yellow)

    先加内边距,再加背景。背景会包含内边距区域。

    Text("Hello")
    .background(.yellow)
    .padding()

    先加背景,再加内边距。内边距在背景外面,背景只包住文字。

    所以,修饰符的顺序会改变结果。你可以把修饰符理解成“一层层包装”。

    UIKit 里,顺序通常不影响属性本身,但会影响视图层级和约束。例如你先加背景视图还是先加 label,会决定谁在上面。SwiftUI 用修饰符顺序把这种层级关系表达得更直接。

    6.3 常用修饰符与 UIKit 对应

    • .font() 对应 UILabel.font;
    • .foregroundStyle() 对应 UILabel.textColor / UIImageView.tintColor;
    • .background() 对应 UIView.backgroundColor 或额外背景视图;
    • .padding() 对应约束常量或 layoutMargins;
    • .frame() 对应 NSLayoutConstraint 的宽高;
    • .clipShape() 对应 layer.cornerRadius + clipsToBounds;
    • .shadow() 对应 layer.shadowColor、shadowOpacity、shadowRadius 等;
    • .opacity() 对应 UIView.alpha。

    6.4 自定义 ViewModifier 与 UIKit 封装

    SwiftUI 可以自定义:

    struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
    content
    .padding()
    .background(.white)
    .clipShape(RoundedRectangle(cornerRadius: 16))
    .shadow(radius: 8)
    .padding()
    }
    }

    extension View {
    func cardStyle() -> some View {
    modifier(CardModifier())
    }
    }

    使用:

    Text("卡片内容")
    .cardStyle()

    UIKit 里也可以封装一个 UIView 子类或扩展:

    extension UIView {
    func applyCardStyle() {
    backgroundColor = .white
    layer.cornerRadius = 16
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOpacity = 0.1
    layer.shadowRadius = 8
    layer.shadowOffset = CGSize(width: 0, height: 4)
    }
    }

    但 SwiftUI 的 ViewModifier 更统一,因为它本身就是 View 组合的一部分。


    七、状态:让界面动起来

    7.1 没有状态,界面就是静态的

    如果界面只是显示固定文字,那很简单。但 App 需要交互:点击、输入、切换、滑动。交互会改变数据,数据改变后界面要更新。这个“会变化的数据”就是状态。

    SwiftUI 里最常用的状态包装器是 @State。

    struct CounterView: View {
    @State private var count = 0

    var body: some View {
    VStack {
    Text("当前计数:\\(count)")
    Button("加一") {
    count += 1
    }
    }
    }
    }

    7.2 @State 做了什么?

    • 它把 count 交给 SwiftUI 管理;
    • 当 count 改变时,SwiftUI 会重新计算 body;
    • 界面根据新的 count 自动更新。

    注意:@State 通常用于 View 内部的私有状态,所以习惯写成:

    @State private var count = 0

    UIKit 里对应的状态就是普通属性:

    class CounterViewController: UIViewController {
    private var count = 0
    private let label = UILabel()

    @objc private func increment() {
    count += 1
    label.text = "当前计数:\\(count)"
    }
    }

    区别在于:SwiftUI 改 count 就够了,界面自动更新;UIKit 改完 count 还要手动更新 label.text。如果有十个控件依赖这个状态,你就要手动更新十次。SwiftUI 不需要。

    7.3 状态驱动界面

    SwiftUI 的核心公式:

    界面 = f(状态)

    你改变状态,界面自动变。你不需要手动刷新。

    再看一个开关例子:

    struct ToggleView: View {
    @State private var isOn = false

    var body: some View {
    VStack {
    Toggle("开启通知", isOn: $isOn)
    Text(isOn ? "通知已开启" : "通知已关闭")
    .foregroundStyle(isOn ? .green : .secondary)
    }
    .padding()
    }
    }

    UIKit 里:

    class ToggleViewController: UIViewController {
    private let toggle = UISwitch()
    private let statusLabel = UILabel()

    override func viewDidLoad() {
    super.viewDidLoad()
    toggle.addTarget(self, action: #selector(toggleChanged), for: .valueChanged)
    statusLabel.text = "通知已关闭"
    statusLabel.textColor = .secondaryLabel
    // 这里省略布局代码
    }

    @objc private func toggleChanged() {
    if toggle.isOn {
    statusLabel.text = "通知已开启"
    statusLabel.textColor = .systemGreen
    } else {
    statusLabel.text = "通知已关闭"
    statusLabel.textColor = .secondaryLabel
    }
    }
    }

    SwiftUI 用 isOn 一个状态,文字自动跟着变。UIKit 要在事件里手动改文字颜色和内容。

    7.4 多个状态

    struct ProfileView: View {
    @State private var name = "张三"
    @State private var age = 28
    @State private var isVip = false

    var body: some View {
    VStack(alignment: .leading, spacing: 12) {
    Text("姓名:\\(name)")
    Text("年龄:\\(age)")
    Text("VIP:\\(isVip ? "是" : "否")")
    }
    .padding()
    }
    }

    状态可以很多,但不要把所有东西都塞进一个 View。复杂状态可以抽成模型。


    八、Binding:把状态传给子视图

    8.1 为什么需要 Binding?

    如果子视图需要修改父视图的状态,直接传值不行,因为值类型是拷贝。这时需要 @Binding。

    struct ParentView: View {
    @State private var name = "张三"

    var body: some View {
    VStack {
    Text("父视图:\\(name)")
    ChildView(name: $name)
    }
    }
    }

    struct ChildView: View {
    @Binding var name: String

    var body: some View {
    TextField("输入名字", text: $name)
    .textFieldStyle(.roundedBorder)
    .padding()
    }
    }

    $name 把 @State 变成了绑定。子视图通过 @Binding 读写同一个状态。

    8.2 Binding 与 UIKit 的代理/闭包

    UIKit 里子视图要通知父视图,通常用:

    • 代理 delegate;
    • 闭包 closure;
    • target-action;
    • NotificationCenter。

    例如一个自定义输入视图:

    class NameInputView: UIView {
    var onNameChanged: ((String) -> Void)?
    private let textField = UITextField()

    @objc private func textChanged() {
    onNameChanged?(textField.text ?? "")
    }
    }

    父视图:

    let inputView = NameInputView()
    inputView.onNameChanged = { [weak self] name in
    self?.nameLabel.text = name
    }

    SwiftUI 的 @Binding 把这种“双向同步”变得非常简单。你不需要写代理方法,也不需要手动调用闭包。绑定本身就是连接。

    8.3 自定义 Binding

    有时需要把绑定转换:

    struct ContentView: View {
    @State private var isOn = false

    var body: some View {
    Toggle("开关", isOn: Binding(
    get: { isOn },
    set: { newValue in
    print("变成 \\(newValue)")
    isOn = newValue
    }
    ))
    }
    }

    这适合在状态变化时做额外处理。UIKit 里你会在 valueChanged 事件里做类似事情。


    九、List 与 ForEach:展示一列数据

    9.1 静态 List 与 UITableView

    SwiftUI:

    List {
    Text("第一项")
    Text("第二项")
    Text("第三项")
    }

    UIKit 里,你要用 UITableView,实现数据源:

    class SimpleListViewController: UITableViewController {
    let items = ["第一项", "第二项", "第三项"]

    override func viewDidLoad() {
    super.viewDidLoad()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = items[indexPath.row]
    return cell
    }
    }

    SwiftUI 的 List 自动处理复用、滚动、样式。你只描述每一项是什么。

    9.2 动态 List 与 ForEach

    SwiftUI:

    struct FruitListView: View {
    let fruits = ["苹果", "香蕉", "橙子", "西瓜"]

    var body: some View {
    List(fruits, id: \\.self) { fruit in
    Text(fruit)
    }
    }
    }

    UIKit 里要维护数组、实现 numberOfRows、cellForRowAt。数据变化时还要 reloadData 或 performBatchUpdates。

    9.3 使用 Identifiable 与 UITableViewDiffableDataSource

    SwiftUI 推荐模型遵循 Identifiable:

    struct Fruit: Identifiable {
    let id = UUID()
    let name: String
    }

    UIKit 现代写法可以用 UITableViewDiffableDataSource,但配置更复杂:

    enum Section {
    case main
    }

    var dataSource: UITableViewDiffableDataSource<Section, Fruit>!

    func configureDataSource() {
    dataSource = UITableViewDiffableDataSource<Section, Fruit>(tableView: tableView) { tableView, indexPath, fruit in
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = fruit.name
    return cell
    }
    }

    func applySnapshot() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Fruit>()
    snapshot.appendSections([.main])
    snapshot.appendItems(fruits)
    dataSource.apply(snapshot, animatingDifferences: true)
    }

    SwiftUI 的 List 和 ForEach 把这套流程简化成声明。

    9.4 删除与移动

    SwiftUI:

    @State private var fruits = ["苹果", "香蕉", "橙子"]

    List {
    ForEach(fruits, id: \\.self) { fruit in
    Text(fruit)
    }
    .onDelete { indexSet in
    fruits.remove(atOffsets: indexSet)
    }
    }

    UIKit:

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
    fruits.remove(at: indexPath.row)
    tableView.deleteRows(at: [indexPath], with: .automatic)
    }
    }

    SwiftUI 的 onDelete 更声明式:你只需要说“删除时怎么改数据”,界面更新交给系统。


    十、导航:从一页到另一页

    10.1 NavigationStack 与 UINavigationController

    SwiftUI:

    NavigationStack {
    List {
    NavigationLink("进入详情") {
    Text("详情页面")
    }
    }
    .navigationTitle("首页")
    }

    UIKit:

    let listViewController = UITableViewController()
    let navigationController = UINavigationController(rootViewController: listViewController)

    // 在点击某一行时:
    let detailViewController = UIViewController()
    detailViewController.view.backgroundColor = .systemBackground
    let label = UILabel()
    label.text = "详情页面"
    // 省略布局
    detailViewController.view.addSubview(label)
    navigationController.pushViewController(detailViewController, animated: true)

    SwiftUI 用 NavigationLink 描述“点了去哪”。UIKit 要手动 pushViewController。

    10.2 传值跳转

    SwiftUI:

    struct User: Identifiable {
    let id = UUID()
    let name: String
    }

    struct UserListView: View {
    let users = [User(name: "张三"), User(name: "李四")]

    var body: some View {
    NavigationStack {
    List(users) { user in
    NavigationLink(user.name) {
    UserDetailView(user: user)
    }
    }
    .navigationTitle("用户")
    }
    }
    }

    UIKit:

    class UserListViewController: UITableViewController {
    let users = [User(name: "张三"), User(name: "李四")]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let user = users[indexPath.row]
    let detail = UserDetailViewController(user: user)
    navigationController?.pushViewController(detail, animated: true)
    }
    }

    SwiftUI 把导航目标写在 NavigationLink 里,UIKit 要在 didSelectRowAt 里手动创建和推入。

    10.3 工具栏

    SwiftUI:

    .toolbar {
    ToolbarItem(placement: .topBarTrailing) {
    Button {
    // 添加操作
    } label: {
    Image(systemName: "plus")
    }
    }
    }

    UIKit:

    navigationItem.rightBarButtonItem = UIBarButtonItem(
    barButtonSystemItem: .add,
    target: self,
    action: #selector(addTapped)
    )

    SwiftUI 的工具栏也是声明式的,UIKit 要创建 UIBarButtonItem 并绑定 target-action。


    十一、表单与常用控件

    11.1 Form 与 UITableView 设置页

    SwiftUI:

    Form {
    Section("基本信息") {
    TextField("姓名", text: $name)
    TextField("年龄", text: $age)
    }

    Section("偏好") {
    Toggle("接收通知", isOn: $isNotificationOn)
    }
    }

    UIKit 里做设置页通常用 UITableView 的 .insetGrouped 样式:

    class SettingsViewController: UITableViewController {
    let nameField = UITextField()
    let ageField = UITextField()
    let notificationSwitch = UISwitch()

    override func viewDidLoad() {
    super.viewDidLoad()
    tableView = UITableView(frame: .zero, style: .insetGrouped)
    // 这里还要实现 numberOfSections、numberOfRows、cellForRowAt
    // 每个 cell 里放不同的控件
    }
    }

    SwiftUI 的 Form 自动处理分组、背景、间距,非常像系统设置页。

    11.2 TextField 与 UITextField

    SwiftUI:

    @State private var username = ""

    TextField("请输入用户名", text: $username)
    .textFieldStyle(.roundedBorder)

    UIKit:

    let textField = UITextField()
    textField.placeholder = "请输入用户名"
    textField.borderStyle = .roundedRect
    textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)

    @objc private func textChanged() {
    username = textField.text ?? ""
    }

    SwiftUI 的 $username 自动双向绑定。UIKit 要手动监听变化并赋值。

    11.3 Toggle 与 UISwitch

    SwiftUI:

    @State private var isOn = true
    Toggle("Wi-Fi", isOn: $isOn)

    UIKit:

    let switchControl = UISwitch()
    switchControl.isOn = true
    switchControl.addTarget(self, action: #selector(switchChanged), for: .valueChanged)

    @objc private func switchChanged() {
    isOn = switchControl.isOn
    }

    11.4 Slider 与 UISlider

    SwiftUI:

    @State private var volume = 0.5
    Slider(value: $volume, in: 0…1)

    UIKit:

    let slider = UISlider()
    slider.minimumValue = 0
    slider.maximumValue = 1
    slider.value = 0.5
    slider.addTarget(self, action: #selector(sliderChanged), for: .valueChanged)

    @objc private func sliderChanged() {
    volume = slider.value
    }

    11.5 Picker 与 UIPickerView / UISegmentedControl

    SwiftUI:

    @State private var selected = "苹果"
    let options = ["苹果", "香蕉", "橙子"]

    Picker("选择水果", selection: $selected) {
    ForEach(options, id: \\.self) { option in
    Text(option)
    }
    }

    UIKit 可以用 UISegmentedControl:

    let segmented = UISegmentedControl(items: ["苹果", "香蕉", "橙子"])
    segmented.selectedSegmentIndex = 0
    segmented.addTarget(self, action: #selector(segmentChanged), for: .valueChanged)

    @objc private func segmentChanged() {
    selected = options[segmented.selectedSegmentIndex]
    }

    SwiftUI 的 Picker 会根据平台自动选择合适样式,UIKit 要自己选控件。

    11.6 Button 与 UIButton

    SwiftUI:

    Button("点击我") {
    print("点击了")
    }

    带样式:

    Button {
    print("点击了")
    } label: {
    Label("添加", systemImage: "plus")
    }
    .buttonStyle(.borderedProminent)

    UIKit:

    let button = UIButton(type: .system)
    button.setTitle("点击我", for: .normal)
    button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)

    @objc private func buttonTapped() {
    print("点击了")
    }

    SwiftUI 的按钮把“动作”和“标签”放在一起,UIKit 要分开设置。


    十二、数据流与架构:从 @State 到 ObservableObject

    12.1 简单状态用 @State

    如果状态只在一个 View 内部使用,@State 就够了。

    12.2 跨视图共享用 ObservableObject

    当数据模型复杂、多个 View 需要共享时,可以创建一个类:

    class TodoStore: ObservableObject {
    @Published var todos: [String] = []

    func add(_ todo: String) {
    todos.append(todo)
    }

    func delete(at offsets: IndexSet) {
    todos.remove(atOffsets: offsets)
    }
    }

    在 View 中使用:

    struct TodoListView: View {
    @StateObject private var store = TodoStore()
    @State private var newTodo = ""

    var body: some View {
    NavigationStack {
    VStack {
    HStack {
    TextField("新待办", text: $newTodo)
    .textFieldStyle(.roundedBorder)
    Button("添加") {
    guard !newTodo.isEmpty else { return }
    store.add(newTodo)
    newTodo = ""
    }
    }
    .padding()

    List {
    ForEach(store.todos, id: \\.self) { todo in
    Text(todo)
    }
    .onDelete(perform: store.delete)
    }
    }
    .navigationTitle("待办清单")
    }
    }
    }

    12.3 UIKit 里对应什么?

    UIKit 没有 ObservableObject 这种内建机制。你通常会用:

    • 模型对象;
    • 代理;
    • 闭包;
    • NotificationCenter;
    • KVO;
    • MVVM 里的 ViewModel。

    例如一个简单的 UIKit ViewModel:

    class TodoViewModel {
    private(set) var todos: [String] = []
    var onChange: (() -> Void)?

    func add(_ todo: String) {
    todos.append(todo)
    onChange?()
    }

    func delete(at index: Int) {
    todos.remove(at: index)
    onChange?()
    }
    }

    控制器里:

    class TodoViewController: UITableViewController {
    let viewModel = TodoViewModel()

    override func viewDidLoad() {
    super.viewDidLoad()
    viewModel.onChange = { [weak self] in
    self?.tableView.reloadData()
    }
    }
    }

    SwiftUI 的 @Published 和 @StateObject 把这种“变化通知”内建化了。你不需要手动写 onChange,SwiftUI 会自动观察。

    12.4 单向数据流

    SwiftUI 推崇单向数据流:

    • 状态在某个地方定义;
    • 状态通过绑定或环境向下传递;
    • 子视图通过事件向上通知;
    • 状态改变后,界面重新计算。

    UIKit 也可以做单向数据流,但需要你自己搭建。SwiftUI 从框架层面鼓励这种模式,所以代码更容易保持清晰。


    十三、综合小项目:待办清单

    我们把前面学的串起来,做一个简单待办清单。先看 SwiftUI 完整版。

    import SwiftUI

    struct Todo: Identifiable {
    let id = UUID()
    var title: String
    var isDone: Bool = false
    }

    class TodoStore: ObservableObject {
    @Published var todos: [Todo] = [
    Todo(title: "学习 SwiftUI"),
    Todo(title: "写一个 Demo"),
    Todo(title: "复习状态管理", isDone: true)
    ]

    func add(title: String) {
    todos.append(Todo(title: title))
    }

    func toggle(_ todo: Todo) {
    if let index = todos.firstIndex(where: { $0.id == todo.id }) {
    todos[index].isDone.toggle()
    }
    }

    func delete(at offsets: IndexSet) {
    todos.remove(atOffsets: offsets)
    }
    }

    struct TodoRowView: View {
    let todo: Todo
    let onToggle: () -> Void

    var body: some View {
    HStack {
    Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
    .foregroundStyle(todo.isDone ? .green : .secondary)
    .onTapGesture {
    onToggle()
    }

    Text(todo.title)
    .strikethrough(todo.isDone)
    .foregroundStyle(todo.isDone ? .secondary : .primary)

    Spacer()
    }
    .padding(.vertical, 4)
    }
    }

    struct TodoListView: View {
    @StateObject private var store = TodoStore()
    @State private var newTitle = ""

    var body: some View {
    NavigationStack {
    VStack(spacing: 0) {
    HStack {
    TextField("输入新待办", text: $newTitle)
    .textFieldStyle(.roundedBorder)

    Button {
    guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return }
    store.add(title: newTitle)
    newTitle = ""
    } label: {
    Image(systemName: "plus.circle.fill")
    .font(.title2)
    }
    }
    .padding()

    List {
    ForEach(store.todos) { todo in
    TodoRowView(todo: todo) {
    store.toggle(todo)
    }
    }
    .onDelete(perform: store.delete)
    }
    .listStyle(.plain)
    }
    .navigationTitle("待办清单")
    }
    }
    }

    这个项目虽小,但包含了:

    • 模型 Todo;
    • 状态管理 TodoStore;
    • @StateObject;
    • @Published;
    • List、ForEach;
    • NavigationStack;
    • TextField、Button;
    • 自定义 Row;
    • 删除、切换完成状态。

    现在看 UIKit 的简化版,感受一下同样的功能需要多少代码。

    import UIKit

    struct Todo {
    var title: String
    var isDone: Bool = false
    }

    class TodoListViewController: UITableViewController {
    private var todos: [Todo] = [
    Todo(title: "学习 SwiftUI"),
    Todo(title: "写一个 Demo"),
    Todo(title: "复习状态管理", isDone: true)
    ]

    private let newTodoField = UITextField()

    override func viewDidLoad() {
    super.viewDidLoad()
    title = "待办清单"
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

    navigationItem.rightBarButtonItem = UIBarButtonItem(
    barButtonSystemItem: .add,
    target: self,
    action: #selector(addTapped)
    )

    let header = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 56))
    newTodoField.placeholder = "输入新待办"
    newTodoField.borderStyle = .roundedRect
    newTodoField.frame = header.bounds.insetBy(dx: 16, dy: 8)
    newTodoField.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    header.addSubview(newTodoField)
    tableView.tableHeaderView = header
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    todos.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let todo = todos[indexPath.row]
    cell.textLabel?.text = todo.title
    cell.textLabel?.textColor = todo.isDone ? .secondaryLabel : .label
    cell.accessoryType = todo.isDone ? .checkmark : .none
    return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    todos[indexPath.row].isDone.toggle()
    tableView.reloadRows(at: [indexPath], with: .automatic)
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
    todos.remove(at: indexPath.row)
    tableView.deleteRows(at: [indexPath], with: .automatic)
    }
    }

    @objc private func addTapped() {
    guard let text = newTodoField.text, !text.isEmpty else { return }
    todos.append(Todo(title: text))
    newTodoField.text = ""
    let indexPath = IndexPath(row: todos.count – 1, section: 0)
    tableView.insertRows(at: [indexPath], with: .automatic)
    }
    }

    你看,UIKit 版本要手动管理:

    • 表格注册;
    • 数据源方法;
    • 行点击;
    • 删除;
    • 添加按钮;
    • 输入框;
    • 刷新行;
    • 插入行。

    SwiftUI 版本则更多是在描述“界面长什么样”和“数据怎么变”。这就是声明式的威力。

    你可以继续扩展 SwiftUI 待办清单:添加优先级、截止日期、分类、搜索、持久化等。你会发现,大多数扩展只是增加状态和 View,而不是增加一堆手动刷新逻辑。


    十四、常见坑与调试建议

    14.1 预览不刷新

    检查:

    • 是否选择了正确的 Preview;
    • 是否有编译错误;
    • 是否在 #Preview 里传入了必要参数。

    新版预览:

    #Preview {
    ContentView()
    }

    14.2 修改了状态但界面没变

    可能原因:

    • 修改的不是 @State、@Published 等可观察状态;
    • 在结构体里直接改了普通属性;
    • 状态在子视图里是值拷贝,没有用 @Binding;
    • 对象没有遵循 ObservableObject,或属性没有 @Published。

    14.3 List 报错“ID 重复”

    确保 ForEach 的 id 唯一。用 Identifiable 或指定唯一 id。

    14.4 视图太复杂

    把大 body 拆成小 View。SwiftUI 里,拆 View 几乎无成本,反而更清晰。

    14.5 不要过度设计

    第1课先求“能跑、能看懂”。架构可以慢慢学。先让界面动起来,再谈优化。


    十五、第1课总结与练习

    这一课我们讲了很多。我们回顾一下核心知识:

  • SwiftUI 是声明式 UI 框架;
  • 界面是状态的函数;
  • App 是入口,Scene 是窗口,View 是内容;
  • body 描述界面;
  • Text、Image、Button 是基础积木;
  • VStack、HStack、ZStack 负责布局;
  • 修饰符给 View 穿衣服,顺序很重要;
  • @State 管理 View 内部状态;
  • @Binding 在父子视图间共享状态;
  • List、ForEach 展示列表;
  • NavigationStack 管理页面跳转;
  • Form、TextField、Toggle、Picker 构建表单;
  • ObservableObject、@StateObject、@Published 管理复杂数据;
  • 单向数据流让代码更清晰。
  • 同时,我们也反复对比了 UIKit:

    • UIKit 是命令式,SwiftUI 是声明式;
    • UIKit 要创建视图、加约束、绑事件、手动刷新;
    • SwiftUI 描述界面和状态,系统负责更新;
    • UIKit 适合精细控制,SwiftUI 适合快速组合;
    • 两者可以共存,不是非此即彼。

    练习一:个人名片

    分别用 SwiftUI 和 UIKit 做一个 ProfileCard,包含头像、姓名、职业、简介。SwiftUI 用 VStack、HStack、Image、Text;UIKit 用 UIStackView、UILabel、UIImageView。做完对比代码量。

    练习二:计数器

    SwiftUI 用 @State 做一个计数器;UIKit 用 UILabel、UIButton 和 target-action 做一个计数器。感受“自动刷新”和“手动刷新”的区别。

    练习三:水果列表

    创建一个水果数组,分别用 SwiftUI 的 List 和 UIKit 的 UITableView 显示。点击某个水果,跳转到详情页。

    练习四:设置页面

    用 SwiftUI 的 Form 做一个设置页,包含用户名输入框、通知开关、音量滑块、主题选择器。然后想一想,如果用 UIKit 的 UITableView,需要写多少数据源方法。

    练习五:待办清单扩展

    给综合项目的待办清单添加显示未完成数量、清空已完成、搜索框、优先级标签。SwiftUI 版本优先,学有余力再尝试 UIKit 版本。


    下一课预告

    第1课我们建立了 SwiftUI 的整体感觉,也看到了它和 UIKit 的差异。下一课,我们会更深入地学习:

    • 布局系统到底怎么工作;
    • frame、padding、Spacer 的优先级;
    • 对齐指南 AlignmentGuide;
    • 自定义布局 Layout 协议;
    • 响应式设计与不同屏幕适配;
    • 更复杂的列表和网格 LazyVGrid、LazyHGrid。

    但在此之前,请一定把这一课的代码亲手敲一遍。SwiftUI 不是“看懂”就会的,它是“写会”的。你每改一个数字、每换一个修饰符、每加一个状态,都会对它有新的理解。

    记住一句话:先让界面跑起来,再让它变漂亮,最后让它变聪明。

    我们下一课见。

    赞(0)
    未经允许不得转载:171主机测评 » 【SwiftUI娓娓道来】第1课:从一行文字开始,走进声明式UI的世界
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址