You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何结合搜索栏获取多页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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 06:06:33