I'm using this code to load an image from a URL: Loading/Downloading image from URL on Swift. That is working fine.
I've take the code out of the viewcontroller and put it into a class. Now the image only loads when I step through the code. I'm guessing this has something to do with the code running fine but everything finishes executing before the image has completely downloaded from the web.
What piece of this am I missing so the code waits for the image without holding up any other code executing in the viewcontroller?
class MyClass:NSObject {
private var myImage:UIImage = UIImage()
func getMyImage() -> UIImage {
if let url = URL(string: self.myUrl) {
self.myImage = self.downloadImage(url: url)
}
return self.myImage
}
func getDataFromUrl(url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}.resume()
}
func downloadImage(url: URL) {
print("Download Started")
getDataFromUrl(url: 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.myImage = UIImage(data: data)!
}
}
}
}
//in ViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async() {
self.theImageView.image = self.myClass?.getImage()
}
}
You have to set up the imageView's image once the asynchronous call back is completed.
class MyClass: NSObject {
// async function
func getMyImage(completion: #escaping (UIImage?) -> Void) {
// if the url is not available, completion with null
guard let url = URL(string: self.myUrl) else {
completion(nil)
return
}
getDataFromUrl(url: url) { data, response, error in
// if something goes wrong, completion with null
guard let data = data, error == nil else {
completion(nil)
return
}
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
completion(UIImage(data: data)!)
}
}
}
// in ViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getImage { image in
// async function will get called when image is ready
DispatchQueue.main.async() { // set up your view in main thread
self.theImageView.image = image
}
}
}
Related
I'm trying to use an extension of UIImage to convert image download URLs to UIImage, but I'm getting this error: 'self.init' isn't called on all paths before returning from initializer
Here is my whole extension:
(https://i.stack.imgur.com/oZGJu.png)
extension UIImage {
convenience init?(url: URL?) {
let session = URLSession(configuration: .default)
let downloadPicTask = session.dataTask(with: url!) { (data, response, error) in
if let e = error {
print("Error downloading picture: \(e)")
} else {
if let res = response as? HTTPURLResponse {
print("Downloaded picture with response code \(res.statusCode)")
if let imageData = data {
let image = UIImage(data: imageData)
} else {
print("Couldn't get image: Image is nil")
}
} else {
print("Couldn't get response code for some reason")
}
}
}
downloadPicTask.resume()
}
}
I don't know what I'm missing or if I should try a new way to convert URL to UIImage. I tried a different way, but it throws an error saying I need to do it asynchronously.
A standard initializer cannot simply return an image that was fetched asynchronously.
Assuming you are not using Swift concurrency with its async-await, you can write a static function with a completion handler closure which will asynchronously return the image:
extension UIImage {
enum ImageError: Error {
case notImage
case unknownError
}
#discardableResult
static func fetchImage(from url: URL, queue: DispatchQueue = .main, completion: #escaping (Result<UIImage, Error>) -> Void) -> URLSessionDataTask? {
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
guard
let data = data,
error == nil,
let response = response as? HTTPURLResponse
else {
queue.async { completion(.failure(error ?? ImageError.unknownError)) }
return
}
print("Downloaded picture with response code \(response.statusCode)")
guard let image = UIImage(data: data) else {
queue.async { completion(.failure(ImageError.notImage)) }
return
}
queue.async { completion(.success(image)) }
}
task.resume()
return task
}
}
And then call this static function with its completion handler, e.g.:
func updateImageView() {
let url = ...
UIImage.fetchImage(from: url) { [weak self] result in
switch result {
case .failure(let error): print(error)
case .success(let image): self?.imageView.image = image
}
}
}
However, if you were using Swift concurrency with its async-await, you can write an async initializer:
extension UIImage {
convenience init?(from url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
self.init(data: data)
}
}
And you can then use this from an asynchronous context, e.g.:
func updateImageView() async throws {
let url = ...
imageView.image = try await UIImage(from: url)
}
You’re missing a self.init call
For convenience inits you’d write self.init and follow up with a standard init that’s already defined
Also you need to return nil where your init fails
self.werteEintragen() should start after weatherManager.linkZusammenfuegen() is done. Right now I use DispatchQueue and let it wait two seconds. I cannot get it done with completion func because I dont know where to put the completion function.
This is my first Swift file:
struct DatenHolen {
let fussballUrl = "deleted="
func linkZusammenfuegen () {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString)
}
func perfromRequest(urlString: String)
{
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error != nil{
print(error!)
return
}
if let safeFile = gettingInfo {
self.parseJSON(datenEintragen: safeFile)
}
}
task.resume()
}
}
func parseJSON(datenEintragen: Data) {
let decoder = JSONDecoder()
do {
let decodedFile = try decoder.decode(JsonDaten.self, from: datenEintragen)
TeamOne = decodedFile.data[0].home_name
} catch {
print(error)
}
}
}
And this is my second Swift File as Viewcontroller.
class HauptBildschirm: UIViewController {
func werteEintragen() {
Tone.text = TeamOne
}
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [unowned self] in
self.werteEintragen()
}
}
}
How can I implement this and where?
func firstTask(completion: (_ success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(true)
}
firstTask { (success) in
if success {
// do second task if success
secondTask()
}
}
You can have a completion handler which will notify when a function finishes, also you could pass any value through it. In your case, you need to know when a function finishes successfully.
Here is how you can do it:
func linkZusammenfuegen (completion: #escaping (_ successful: Bool) -> ()) {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString, completion: completion)
}
func perfromRequest(urlString: String, completion: #escaping (_ successful: Bool) -> ()) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
guard error == nil else {
print("Error: ", error!)
completion(false)
return
}
guard let safeFile = gettingInfo else {
print("Error: Getting Info is nil")
completion(false)
return
}
self.parseJSON(datenEintragen: safeFile)
completion(true)
}
task.resume()
} else {
//can't create URL
completion(false)
}
}
Now, in your second view controller, call this func like this:
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen { [weak self] successful in
guard let self = self else { return }
DispatchQueue.main.async {
if successful {
self.werteEintragen()
} else {
//do something else
}
}
}
}
I highly recommend Google's Promises Framework:
https://github.com/google/promises/blob/master/g3doc/index.md
It is well explained and documented. The basic concept works like this:
import Foundation
import Promises
struct DataFromServer {
var name: String
//.. and more data fields
}
func fetchDataFromServer() -> Promise <DataFromServer> {
return Promise { fulfill, reject in
//Perform work
//This block will be executed asynchronously
//call fulfill() if your value is ready
//call reject() if an error occurred
fulfill(data)
}
}
func visualizeData(data: DataFromServer) {
// do something with data
}
func start() {
fetchDataFromServer
.then { dataFromServer in
visualizeData(data: dataFromServer)
}
}
The closure after "then" will always be executed after the previous Promise has been resolved, making it easy to fulfill asynchronous tasks in order.
This is especially helpful to avoid nested closures (pyramid of death), as you can chain promises instead.
I got a getJSON Function with url parameter:
func getJsons(jsonUrl: String) {
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url:) { (data, response, err) in
if err != nil {
print("ERROR: \(err!.localizedDescription)")
let alertController = UIAlertController(title: "Error", message:
err!.localizedDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while ((topController.presentedViewController) != nil) {
topController = topController.presentedViewController!;
}
topController.present(alertController, animated: true, completion: nil)
}
guard let data = data else { return }
do {
let test = try JSONDecoder().decode([ArticleStruct].self, from: data)
DispatchQueue.main.async {
self.myArticles = test
print(self.myArticles?.count ?? 0 )
self.myTableView.reloadData()
}
} catch let jsonErr {
print("Error:", jsonErr)
}
}.resume()
}
now i want to move the function to another class (network class).
what must I do to add a completionHandler to the function and how do I call it from other classes.
i want to return the json to the caller class.
My plan:
in MainActivity -> viewDidLoad: call network completionHandler(getJsons(192.168.178.100/getPicture.php))
on completion -> myJsonDataMainActivity = (json data from completionHandler)
-> MainActivity.TableView.reload
in otherClass -> call network completionHandler(getJsons(192.168.178.100/getData.php))
on completion -> myJsonDataOtherClass = (json data from completionHandler)
-> otherClass.TableView.reload
Thanks for your help!
You can use delegate.
myJsonDataOtherClass:
protocol NetworkDelegate {
func didFinish(result: Data)
}
class myJsonDataOtherClass {
var delegate: NetworkDelegate? = nil
...
func getJsons(jsonUrl: String) {
...
URLSession.shared.dataTask(with: url:) { (data, response, err) in
...
delegate?.didFinish(data)
}.resume()
}
}
and set delegate at MainActivity
class MainActivity: UIViewController, NetworkDelegate{
...
let jsonClass = myJsonDataOtherClass()
jsonClass.delegate = self
jsonClass.getJsons(jsonUrl:url)
func didFinish(result:Data) {
// process data
}
}
You should add a completion handler in your function and pass the JSON object.
func getJsons(jsonUrl: String, completion:#escaping (_ success: Bool,_ json: [String: Any]?) -> Void) {
...
completion(true, json)
}
I try to create my first project with swift 3.
I try to get data from my API. This works pretty good if I manually start the function. I need to synchronize the async request.
I need to trigger my function 3 times and wait for the others to complete.
makeGetCall(URLstring: "api1")
wait to complete
makeGetCall(URLstring: "api2")
wait to complete
makeGetCall(URLstring: "api3")
Set this to background and trigger every 5 seconds.
func makeGetCall(URLstring: String, update: Bool) {
let completeURL = "http://myapi/" + URLstring
// Set up the URL request
guard let url = URL(string: completeURL) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error as Any)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as XML
if URLstring == "devicelist.cgi" {
self.readDevice(XMLData: responseData)
}
if URLstring == "statelist.cgi" {
self.readDeviceData(XMLData: responseData, update: update)
}
if URLstring == "functionlist.cgi" {
self.readGewerke(XMLData: responseData)
}
}
task.resume()
}
Can somebody help please.
Hagen
This is what I tried with completion handler:
override func viewDidLoad() {
super.viewDidLoad()
makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
}
makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
}
makeGetCall(input: "functionlist.cgi") {
(result: Bool) in
print("finished functionlist")
}
}
func makeGetCall(input: String, completion: #escaping (_ result: Bool) -> Void) {
let completeURL = "http://192.168.0.25/addons/xmlapi/" + input
// Set up the URL request
guard let url = URL(string: completeURL) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error as Any)
return
}
// make sure we got data
guard data != nil else {
print("Error: did not receive data")
return
}
completion(true)
}
task.resume()
}
If I put the 3 calls together it worked as it should.
I thing it should also work with GCD but most of examples for swift 2.
makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
self.makeGetCall(input: "functionlist.cgi") {
(result: Bool) in
print("finished functionlist")
self.makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
}
}
}
Maybe now somebody can help.
Thanks Hagen
Use a DispatchGroup to get notified when something happens.
override func viewDidLoad() {
super.viewDidLoad()
let stateListGroup = DispatchGroup()
stateListGroup.enter()
makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
stateListGroup.leave()
}
let deviceListGroup = DispatchGroup()
deviceListGroup.enter()
// the notify closure is called when the (stateList-) groups enter and leave counts are balanced.
stateListGroup.notify(queue: DispatchQueue.main) {
self.makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
deviceListGroup.leave()
}
}
let functionListGroup = DispatchGroup()
functionListGroup.enter()
deviceListGroup.notify(queue: DispatchQueue.main) {
self.makeGetCall(input: "functionList") {
(result: Bool) in
print("finished functionlist")
functionListGroup.leave()
}
}
functionListGroup.notify(queue: DispatchQueue.main) {
print("update ui here")
}
}
Prints:
statelist.cgi
finished statelist
devicelist.cgi
finished devicelist
functionList
finished functionlist
update ui here
Also keep in mind that the completion handler of session.dataTask() is called on a background queue, so I recommend to dispatch completion(true) on the main queue to avoid unexpected behaviour:
DispatchQueue.main.async {
completion(true)
}
I have some default images in the viewController and after a user action, it loads an external image and replace the default one.
The problem, it loads the image but doesnt refresh unless i change the viewController to other one
DispatchQueue.main.async {
self.getDataFromUrl(thumbURL, completion: { (data) in
let image = UIImage(data: data!)
self.cityImageView.image = image
print("img refreshed")
})
}
func getDataFromUrl(_ url:String, completion: #escaping ((_ data: Data?) -> Void)) {
URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
if let newData = data {
completion(newData)
}
}) .resume()
}
So i get a print out img refreshed but in front-end nothing changes, what am i missing?
Can you set image on the main thread?
DispatchQueue.main.async {
self.getDataFromUrl(thumbURL, completion: { (data) in
DispatchQueue.main.async {
let image = UIImage(data: data!)
self.cityImageView.image = image
print("img refreshed")
}
})
}
you don't have to put it into GCD queue.
Completion block will be executed in the delegate queue (in this case, main queue). Just notice you should define capture list to avoid memory leak.
self.getDataFromUrl(imageURL, completion: { [unowned self] (data) in
let image = UIImage(data: data!)
self.imageView.image = image
print("img refreshed")
})
func getDataFromUrl(_ url:String, completion: #escaping ((_ data: Data?) -> Void)) {
URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
if let newData = data {
completion(newData)
}
}) .resume()
}
Let me know if it fixed your issue.