0

I want to create UIImage(urlString: String?). There is no error when I run this code but it is not working.

extension UIImage {
    convenience init?(urlString: String?) { 
        var imageData = Data()
        guard let urlString = urlString else { return nil}
        guard let url = URL(string: urlString) else { return nil}
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Error: \(error.localizedDescription)")
                return
            }
            guard let response = response as? HTTPURLResponse else {
                print("Empty image response")
                return
            }
            print("HTTP image response code: \(response.statusCode)")
            guard let data = data else {
                print("Empty image data")
                return
            }
            imageData = data
        }.resume()
        self.init(data: imageData)
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
mevmev
  • 13
  • 3

2 Answers2

0

UIImage initializer init(data:) is a synchronous method. Your self.init(data: imageData) method is called before the async method dataTask finish its request and execute its completion handler.

What I suggest is to create an instance method extending URL and add a completion handler to it:

extension URL {
    func asyncImage(completion: @escaping (UIImage?) -> Void) {
        URLSession.shared.dataTask(with: self) { data, _, _ in
            guard let data = data, let image = UIImage(data: data) else {
                completion(nil)
                return
            }
            completion(image)
        }.resume()
    }
}

Usage:

yourURL.asyncImage { image in 
    guard let image = image else { return }
    // use image here
}

Another option is to extend UIImageView as shown in this post

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • i think so, but when i take "self.init" affter data in closure, i take error: "Initializer delegation ('self.init') cannot be nested in another expression" – mevmev Jan 30 '22 at 13:55
  • you can write code where the goal is to have method: UIImage(urlString: String?). Help me – mevmev Jan 30 '22 at 13:58
  • Whatever as I said you can't make a synchronous method become asynchronous. What you need is completion handler. Forget about this convenience initializer and create an instance method extending URL – Leo Dabus Jan 30 '22 at 13:59
  • @LeoDabus It *is* possible to mark an initialiser `async` though (though that's probably not what the OP want...). – Sweeper Jan 30 '22 at 14:07
  • @Sweeper yes `UIImage` is immutable. There is no way to change it after calling `self.init()`. I have edited the post to make it more precise. – Leo Dabus Jan 30 '22 at 14:07
-1

My solution for you:

  • Use Data(contentsOf:URL) to get Data from an URL, instead of using URLSession.dataTask
   extension UIImage {  
    convenience init?(urlString: String) {
        self.init()
        guard let url = URL(string: urlString), let data = try? Data(contentsOf: url) else { return }
        self.init(data: data)
    }
}

You have to call self.init() because it required self.init() to be called before return. Because this is a Failable Initializer, if your URL is valid then you get an image from that URL, otherwise, you get nil.

In case you want to use a default image when your URL is invalid, just replace init() with init(named:):

   extension UIImage {  
    convenience init?(urlString: String) {
        self.init(named: "YOUR_IMAGE_NAME")
        guard let url = URL(string: urlString), let data = try? Data(contentsOf: url) else { return }
        self.init(data: data)
    }
}
James H
  • 14
  • 3
  • `Data(contentsOf:)` should only be used for loading local resources. This will freeze your app if there is no internet connection and you pass a remote image url. – Leo Dabus May 17 '22 at 02:38
  • Who told you that `Data(contentsOf:)` should be used for local resources? Even assuming that you're right, then why Apple makes it work with remote URL too. This is a [article](https://www.hackingwithswift.com/example-code/uikit/how-to-load-a-remote-image-url-into-uiimageview) from hackingwithswift.com, he used `Data(contentsOf:)` to get image from a remote URL. Beside, this code will not freeze your app if there is no internet connection. Your app only be freezed if the connection is really slow. The solution is simply init image in background thread, then update UI in main thread. – James H May 23 '22 at 07:29
  • And with @mevmev, his goal is to init image using UIImage(urlString: string). Even though this is not a perfect solution but at least, it's possible. – James H May 23 '22 at 07:40
  • Thanks for both of you. No way to creat convenience init for UIImage with URL. James H' solution work but it block of main thread or used in a complicated way. Forget it, and use extension for UIImageView or AsyncImage from SwiftUI – mevmev May 23 '22 at 10:09
  • From the docs [Data contentsOf:](https://developer.apple.com/documentation/foundation/nsdata/1413892-init) **Important Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated. Instead, for non-file URLs, consider using the dataTask(with:completionHandler:) method of the URLSession class. See Fetching Website Data into Memory for an example.** – Leo Dabus May 23 '22 at 12:16