使用 combine's Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像
Posted
技术标签:
【中文标题】使用 combine\'s Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像【英文标题】:Loading image from remote URL asynchronously in SwiftUI Image using combine's Publisher使用 combine's Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像 【发布时间】:2020-11-16 08:33:27 【问题描述】:我一直在寻找从远程服务器图像 URL 异步加载图像的良好解决方案。网上有很多解决方案。遗憾的是,Apple 没有为如此常见的东西提供原生服务。不管怎样,我发现Sundell's blog 真的很有趣,并从中汲取了一些好处,创建了我自己的 ImageLoader,如下所示:
import Combine
class ImageLoader
private let urlSession: URLSession
private let cache: NSCache<NSURL, UIImage>
init(urlSession: URLSession = .shared,
cache: NSCache<NSURL, UIImage> = .init())
self.urlSession = urlSession
self.cache = cache
func publisher(for url: URL) -> AnyPublisher<UIImage, Error>
if let image = cache.object(forKey: url as NSURL)
return Just(image)
.setFailureType(to: Error.self)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
else
return urlSession
.dataTaskPublisher(for: url)
.map(\.data)
.tryMap data in
guard let image = UIImage(data: data) else
throw URLError(.badServerResponse, userInfo: [
NSURLErrorFailingURLErrorKey: url
])
return image
.receive(on: DispatchQueue.main)
.handleEvents(receiveOutput: [cache] image in
cache.setObject(image, forKey: url as NSURL)
)
.eraseToAnyPublisher()
如您所见,发布者提供了AnyPublisher<UIImage, Error>
的实例。我不完全确定如何在我的MyImageView
中使用这个ImageLoader
,如下所示:
struct MyImageView: View
var url: URL
var imageLoader = ImageLoader()
@State private var image = #imageLiteral(resourceName: "placeholder")
var body: some View
Image(uiImage: image)
.onAppear
let cancellable = imageLoader.publisher(for: url).sink(receiveCompletion: failure in
print(failure) // doesn't print
, receiveValue: image in
self.image = image // not getting executed
)
cancellable.cancel() // tried with and without this line.
如何从返回AnyPublisher<UIImage, Error>
实例的ImageLoader
发布者中提取UIImage
?
【问题讨论】:
您的let cancellable
将在您完成执行onAppear
后立即释放。您需要将引用保留足够长的时间以执行请求。最好将其移至某个 class(如 ObservableObject
)。请注意,您仍然需要在类级别声明您的 cancellable
,因此它不会立即被释放。
@pawello2222 如果我让ImageLoader
符合ObservableObject
会不会出错?
不,如果您不想要太多类,将ImageLoader
与ObservableObject
一致是有意义的。
@pawello2222 我在下面添加了我的方法,让我知道它需要任何更改或可以进一步改进。
【参考方案1】:
您必须使用ObservableObject
订阅 ImageLoader 提供的发布者。
class ImageProvider: ObservableObject
@Published var image = UIImage(named: "icHamburger")!
private var cancellable: AnyCancellable?
private let imageLoader = ImageLoader()
func loadImage(url: URL)
self.cancellable = imageLoader.publisher(for: url)
.sink(receiveCompletion: failure in
print(failure)
, receiveValue: image in
self.image = image
)
struct MyImageView: View
var url: URL
@StateObject var viewModel = ImageProvider()
var body: some View
Image(uiImage: viewModel.image)
.onAppear
viewModel.loadImage(url: url)
【讨论】:
您能解释一下为什么您使用bag
属性来存储AnyCancellable
吗?如果不使用会发生什么?有没有其他选择?此外,在应用此解决方案后,我在控制台中打印了关键字“完成”(来自失败)而不是图像。仅供参考:网址包含有效图片。
图像加载现在可以完美运行。由于ios 13.0
支持要求,不得不将StateObject
更改为ObservedObject
。
bag 用于存储订阅并保留它。我们也可以使用一个简单的 AnyCancellable 变量。我们只需要保留订阅,否则它会在达到目的之前被释放。
bag 在我们必须存储多个订阅时很有用。使用了一个简单的 AnyCancellable 变量并编辑了我的答案。我希望这能让你明白。【参考方案2】:
因为我不想对一个简单的ImageLoader
进行太多隔离,所以我让它符合ObservableObject
。所以,我只是在修改@sElanthiaiyan 提供的答案。此外,通过更多研究,我发现发布者需要在需要时存储,并在不再使用时释放。这是修改后的代码
图像加载器:
class ImageLoader: ObservableObject
@Published var image: UIImage
private var bag = Set<AnyCancellable>()
//...
init(placeholder: UIImage = UIImage(),
urlSession: URLSession = .shared,
cache: NSCache<NSURL, UIImage> = .init())
self.image = placeholder
//...
//...
func load(from url: URL)
publisher(for: url)
.sink(receiveCompletion: _ in )
image in
self.image = image
.store(in: &bag)
我的图像视图:
struct MyImageView: View
var url: URL
@StateObject var imageLoader = ImageLoader(placeholder: UIImage(named: "placeholder")!) // or @ObservedObject if iOS 13 support is required
var body: some View
Image(uiImage: imageLoader.image)
.onAppear
imageLoader.load(from: url)
【讨论】:
我相信你不需要deinit
- 来自Apple documentation:“AnyCancellable 实例在取消初始化时会自动调用cancel()。”
感谢您的意见。我已经删除了自定义的 deinit
实现。【参考方案3】:
iOS 15+
struct ContentView: View
var body: some View
if #available(iOS 15.0, *)
AsyncImage(url: URL(string: "https://unsplash.com/photos/_ce9iqvtF90/download?force=true"), transaction: .init(animation: .spring())) phase in
switch phase
case .empty:
Color.green
.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):
Color.red
@unknown default:
Color.yellow
.frame(width: 400, height: 266)
.mask(RoundedRectangle(cornerRadius: 16))
else
// Fallback on earlier versions
【讨论】:
以上是关于使用 combine's Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像的主要内容,如果未能解决你的问题,请参考以下文章
使用 Swift Combine 创建一个 Timer Publisher
让自定义 Publisher 在 Swift Combine 上的不同 DispatchQueue 上运行