本节内容
- 为未知数量的数据创建列表视图
- 将一组可变数据存储在数组中
- 使用
ForEach来创建与数组中每个元素对应的视图 - 学习用于(用户)输入文本的
TextField和.onSubmit用法 - 跨视图的数据绑定
- 按钮的自定义标签(这种方法让标签名以Text的形式呈现,可以修改属性,携带闭包)
List视图
一个List视图向其 body 中的内容添加其他视图
VStack { List { Text("Elisha") Text("Andre") Text("Jasmine") Text("Po-Chun") } }使用应用添加和删除名称
ForEach视图使用带参数的闭包来表示你提供(数组或其他数据集)中的每个值。
name in表示“一个值进入闭包。”
@State private var nameToAdd = ""//类型推断,通过""表示类型为StringTextField用法:输入框
在列表下方创建一个TextField
struct ContentView: View { @State private var names: [String] = ["Elisha", "Andre", "Jasmine", "Po-chun"]//把这个数组设定为UI变动根据之一 @State private var nameToAdd = "" //类型推断,通过""表示类型为String,此处表示用来存放即时输入的内容 var body: some View { VStack { List { ForEach(names, id: \\.description) { name in Text(name) } } TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .onSubmit { //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 names.append(nameToAdd) nameToAdd = "" } } .padding() } }将文本字段绑定到该属性(nameToAdd),以便在用户在键盘上输入时可以修改该属性
绑定是一种让你将一个视图访问另一个视图拥有的状态的方式。要创建一个绑定到状态属性,属性名前缀为$。
当输入TextField时,它会改变nameToAdd的值,而 SwiftUI 会更新视图。按换行的时候就会自动运行.使用.onSubmit修饰符与闭包来执行当用户点击返回时的操作
TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .autocorrectionDisabled() .onSubmit { //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 if !nameToAdd.isEmpty { names.append(nameToAdd) nameToAdd = "" } }但是空字符串可能会被添加进去,需要防止这种情况。使用!.isEmpty避免
!表示否的意思
TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .onSubmit { //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 if !nameToAdd.isEmpty { names.append(nameToAdd) nameToAdd = "" } }Autocorrection(自动更正)
是一种文本输入功能,旨在帮助用户在输入时自动识别并纠正拼写错误或不规范的词汇。在许多应用和设备中,自动更正可以提高输入效率和准确性,尤其是在输入常见词汇或短语时。
这里需要禁用。把它disabled掉
TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .autocorrectionDisabled() .onSubmit { //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 if !nameToAdd.isEmpty { names.append(nameToAdd) nameToAdd = "" } }从数组中随机选择一个元素
添加一个属性来跟踪选定的名称
@State private var pickedName = ""添加一个按钮供用户选择元素
Button("Pick Random Name") { //这个按钮用于选择数组内的随机一个元素 if let randomName = names.randomElement() {//randomElement表示在(数组)内随机获取元素.配合if else如果数组为空,将返回nil} pickedName = randomName } else { pickedName = "" } }添加Divider()分割线
Divider()List中会自动使用Divider()分割项目,在其他地方需要手动添加。
使用开关(Toggles)
提供选项让用户在选定姓名后可以移除。要实现此功能,为用户添加一个Toggle以在两种行为之间进行选择。
添加一个布尔状态属性来跟踪姓名移除功能。
@State private var shouldRemovePickedName = falseToggles开关,与文本类似
两者都需要一个单一的绑定。因为开关控制开/关状态,其绑定是一个Bool。(文本是isEmpty)
Toggle("Romove when picked", isOn: $shouldRemovePickedName)removeAll 方法用于从数组中移除元素。
这里的闭包 name in return (name == randomName) 将对 names 数组中的每个元素进行检查: 如果 name 等于 randomName,则返回 true,表示该元素应该被移除。 该方法会移除所有与 randomName 相等的元素。
if let是一种可选绑定语法。它的作用是将 randomElement() 返回的值绑定到一个新的常量 randomName 上,同时检查这个值是否为 nil。
这里其实就是可选链的内容,if let xx = xx() { code } else { code }
Button("Pick Random Name") { //这个按钮用于选择数组内的随机一个元素 if let randomName = names.randomElement() { //randomElement表示在(数组)内随机获取元素. //配合if else如果数组为空,将返回nil} pickedName = randomName //使用 removeAll 在数组中的每个元素上运行闭包中的代码 //这个闭包用 name 参数表示元素,并返回一个 Bool 值来指示是否应该移除该元素。 if shouldRemovePickedName { names.removeAll { name in return (name == randomName) } } }else { pickedName = "" } }就是一个开关,如果打开,就表示在选中的同时移除内容。
使用自定义按钮标签
使用一个新的初始化器,该初始化器接受两个闭包来定制按钮标签的大小:一个用于操作,一个用于标签。
.buttonStyle(.borderedProminent)突出显示按钮
使用自定义标签的按钮,其实就是把名字标记为label
这个Button包含两个闭包,操作闭包包含命令式代码,标签闭包包含声明式视图代码。
Button { if let randomName = names.randomElement() { pickedName = randomName if shouldRemovePickedName { names.removeAll { name in return (name == randomName) } } } else { pickedName = "" }//label:添加在其他闭包后面 } label: { Text("Pick Random Name") } .buttonStyle(.borderedProminent) .font(.title2)为label的Text添加内边距
} label: { Text("Pick Random Name") .padding(.vertical, 8) //为标签视图添加内边距,这将扩大整个按钮. //如果修饰符放在Button的层级,则不会对Text的内边距产生影响 .padding(.horizontal, 16) }补充标题和图标等
Hierarchical分层渲染模式:改变符号组件的不透明度(注意小人)
SF Symbols 有多种不同的渲染模式。层级符号渲染模式会改变符号组件的不透明度。
.symbolRenderingMode(.hierarchical)
使用分层渲染后(右):
更改一下所选名称的样式
在列表中添加圆角矩形剪裁形状.clipShape(RoundedRectangle(cornerRadius: 8))
List { ForEach(names, id: \\.description) { name in Text(name) } } .clipShape(RoundedRectangle(cornerRadius: 8)) //为List添加圆角矩形剪裁形状如图所示
.tint默认调用accentcolor,accentcolor可以在assets里设置,更改accentcolor
使用空数组作为names的默认值,这样这就是一个记录软件了。
其实就是把原数组中的内容设置为空。
这种思想应该广泛用在设计应用里…一开始假设其有,最后再消除全部,默认为空。
选一个删一个,这根本就是——
练习:
- 当前设计的一个问题是你可以多次输入相同的名称——当随机选中该名称时,两者都会被删除。修改
.onSubmit中的逻辑,以确保每个新输入都是唯一的。(提示:可以像使用.removeAll一样,用.contains在数组中进行操作。) - 修改你的应用,以在输入名称时去除开头和结尾的空白字符。查看
CharacterSet以找到正确的参数值类型。去除空白有助于确保应用中的数据集干净。 - 添加保存列表和加载列表按钮,实现名称列表复用(即退出后可以重新加载)。(提示:需要一个新的状态属性。)
功能1与功能2:使用&&限制重复元素、使用.trimmingCharacters去除空白
.trimmingCharacters一定需要一个参数接收它,所以只能创建临时变量
let qukongbai = nameToAdd.trimmingCharacters(in: .whitespacesAndNewlines)TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .autocorrectionDisabled() .onSubmit { let qukongbai = nameToAdd.trimmingCharacters(in: .whitespacesAndNewlines) //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 if !qukongbai.isEmpty && !names.contains(qukongbai) { names.append(qukongbai) nameToAdd = "" }功能3:使用UserDefaults临时存储数据
UserDefaults 是一个方便的类,用于存储小型数据(例如,用户设置、应用状态等),数据会在应用关闭后依然保持。适合存储键值对数据,例如 String、Int、Bool、Double 等基本数据类型以及 Array 和 Dictionary 形式的集合。
用法:
使用 set(参数一: , forKey:) 方法来保存数据,接受两个参数:
第一个参数:你想要保存的数据。 第二个参数:一个字符串,作为键,用于标识该数据。
let userDefaults = UserDefaults.standard // 获取标准 UserDefaults 实例 userDefaults.set("John Doe", forKey: "username") // 保存字符串 userDefaults.set(25, forKey: "age") // 保存整数 userDefaults.set(true, forKey: "isPremiumUser") // 保存布尔值注意:如果不用临时常量,则必须要用.standard才能在UserDefaults里储存数据
创建两个按钮,按下按钮的时候会调用某个函数。功能分别为保存和加载列表
Button("Save List") { saveList() } .padding(.top) Button("Load List") { loadList() }如果要使用UserDefaults,则需要储存的值必须要有一个键名称(为了指明需要储存或访问的数据)。
创建需要储存的值列表
@State private var savenamekey = "saveNames"savenamekey是一个键(key)的名称,它用于在 UserDefaults 中唯一标识一项数据。
编写两个函数的内容
UserDefaults是 iOS 提供的一种轻量级存储机制,主要用于保存用户的偏好设置和小型数据。
standard是UserDefaults的一个实例,允许开发者对系统默认的用户设置进行读写操作。
set(names, forKey: savedNamesKey)set是一个方法,把数据保存到UserDefaults中
names是要保存的实际数据,这代表名字数组。
forKey: savedNamesKey指定了一个字符串作为标识符(这个标识符称为“键”),savedNamesKey是一个常量,代表我们在存储和读取数据时使用的唯一名称。
func saveList(){//保存列表到 UserDefaults UserDefaults.standard.set(names, forKey: savenamekey) }接下来尝试从 UserDefaults 中读取数据,并在需要用的时候重新加载。
stringArray(forKey:) 是UserDefaults的一个方法,用于从 UserDefaults 中获取一个存储的字符串数组。
使用UserDefaults读取数据,注意,要使用UserDefaults.standard用于(在UserDefaults内)储存数据,读取数据。我想这是个固定用法…
if let 这是可选链,建立一个临时常量saveNames,调取(储存进了)UserDefaults里的值
表示如果能获取到一个非nil的数组,则把savedNames赋值给name(带有显示功能的原数组)
func loadList(){ if let savedNames = UserDefaults.standard.stringArray(forKey: savenamekey) { names = savedNames //if let这是可选链同时使用闭包,表示如果能获取到一个非nil的数组,则赋值给savedNames } }如果希望按钮实现一些操作,需要在里面写一写函数/闭包,来实现。
练习总代码:
// // ContentView.swift // Pick-a-Pal // // Created by sakiko on 2026/4/23. // import SwiftUI struct ContentView: View { @State private var names: [String] = []//把这个数组设定为UI变动根据之一 @State private var nameToAdd = "" //类型推断,通过""表示类型为String,此处表示用来存放即时输入的内容 @State private var pickedName = "" //添加一个属性来跟踪选定的名称 @State private var shouldRemovePickedName = false //这是为了控制删除数据 @State private var savenamekey = "saveNames" //这是为了使用UserDefaults而设置的键名称 var body: some View { VStack { VStack(spacing: 8) { Image(systemName: "person.3.sequence.fill") .foregroundStyle(.tint)//改变图片前景色 .symbolRenderingMode(.hierarchical) Text("Death Note?") } .font(.title) .bold() Text(pickedName.isEmpty ? " " : pickedName)//这是一个三元运算符,如果是空的,则返回空格字符串,否则返回选定的名称 .font(.title2) .bold() .foregroundStyle(.tint) List { ForEach(names, id: \\.description) { name in Text(name) } } .clipShape(RoundedRectangle(cornerRadius: 8)) //为List添加圆角矩形剪裁形状 TextField("Add Name", text: $nameToAdd)//这是下面的添加框 .autocorrectionDisabled() .onSubmit { let qukongbai = nameToAdd.trimmingCharacters(in: .whitespacesAndNewlines) //使用闭包,onSubmint修饰符,将输入的东西(nameToAdd)添加到指定数组中 if !qukongbai.isEmpty && !names.contains(qukongbai) { names.append(qukongbai) nameToAdd = "" } } Divider() Toggle("Remove when picked", isOn: $shouldRemovePickedName) Button { //这个按钮用于选择数组内的随机一个元素 if let randomName = names.randomElement() { //randomElement表示在(数组)内随机获取元素.配合if else如果数组为空,将返回nil} pickedName = randomName //使用 removeAll 在数组中的每个元素上运行闭包中的代码 //这个闭包用 name 参数表示元素,并返回一个 Bool 值来指示是否应该移除该元素。 if shouldRemovePickedName { //如果开关状态是On,就把它从里面移除 names.removeAll { name in return (name == randomName) } } } else { pickedName = "" } } label: { Text("Pick Random Name") .padding(.vertical, 8) //为标签视图添加内边距,这将扩大整个按钮. //如果修饰符放在Button的层级,则不会对Text的内边距产生影响 .padding(.horizontal, 16) } .padding(.top) .buttonStyle(.borderedProminent) .font(.title2) Button("Save List") { saveList() } .padding(.top) .buttonStyle(.borderedProminent) .font(.title3) Button("Load List") { loadList() } .buttonStyle(.borderedProminent) .font(.title3) } .padding() } func saveList(){//保存列表到 UserDefaults UserDefaults.standard.set(names, forKey: savenamekey) } func loadList(){ if let savedNames = UserDefaults.standard.stringArray(forKey: savenamekey) { names = savedNames //stringArray(forKey:) 是一个方法,用于从 UserDefaults 中获取一个存储的字符串数组。 //if let 这是可选链,建立一个临时常量saveNames,调取(储存进了)UserDefaults里的值 //表示如果能获取到一个非nil的数组,则把savedNames赋值给name(带有显示功能的原数组) } } } #Preview { ContentView() }最终效果