How to display Image from a url in SwiftUI
SwiftSwiftuiSwift Problem Overview
So I'm trying to create a content feed using data fetched from my Node JS server.
Here I fetch data from my API
class Webservice {
func getAllPosts(completion: @escaping ([Post]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Set the variables to the data fetched from the API
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
@Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
}
}
}
struct Post: Codable, Hashable, Identifiable {
let id: String
let title: String
let path: String
let description: String
}
SwiftUI
struct ContentView: View {
@ObservedObject var model = PostListViewModel()
var body: some View {
List(model.posts) { post in
HStack {
Text(post.title)
Image("http://localhost:8000/" + post.path)
Text(post.description)
}
}
}
}
The Text from post.title
and post.description
are display correctly but nothing displays from Image()
. How can I use a URL from my server to display with my image?
Swift Solutions
Solution 1 - Swift
you can use asyncImage in this way: iOS 15 update:
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
Solution 2 - Swift
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
Solution 3 - Swift
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()
}
}
Solution 4 - Swift
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()
}
}
Solution 5 - Swift
New in iOS 15 , SwiftUI
has a dedicated AsyncImage
for downloading and displaying remote images from the internet. In its simplest form you can just pass a URL, like this:
AsyncImage(url: URL(string: "https://www.thiscoolsite.com/img/nice.png"))
Solution 6 - Swift
AsyncImage
with animation transactions, placeholders, and network phase states in iOS 15+!
As other answers have covered, AsyncImage
is the recommended way to achieve this in SwiftUI
but the new View
is much more capable than the standard config shown here:
AsyncImage(url: URL(string: "https://your_image_url_address"))
AsyncImage
downloads images from URLs without URLSession
s boilerplate. However, rather than simply downloading the image and displaying nothing while loading, Apple recommends using placeholders while waiting for the best UX. Oh, we can also display custom views for error states, and add animations to further improve phase transitions. :D
Animations
We can add animations using transaction:
and change the underlying Image
properties between states. Placeholders can have a different aspect mode, image, or have different modifiers. e.g. .resizable
.
Here's an example of that:
AsyncImage(
url: "https://dogecoin.com/assets/img/doge.png",
transaction: .init(animation: .easeInOut),
content: { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
}, placeholder: {
Color.gray
})
.frame(width: 500, height: 500)
.mask(RoundedRectangle(cornerRadius: 16)
Handling Network Result State
To display different views when a request fails, succeeds, is unknown, or is in progress, we can use a phase handler. This updates the view dynamically, similar to a URLSessionDelegate
handler. Animations are applied automatically between states using SwiftUI syntax in a param.
AsyncImage(url: url, transaction: .init(animation: .spring())) { phase in
switch phase {
case .empty:
randomPlaceholderColor()
.opacity(0.2)
.transition(.opacity.combined(with: .scale))
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity.combined(with: .scale))
case .failure(let error):
ErrorView(error)
@unknown default:
ErrorView()
}
}
.frame(width: 400, height: 266)
.mask(RoundedRectangle(cornerRadius: 16))
NOTE
We shouldn't use AsyncImage
for all instances where we need to load an image from a URL. Instead, when images need to be downloaded on request, it's better to use the .refreshable
or .task
modifiers. Only use AsyncImage
sparingly because the image will be re-downloaded for every View
state change (streamline requests). Here, Apple suggests await
to prevent blocking the main thread 0 (Swift 5.5+).
Solution 7 - Swift
You can use KingFisher and SDWebImage
-
KingFisher https://github.com/onevcat/Kingfisher
var body: some View { KFImage(URL(string: "https://example.com/image.png")!) }
-
SDWebImage https://github.com/SDWebImage/SDWebImageSwiftUI
WebImage(url: url)
Solution 8 - Swift
Button(action: {
self.onClickImage()
}, label: {
CustomNetworkImageView(urlString: self.checkLocalization())
})
Spacer()
}
if self.isVisionCountryPicker {
if #available(iOS 14.0, *) {
Picker(selection: $selection, label: EmptyView()) {
ForEach(0 ..< self.countries.count) {
Text(self.countries[$0].name?[self.language] ?? "N/A").tag($0)
}
}
.labelsHidden()
.onChange(of: selection) { tag in self.countryChange(tag) }
} else {
Picker(selection: $selection.onChange(countryChange), label: EmptyView()) {
ForEach(0 ..< self.countries.count) {
Text(self.countries[$0].name?[self.language] ?? "N/A").tag($0)
}
}
.labelsHidden()
}
}
fileprivate struct CustomNetworkImageView: View { var urlString: String @ObservedObject var imageLoader = ImageLoaderService() @State var image: UIImage = UIImage()
var body: some View {
Group {
if image.pngData() == nil {
if #available(iOS 14.0, *) {
ProgressView()
.frame(height: 120.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
} else {
EmptyView()
.frame(height: 120.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
} else {
Image(uiImage: image)
.resizable()
.cornerRadius(15)
.scaledToFit()
.frame(width: 150.0)
.onReceive(imageLoader.$image) { image in
self.image = image
self.image = image
if imageLoader.image == image {
imageLoader.loadImage(for: urlString)
}
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
}
}
fileprivate 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()
}
}
Solution 9 - Swift
You also can try my way. This is the documentation link
> https://sdwebimage.github.io/SDWebImageSwiftUI/
Here is my code Snippet
struct SettingsProfileImageSectionView: View {
var body: some View {
ZStack(alignment: .leading) {
Color(hex: "fcfcfc")
HStack(spacing: 20) {
Spacer()
.frame(width: 4)
CustomImageView(imageManager: ImageManager(url: URL(string: imageURL))) }
}
.frame(height: 104)
}
}
Load image from URL
struct CustomImageView: View {
@State private var myImage: UIImage = UIImage(named: "Icon/User")!
@ObservedObject var imageManager: ImageManager
var body: some View {
Image(uiImage: myImage)
.resizable()
.frame(width: 56.0, height: 56.0)
.background(Color.gray)
.scaledToFit()
.clipShape(Circle())
.onReceive(imageManager.$image) { image in
if imageManager.image != nil {
myImage = imageManager.image!
}
}
.onAppear {self.imageManager.load()}
.onDisappear { self.imageManager.cancel() }
}
}
Solution 10 - Swift
Example for iOS 15+ with loader :
AsyncImage(
url: URL(string: "https://XXX"),
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 100)
},
placeholder: {
ProgressView()
}
)