如何将UIImage异步加载到SwiftUI图像中?

Alb*_*nas 6 ios swift swiftui

在SwiftUI中,有一些.init创建图像的方法,但是它们都不允许使用块或任何其他方式从网络/缓存加载UIImage ...

我正在使用Kingfisher从网络加载图像并在列表行中缓存,但是在视图中绘制图像的方法是再次重新渲染它,我不想这样做。另外,在获取图像时,我正在创建一个假图像(仅彩色)作为占位符。另一种方法是将所有内容包装在自定义视图中,然后仅重新渲染包装器。但是我还没有尝试过。

此示例现在正在工作。任何改善当前想法的想法都会很棒

使用加载程序的某些视图

struct SampleView : View {

    @ObjectBinding let imageLoader: ImageLoader

    init(imageLoader: ImageLoader) {
        self.imageLoader = imageLoader
    }

    var body: some View {
       Image(uiImage: imageLoader.image(for: "https://url-for-image"))
          .frame(width: 128, height: 128)
          .aspectRatio(contentMode: ContentMode.fit)
    }

}
Run Code Online (Sandbox Code Playgroud)
import UIKit.UIImage
import SwiftUI
import Combine
import class Kingfisher.ImageDownloader
import struct Kingfisher.DownloadTask
import class Kingfisher.ImageCache
import class Kingfisher.KingfisherManager

class ImageLoader: BindableObject {

    var didChange = PassthroughSubject<ImageLoader, Never>()
    private let downloader: ImageDownloader
    private let cache: ImageCache
    private var image: UIImage? {
        didSet {
            dispatchqueue.async { [weak self] in
                guard let self = self else { return }
                self.didChange.send(self)
            }
        }
    }
    private var task: DownloadTask?
    private let dispatchqueue: DispatchQueue

    init(downloader: ImageDownloader = KingfisherManager.shared.downloader,
         cache: ImageCache = KingfisherManager.shared.cache,
         dispatchqueue: DispatchQueue = DispatchQueue.main) {
        self.downloader = downloader
        self.cache = cache
        self.dispatchqueue = dispatchqueue
    }

    deinit {
        task?.cancel()
    }

    func image(for url: URL?) -> UIImage {
        guard let targetUrl = url else {
            return UIImage.from(color: .gray)
        }
        guard let image = image else {
            load(url: targetUrl)
            return UIImage.from(color: .gray)
        }
        return image
    }

    private func load(url: URL) {
        let key = url.absoluteString
        if cache.isCached(forKey: key) {
            cache.retrieveImage(forKey: key) {  [weak self] (result) in
                guard let self = self else { return }
                switch result {
                case .success(let value):
                    self.image = value.image
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        } else {
            downloader.downloadImage(with: url, options: nil, progressBlock: nil) {  [weak self] (result) in
                guard let self = self else { return }
                switch result {
                case .success(let value):
                    self.cache.storeToDisk(value.originalData, forKey: url.absoluteString)
                    self.image = value.image
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

paw*_*222 17

SwiftUI 3

从 iOS 15 开始我们现在可以使用AsyncImage

AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
    image.resizable()
} placeholder: {
    ProgressView()
}
.frame(width: 50, height: 50)
Run Code Online (Sandbox Code Playgroud)

SwiftUI 2

这是一个支持缓存和多种加载状态的原生 SwiftUI 解决方案:

import Combine
import SwiftUI

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: .fit)
            } else if viewModel.isLoading {
                ProgressView()
            } else {
                Image(systemName: "photo")
            }
        }
        .onAppear {
            viewModel.loadImage(from: url)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
extension NetworkImage {
    class ViewModel: ObservableObject {
        @Published var imageData: Data?
        @Published var isLoading = false

        private static let cache = NSCache<NSURL, NSData>()

        private var cancellables = Set<AnyCancellable>()

        func loadImage(from url: URL?) {
            isLoading = true
            guard let url = url else {
                isLoading = false
                return
            }
            if let data = Self.cache.object(forKey: url as NSURL) {
                imageData = data as Data
                isLoading = false
                return
            }
            URLSession.shared.dataTaskPublisher(for: url)
                .map { $0.data }
                .replaceError(with: nil)
                .receive(on: DispatchQueue.main)
                .sink { [weak self] in
                    if let data = $0 {
                        Self.cache.setObject(data as NSData, forKey: url as NSURL)
                        self?.imageData = data
                    }
                    self?.isLoading = false
                }
                .store(in: &cancellables)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

(上面的代码没有使用任何第三方库,因此很容易NetworkImage以任何方式进行更改。)


演示

在此输入图像描述

import Combine
import SwiftUI

struct ContentView: View {
    @State private var showImage = false

    var body: some View {
        if showImage {
            NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
                .frame(maxHeight: 150)
                .padding()
        } else {
            Button("Load") {
                showImage = true
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

(我使用了一个特别大的 Stack Overflow 徽标来显示加载状态。)


Sur*_*ezz 9

将您的模型传递给包含 url 的 ImageRow 结构。

import SwiftUI
import Combine

struct ContentView : View {
    var listData: Post
    var body: some View {
        List(model.post) { post in
            ImageRow(model: post) // Get image
        }
    }
}

/********************************************************************/
// Download Image

struct ImageRow: View {
    let model: Post
    var body: some View {
        VStack(alignment: .center) {
            ImageViewContainer(imageUrl: model.avatar_url)
        }
    }
}

struct ImageViewContainer: View {
    @ObjectBinding var remoteImageURL: RemoteImageURL

    init(imageUrl: String) {
        remoteImageURL = RemoteImageURL(imageURL: imageUrl)
    }

    var body: some View {
        Image(uiImage: UIImage(data: remoteImageURL.data) ?? UIImage())
            .resizable()
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.black, lineWidth: 3.0))
            .frame(width: 70.0, height: 70.0)
    }
}

class RemoteImageURL: BindableObject {
    var didChange = PassthroughSubject<Data, Never>()
    var data = Data() {
        didSet {
            didChange.send(data)
        }
    }
    init(imageURL: String) {
        guard let url = URL(string: imageURL) else { return }

        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else { return }

            DispatchQueue.main.async { self.data = data }

            }.resume()
    }
}
/********************************************************************/
Run Code Online (Sandbox Code Playgroud)

  • 更新:处理该更改 --&gt; `@Published var data = Data()` (3认同)

gra*_*spo 5

在 SwiftUI 中加载图像的一种更简单、更清晰的方法是使用著名的 Kingfisher 库。

  1. Kingfisher通过 Swift 包管理器添加

选择文件 > Swift 包 > 添加包依赖项。输入 https://github.com/onevcat/Kingfisher.git

在“选择包存储库”对话框中。在下一页中,指定版本解析规则为“Up to Next Major”,其中“5.8.0”为其最早版本。

Xcode 检查源代码并解析版本后,您可以选择“KingfisherSwiftUI”库并将其添加到您的应用程序目标中。

  1. import KingfisherSwiftUI
  2. KFImage(myUrl)

完毕!就是这么简单


iel*_*ani 2

定义imageLoader@ObjectBinding

@ObjectBinding private var imageLoader: ImageLoader
Run Code Online (Sandbox Code Playgroud)

使用图像的 url 初始化视图会更有意义:

struct SampleView : View {

    var imageUrl: URL

    private var image: UIImage {
        imageLoader.image(for: imageUrl)
    }

    @ObjectBinding private var imageLoader: ImageLoader

    init(url: URL) {
        self.imageUrl = url
        self.imageLoader = ImageLoader()
    }

    var body: some View {
        Image(uiImage: image)
            .frame(width: 200, height: 300)
            .aspectRatio(contentMode: ContentMode.fit)
    }
}
Run Code Online (Sandbox Code Playgroud)

例如 :

//Create a SampleView with an initial photo
var s = SampleView(url: URL(string: "https://placebear.com/200/300")!)
//You could then update the photo by changing the imageUrl
s.imageUrl = URL(string: "https://placebear.com/200/280")!
Run Code Online (Sandbox Code Playgroud)