How to display Image from a url in SwiftUI
Try with this implementation:
AsyncImage(url: URL(string: "http://mydomain/image.png")!,
placeholder: { Text("Loading ...") },
image: { Image(uiImage: $0).resizable() })
.frame(idealHeight: UIScreen.main.bounds.width / 2 * 3) // 2:3 aspect ratio
Looks simple, right? This function has the ability to save in cache the images, and also to make an async image request.
Now, copy this in a new file:
import Foundation
import SwiftUI
import UIKit
import Combine
struct AsyncImage<Placeholder: View>: View {
@StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
@ViewBuilder placeholder: () -> Placeholder,
@ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
image(loader.image!)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
class ImageLoader: ObservableObject {
@Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
Done!
Original source code: https://github.com/V8tr/AsyncImage
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
For iOS 13, 14 (before AsyncImage
) and with the latest property wrappers ( without having to use PassthroughSubject<Data, Never>()
Main View
import Foundation
import SwiftUI
import Combine
struct TransactionCardRow: View {
var transaction: Transaction
var body: some View {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png") // This is where you extract urlString from Model ( transaction.imageUrl)
}
}
Creating CustomImageView
struct CustomImageView: View {
var urlString: String
@ObservedObject var imageLoader = ImageLoaderService()
@State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
Creating a service layer to download the Images from url string, using a Publisher
class ImageLoaderService: ObservableObject {
@Published var image: UIImage = UIImage()
func loadImage(for 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.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
Combining @naishta (iOS 13+) and @mrmins (placeholder & configure) answers, plus exposing Image
(instead UIImage
) to allow configuring it (resize, clip, etc)
Usage Example:
var body: some View {
RemoteImageView(
url: someUrl,
placeholder: {
Image("placeholder").frame(width: 40) // etc.
},
image: {
$0.scaledToFit().clipShape(Circle()) // etc.
}
)
}
struct RemoteImageView<Placeholder: View, ConfiguredImage: View>: View {
var url: URL
private let placeholder: () -> Placeholder
private let image: (Image) -> ConfiguredImage
@ObservedObject var imageLoader: ImageLoaderService
@State var imageData: UIImage?
init(
url: URL,
@ViewBuilder placeholder: @escaping () -> Placeholder,
@ViewBuilder image: @escaping (Image) -> ConfiguredImage
) {
self.url = url
self.placeholder = placeholder
self.image = image
self.imageLoader = ImageLoaderService(url: url)
}
@ViewBuilder private var imageContent: some View {
if let data = imageData {
image(Image(uiImage: data))
} else {
placeholder()
}
}
var body: some View {
imageContent
.onReceive(imageLoader.$image) { imageData in
self.imageData = imageData
}
}
}
class ImageLoaderService: ObservableObject {
@Published var image = UIImage()
convenience init(url: URL) {
self.init()
loadImage(for: url)
}
func loadImage(for url: URL) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}