SwiftUI基础_1.0_搜索列表

527 阅读6分钟

案例: 搜索列表

Simulator Screen Recording - iPhone 14 Pro - 2023-07-27 at 23.40.54.gif

 //
 //  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 中的 UITableViewList 有一些特性和行为,这些特性和行为可能会影响其内部视图的布局:

  1. 自动填充List 中的行会自动填充整个宽度,无论内容的实际大小如何。这意味着,如果你在 List 的行中放置一个视图,这个视图会自动拉伸以填充整个行宽。
  2. 默认间距List 为其行提供了默认的垂直间距。这意味着,你不需要手动添加间距来分隔行。
  3. 默认背景和分隔线List 为其行提供了默认的背景和分隔线。你可以使用 .listRowBackground.listRowSeparator 修饰符来自定义这些样式。
  4. 内部视图的点击行为List 中的行默认是可以点击的,点击行会触发一个高亮效果。如果你在行中添加了一个 Button 或者其他可以点击的视图,这个视图的点击区域可能会覆盖整个行的点击区域。
  5. 内部视图的布局List 中的行默认是从左到右布局的。如果你在行中添加了一个 HStack 或者 VStack,这些视图会按照其自身的布局规则进行布局,可能不会填充整个行宽。
  6. 滚动行为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显示的地方,空白的则点击没反应

image-20230727101058744

(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)")
     }
 }

image-20230727101708464

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 类型的参数,例如 RectangleCircleCapsule 等。当你为一个视图设置了 contentShape,这个视图的可点击区域就会变成你指定的形状,无论这个视图的内容和布局如何。

设定只有图片可点击,空白的地方不能点击

 Rectangle()
 .fill(Color.red)
 .frame(width: 300, height: 300)
 .overlay {
   Image("to_do")
   .resizable()
   .scaledToFit()
   .clipShape(Circle())
   .onTapGesture {
     print("tap")
   }
 }

image-20230727172844207

目前上面的写法即使是图片之外的区域(红色的区域)点击还是能触发的,解决的方法就是通过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")
     }
 }

image-20230727173813861

扩大点击区域

如果在原始的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")
 }

image-20230727231905903

只有点击红色框框内的区域才有点击效果,中间空白的地方点击是无效的

(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")
 }

image-20230727232403930

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")