如何将 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?