84

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?

DDavis25
  • 1,149
  • 1
  • 13
  • 25

11 Answers11

117

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

Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44
Mac3n
  • 4,189
  • 3
  • 16
  • 29
26

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

Benjamin RD
  • 11,516
  • 14
  • 87
  • 157
  • AsyncImage(url: URL(string: item.imageUrl)!, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() }) .frame(width: 80, height: 57) Only Text Loading .... visible....no image getting downloading. – nitin.agam Feb 09 '21 at 08:58
  • Only a few images load for me. The rest just return the "Loading..." text. – Ethan Strider Feb 11 '21 at 00:27
  • @EthanStrider Are the images from https? Maybe you need to allow https execution: https://stackoverflow.com/questions/49611336/enable-allow-arbitrary-loads-of-app-transport-security-setting-not-working-in-xc – Benjamin RD Feb 11 '21 at 00:29
  • @MrMins I am using an `https` URL, but setting the `AllowsArbitraryLoads` key to `YES` (as per the linked info) didn't help. – Ethan Strider Feb 11 '21 at 00:43
  • @EthanStrider can you send me a sample url? – Benjamin RD Feb 11 '21 at 01:11
  • Does cache exist only in memory i.e. only while app is running? Quitting the app purges cache? – Paul B Nov 10 '21 at 15:57
  • @PaulB I believe so, yes. It's an in-memory cache. – smat88dd Dec 15 '21 at 09:09
16

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()
    }
}
Aviel Gross
  • 9,770
  • 3
  • 52
  • 62
  • Awesome with iOS 15 and lower – Manish Feb 18 '22 at 07:11
  • 4
    In order to get the placeholder to work correctly, I had to drop the first instance of imageLoader that gets called with initial blank UIImage(). I replaced it with `.onReceive(imageLoader.$image.dropFirst())` – NSRover Mar 15 '22 at 09:20
11

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

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
9

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()
    }
)
Medhi
  • 2,656
  • 23
  • 16
8

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()
    }
    
}
Naishta
  • 11,885
  • 4
  • 72
  • 54
5

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"))
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
5

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)
    
Li Jin
  • 1,879
  • 2
  • 16
  • 23
0
            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()
}

}

siki
  • 33
  • 1
  • 4
0

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() }
        }
    }
Md. Shofiulla
  • 2,135
  • 1
  • 13
  • 19
0

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