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!
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")
}
}
}
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")
}
}
}
"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)