How to Load an Uiimage into a Swiftui Image Asynchronously

Loading image from remote URL asynchronously in SwiftUI Image using combine's Publisher

You have to use an ObservableObject for subscribing to the publisher provided by ImageLoader.

class ImageProvider: ObservableObject {
@Published var image = UIImage(named: "icHamburger")!
private var cancellable: AnyCancellable?
private let imageLoader = ImageLoader()

func loadImage(url: URL) {
self.cancellable = imageLoader.publisher(for: url)
.sink(receiveCompletion: { failure in
print(failure)
}, receiveValue: { image in
self.image = image
})
}
}

struct MyImageView: View {
var url: URL
@StateObject var viewModel = ImageProvider()
var body: some View {
Image(uiImage: viewModel.image)
.onAppear {
viewModel.loadImage(url: url)
}
}
}

Loading/Downloading image from URL on Swift

Xcode 8 or later • Swift 3 or later

Synchronously:

if let filePath = Bundle.main.path(forResource: "imageName", ofType: "jpg"), let image = UIImage(contentsOfFile: filePath) {
imageView.contentMode = .scaleAspectFit
imageView.image = image
}

Asynchronously:

Create a method with a completion handler to get the image data from your url

func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}

Create a method to download the image (start the task)

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")
// always update the UI from the main thread
DispatchQueue.main.async() { [weak self] in
self?.imageView.image = UIImage(data: data)
}
}
}

Usage:

override func viewDidLoad() {
super.viewDidLoad()
print("Begin of code")
let url = URL(string: "https://cdn.arstechnica.net/wp-content/uploads/2018/06/macOS-Mojave-Dynamic-Wallpaper-transition.jpg")!
downloadImage(from: url)
print("End of code. The image will continue downloading in the background and it will be loaded when it ends.")
}

Extension:

extension UIImageView {
func downloaded(from url: URL, contentMode mode: ContentMode = .scaleAspectFit) {
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() { [weak self] in
self?.image = image
}
}.resume()
}
func downloaded(from link: String, contentMode mode: ContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloaded(from: url, contentMode: mode)
}
}

Usage:

imageView.downloaded(from: "https://cdn.arstechnica.net/wp-content/uploads/2018/06/macOS-Mojave-Dynamic-Wallpaper-transition.jpg")

Load images async in SwiftUI with DispatchQueue

Take a look at some blog posts about asynchronous image loading in SwiftUI. This one for instance looks like it describes the technique you need to use perfectly.

In a nutshell: put a custom View in your hierarchy that handles downloading on a background thread and then updates the image when the view has been downloaded.

There are Cocoapods available as well, like this one for instance.

How can I load an Image to Image() from a valid path or URL from Bundle in SwiftUI?

If you want to init from a filepath you could create an extension on Image that handles the boilerplate code for you. But you need to handle the case if the UIImage doesn't exist as the Image(uiImage:) expects a non-optional UIImage as a parameter.

Something like this should work:

extension Image {
init(contentsOfFile: String) {
// You could force unwrap here if you are 100% sure the image exists
// but it is better to handle it gracefully
if let image = UIImage(contentsOfFile: contentsOfFile) {
self.init(uiImage: image)
} else {
// You need to handle the option if the image doesn't exist at the file path
// let's just initialize with a SF Symbol as that will exist
// you could pass a default name or otherwise if you like
self.init(systemName: "xmark.octagon")
}
}
}

You would then use it like this:

Image(contentsOfFile: "path/to/image/here")

Or you could use the property of UIImage to load from the bundle

extension Image {
init(uiImageNamed: String) {
if let image = UIImage(named: uiImageNamed, in: .main, compatibleWith: nil) {
self.init(uiImage: image)
} else {
// You need to handle the option if the image doesn't exist at the file path
self.init(systemName: "xmark.octagon")
}
}
}

You would then use it like this:

Image(uiImageNamed: "ImageName")

Async load UIImages stored in a observable object in SwiftUI

You can load images in background queue, like

class ImagesModel: ObservableObject {
@Published var images: [UIImage] = []
@Published var imageName: String = ""

func load() {
guard !images.isEmpty else { return }

DispatchQueue.global(qos: .background).async {
// ... load here

DispatchQueue.main.async {
// ... update here
self.images = loadedImages
}
}
}
}

Note: currently you do not observe ImageModel, so it is better to move one model related ForEach(imgEle.images) into separated view and make ImagesModel observed there.

How to display Image from a url in SwiftUI


iOS 15 update:

you can use asyncImage in this way:
AsyncImage(url: URL(string: "https://your_image_url_address"))

more info on Apple developers document:
AsyncImage

Using ObservableObject (Before iOS 15)

first you need to fetch image from url :

class ImageLoader: ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}

init(urlString:String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
task.resume()
}
}

you can put this as a part of your Webservice class function too.

then in your ContentView struct you can set @State image in this way :

struct ImageView: View {
@ObservedObject var imageLoader:ImageLoader
@State var image:UIImage = UIImage()

init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}

var body: some View {

Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage()
}
}
}

Also, this tutorial is a good reference if you need more

SwiftUI Network Image show different views on loading and error

You can create a ViewModel to handle the downloading logic:

extension NetworkImage {
class ViewModel: ObservableObject {
@Published var imageData: Data?
@Published var isLoading = false

private var cancellables = Set<AnyCancellable>()

func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}

And modify your NetworkImage to display a placeholder image as well:

struct NetworkImage: View {
@StateObject private var viewModel = ViewModel()

let url: URL?

var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}

Then you can use it like:

NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))

(Note that the url parameter is not force unwrapped).



Related Topics



Leave a reply



Submit