API Calls customization

Hi there,

I am trying to make an app working with Dogs API. So far, I have been able to get the get request from the API, where I am getting all the dogs and displaying them in the ListView in my App. The second thing I am aiming at is to be able to click on the dog from the list, which brings you in to DogDetailView. In this view I would like to display pictures only of the selected dog. Here’s where my struggle is.

Let me walk you through my thoughts and how I planned to do it.

  1. I have made 2 Models one for Dog and one for Breed. I am not going to show here the Dog class because it is irrelevant, so here is a Breed struct, where I am only basically re-writing JSON.
struct Breed: Decodable, Identifiable {
    
    var id:UUID?
    var message: [String]?
    var status: String
    
}

Here’s also the JSON ( part of it ). Basically, it just consists of message array of all images of that specific breed, in that case it is boxer and status string.

{
  "message": [
    "https://images.dog.ceo/breeds/boxer/28082007167-min.jpg",
    "https://images.dog.ceo/breeds/boxer/IMG_0002.jpg",
    "https://images.dog.ceo/breeds/boxer/IMG_3394.jpg",
    "https://images.dog.ceo/breeds/boxer/n02108089_1.jpg"
],
 "status": "success"
}
  1. Then I have ContentModel, where I am parsing, decoding the JSON and assigning it to my variables. See below.
class ContentModel: ObservableObject {
    
    @Published var dogs = [Dog]()
    @Published var breeds = [Breed]()
    
    init() {
        getDogs()
        getBreeds()
    }
    
    func getDogs(){
        
        // Create URL
        let urlString = Constants.apiUrl
        let url = URL(string: urlString)
        
        if let url = url {
            // Create URL request
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
            request.httpMethod = "GET"
             
            // Get URLSession
            let session = URLSession.shared
            
            // Create Data Task
            let dataTask = session.dataTask(with: request) { (data, response, error) in
                // Check that there is not an error
                if error == nil {
                    
                    do {
                        // Parse JSON
                        let decoder = JSONDecoder()
                        let result = try decoder.decode(Response.self, from: data!)
                        print(result)
                        
                        // Assign result to the dogs property
                        DispatchQueue.main.async {
                            self.dogs = result.dogs
                        }
                        
                    } catch {
                        print(error)
                    }
                }
            }
            
            // Start the Data Task
            dataTask.resume()
        }
    }
    
    func getBreeds() {
        
        // Create URL
        let urlString = Constants.breedsUrl
        let url = URL(string: urlString)
        
        if let url = url {
            // Create URL request
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
            request.httpMethod = "GET"
             
            // Get URLSession
            let session = URLSession.shared
            
            // Create Data Task
            let dataTask = session.dataTask(with: request) { (data, response, error) in
                // Check that there is not an error
                if error == nil {
                    
                    do {
                        // Parse JSON
                        let decoder = JSONDecoder()
                        let resultBreed = try decoder.decode(Breed.self, from: data!)
                        print(resultBreed)
                        
                        // Assign result to the dogs property
                        DispatchQueue.main.async {
                            self.breeds = [resultBreed]
                        }
                        
                    } catch {
                        print(error)
                    }
                }
            }
            
            // Start the Data Task
            dataTask.resume()
        }
    }
}

As you can see methods getDogs() and getBreeds() are identical, because I wanted to have a same approach and see if the one works then the other has too. At least that was my vision, haha.

By the way this is breedsUrl = https://dog.ceo/api/breed/boxer/images

Anyways, in the step 3. I am creating a detail view for a Dog. Where I want to display his name and pictures of that breed. This is how I am trying to do it.

struct DogDetailView: View {
    
    @EnvironmentObject var model: ContentModel
    
    var dog:Dog
    
    
    
    var body: some View {
        
        VStack {            
            List(0..<model.breeds.count, id: \.self) { index in
                ForEach(model.breeds[index].message!, id: \.self) { m in
                    AsyncImage(url: URL(string: "\(m)")) { phase in
                        switch phase {
                        case .empty:
                            Color.purple.opacity(0.1)
                        case .success(let image):
                            image
                                .resizable()
                                .scaledToFit()
                        case .failure(_):
                            Image(systemName: "exclamationmark.icloud")
                                .resizable()
                                .scaledToFit()
                        @unknown default:
                            Image(systemName: "exclamationmark.icloud")
                        }
                    }
                    .frame(width:300, height: 300)
                    .cornerRadius(20)
                }
            }
        }
        .navigationBarTitle(dog.name)
        .padding(.top, 0)
    }
}

struct DogDetailView_Previews: PreviewProvider {
    static var previews: some View {
        
        let model = ContentModel()
        
        DogDetailView(dog: model.dogs[0])
        
    }
}

This way I am getting the images of the one breed of the dog, but it is ONLY of the one that is specified in the breedsUrl. How can I substitute this value in the URL itself? So let’s say in this case, I would like to have in that class to like like this: https://dog.ceo/api/breed/\(dog.name)/images.

Any idea how can I do this? Or is there any other way how can should I approach this problem?

Thanks in advance!
Cheers

Dobrý deň Peter! I have a good Slovak friend, so couldn’t resist saying hello in your native tongue :slight_smile:

For this, I would create a new method in your ContentModel. You would create a method, which fetches images based on the breed’s name that you pass in, or have a separate method for a Breed class that would fetch this.

Or perhaps change up your SwiftUI code. It seems like you only care about a single breed/ dog. Therefore, there’s no reason to loop through the rest of the dogs. With this information, you might be able to change your code to call only the URL for that specific breed.

var breedImagesUrlString = "https://images.dog.ceo/breeds/\(dog.breed)"

From here, you would call this API endpoint, then make another loop to loop through the list of URLs containing images for this breed, and create an image for each one of them.

Hope this helps!

Cheers,
Andrew

Dobrý deň RedFox1! That’s awesome to hear!

I think I understand what you mean, when it comes to creating a new method in ContentModel for fetching images based on breed’s name. I am not sure though if I understand what you meant by changing the code in the SwiftUI.

I don’t think I can just use this URL in my SwiftUI, because in my ContentModel I am fetching those data from a specific URL. Maybe I am just not getting in correctly :slightly_frowning_face:

I feel like I am super close but super far at the same time haha. I have tried for so many times already and I feel my brian is not working anymore.

1 Like

Hi Peter, thanks for posting all this code! I’m going to try to recreate your project locally. If you have a GitHub repo, please share that next time, then someone can just clone it (if you’re okay making your repo public of course…). Still, this is pretty good with a couple of copy and clicks!

Fingers-crossed I get something working :slight_smile:

Will write later…

Cheers,
Andrew

1 Like

Ugh yikes, looks like my response earlier didn’t make it to you. I wasn’t able to recreate everything on my own. I ran into a couple of errors. However, understand better now the API/ what’s required. Here’s my current repo: https://github.com/agholson/DogsApp.

Right now, I’m getting the following error within the getBreeds method:

Error in line 92 ViewModel typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: “message”, intValue: nil)], debugDescription: “Expected to decode Array but found a dictionary instead.”, underlyingError: nil))

From this code:

func getBreeds() {
    
    // Create URL
    let urlString = Constants.breedsUrl
    let url = URL(string: urlString)
    
    if let url = url {
        // Create URL request
        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
        request.httpMethod = "GET"
         
        // Get URLSession
        let session = URLSession.shared
        
        // Create Data Task
        let dataTask = session.dataTask(with: request) { (data, response, error) in
            // Check that there is not an error
            if error == nil {
                
                do {
                    // Parse JSON
                    let decoder = JSONDecoder()
                    let resultBreed = try decoder.decode(Breed.self, from: data!)
                    print("Breads line 84 \(resultBreed)")
                    
                    // Assign result to the dogs property
                    DispatchQueue.main.async {
                        self.breeds = [resultBreed]
                    }
                // Check that there is not a parsing error
                } catch {
                    print("Error in line 92 ViewModel \(error)")
                }
            }
        }
        
        // Start the Data Task
        dataTask.resume()
    }
}

I ended up making a separate class for the Breed as well as a method to grab the breed’s image list, and just got that piece working as part of this. Will try to implement the View code tomorrow. It took me much longer than anticipated, because I kept calling the wrong URL. It harkens back to, if Swift is telling you an error then listen to it.

Sorry, I haven’t been more help so far! You might want to tag one of the Teaching Assistants (TAs) if you want some quicker assistance.

By the way, I hate the key names chosen for this API! Message for everything…

1 Like

Hi @RedFox1 ,

First of all, thanks for spending your own time on that! I really appreciate it.

I think, I have been able to do it eventually on my own. I mean at least it works now as it should. I can share it with you later on if you are interested.

Regarding your code you have posted here and the error. It looks like that the issue is with parsing JSON and that you’re sending there a wrong type. How does your Breed model look like? Maybe the issue is there.

Cheers

1 Like

@Bcxxx please post the solution to your question! It can be marked as the solution and may help someone else

1 Like

Hello my friend, in this post I actually got the class stuff working :slight_smile:

However, I hadn’t gotten the View code working yet. I just finished a piece of that:

What a cool thing the AsyncImage is!!! Way easier than other methods to load images. Thanks for teaching me this piece of code.

@Bcxxx which tutorial did you follow to create this app?