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
How to Use Autolayout to Set Constraints on My Uiscrollview
Formatting a Uitextfield for Credit Card Input Like (Xxxx Xxxx Xxxx Xxxx)
What Does "@Private" Mean in Objective-C
Animate Text Change in Uilabel
How to Keep Uitableview Contentoffset After Calling -Reloaddata
In iOS 12, When Does the Uicollectionview Layout Cells, Use Autolayout in Nib
Force Landscape Mode in One Viewcontroller Using Swift
Core Audio Offline Rendering Genericoutput
Replacement for Stringbyaddingpercentescapesusingencoding in iOS9
Nsindexpath.Item VS Nsindexpath.Row
Entitlements File Do Not Match Those Specified in Your Provisioning Profile.(0Xe8008016)
Itms-90535 Unable to Publish iOS App with Latest Google Signin Sdk
Designing Inside a Scrollview in Xcode 4.2 with Storyboards