Model 与 UI

Model 核心逻辑代码,UI 各种View组件的拼接布局。

二者应该较好的分离开,就像 React 的函数组件返回<></>和内部的代码逻辑分开放一样。

Model 可以是结构体、SQL 数据库、网络连接处理、REST API查询、某个正在执行机器学习任务的程序等等,模型通常是和用户界面无关的。用户界面只关注接受显示内容然后将新内容刷新到界面上就好了,复杂的逻辑不应和UI缠绕在一起。

SwiftUI 模型是 MVVM。

MVVM 简介

MVVM(Model-View-ViewModel)是一种常见的软件架构模式,在 iOS(SwiftUI)、Android、WPF、前端框架(Vue)等领域都非常流行。 它的核心目标只有一个: 让界面(UI)和业务逻辑分离。

毕竟把网络请求、数据处理、按钮点击、界面刷新全塞进 ViewController 里,最后会长成一个几千行的怪物文件。人类管它叫架构失控,编译器管它叫正常现象。

  1. Model 数据模型,负责数据。Model 不关心界面长什么样。数据结构变化:只改 Model
  2. View 界面,负责显示内容和接收用户操作。View 不负责:网络请求、数据计算、数据库操作。它只负责:显示什么、用户点了什么。UI 改版:只改 View
  3. ViewModel 视图模型,MVVM 的核心。负责:调用业务逻辑、调用网络接口、格式化数据、通知 View 更新。接口变更:只改 ViewModel

MVVM 可以理解为:

  1. Model = 数据
  2. View = 界面
  3. ViewModel = 界面与数据之间的翻译官

Swift 类型系统

先知道是什么就好了,它们各有非常多的特性与注意事项,我们一时半会学不完的。

Struct

store vars

struct Player {
    var name: String
    var level: Int
}
let p = Player(name: "Tom", level: 10)

computed vars

不是 stored property,而是 computed property(计算属性)。它不占用额外存储空间。每次访问会重新计算。

struct Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        width * height
    }
}

constant lets

在 Swift 中,struct 里的 constant lets 通常指使用 let 声明的存储属性(Stored Property)。

即使属性是 var,如果实例本身是 let,也不能修改

struct Person {
    let name: String
    let age: Int
}
// 创建时必须初始化:
let p = Person(name: "Tom", age: 20)
// 初始化后不能修改:
p.name = "Jerry" // 编译错误
p.age = 30       // 编译错误

functions

Swift 中 struct 不仅可以有存储属性(stored properties),还可以定义函数(Methods)。和很多语言不同,Swift 的结构体是“一等公民”,功能非常完整,甚至可以实现协议、扩展、泛型等。

struct Player {
    var name: String
    var hp: Int

    func printInfo() {
        print("name=\(name), hp=\(hp)")
    }
}

let p = Player(name: "Tom", hp: 100)
p.printInfo()

函数参数 内参名 和 外参名 能有两套

func multiply2(_ operand: Int, by otherOperand: Int) -> Int {
    return operand * otherOperand
}

struct SwiftUIView3: View {
    var body: some View {
        let p = Player(name: "Tom", level: 10, constIntNum: 100)
        // 2
        Text(String(p.multiLevel))
        // 2
        Text(String(p.multiply1(operand: 1, by: 2)))
        // 6
        Text(String(p.multiply2(2, by: 3)))
    }
}

initializers

可以理解为构造函数

struct Player2 {
    var name: String
    var age: Int
    var isStudent: Bool = false // 默认值
}
// _ = Player2(name: "gwl", age: 24)

struct Player3 {
    var name: String
    var age: Int = 0
    init(name: String) {
        self.name = name
    }
}
// _ = Player3(name: "gwl")

struct vs class

所以函数传参就要注意了

如果传结构体里面东西特别大,这会不会成为传参拷贝效率问题,swift 中有写时拷贝优化一般不用担心,我们以为拷贝了但背后不一定拷贝出副本。

truct Player4{
    var age: Int
}
// let p4 = Player4(age: 100)
// 不合法: p4.age = 99

class Player5{
    var age: Int = 0
}

// let p5 = Player5()
// p5是引用类型可以改,p5.age = 99

View 都是 struct

View 都是 struct,不是 class。View 是值类型,View在顶层是 let 的 不可修改的。

你有一系列视图,每当发生某些变化导致var body 不同时,系统就会调用变量 body, 获取新的值,并将其放置到位。

@State

如果想在视图中实现可变状态,就必须使用 @State 这种特殊的表示方法。

非常像 React.js 的 useState

struct CounterView: View {
    // 声明一个状态变量:初始值为 0
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("当前计数: \(count)")
                .font(.largeTitle)

            Button("增加") {
                count += 1  // 修改 @State 值会自动触发视图更新!
            }
            .buttonStyle(.borderedProminent)

            Button("重置") {
                count = 0
            }
        }
        .padding()
    }
}

@Binding

@Binding 是 SwiftUI 中的一个属性包装器(Property Wrapper),用于在视图之间双向绑定(two-way binding)数据。它允许子视图读取并修改父视图中的状态(@State@StateObject 等管理的数据),从而实现数据流的更新和 UI 同步。

父视图 持有状态

struct ParentView: View {
    @State private var isOn = false  // 父视图的可变状态

    var body: some View {
        VStack {
            Text(isOn ? "✅ 开启" : "❌ 关闭")
            
            // 传递绑定(注意:使用 $ 符号)
            ChildView(isOn: $isOn)
        }
    }
}

子视图 接收绑定

struct ChildView: View {
    @Binding var isOn: Bool  // 声明为 @Binding

    var body: some View {
        Toggle("切换开关", isOn: $isOn)  // 直接绑定到本地属性,自动同步
        // 或手动修改:
        // Button("切换") { isOn.toggle() }
    }
}

class 的优势与弱势

优势是 sharing、同时也是弱势带来风险使得它们是可变的。

class 可以用自身的指针哈希地址标识自己,而 struct 就要自己想办法了,比如在 ForEach 中指定 id 标识问题。

init 函数

init 是一个特殊的 static func 当创建实例时被调用。

enum

enum FastFoodMenuItem {
    case hamburger
    case fries
    case drink
    case cookie
}

enum 关联数据

在 Swift 中,枚举(enum)的关联值(Associated Values) 是一个非常强大且灵活的特性,允许你为枚举的每个具体 case 携带额外的数据(类型安全的元组)。这些数据是临时的、每次实例化时可变的,并只存在于该枚举实例的生命周期中。

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(points: (Double, Double), (Double, Double), (Double, Double))
    
    func area() -> Double {
        switch self {
        case .circle(let r):
            return .pi * r * r
        case .rectangle(let w, let h):
            return w * h
        // 允许使用 _ 省略参数
        case .triangle(_, let p2, let p3):
            // 三角形面积计算(省略具体实现)
            return 0.0
        }
    }
}
//let c = Shape.circle(radius: 2.0)
//print(c.area())  // 12.566370614359172

变量类型推断

let menuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = .cookie
// 下面的是不允许的
// var yetAnotherItem = .cookie

迭代 enum

enum TeslaModel: CaseIterable {
    case X
    case S
    case Three
    case Y
}
for model in TeslaModel.allCases {
    reportSalesNumbers(for: model)
}
func reportSalesNumbers(for model: TeslaModel) {
    switch model { ... }
}

switch case

switch menuItem {
    case .hamburger: print("burger")
    case .fries: print("fries")
    case .drink: print("drink")
    case .cookie: print("cookie")
}

break

什么都不做

var menuItem = FastFoodMenuItem.hamburger(patties: 2)
switch menuItem {
    case .hamburger: break
    case .fries: print("fries")
    case .drink: print("drink")
    case .cookie: print("cookie")
}

default

很常规,switch case 中的 default

var menuItem = FastFoodMenuItem.cookie
switch menuItem {
    case .hamburger: break
    case .fries: print("fries")
    case .drink: print("drink")
    default: print("other")
}

支持 string

let s: String = "hello"
switch s {
    case "goodbye": ...
    case "hello": ...
    default: ...
}

不支持fall through

每个case可以多行代码,不允许 fall through到下一个case

默认不贯穿,每个 case 执行完自动退出,用 fallthrough 明确声明(但通常不推荐)

var menuItem = FastFoodMenuItem.fries(size: .large)
switch menuItem {
    case .hamburger: print("burger")
    case .fries:
        print("yummy")
        print("fries")
    case .drink:
        print("drink")
    case .cookie: print("cookie")
}

C++ 与 swift 对比

// C++:默认贯穿(易错!)
switch(x) {
  case 1:
    cout << "One";  // 没有 break → 继续执行 case 2!
  case 2:
    cout << "Two";
}
// Swift:不会贯穿(安全)
switch x {
case 1:
    print("One")   // 自动 break,不会执行 case 2
case 2:
    print("Two")
}

泛型

Swift是个强类型语言,支持泛型数据结构,如之前用到的 Array

struct Array<Element>{
    ...
    func append(_ element: Element) { ... }
}

var a = Array<Int>()
a.append(5)
a.append(22)

Optionals

Swift 要求每个变量必须始终具有值。Optional适用于任何变量。

和其他语言 -1、null pointer 相似。Swift不使用空指针,也不用-1 表示无效号码。使用Optional,Optional是一种类型。

Optional只能处于两种状态,已设置和未设置。

enum Optional<T> {
    case none
    case some(T)
}

使用场景如

let colors = [Color.blue, .red, .green, .yellow]
let index: Optional<Int> = colors.firstIndex(of: Color.orange)

Optional 语法糖

// var hello: Optional<String> = .none
var hello: String?
// var hello: Optional<String> = .some("hello")
var hello: String> = "hello"
// var hello: Optional<String> = .none
var hello: String> = nil

解包为非 Optional

let hello: String? = "Hello"
print(hello!)  // 强制解包(force unwrapping)
// ! 是 强制解包运算符(Force Unwrapping Operator),作用是:
// 将可选类型(String?)直接解包为非可选类型(String),前提是该可选值确实有值(not nil)

if let

使用了 Swift 的 可选绑定(Optional Binding),具体是 if let 语法,作用是: 安全地解包(unwrap)可选值(Optional),并仅在 hello 有值(即非 nil)时执行 if 块中的代码。

let hello: String? = "Hello"
if let safehello = hello {
    print(safehello)
} else {
    // do something else
}

配合 switch case

switch hello {
    case .none: // raise an exception(crash)
    case .some(let data): print(data)
}

switch hello {
    case .none: { //do something else }
    case .some(let data): print(data)
}

?? Optional defaulting

let x: String? = ...
// x不为 .none 就x赋给y,否则用 "foo"
let y = x ?? "foo"

// 形如
switch x {
    case .none: y = "foo"
    case .some(let data): y = data
}

Optional initializers

struct Person {
    let name: String
    
    init?(name: String) {
        guard !name.isEmpty else { return nil } // fails if name is empty
        self.name = name
    }
}

let p1 = Person(name: "Ada")   // Person? = Person(name: "Ada")
let p2 = Person(name: "")      // Person? = nil
struct Vehicle {
    init?(type: String) {
        // return nil if there is no Vehicle with that type
    }
}
if let car = Vehicle(type: "car") {
    // do something with the car
}

Extension

在 Swift 中,扩展(Extensions) 是一种强大的机制,允许你在不修改原始源码的前提下,为已有的类型(如类、结构体、枚举,甚至协议)添加新功能。

尤其这对 protocols 及其重要。

扩展 Int

extension Int {
    // 计算当前数字是否为偶数
    var isEven: Bool {
        self % 2 == 0
    }
    
    // 计算当前数字是否为奇数
    var isOdd: Bool {
        !isEven
    }
}

let num = 7
print(num.isEven) // false
print(num.isOdd)  // true
extension Color {
    var name: String {
        // figure out and return ad name for this Color
    }
}

扩展 String

extension String {
    /// 将字符串重复指定次数并返回新字符串
    func repeated(times: Int) -> String {
        guard times > 0 else { return "" }
        return String(repeating: self, count: times)
    }
    
    /// 去除首尾空格(类似 `trim()`,但更安全)
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

let result = "swift".repeated(times: 3) // "swiftswiftswift"
let clean = "  hello  ".trimmed()       // "hello"

扩展 Array

extension Array {
    /// 安全下标访问(越界返回 nil)
    subscript(safe index: Index) -> Element? {
        indices.contains(index) ? self[index] : nil
    }
}

let arr = [10, 20, 30]
print(arr[safe: 1])   // Optional(20)
print(arr[safe: 5])   // nil(安全避免崩溃)

扩展 Color

extension Color {
    init?(named name: String) {
        switch name {
            case "green": self = Color.green
            ...
            default: return nil
        }
    }
    var name: String? {
        // return the name of this color (if it has one, else nil)
    }
}