SwiftUI学习笔记day2---Stanford lecture2

发布于:2025-03-21 ⋅ 阅读:(19) ⋅ 点赞:(0)

SwiftUI学习笔记day2—Stanford lecture2


在这里插入图片描述

1.let和var

在 SwiftUI 中,let 和 var 的基本用法与纯 Swift 语言中完全一致,但在视图构建和状态管理上有一些特别的考虑:

  • let(常量):
    用于声明不可变的值。对于在视图生命周期内不需要改变的属性(例如静态文本、固定配置等),使用 let 可以提高代码的安全性和可读性,同时也有助于 SwiftUI 做出更好的优化,因为它知道这些值不会变化。

  • var(变量):
    用于声明可变的值。如果一个属性需要在运行过程中修改,比如用户交互后的数值变化,那么就应使用 var。特别是在 SwiftUI 中,很多动态数据会配合状态属性包装器(如 @State、@Binding、@ObservedObject 等)一起使用,确保数据更新能够触发视图刷新。

示例:

struct ContentView: View {
    // 使用 let 声明静态、不会变化的数据
    let title: String = "欢迎使用 SwiftUI"
    
    // 使用 var 结合 @State 声明会改变的状态数据
    @State var count: Int = 0

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

小结:

  • 选择 let: 当确定属性在视图创建后不会发生改变时使用,可以提高代码的安全性。
  • 选择 var: 当属性的值需要根据用户交互或其他逻辑进行修改时使用,并结合状态管理属性包

2.Type Inference

简单来说类似于C++中的auto关键字;

在 SwiftUI 中,类型推断(Type Inference)是 Swift 编译器的一项强大特性,它能够根据上下文自动推断出变量、常量和函数返回值的类型,从而减少了显式声明的冗余代码。这一特性在 SwiftUI 的视图构建中尤为重要,具体体现在以下几个方面:

  1. 简化代码书写:
    Swift 编译器可以根据初始化表达式推断出变量或常量的类型,例如:

    let title = "欢迎使用 SwiftUI"  // 编译器自动推断为 String 类型
    var count = 0                   // 自动推断为 Int 类型
    

    这使得代码更加简洁易读。

  2. 视图组合与返回值类型:
    在 SwiftUI 中,每个视图的 body 属性要求返回一个符合 View 协议的类型。使用类型推断,可以让你专注于视图的布局而不必担心复杂的类型声明。例如:

    struct ContentView: View {
        var body: some View {
            VStack {
                Text("Hello, SwiftUI!")
                Button("Click me") {
                    print("Button clicked")
                }
            }
        }
    }
    

    这里,编译器会自动推断出 VStack 内部各个子视图的组合类型,而开发者只需要关心布局逻辑。

  3. 使用 opaque types:
    SwiftUI 中常用的 some View 就是一种不暴露具体类型的 opaque 类型,它依赖于类型推断隐藏具体的视图实现细节,从而简化了 API 设计和使用。

  4. 减少类型标注冗余:
    在复杂的视图层次结构中,类型推断能够帮助避免显式声明每个视图组件的具体类型,使代码更易于维护。例如,在视图构建器(ViewBuilder)闭包中,编译器可以自动推断出返回的视图组合,而不需要开发者手动组合复杂的泛型类型。

3. .OnTapGesture

在 SwiftUI 中,.onTapGesture 是一个非常便捷的视图修饰符,用于为视图添加点击(tap)手势处理。它允许你在用户点击某个视图时执行特定的操作,而无需显式使用手势识别器。以下是一些关键点和示例:

主要特点

  • 简化手势处理:
    使用 .onTapGesture 可以很方便地为任何视图添加点击事件,而无需手动设置 UIGestureRecognizer。

  • 可定制点击次数:
    默认情况下,.onTapGesture 响应单次点击,但你可以通过传递 count 参数来设置响应双击或多次点击。例如,.onTapGesture(count: 2) { ... } 用于双击事件。

  • 与状态结合:
    你可以利用该手势来修改视图状态,从而触发 SwiftUI 的视图更新。例如,通过点击按钮改变状态值,从而动态更新界面。

示例代码

下面是一个简单示例,展示如何在 Text 视图上添加单次点击手势,点击后修改状态:

struct ContentView: View {
    @State private var message = "点击我试试!"
    
    var body: some View {
        Text(message)
            .padding()
            .background(Color.yellow)
            .cornerRadius(8)
            .onTapGesture {
                message = "你点击了 Text 视图!"
            }
    }
}

在这个示例中,Text 视图通过 .onTapGesture 添加了点击事件。当用户点击视图时,闭包内的代码会执行,将 message 的值修改,从而触发视图刷新,显示新的文本内容。

使用场景

  • 按钮或交互视图:
    可以将 .onTapGesture 用于自定义按钮、图片、甚至整个视图的点击响应,而不需要使用 UIButton 或其他 UIKit 组件。

  • 简单的用户交互:
    对于简单的点击操作,比如展示提示信息、切换状态或触发动画,.onTapGesture 是一个理想的选择。

注意事项

  • 手势冲突:
    如果一个视图同时添加了多个手势(例如 .onTapGesture 与 .gesture),需要注意手势之间的优先级和冲突问题,可以通过手势组合来解决。

  • 多点击数限制:
    当需要处理双击或三击时,要使用 count 参数进行区分,确保用户体验符合预期。

4.@State关键字

SwiftUI 中的 @State 关键字是一种属性包装器,用于管理视图内部的状态数据,并确保当状态发生变化时能够自动触发视图刷新。下面详细说明 @State 的作用、使用场景以及其底层原理。


1. @State 的作用与使用场景

  • 局部状态管理:
    @State 用于存储那些只在单个视图内部有效的状态数据,例如开关状态、计数器、文本输入内容等。因为这些状态不需要被外部访问或共享,所以将它们声明为私有状态非常合适。

  • 自动视图更新:
    当 @State 修饰的变量发生变化时,SwiftUI 会自动重新计算视图的 body。这意味着你不必手动调用刷新或更新函数,而是通过修改状态,系统自动帮你实现视图的响应式更新。

  • 示例代码:

    struct ContentView: View {
        @State private var count: Int = 0
        
        var body: some View {
            VStack {
                Text("计数:\(count)")
                    .font(.largeTitle)
                Button("增加计数") {
                    count += 1
                }
            }
            .padding()
        }
    }
    

    在上面的例子中,@State 属性 count 的改变会导致 ContentView 的 body 被重新计算,从而更新界面显示的计数值。


2. 底层原理解析

在 SwiftUI 中,视图本质上是值类型(struct),而状态(state)需要在视图的生命周期内持续存在,即使视图的结构体可能会频繁被销毁和重建。为了解决这一矛盾,@State 采用了一种类似“指针”的机制,将状态存储在堆上,从而实现持久化和引用语义。下面从指针的角度解析 @State 的底层原理:

状态存储的间接性

  • 视图与状态的分离:
    由于 SwiftUI 的视图是值类型,每次刷新时都会重新创建,但状态需要持久化保存。为此,@State 会在幕后创建一个存储“盒子”(Box),这个盒子在堆上分配内存,用于存放实际的数据。

  • 指针引用:
    当你在视图中声明一个 @State 属性时,编译器实际上会生成一个包装类型,其中包含了一个指向这个堆上状态存储盒子的指针。这个指针保持不变,保证了即使视图结构体重新构造,也能通过该指针访问到原始的状态数据。

更新与视图刷新机制

  • 通过指针更新状态:
    当 @State 修饰的变量发生改变时,修改的实际上是堆内存中存储的值。由于视图内部持有对该状态存储盒子的指针,所有的状态更新都是在同一个内存地址上完成的。

  • 触发响应式更新:
    SwiftUI 内部有一套依赖关系追踪机制,当状态盒子中的数据通过指针发生改变后,系统检测到这一变化,并将依赖该状态的视图标记为“脏”,进而只重新渲染必要的部分。这里的“指针”作用就像一个全局的标识符,确保更新能够准确地映射到相应的状态存储上。

指针机制的优势

  • 持久性:
    通过指针间接引用堆内存,即使视图的值类型实例被重新创建,指向状态的指针依然指向相同的内存区域,确保了状态不会丢失。

  • 性能优化:
    指针让 SwiftUI 能够高效地比较状态前后变化,仅更新真正需要刷新的一部分视图,而不是整个视图结构,这对于性能优化至关重要。

总结

从指针的角度看,@State 的底层实现核心在于使用一个存储盒子(堆上的内存块)来持有状态数据,并在视图中通过一个不变的指针来引用这个盒子。这样,即使视图(值类型)的实例不断重建,指向状态的指针依然稳定,保证了状态的持久性和一致性。同时,状态的更新通过修改该内存区域的数据来完成,进而触发 SwiftUI 内部的依赖追踪和视图刷新机制,实现了高效的响应式界面更新。

5.Array

在 SwiftUI 中,Array 本身并没有特殊的实现,而是依然使用 Swift 的内置数组。不过,数组在 SwiftUI 的数据驱动视图构建中扮演着极其重要的角色,特别是在生成动态内容和列表时。下面从几个角度详细说明 SwiftUI 中 Array 的使用和其在数据绑定、视图刷新中的作用:


1. 动态视图生成

SwiftUI 提倡声明式、数据驱动的 UI 构建模式。利用数组,可以根据数据源动态生成多个视图。通常会使用 ForEach 与数组配合,自动遍历数组中的每个元素,生成对应的视图。例如:

let emojis = ["👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀","👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀"]
    
@State var cardCount: Int = 4

var body: some View {
    VStack {
        ScrollView {
            cards
        }
        Spacer()
        cardCountAdjusters
    }
    .padding()
}   

var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], content: {
            ForEach(0..<cardCount, id: \.self) { index in
                CardView(content: emojis[index])
                    .aspectRatio(2/3, contentMode: .fit)
            }
        })
        .foregroundColor(.blue)
}

这段代码展示了如何利用 SwiftUI 的声明式语法动态生成视图,并结合状态管理和响应式更新机制来控制显示的卡片数量。下面详细解析各个部分及其底层实现原理:


2. 与状态绑定

在需要动态更新数据时,数组通常会被声明为 @State 或者放在 ObservableObject 中。这样,数组中的数据发生变化时,SwiftUI 会自动重新计算视图的 body,从而实现 UI 更新。例如:

struct DynamicListView: View {
    @State private var items = ["Swift", "Kotlin", "JavaScript"]

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
            }
            .onDelete(perform: deleteItems)
        }
    }

    func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

这里,数组 items 被 @State 修饰,当用户通过删除操作修改数组内容时,SwiftUI 自动刷新视图,确保界面与数据状态保持一致。


3. 数组的值类型特性

Swift 的 Array 是值类型,遵循拷贝-写入(copy-on-write)机制。这意味着当你对数组进行修改操作时,实际上可能会触发拷贝,但 Swift 通过优化保证了性能不会受到较大影响。在 SwiftUI 中,当数组被用作状态时(比如 @State 数组),每次状态变更都会触发视图刷新。开发者需要注意在进行大量数据操作时,尽可能利用 Swift 的高效集合操作方法(如 map、filter 等)来简化代码。


总结

  • 声明式动态视图: SwiftUI 利用 ForEach 与 Array 的结合,实现了基于数据驱动的动态视图生成。
  • 状态绑定: 通过 @State 或 ObservableObject 管理数组数据,确保数据变化能自动刷新 UI。
  • 底层实现: Swift 的 Array 作为值类型在内存中以连续块存储,通过指针高效管理;而 SwiftUI 的状态更新机制则利用 diffing 和依赖追踪,实现局部视图刷新,优化性能。

这种设计充分体现了 SwiftUI 数据驱动与高效状态管理的理念,使得开发者可以以简洁的代码快速构建响应式和动态的用户界面。


6.ForEach

下面我结合你给的代码,系统、全面地介绍一下SwiftUI中ForEach的用法:

  • 你使用了LazyVGrid进行网格布局;
  • 通过ForEach构造了一系列的CardView
  • 每张卡片通过索引index从数组emojis中取出具体的内容。

1. ForEach基础概念

(1)ForEach是什么?

在SwiftUI中,ForEach是一个用于生成一系列视图的结构,它允许你从数据集合中创建动态的视图列表。

  • 常见用途:
    • 动态生成列表视图、网格布局;
    • 根据数据源自动更新界面。

(2)基本语法结构

ForEach(数据集合, id: 唯一标识符) { 单个数据项 in
    // 根据单个数据项创建视图
}
  • 数据集合可以是一个数组、范围或任意可遍历的集合;
  • id 是标识数据项唯一性的键值路径,用来告诉SwiftUI如何识别每个视图。

2.代码实例详解

(1)使用范围作为数据源

ForEach(0..<cardCount, id: \.self) { index in
  • 数据源是一个整数范围(0..<cardCount);
  • id: \.self 表示用数据自身(这里即整数本身)作为唯一标识符。
  • 这种方式适合当数据本身是唯一的(如整数索引)时使用。

注意:
如果数据项不是简单的整数,而是复杂的结构体或类,应提供一个独特的标识符,比如id: \.id

(2)闭包参数

{ index in
    CardView(content: emojis[index])
        .aspectRatio(2/3, contentMode: .fit)
}
  • 这里的index是SwiftUI自动传递进闭包的当前遍历索引。
  • 通过emojis[index],你从数组中取到对应的内容。
  • 每个循环都会创建一个CardView,形成网格布局。

3.配合LazyVGrid实现网格布局

你使用的LazyVGrid结合ForEach实现了网格:

LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) { ... }
  • LazyVGrid允许你创建一个垂直网格布局;
  • 参数GridItem(.adaptive(minimum: 120))表示自动根据屏幕宽度自适应,最小宽度为120;
  • 在网格内部使用ForEach动态生成多个视图,使视图更加灵活、响应式。

4.常见的其他ForEach用法

为了帮助你更全面理解,下面补充几个常见的场景:

(1)遍历数组中的对象(推荐用法)

假如你有这样一个模型:

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

遍历:

let people = [Person(name: "Alice"), Person(name: "Bob")]

ForEach(people) { person in
    Text(person.name)
}
  • 因为Person符合了Identifiable协议,所以不需要显式提供id

(2)绑定数据的遍历(可编辑数据)

@State var names = ["Tom", "Jerry", "Bob"]

ForEach($names, id: \.self) { $name in
    TextField("名字", text: $name)
}
  • 这里使用了绑定(Binding)数据,使每个视图可编辑并实时更新。

5.ForEach的注意事项

  • SwiftUI 中的ForEach本身不是循环语句(不像传统编程语言中的for循环),而是一个结构体
  • 它专注于数据到视图的转换,不要用ForEach来做非视图构造相关的逻辑;
  • 注意唯一标识符(id)的重要性,确保它们能准确代表数据唯一性,避免视图刷新异常。

6.总结:

  • ForEach用于数据驱动动态生成视图;
  • 数据集合可以是范围、数组等;
  • 提供唯一标识符(id)帮助SwiftUI区分视图;
  • 经常配合LazyVGridLazyHGridList等容器使用;
  • 支持数据绑定,让视图与数据实时联动。

你提供的代码很好地体现了ForEach在动态布局场景下的应用,希望以上内容能帮助你更全面地理解SwiftUI的ForEach

7. Button

下面详细、全面地介绍一下 SwiftUI 中的 Button(按钮):

7.1 SwiftUI 中的 Button 基础概念

在 SwiftUI 中,Button 是用户交互的最基本控件之一。当用户点击按钮时,会触发指定的操作(Action)。

基本语法:

Button("按钮文字") {
    // 点击按钮后执行的代码
}

或:

Button(action: {
    // 点击按钮后执行的代码
}, label: {
    // 自定义按钮的显示样式
})

7.2 简单实例

最常见的按钮形式:

Button("点我一下") {
    print("按钮被点击了!")
}

7.3 Button的常见样式与修饰符

(1)基础按钮样式

Button("确定") {
    // 动作代码
}
.buttonStyle(.bordered) // SwiftUI内置风格

内置样式包括:

  • .bordered
  • .borderedProminent
  • .plain

(2)自定义按钮的内容

按钮不仅限于文字,可以自定义视图:

Button(action: {
    print("按钮点击!")
}) {
    HStack {
        Image(systemName: "heart.fill")
        Text("喜欢")
    }
    .padding()
    .background(.red.opacity(0.2))
    .cornerRadius(10)
}

7.4 按钮状态与交互动画

SwiftUI 中可通过按钮的状态提供视觉反馈:

@State private var isPressed = false

Button {
    isPressed.toggle()
} label: {
    Text(isPressed ? "取消" : "确定")
        .padding()
        .foregroundColor(.white)
        .background(isPressed ? Color.red : Color.blue)
        .cornerRadius(8)
        .animation(.default, value: isPressed)
}
  • 点击后按钮颜色和文字实时切换,使用了动画效果。

7.5 按钮与状态管理(@State@Binding

Button经常用于修改状态,驱动界面更新:

(1)使用@State

@State private var count = 0

Button("点击次数: \(count)") {
    count += 1
}

(2)配合@Binding(适用于子视图)

struct CounterButton: View {
    @Binding var count: Int

    var body: some View {
        Button("增加数量") {
            count += 1
        }
    }
}

在父视图中使用:

@State private var number = 0

CounterButton(count: $number)

7.6 禁用与启用按钮

按钮可以动态启用或禁用:

@State private var agreeTerms = false

VStack {
    Toggle("我同意使用条款", isOn: $agreeTerms)
    
    Button("下一步") {
        print("进入下一步")
    }
    .disabled(!agreeTerms) // 如果未勾选同意条款,则按钮被禁用
}

7.7 按钮角色(ButtonRole)

SwiftUI 可以明确指定按钮角色,用于明确按钮意图和外观:

Button("删除", role: .destructive) {
    print("删除")
}

Button("取消", role: .cancel) {
    print("取消")
}
  • .destructive:用于强调删除等破坏性操作,通常以红色呈现。
  • .cancel:用于表示取消操作。

7.8 确认对话框(ConfirmationDialog)

SwiftUI 提供了按钮点击后弹出确认对话框的方式:

@State private var showDialog = false

Button("删除文件") {
    showDialog = true
}
.confirmationDialog("确认删除吗?", isPresented: $showDialog) {
    Button("删除", role: .destructive) { print("删除成功") }
    Button("取消", role: .cancel) { }
}

7.9 菜单按钮(Menu)

SwiftUI 中提供 Menu 按钮:

Menu("更多选项") {
    Button("分享") { }
    Button("保存") { }
    Button("删除", role: .destructive) { }
}

7.10 SwiftUI 中按钮的注意点:

  • 按钮的默认点击范围自动适配内容大小,但你可以通过 .padding() 扩展其点击区域;
  • Button 中的操作代码应尽量简洁,不要进行耗时的操作;
  • 复杂交互场景推荐配合 @State@Binding 来管理状态;
  • Button 标签可以是任意视图,自定义能力非常强。

8.系统自带的Symbol

这里分别是两个使用了系统自带的Symbol的按钮的代码:

var Adder: some View {
        cardCountAjdusters(by: +1, symbol: "rectangle.stack.badge.plus.fill")
    }
    
    var Remover: some View {
        cardCountAjdusters(by: -1, symbol: "rectangle.stack.badge.minus.fill")
    }

UI效果:
在这里插入图片描述
如何查看有哪些系统Symbol:
在这里插入图片描述

9.优化代码

使用var或者struct来优化代码结构提高可读性

  • 增加了垂直滚动视图ScrollView。
  • 改用LazyVGrid实现了灵活的网格布局,能自适应更多卡片。
  • 引入了调整卡片数量的功能,新增了底部按钮栏(cardCountAdjusters)。
  • 通过@State变量cardCount实现动态显示。
import SwiftUI
struct ContentView: View {
    let emojis = ["👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀","👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀"]
    
    @State var cardCount: Int = 4
    
    var body: some View {
        VStack {
            ScrollView {
                cards
            }
            Spacer()
            cardCountAdjusters
        }
        .padding()
    }
    
    var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], content: {
            ForEach(0..<cardCount, id: \.self) { index in
                CardView(content: emojis[index])
                    .aspectRatio(2/3, contentMode: .fit)
            }
        })
        .foregroundColor(.blue)
    }
                            
                            
    
    var cardCountAdjusters: some View {
        HStack{
            Adder
            Spacer()
            Remover
        }
        .imageScale(.large)
        .font(.largeTitle)
        .padding()
    }
    
    func cardCountAjdusters(by offset: Int, symbol: String) -> some View {
        Button(action: {
            cardCount += offset
        }, label: {
            Image(systemName: symbol)
        })
        .disabled(cardCount + offset < 1 || cardCount + offset >= emojis.count)
    }
    
    var Adder: some View {
        cardCountAjdusters(by: +1, symbol: "rectangle.stack.badge.plus.fill")
    }
    
    var Remover: some View {
        cardCountAjdusters(by: -1, symbol: "rectangle.stack.badge.minus.fill")
    }
}


struct CardView: View {
    let content: String
    @State var isFaceUp: Bool = true
    var body: some View {
        ZStack(alignment: .center) {
            let base = RoundedRectangle(cornerRadius: 12)
            Group {
                base.foregroundColor(.white)
                base.strokeBorder(lineWidth: 2)
                Text(content)
                    .font(.largeTitle)


            }
            .opacity(isFaceUp ? 1 : 0)
            base.opacity(isFaceUp ? 0 : 1)
        }
        .onTapGesture {
            isFaceUp.toggle()
        }
    }
}



#Preview {
    ContentView()
}

总结

以下是你的SwiftUI学习笔记(Day2)的简短总结:


SwiftUI学习笔记 Day2 小结(Stanford CS193p Lecture2)

本次学习了SwiftUI中的几个核心概念:

  1. letvar

    • let声明常量,适用于不变的数据。
    • var声明变量,配合@State用于视图的动态更新。
  2. 类型推断(Type Inference)
    Swift自动推导变量类型,无需显式声明,简化代码。

  3. .onTapGesture手势
    快捷为视图添加点击交互,能与状态绑定,实现交互式UI。

  4. @State 属性包装器
    用于视图内部状态管理,数据变化自动触发视图刷新,底层通过堆存储和指针实现高效更新。

  5. Array 与动态视图
    利用数组和ForEach动态生成视图,配合状态管理可实现数据驱动的UI更新。

  6. ForEach的用法
    用于动态生成视图,常见于网格布局(如LazyVGrid),需提供唯一标识符。

  7. Button控件
    基本交互元素,提供丰富样式、状态管理和动画支持。

  8. 系统自带Symbol
    使用系统图标美化按钮,提升界面美观度和一致性。

  9. 代码优化实践
    通过结构化代码、引入滚动视图ScrollView、网格布局LazyVGrid,并抽取功能为函数,提高代码的可读性和可维护性。