DDavis25
DDavis25

Reputation: 1309

How to display Image from a URL in SwiftUI

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
        }
    } 
}

Post model:

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 displayed correctly but nothing displays from Image. How can I use a URL from my server to display with my image?

Upvotes: 98

Views: 129706

Answers (11)

Md. Shofiulla
Md. Shofiulla

Reputation: 2305

You also can try my way. This is the documentation link

https://sdwebimage.github.io/documentation/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() }
        }
    }

Upvotes: 0

Kuralay Biehler
Kuralay Biehler

Reputation: 67

Here's how to do it with using NSCache in SwiftUI:

import UIKit

class CacheService {
    
    static let shared = CacheService() // Singleton
    private  init() {}
    
    var imageCache: NSCache<NSString, UIImage> = {
        let cache = NSCache<NSString, UIImage>()
        cache.countLimit = 100 // limits are imprecise
        cache.totalCostLimit = 1024 * 1024 * 100 // limit in 100mb
        return cache
    }()
    func addImage(image: UIImage, name: String) -> String {
        imageCache.setObject(image, forKey: name as NSString)
        return "Added to cach"
    }
    func removeImage(name: String) -> String {
        imageCache.removeObject(forKey: name as NSString)
        return "Removed from cach"
    }
    func getImage(name: String) -> UIImage? {
        return imageCache.object(forKey: name as NSString)
    }
}

Upvotes: 0

Mac3n
Mac3n

Reputation: 4719

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) { [weak self] data, response, error in
            guard let data = data, self != nil else { return }
            DispatchQueue.main.async { [weak self]
                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

Upvotes: 142

Medhi
Medhi

Reputation: 3225

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()
    }
)

Upvotes: 14

Li Jin
Li Jin

Reputation: 2147

You can use KingFisher and SDWebImage

  1. KingFisher https://github.com/onevcat/Kingfisher

     var body: some View {
         KFImage(URL(string: "https://example.com/image.png")!)
     }
    
  2. SDWebImage https://github.com/SDWebImage/SDWebImageSwiftUI

     WebImage(url: url)
    

Upvotes: 5

Pranav Kasetti
Pranav Kasetti

Reputation: 9915

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 URLSessions 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+).

Upvotes: 12

siki
siki

Reputation: 33

            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()
}

}

Upvotes: 0

Aviel Gross
Aviel Gross

Reputation: 9965

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()
    }
}

Upvotes: 17

Naishta
Naishta

Reputation: 12343

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()
    }
    
}

Upvotes: 9

Shehata Gamal
Shehata Gamal

Reputation: 100503

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"))

Upvotes: 5

Benjamin RD
Benjamin RD

Reputation: 12034

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

Upvotes: 27

Related Questions