Observer and image fetching does not work as expected - swift

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.

Related

How to wait until data from network call comes and only then return value of a function #Swift

I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

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

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.

swift - order of functions - which code runs when?

I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)

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.