A function always returns nil, due to misplacement of statements assigning values - swift

In such a function I get a nil value but I do not understand why. The code in the middle returns an image (and I'm sure about it, I've also checked with some print statements). I do not know how is it possible that it always returns nil. It is like it ignores all the code running in the middle and considers just the first and last statement.
func getImagesDownloaded(reference: StorageReference) -> UIImage {
var imagePassedIn : UIImage?
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard let imageObject = UIImage(data: data!) else {print("Error has occurred: \(String(describing: error?.localizedDescription))"); return}
imagePassedIn = imageObject
}
if imagePassedIn == nil {
print("Error, getImagesDownloaded is not working")
}
return imagePassedIn!
}

The issue is that StorageReference.getData is an asynchronous function, but you're trying to synchronously return a value. You need to use a completion handler to return the asyncronously retrieved value.
func getImagesDownloaded(reference: StorageReference, completion: (UIImage?,Error?)->()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard error == nil, let data = data else {
completion(nil,error)
return
}
guard let image = UIImage(data: data) else {
completion(nil, FirebaseErrors.expectedImage)
return
}
completion(image,nil)
}
}
enum FirebaseErrors: Error {
case expectedImage
}
Then you need to use it like this:
getImagesDownloaded(reference: yourStorageReference, completion: { image, error in
guard let image = image, error == nil else {
print(error)
return
}
// Assign your image to a UIImageView or do anything else with it inside the closure (before the ending `}`)
yourImageView.image = image
})

You are using a closure and the imageObject is probably not returned when you are doing the nil check.
Swift is executed line by line and when you have async code it executes the next line and doesn't wait for the result.
you should move the imagePassedIn check in the closure.

You're setting imagePassedIn inside a completion block. This means that when you are ready to set it, you've already retuned it.
In a few words, when using completion blocks, the code bellow won't be waiting for it to finish in order to execute.
Update your functions to this:
func getImagesDownloaded(reference: StorageReference, _ completion: (UIImage) -> ()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard let data = data, let imageObject = UIImage(data: data) else {print("Error has occurred: \(String(describing: error?.localizedDescription))"); return}
completion(imageObject)
} else {
print("Error, getImagesDownloaded is not working")
}
}

Related

Asynchronous thread in Swift - How to handle?

I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)

Using completion handlers inside while loop

How to use completion handlers/dispatchqueue within while loops?
I have this method called getHub() which is a completion handler as I would like code to be executed after it has finished with the relevant values. I call this when a user presses a button:
SetupAPI().getHub(completion: { response, error in
print("testing")
print(response)
print(error)
})
(The code above is where all of the code below should end at)
It calls my API and if the API returns an error/a value that I wasn't expecting, or if Almofire couldn't do the request for some reason, then it adds one onto the tries variable. The max amount of tries allowed is 3 given by the maxTries variable. If the tries variable is equal to the maxTries variable, then a bool timeout is set to true. If the tries variable is below the maxTries variable then the code waits timeoutInSeconds - which is 10 seconds - amount of time before exiting the while loop, which should run the code once more.
Similarly, If the right value is returned from fetching the data from my API then a bool found is set to true.
If either of these variables are true, then the while loop breaks. And an error is sent back to the completion handler for the code above (which then allows me to tell the user that something has gone wrong).
However, when I run it, the completion handler above is not finished, and the code just runs through the while loop and called function over and over again as my console fills with starting and fetching via my two print statements for debugging in below code. What's the problem, can I use a DispatchQueue/ completion handlers in this situation?
Function that gets called via above code:
func getHub(completion: #escaping (Bool, Error?) -> Void) {
var tries = 0
let maxTries = 3
let timeoutInSeconds = 10.0
var found = false
var timeout = false
while !found || !timeout{
print("starting")
getHubCallAPI(completion: {status, error in
if(error == nil){
print(status)
if (status == "No documents found"){
if(tries >= maxTries){
print("Tired too many times")
timeout = true
return completion(false, nil)
}
tries += 1
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInSeconds){
return
}
}else{
found = true
print("Hub found")
return completion(true, nil)
}
}else{
print("error")
return completion(false, error)
}
})
}
}
Function that calls the API and returns it back to the function above ^^:
func getHubCallAPI(completion: #escaping (String, Error?) -> Void) {
print("fetching")
AF.request("https://discovery.ellisn.com", encoding: URLEncoding.default).response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.error)")
if(response.error != nil){
return completion("", response.error)
}
if let data = response.data, let status = String(data: data, encoding: .utf8) {
return completion(status, nil)
}
}
}
Any questions, or more clarification needed, then just ask. Thanks.
You can try the following:
func getHub(triesLeft: Int = 3, completion: #escaping (Bool, Error?) -> Void) {
let timeoutInSeconds = 1.0
print("starting")
getHubCallAPI(completion: { status, error in
if error == nil {
print(status)
if status != "No documents found" {
print("Hub found")
return completion(true, nil)
}
} else {
print("error")
return completion(false, error) // comment out if the loop should continue on error
}
if triesLeft <= 1 {
print("Tried too many times")
return completion(false, nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInSeconds) {
getHub(triesLeft: triesLeft - 1, completion: completion)
}
})
}
And just call it once like this:
getHub(triesLeft: 2, completion: { ... })
Note that unless you need it for some other reason, there is no need to return (Bool, Error?). And the second parameter is always nil - you may want to propagate your error. You could in theory return (String?, Error?).

Observer and image fetching does not work as expected

I have a confusing behaviour of my image fetcher.
If I use following code the image from url loads properly:
observer = "http://d279m997dpfwgl.cloudfront.net/wp/2019/09/0920_tiger-edited-1000x837.jpg"
setImageToImageView(url: observer)
func setImageToImageView(url: String) {
imageFetcher.fetchImage(from: url) { (imageData) in
if let data = imageData {
DispatchQueue.main.async {
self.testingView.image = UIImage(data: data)
}
} else {
print("Error loading image");
}
}
}
but my goal is to initiate function if observer gets info like following:
var observer = "" {
didSet {
print(observer)
setImageToImageView(url: observer)
}
}
What happens is that observer receives its new value of "http://d279m997dpfwgl.cloudfront.net/wp/2019/09/0920_tiger-edited-1000x837.jpg" from remote class (proved by printing it) but then I get an error in setImageToImageView func in following line:
self.testingView.image = UIImage(data: data)
which says
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional...
And I can't figure why.... I see that observer is is not empty and also the function works properly when initiated manually without observer....
Any suggestions where to look?
This is where image is fetched:
class ImageFetcher{
func fetchImage(from urlString: String, completionHandler: #escaping (_ data: Data?) -> ()) {
let session = URLSession.shared
let url = URL(string: urlString)
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("Error fetching the image!")
completionHandler(nil)
} else {
completionHandler(data)
}
}
dataTask.resume()
}
}
EDIT:
//printing:
print(UIImage(data: data)!)
//before:
self.testingView.image = (UIImage(data: data))!
//prints: <UIImage:0x600000a84360 anonymous {1000, 837}>
//so why the:
self.testingView.image = (UIImage(data: data))!
//still gives an error?
EDIT 2:
any UI to be printed like print(testingView) inside didset of observer gives nil even if in other parts of the code are accessible. What could be the reason?
I was referring to other class thus all UI components happened to be empty while attempting to be modified. With notification centre problem solved.

Making a constructor swift that relies on network (making UIImage from url)

Based on https://stackoverflow.com/a/27712427/4166655, I found that I can create a UIImage with this code
func downloadImage(from url: URL) {
print("Download Started")
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
DispatchQueue.main.async() {
self.imageView.image = UIImage(data: data)
}
}
}
I saw many UIImageView extensions incorporating this code, but I wondered if I could make this work as a direct init for UIImage.
I came up with this —
extension UIImage {
convenience init?(url: String) {
guard let urlObj = URL(string: url) else { return nil }
self.init(url: urlObj)
}
convenience init?(url: URL) {
let semaphore = DispatchSemaphore(value: 0)
var imageData: Data? = nil
URLSession.shared.dataTask(with: url) { (data, response, err) in
// breakpoint put here
guard let data = data else { return }
imageData = data
semaphore.signal()
}
semaphore.wait()
if let data = imageData {
self.init(data: data)
} else {
return nil
}
}
}
But this just times out, and my breakpoint (noted in the code) never triggers.
What's wrong with this approach?
my breakpoint (noted in the code) never triggers
Because you never actually started the URLSession data task (by telling it to resume):
URLSession.shared.dataTask(with: url) { (data, response, err) in
// whatever
}.resume() // <-- this is what's missing
However, your entire approach here is wrong:
You cannot "pause" during an initializer and "wait" for some asynchronous operation to complete while blocking the main queue. You need to return the initialized instance right now.
If this were not an initializer and were happening on a background queue, the best way to "wait" for another background operation to finish would be with a DispatchQueue.

public function that loads image async and returns UIImage

I am trying to write public function that loads image async and returns UIImage. I want to use it in filling UITableView with images.
class CommonFunctions {
func loadImageAsync(StringURL: NSString) -> UIImage{
var returningImage = UIImage()
let url = NSURL(string: StringURL)
let requestedURL = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(requestedURL, queue: NSOperationQueue.mainQueue(), completionHandler: {
response, data, error in
if error != nil {
println("there is some error loading image")
}
else {
if let image = UIImage(data: data){
returningImage = image
}
}
})
return returningImage
}
}
the problem is that when I want to use this function :
cell.imageModelImage.image = CommonFunctions.loadImageAsync(CommonFunctions)
Instead of String argument I get the class? Why is that so?
You ask:
Instead of String argument I get the class? Why is that so?
It is because you are calling it as if it was a class function, but didn't define it as such. If you add the class keyword on the declaration of the function, you won't see that behavior.
But there are deeper issues here: You cannot return a value from an asynchronous function (because the function will return immediately while the asynchronous request will not complete until later).
One solution is to provide a completion handler that will be called if the image is retrieved successfully:
class func loadImageAsync(stringURL: String, completion: #escaping (UIImage?, Error?) -> Void) {
let url = URL(string: stringURL)!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async { completion(nil, error) }
return
}
let image = UIImage(data: data)
DispatchQueue.main.async { completion(image, nil) }
}.resume()
}
Note, I'm dispatching the completion handler back to the main queue, where all UI updates should take place.
Then, in tableView(_:cellForRowAt:), you can supply a block that will be called when the asynchronous request is done:
cell.imageModelImage.image = ... // remember to initialize image view before starting async method
CommonFunctions.loadImageAsync(with: "http://example.com/test.jpg") { image, error in
if let cell = tableView.cellForRow(at: indexPath) as? CustomCell { // make sure cell hasn't scrolled out of view
cell.imageModelImage.image = image
}
}
Note, the above assumes that it's impossible to insert a row in the table in the intervening period of time. If that assumption is not valid, rather than using the old indexPath, you might have requery your model to identify what IndexPath is valid for this cell when the asynchronous completion handler is called.
For Swift 2 rendition, see the previous revision of this answer.