Search code examples
functiontypesswiftuiabstractioncompletionhandler

'Unexpected non-void return value in void function' in swiftui


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!


Solution

  • 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)