Learn Courses My Dashboard

Need Help for Filter

I’m having trouble filtering when using the searchable modifier. I am retrieving data from an API and I have my web service function with my ViewModel. I am getting an error of "Value of type ‘[CharacterResults]’ has no member ‘name’ " even though I do have the property within my CharacterResults. Any help would be appreciated!

Model

struct Character: Codable {
   
    var results: [CharacterResults]
}




struct CharacterResults: Codable, Identifiable {
    

    
    let id: Int
    var name: String?
    var status: String?
    var species: String?
    var origin: Origin?
    var location: Location?
    var image: String?
    

   
}

struct Origin: Codable {
    var name: String
}

struct Location: Codable {
    var name: String
}

ViewModel

@MainActor
class CharacterViewModel: ObservableObject {
    @Published var characterData: Character?

    
    


 
    
    init() {

        fetchallCharacters(page: 1) { [weak self] char in

                self?.characterData = char
           
            if let data = UserDefaults.standard.data(forKey: "SaveFavorites") {

                if let decoded = try? JSONDecoder().decode(Character.self, from: data) {
                    self?.characterData = decoded
                    return
                }
            }
            
           
        }
    }
    

    


    //MARK: - Fetch all Characters
    
    //Closure is going to provide all the character data for that particular page number

    func fetchallCharacters(page: Int, completion: @escaping ((Character) -> ())) {
        guard let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(page)") else {
            fatalError("Bad URL")
        }
        
        let request = URLRequest(url: url)
        
        let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Request error:", error)
                return
            }
            
            
            
            do {
                let decoder = JSONDecoder()
                let result = try decoder.decode(Character.self, from: data!)
                
                DispatchQueue.main.async {
                    //Calling our completion closure which passes in the result
                    completion(result)
                }
            } catch {
                print(error)
            }
        }
        dataTask.resume()
                
    }
    
    
    //MARK: - Next page
    func nextPage(page: Int) {
        //adding weak self since it is calling on the background
        fetchallCharacters(page: page) { [weak self ] char in
            self?.characterData = char
        }
    }
    
    
    //MARK: - Previous page
    func previousPage(page: Int) {
        fetchallCharacters(page: page) { [weak self ] char in
            self?.characterData = char
        }
    }
    
}

ContentView
I know I will need to replace the parameter of List with “filteredCharacters” once solved.

struct ContentView: View {
    @StateObject var model = CharacterViewModel()
    @StateObject var favorites = Favorites()
    @State private var searchText = ""
    @State var filterResults = CharacterViewModel().characterData
    @State private var page = 1


    func filteredCharacters() {
        if searchText.isEmpty {
            filterResults = model.characterData
        } else {
            filterResults = model.characterData.map {
                $0.results.name.localizedCaseInsensitiveContains(searchText)
            }
        }
    }
    
    
    
    var body: some View {

        
        NavigationView {
          
                List(model.characterData?.results ?? []){
                   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("This is a favorite character")
                                        .foregroundColor(.red)
                                }
                            }
                        
                        }
                }
                .navigationTitle("Characters")
                .searchable(text: $searchText, prompt: "Search for a character")
                .navigationBarItems(leading:
                self.page > 1 ?
                    Button("Previous") {
                    self.page -= 1
                        model.previousPage(page: self.page)
                } : nil,
                trailing:
                self.page <= 43 ?
                Button("Next") {
                    self.page += 1
                    model.nextPage(page: self.page)
                } : nil )
        }
        .phoneOnlyNavigationView()
        .environmentObject(favorites)
    }
}

CharacterResults has a name property but [CharacterResults] (i.e. an array of CharacterResult) does not.

Oh okay. How would I get the name property then?

Okay, I tried to give you a short answer but I ran into additional issues with your code. So I rewrote it and present it here as an example. I tried to be liberal with comments so everything would be self-explanatory but feel free to ask about anything you don’t understand.

Note that I left out the stuff about favorites. I leave that as an exercise for the reader…

Model:

import Foundation

//get rid of this Character struct
//we don't need it and it's just confusing
//struct Character: Codable {
//    var results: [CharacterResults]
//}

//rename CharacterResults as Character
//because this struct describes a single character
//and calling it CharacterResults is just confusing
struct Character: Codable, Identifiable {
    let id: Int
    //none of these properties need to be Optional
    var name: String
    var status: String
    var species: String
    var origin: Origin
    var location: Location
    var image: String
}

struct Origin: Codable {
    var name: String
}

struct Location: Codable {
    var name: String
}

ViewModel:

import Foundation

//since you have this annotated as MainActor,
//we can use Swift Concurrency features
//so let's do that
@MainActor
class CharacterViewModel: ObservableObject {
    //make this array empty by default
    @Published var characters: [Character] = []
    
    //marking the following properties as private(set)
    //means they can be read from outside the class
    //but they can only be set from inside the class
    
    //store some info re: pages of JSON response
    private(set) var maxPages: Int = 1
    private(set) var currentPage = 1
    
    //this tells us if there is a page before the currentPage
    //a nil value means no, otherwise we get a URL to the next page
    private(set) var hasPreviousPage: Bool = false
    //this tells us if there is a page after the currentPage
    //a nil value means no, otherwise we get a URL to the next page
    private(set) var hasNextPage: Bool = true
    
    //we only need this struct here, when we fetch the JSON
    //so declare it internally to CharacterViewModel
    struct CharacterResults: Codable {
        struct Info: Codable {
            var pages: Int
            var next: URL?
            var prev: URL?
        }
        var info: Info
        var results: [Character]
    }

    //all of the class properties are given default values
    //so we don't need an init function
    //init() {
    //}
    
    //MARK: - Fetch all Characters
    
    //to use Swift Concurrency, let's make this an async function
    //we can also remove the parameter because we track the currentPage elsewhere
    func fetchCharacters() async {
        //it's safe to force unwrap here because we can guarantee that our URL string
        //will always be a valid URL
        //although whether to actually force unwrap or not is a stylistic choice
        let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(currentPage)")!
        
        //unless you want to adjust the HTTP headers in some way
        //we don't need a URLRequest and can just pass the URL to
        //URLSession
        //let request = URLRequest(url: url)
        
        //replace the old style closure-based URLSession methods
        //with the new-fangled Concurrency way
        do {
            //use URLSession.shared.data(from:delegate:) instead of data(for:delegate:)
            //since we have a URL rather than a URLRequest
            let (data, _) = try await URLSession.shared.data(from: url)
            let decodedCharacters = try JSONDecoder().decode(CharacterResults.self, from: data)
            //pull out page data from the decoded JSON
            maxPages = decodedCharacters.info.pages
            hasPreviousPage = decodedCharacters.info.prev != nil
            hasNextPage = decodedCharacters.info.next != nil
            
            //and finally grab the list of characters from this page
            characters = decodedCharacters.results
        } catch {
            //if something goes wrong, we just supply an empty array
            characters = []
        }
    }
    
    //MARK: - Next page
    func nextPage() async {
        //update currentPage and refetch the JSON
        currentPage += 1
        await fetchCharacters()
    }
    
    //MARK: - Previous page
    func previousPage() async {
        //update currentPage and refetch the JSON
        currentPage -= 1
        await fetchCharacters()
    }
}

ContentView:

import SwiftUI

struct ContentView: View {
    //create our ViewModel
    @StateObject var model = CharacterViewModel()
    //keep track of the user's search text
    @State private var searchText = ""
    
    //make filteredCharacters a computed property
    var filteredCharacters: [Character] {
        if searchText.isEmpty {
            //just return the full characters array
            return model.characters
        } else {
            //filter the characters array
            return model.characters.filter {
                //$0 is a Character item
                $0.name.localizedCaseInsensitiveContains(searchText)
            }
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredCharacters) { character in
                NavigationLink {
                    //I don't have your other Views so I just used Text
                    Text(character.name)
                } label: {
                    HStack{
                        //I don't have your other Views so I just used Text
                        Text(character.name)
                    }
                }
            }
            .searchable(text: $searchText, prompt: "Search for a character")
            //navigationBarItems is deprecated so let's use the newer
            //toolbar modifier instead
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Previous") {
                        //let the view model handle changing pages
                        Task {
                            await model.previousPage()
                        }
                    }
                    //we enable/disable based on whether or not
                    //there is a previous page
                    .disabled(!model.hasPreviousPage)
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Next") {
                        //let the view model handle changing pages
                        Task {
                            await model.nextPage()
                        }
                    }
                    //we enable/disable based on whether or not
                    //there is a next page
                    .disabled(!model.hasNextPage)
                }
            }
            .navigationTitle("Characters")
        }
        //kick off the initial request to get page 1 of characters
        //this task fires before the View appears
        .task {
            await model.fetchCharacters()
        }
    }
}

Thanks, I really appreciate the help. I haven’t dove into much Concurrency. Even though the character results are within the main JSON object and then as an array, I didn’t have to make the original Character struct? I always thought you had to and then make another struct (i.e. Results) to get the information. I’m guessing its dependent on how the JSON provides the dictionary information?

Also, I’m running into an issue where the filter results is for a specific page that I’m on and not through the whole list of characters. Is that a limitation of not displaying all data on one page?

I did figure out part of my issue on getting a character when filtering but cannot access all of them or a way to display all of them at once

Yeah, you do need a struct to read the JSON returned by the API, but since it is only needed in the view model that reads the API data, I made it internal to that class.

The bigger issue there, however, was calling the entire JSON payload returned by the API Character and a single character included in the JSON CharacterResults. That’s backwards and very confusing.

Yep. Since you are fetching the data by pages and not storing them, the search will only look through the current page of character data. You would need a different approach to be able to search all of the characters.

That makes sense. Thanks.