如何结合搜索栏获取多页JSON中的全部角色数据?
问题:搜索时展示所有符合条件的角色(解决分页数据限制)
我希望在使用搜索栏时,能把所有符合搜索条件的角色都展示在列表里。现在虽然能筛选角色,但因为返回的JSON数据是分页的,导致能展示的角色数量受限。我已经有了翻页逻辑,但不知道怎么把它和搜索栏结合起来实现需求。
当前代码
ViewModel
import Foundation @MainActor class CharacterViewModel: ObservableObject { @Published var characters: [Character] = [] // 无需init方法,所有属性已有默认值 // 使用并发特性 @Published var searchText = "" var filteredCharacters: [Character] { if searchText.isEmpty { return characters } else { return characters.filter { $0.name.localizedCaseInsensitiveContains(searchText) } } } private(set) var maxPages = 1 private(set) var currentPage = 1 private(set) var hasPreviousPage: Bool = false private(set) var hasNextPage: Bool = true struct CharacterResults: Codable { var info: Info var results: [Character] struct Info: Codable { var pages: Int var next: URL? var prev: URL? } } //MARK: - 获取所有角色 func fetchallCharacters() async { guard let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(currentPage)&name=\(searchText.trimmed())") else { fatalError("无效URL") } do { let (data, _) = try await URLSession.shared.data(from: url) let decodedCharacters = try JSONDecoder().decode(CharacterResults.self, from: data) maxPages = decodedCharacters.info.pages hasPreviousPage = decodedCharacters.info.prev != nil hasNextPage = decodedCharacters.info.next != nil characters = decodedCharacters.results } catch { characters = [] } } //MARK: - 下一页 func nextPage() async { // 更新当前页并请求数据 currentPage += 1 await fetchallCharacters() } //MARK: - 上一页 func previousPage() async { // 更新当前页并请求数据 currentPage -= 1 await fetchallCharacters() } }
ContentView
struct ContentView: View { @StateObject var model = CharacterViewModel() @StateObject var favorites = Favorites() var previousButton: some View { Button("上一页") { Task { await model.previousPage() } } .disabled(!model.hasPreviousPage) } var nextButton: some View { Button("下一页") { Task { await model.nextPage() } } .disabled(!model.hasNextPage) } //MARK: - 页面主体 var body: some View { NavigationView { List(model.filteredCharacters){ character in NavigationLink { CharacterDetailsView(character: character) } label: { HStack{ CharacterRowView(imageUrlString: character.image, name: character.name, species: character.species) if favorites.contains(character) { Spacer() Image(systemName: "heart.fill") .accessibilityLabel("这是收藏的角色") .foregroundColor(.red) } } } } .searchable(text: $model.searchText, prompt: "搜索角色") .onChange(of: model.searchText, perform: { newValue in Task { await model.fetchallCharacters() } }) .toolbar{ ToolbarItem(placement: .navigationBarLeading) { previousButton } ToolbarItem(placement: .navigationBarTrailing) { nextButton } } .navigationBarTitle("角色列表") }//Navigationview .task({ await model.fetchallCharacters() }) .phoneOnlyNavigationView() .environmentObject(favorites) } }
解决方案
要实现搜索时展示所有符合条件的角色,需要调整分页逻辑和数据加载方式,核心是搜索时重置分页状态,滚动到底部自动加载下一页,同时避免重复请求。
1. 修改ViewModel
添加加载状态、重置搜索逻辑,调整数据追加方式:
import Foundation @MainActor class CharacterViewModel: ObservableObject { @Published var characters: [Character] = [] @Published var searchText = "" @Published var isLoading = false // 新增:防止重复加载 // 优化:API已按name筛选,本地无需再过滤,直接返回characters即可 var filteredCharacters: [Character] { characters } private(set) var maxPages = 1 private(set) var currentPage = 1 private(set) var hasPreviousPage: Bool = false private(set) var hasNextPage: Bool = true struct CharacterResults: Codable { var info: Info var results: [Character] struct Info: Codable { var pages: Int var next: URL? var prev: URL? } } // 新增:重置搜索状态 func resetSearch() { currentPage = 1 characters.removeAll() hasNextPage = true } // 修改:加载数据时追加(第一页替换),添加加载状态 func fetchallCharacters() async { guard !isLoading else { return } // 避免重复请求 isLoading = true defer { isLoading = false } // 请求结束后关闭加载状态 guard let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(currentPage)&name=\(searchText.trimmed())") else { fatalError("无效URL") } do { let (data, _) = try await URLSession.shared.data(from: url) let decodedCharacters = try JSONDecoder().decode(CharacterResults.self, from: data) maxPages = decodedCharacters.info.pages hasPreviousPage = decodedCharacters.info.prev != nil hasNextPage = decodedCharacters.info.next != nil // 第一页替换数组,后续页追加数据 if currentPage == 1 { characters = decodedCharacters.results } else { characters.append(contentsOf: decodedCharacters.results) } } catch { print("加载失败:\(error.localizedDescription)") if currentPage == 1 { characters = [] } } } // 修改:检查是否可加载下一页,避免无效请求 func nextPage() async { guard hasNextPage, !isLoading else { return } currentPage += 1 await fetchallCharacters() } func previousPage() async { guard hasPreviousPage, !isLoading else { return } currentPage -= 1 await fetchallCharacters() } }
2. 修改ContentView
添加滚动到底部自动加载下一页的逻辑,调整搜索时的触发行为:
struct ContentView: View { @StateObject var model = CharacterViewModel() @StateObject var favorites = Favorites() var previousButton: some View { Button("上一页") { Task { await model.previousPage() } } .disabled(!model.hasPreviousPage || model.isLoading) } var nextButton: some View { Button("下一页") { Task { await model.nextPage() } } .disabled(!model.hasNextPage || model.isLoading) } var body: some View { NavigationView { List { ForEach(model.filteredCharacters) { character in NavigationLink { CharacterDetailsView(character: character) } label: { HStack{ CharacterRowView(imageUrlString: character.image, name: character.name, species: character.species) if favorites.contains(character) { Spacer() Image(systemName: "heart.fill") .accessibilityLabel("这是收藏的角色") .foregroundColor(.red) } } } // 当最后一个元素出现时,自动加载下一页 .onAppear { if character == model.filteredCharacters.last { Task { await model.nextPage() } } } } // 加载中提示 if model.isLoading { HStack { Spacer() ProgressView() Spacer() } } } .searchable(text: $model.searchText, prompt: "搜索角色") .onChange(of: model.searchText) { _ in Task { model.resetSearch() await model.fetchallCharacters() } } .toolbar{ ToolbarItem(placement: .navigationBarLeading) { previousButton } ToolbarItem(placement: .navigationBarTrailing) { nextButton } } .navigationBarTitle("角色列表") } .task { await model.fetchallCharacters() } .phoneOnlyNavigationView() .environmentObject(favorites) } }
关键改动说明
- 搜索重置:每次搜索文本变化时,重置分页到第1页并清空现有数据,确保从新的搜索结果开始加载。
- 数据追加:加载后续页面时,将新数据追加到现有数组中,而不是替换,实现所有结果的累积展示。
- 加载状态控制:添加
isLoading变量防止重复请求,同时在列表底部显示加载提示。 - 自动加载下一页:利用列表元素的
onAppear方法,当最后一个角色出现时自动触发下一页加载,实现无限滚动效果。
内容的提问来源于stack exchange,提问作者Kevin
相关产品推荐
相关产品推荐

