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

Posted

技术标签:

【中文标题】如何将 UIImage 异步加载到 SwiftUI Image 中?【英文标题】:How can I load an UIImage into a SwiftUI Image asynchronously? 【发布时间】:2019-06-11 14:04:04 【问题描述】:

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


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)
                
            
        
    


【问题讨论】:

在这种情况下,您需要使用Image(uiImage: imageLoader.image 设置要查看的图像。并在初始化或呈现图像视图结构之前开始下载图像 Kingfisher 现在支持 swiftUI 【参考方案1】:

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)

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)
        
    

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)
        
    

(以上代码没有使用任何第三方库,所以随便改NetworkImage都很方便。)


演示

import Combine
import SwiftUI

struct ContentView: View 
    @State private var showImage = false

    var body: some View 
        if showImage 
            NetworkImage(url: URL(string: "https://***.design/assets/img/logos/so/logo-***.png"))
                .frame(maxHeight: 150)
                .padding()
         else 
            Button("Load") 
                showImage = true
            
        
    

(我使用了一个非常大的 Stack Overflow 标志来显示加载状态。)

【讨论】:

【参考方案2】:

将您的模型传递给包含 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()
    

/********************************************************************/

【讨论】:

这在 Xcode 11 beta 7 中不起作用,GM 将 BindableObject 替换为 ObservableObject 并将 ObjectBinding 替换为 ObservedObject,但无法加载远程图像。任何建议 更新:使用该更改 --> @Published var data = Data()【参考方案3】:

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

    通过 Swift 包管理器添加 Kingfisher

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

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

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

    import KingfisherSwiftUI KFImage(myUrl)

完成!就这么简单

【讨论】:

我今天使用的版本(2021 年 10 月 10 日)似乎依赖于 ProgressView(预览的一部分)。我可以将其注释掉并忽略该问题,但我更喜欢包括在内。知道 ProgressView 依赖项来自哪里吗?【参考方案4】:

imageLoader 定义为@ObjectBinding

@ObjectBinding private var imageLoader: ImageLoader

使用图片的 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)
    

例如:

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

【讨论】:

我留下的代码是一个示例,我忘记添加了。感谢您指出@ielyamani【参考方案5】:

我只会使用onAppear 回调

import Foundation
import SwiftUI
import Combine
import UIKit
    struct ImagePreviewModel 
        var urlString : String
        var width : CGFloat = 100.0
        var height : CGFloat = 100.0
    

    struct ImagePreview: View 
        let viewModel: ImagePreviewModel
        @State var initialImage = UIImage()
        var body: some View 
            Image(uiImage: initialImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: self.width, height: self.height)
                .onAppear 
                    guard let url = URL(string: self.viewModel.urlString) else  return 
                    URLSession.shared.dataTask(with: url)  (data, response, error) in
                        guard let data = data else  return 
                        guard let image = UIImage(data: data) else  return 

                        RunLoop.main.perform 
                            self.initialImage = image
                        

                    .resume()
                
        
        var width: CGFloat  return max(viewModel.width, 100.0) 
        var height: CGFloat  return max(viewModel.height, 100.0) 
    

【讨论】:

【参考方案6】:
import SwiftUI
struct UrlImageView: View 
@ObservedObject var urlImageModel: UrlImageModel

init(urlString: String?) 
    urlImageModel = UrlImageModel(urlString: urlString)


var body: some View 
    Image(uiImage: urlImageModel.image ?? UrlImageView.defaultImage!)
        .resizable()
        .scaledToFill()


static var defaultImage = UIImage(systemName: "photo")


class UrlImageModel: ObservableObject 
@Published var image: UIImage?
var urlString: String?

init(urlString: String?) 
    self.urlString = urlString
    loadImage()


func loadImage() 
    loadImageFromUrl()


func loadImageFromUrl() 
    guard let urlString = urlString else 
        return
    
    
    let url = URL(string: urlString)!
    let task = URLSession.shared.dataTask(with: url, completionHandler: 
getImageFromResponse(data:response:error:))
    task.resume()



func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) 

    guard error == nil else 
        print("Error: \(error!)")
        return
    
    guard let data = data else 
        print("No data found")
        return
    
    
    DispatchQueue.main.async 
        guard let loadedImage = UIImage(data: data) else 
            return
        
        self.image = loadedImage
    


并像这样使用:

UrlImageView(urlString: "https://developer.apple.com/assets/elements/icons/swiftui/swiftui-96x96_2x.png").frame(width:100, height:100)

【讨论】:

【参考方案7】:

随着 2021 年 iOS 15 和 macOS 12 的发布,SwiftUI 提供了原生的AsyncImage 视图,可以异步加载图像。请记住,您仍然必须回退到早期操作系统版本的自定义实现。

AsyncImage(url: URL(string: "https://example.com/tile.png"))

API本身也提供了各种自定义图片或提供占位符的方式,例如:

AsyncImage(url: URL(string: "https://example.com/tile.png"))  image in
    image.resizable(resizingMode: .tile)
 placeholder: 
    Color.green

更多内容请关注Apple Developer Documentation.

【讨论】:

以上是关于如何将 UIImage 异步加载到 SwiftUI Image 中?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 swiftUI 的图像传递给 UIImage? [关闭]

将 SwiftUI 视图渲染为 UIImage

如何将不在根层次结构中的 SwiftUI 视图呈现为 UIImage?

如何创建动画 GIF

将 SwiftUI 视图渲染到屏幕外并将视图另存为 UIImage 以共享

SwiftUI基础 - Image 图片