0

Basically, I'm having trouble returning a url in the form of a string in my API call.

I'm trying to implement abstraction by having one big function that takes a 'breed' parameter to call into its endpoint. That way I avoid writing the same exact function multiple times. The functions getMamalute(), getGolden(), etc. all pass in this parameter to get a URL so that I can display it in my View as an Image -- as you can probably tell. But I'm getting the following error 'Unexpected non-void return value in void function' at the return line in the 'getFavoriteDoggo' function. Do I need to use a completion handler? If so, how will that look like?

    @Published var mamaluteImg = ""
    @Published var goldenImg = ""
    @Published var samoyedImg = ""
    @Published var chowImg = ""
    @Published var huskyImg = ""

    func getMamalute() -> String{
        return getFavoriteDoggo(breed: "malamute")
    }
    
    func getChowChow() -> String{
        return getFavoriteDoggo(breed: "chow")
    }
    
    func getHusky() -> String{
        return getFavoriteDoggo(breed: "husky")
    }
    
    func getSamoyed() -> String{
        return getFavoriteDoggo(breed: "samoyed")
    }
    
    func getGoldenRetriever() -> String{
        return getFavoriteDoggo(breed: "retriever/golden")
    }

func getFavoriteDoggo(breed: String) -> String{
        
        guard let url = URL(string: "https://dog.ceo/api/breed/\(breed)/images/random") else {
            print("Trouble parsing url")
            return ""
        }
        
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTask(with: request){ data, response, error in
            if error != nil {
                print((error?.localizedDescription)!)
                return
            }
            
            if let data = data {
                let response = try! JSON(data: data)
//                let randoIndex = Int.random(in: 0...(response.count - 1))
                let img = response["message"]
                
//                print(img)
                
//                DispatchQueue.main.async {
//                    self.mamaluteImg = img.string!
//                }
                return img.string
            }
        }.resume()
    }

Hopefully I explained my problem clearly, if not my apologies my brain is running really low on battery juice, so I'd be more than happy to help clarify down below:)

Thanks once again!

Maanas
  • 43
  • 8
  • Does this answer your question? [Returning data from async call in Swift function](https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function) The question is not particularly related to SwiftUI – vadian Aug 24 '21 at 06:58

1 Answers1

1

You are using an asynchronous method (dataTask). You don't know when it will be finished running (network request). It therefore cannot have a return value. When it finishes it executes the closure (URLSession.shared.dataTask (with: request) {// this block}).

You would certainly like to do it this way:

class DogManager {
    var imageInfos: String?
    
    func getFavoriteDoggo(breed: String) {
        guard let url = URL(string: "https://dog.ceo/api/breed/\(breed)/images/random") else {
            print("Trouble parsing url")
            return
        }

        URLSession.shared.dataTask(with: url) { data, response, error in
            guard error == nil, (response as? HTTPURLResponse)?.statusCode == 200 else {
                return
            }
            if let data = data {
                self.imageInfos = String(data: data, encoding: .utf8)
                print(self.imageInfos ?? "no infos")
            }
        }.resume()
    }
}

let manager = DogManager()
manager.getFavoriteDoggo(breed: "retriever/golden")

You can test in a Playground.

Now if you want to use SwiftUI and your View is redrawn when imageInfos changes you have to change your class to ObservableObject:

class DogManager: ObservableObject {
    @Published var imageInfos: String?
    //....//
}

And use it like this:

struct MainView: View {
    @StateObject private var dm = DogManager()

    var body: some View {
        Text(dm.imageInfos ?? "nothing")
            .onAppear {
                dm.getFavoriteDoggo(breed: "retriever/golden")
            }
    }
}

iOS15 :

Note that with the introduction of async / await (iOS15) you can write asynchronous methods that have return values ​​(like you did) :

    @available(iOS 15.0, *)
    func getFavoriteDoggo(breed: String) async -> String? {
        guard let url = URL(string: "https://dog.ceo/api/breed/\(breed)/images/random"),
              let (data, response) = try? await URLSession.shared.data(from: url),
              (response as? HTTPURLResponse)?.statusCode == 200 else { return nil }
        return String(data: data, encoding: .utf8)
    }

You can use it with the new .task modifier :

struct MainView: View {
    var dm = DogManager()
    @State private var imageInfos: String?
    var body: some View {
        Text(imageInfos ?? "nothing")
            .task {
                await imageInfos = dm.getFavoriteDoggo(breed: "retriever/golden")
            }
    }
}

EDIT :

"Hey thank you for helping me out, but this would only work for only 1 dog breed."

First, let's create a new Dog structure. A Dog has a breed and the information on its image, which initially does not exist (nil).

struct Dog: Identifiable {
    let id = UUID()
    let breed: String
    var imageInfos: String?

    init(_ breed: String) {
        self.breed = breed
    }
}

Our view will show an array of dogs:

@State private var dogs: [Dog] = ["malamute", "chow", "husky", "samoyed"].map(Dog.init)

Now we change our function that fetches the image of a dog: it takes a Dog as a parameter, and returns (when it has finished) a Dog (with imageInfos filled) :

func updateImageOf(dog: Dog) async -> Dog {
        var newDog = dog
        guard let url = URL(string: "https://dog.ceo/api/breed/\(dog.breed)/images/random"),
              let (data, response) = try? await URLSession.shared.data(from: url),
              (response as? HTTPURLResponse)?.statusCode == 200 else { return dog }
        newDog.imageInfos = String(data: data, encoding: .utf8)
        return newDog
    }

We create a second function that does the same for several dogs.

func updateImagesOf(favoriteDogs: [Dog]) async -> [Dog] {
        var results: [Dog] = []
        await withTaskGroup(of: Dog.self) { group in
            for dog in favoriteDogs {
                group.async {
                    await self.updateImageOf(dog: dog)
                }
            }
            for await result in group {
                results.append(result)
            }
        }
        return results
    }

We use this function in our View:

struct MainView: View {
    var dm = DogManager()

    @State private var dogs: [Dog] = ["malamute", "chow", "husky", "samoyed"].map(Dog.init)

    var body: some View {
        List(dogs) { dog in
            HStack {
                Text(dog.breed)
                    .padding(.trailing, 40)
                Text(dog.imageInfos ?? "rien")
            }
        }
        .task {
            await dogs = dm.updateImagesOf(favoriteDogs: dogs)
        }
    }
}

It works (Simulator, Xcode 13 beta2)

Adrien
  • 1,579
  • 6
  • 25
  • Hey thank you for helping me out, but this would only work for only 1 dog breed. No matter how many different functions I write with different breed values, it will still show the same value because imageInfos gives the URL Link to only 1 dog. Is there a way for me to use a completion handler instead to show all the images at the same time? – Maanas Aug 24 '21 at 18:50
  • If you want to use `dataTask` with `completionHandler` you could search infos about `DispatchGroup`. If you want to use async/await that will be easy, I can show you a little later. – Adrien Aug 24 '21 at 19:03