案例: 搜索列表
//
// ContentView.swift
// SwiftUIIphontDemo
//
// Created by Liven on 2023/7/3.
//
import SwiftUI
struct ContentView: View {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
@State var users: [User] = []
@State var searchText: String = ""
var body: some View {
let searchResults = users.filter { user in
return searchText.isEmpty || user.email.lowercased().contains(searchText.lowercased())
}
NavigationView {
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
print("click index at : (index)")
}
}
// adding refresh control...
// indicator will show until async task finished...
.refreshable(action: {
await fetchUsers()
})
// adding Search Bar
// Note Only Availble with Navigation View...
.searchable(text: $searchText, prompt: Text("Search User"))
.navigationTitle("Pull to Refresh")
}
}
// fetching user in async...
func fetchUsers() async {
let session = URLSession(configuration: .default)
do {
let task = try await session.data(from: url)
self.users = try JSONDecoder().decode([User].self, from: task.0)
} catch {
print("error ~ ")
}
}
}
// User
struct User: Identifiable, Hashable, Decodable {
var id: Int
var name: String
var email: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
知识点
1.0 List
在 SwiftUI 中,List 是一个用于显示行数据的视图,类似于 UIKit 中的 UITableView。List 有一些特性和行为,这些特性和行为可能会影响其内部视图的布局:
- 自动填充:
List中的行会自动填充整个宽度,无论内容的实际大小如何。这意味着,如果你在List的行中放置一个视图,这个视图会自动拉伸以填充整个行宽。 - 默认间距:
List为其行提供了默认的垂直间距。这意味着,你不需要手动添加间距来分隔行。 - 默认背景和分隔线:
List为其行提供了默认的背景和分隔线。你可以使用.listRowBackground和.listRowSeparator修饰符来自定义这些样式。 - 内部视图的点击行为:
List中的行默认是可以点击的,点击行会触发一个高亮效果。如果你在行中添加了一个Button或者其他可以点击的视图,这个视图的点击区域可能会覆盖整个行的点击区域。 - 内部视图的布局:
List中的行默认是从左到右布局的。如果你在行中添加了一个HStack或者VStack,这些视图会按照其自身的布局规则进行布局,可能不会填充整个行宽。 - 滚动行为:
List默认是可以滚动的,无论其内部的视图是否可以滚动。这意味着,如果你在List的行中添加了一个可以滚动的视图(例如ScrollView),这个视图可能无法正常滚动,因为List已经捕获了滚动手势。
问题1:如何让List Item能点击?
解决方法1: 使用onTapGesture
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
.onTapGesture {
print("click index at : (index)")
}
}
解决方法2: 添加Button
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
Button {
print("click index at : (index)")
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
}
}
问题2:如何让List Item的点击范围覆盖到整一行
解决1: 如果用onTapGesture, 点击区域只会覆盖到Text显示的地方,空白的则点击没反应
(1)修改frame并添加背景颜色
給VStackView添加了紅色的背景顏色,可點擊區域的就是紅色區域,其餘的點擊沒有效果,所以是因為VStackView水平方向沒有被拉伸導致
以下通過設置VStackView的frame達到水平能尽可能覆盖到整行,效果如下,而且确实是可以点击到的
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.red)
.onTapGesture {
print("click index at : (index)")
}
}
PS: 不能添加Color.Clear透明颜色
(2)修改frame并且用contentShape填充空白的地方
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
print("click index at : (index)")
}
}
解决2: 用button的方式实现点击功能,能覆盖到整一行,即使是空白的地方,问题是默认有一个高亮的效果,
通过添加ListRow的背景颜色可以将高亮效果移除,不过必须是在Button上面添加,在VStack添加没效果
List(searchResults.enumerated().map({ $0 }), id: .element) { index, user in
Button {
print("click index at : (index)")
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.email).font(.caption)
}
}
.listRowBackground(Color.white)
}
2.0 contentShape
在 SwiftUI 中,contentShape 是一个非常有用的修饰符,它允许你定义一个视图的可点击区域或者可交互区域。默认情况下,一个视图的可点击区域是由其内容和布局决定的,但是你可以使用 contentShape 来改变这个区域。
contentShape 接受一个 Shape 类型的参数,例如 Rectangle、Circle、Capsule 等。当你为一个视图设置了 contentShape,这个视图的可点击区域就会变成你指定的形状,无论这个视图的内容和布局如何。
设定只有图片可点击,空白的地方不能点击
Rectangle()
.fill(Color.red)
.frame(width: 300, height: 300)
.overlay {
Image("to_do")
.resizable()
.scaledToFit()
.clipShape(Circle())
.onTapGesture {
print("tap")
}
}
目前上面的写法即使是图片之外的区域(红色的区域)点击还是能触发的,解决的方法就是通过ContentShape设置Image的点击区域
现在只要是绿色框框内的区域都是可以点的,可能会有一个疑问,为什么绿色框内的红色区域还是可以点击
首先Image的frame大小是300 * 300, clipShape()这是添加了一个层蒙层,让部分图片裁剪掉了,但是Image的frame是不会变的,还是300* * 300
其次contentShape(Circle())是在Image的frame范围内做一个内切圆,也就是下方截图的绿色框框(这个框框是我截图手动画上的)
Rectangle()
.fill(Color.red)
.frame(width: 300, height: 300)
.overlay {
Image("to_do")
.resizable()
.scaledToFit()
.clipShape(Circle())
.contentShape(Circle()) // 添加了这一行
.onTapGesture {
print("tap")
}
}
扩大点击区域
如果在原始的SwiftUI视图(如Text或Image)添加点击手势,则整个视图将变为可点击
如果将点击手势添加到容器SwiftUI视图(例如VStack或HStack),则SwiftUI只会将手势添加到内部具有某些内容的部分,空白的(透明)地方将无法响应点击手势
比如下面的案例:
VStack {
Image(systemName: "person.circle").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("What's your name?")
}
.onTapGesture {
print("pls write your name hear")
}
只有点击红色框框内的区域才有点击效果,中间空白的地方点击是无效的
(1) 最简单的方法就是给VStack添加背景颜色(可以是白色,但是不能是透明色),就能解决
VStack {
Image(systemName: "person.circle").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("What's your name?")
}
.background(Color.green)
.onTapGesture {
print("pls write your name hear")
}
(2) 使用contentShape覆盖整个VStack视图,那么整个VStack都可以点击了
可点击的范围就是整个VStack视图,视图占用的范围就是红色框框的地方
VStack {
Image(systemName: "person.circle").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("What's your name?")
}
.contentShape(Rectangle())
.onTapGesture {
print("pls write your name hear")
}
3.0 enumerated
enumerated是Swift的一个方法,它会返回一个新的序列,这个序列中的每一个元素都是一个元组,元组的第一个元素是原始元素的索引,第二个元素是原始元素的值。
例如:
let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"]
names.enumerated().forEach { item in
print(item)
}
// 打印结果
(offset: 0, element: "Nicolás")
(offset: 1, element: "Martina")
(offset: 2, element: "Mateo")
(offset: 3, element: "Sofia")
(offset: 4, element: "Camilla")