はじめに
以下のリポジトリを参考に、画像URLから取得した画像を表示し、画像をキャッシュする実装を試したので記事に残します。
※参考にしたリポジトリ:https://github.com/V8tr/ModernMVVM
サンプルイメージ
開発環境
- macOS Big Sur 11.3.1
- Xcode 12.5
- Swift 5.4
実装
ContentView.swift
import SwiftUI struct ContentView: View { @Environment(\.imageCache) var cache: ImageCache var body: some View { ImageView( url: URL(string: "{画像URL}")!, cache: cache, placeholder: IndicatorView(isAnimating: true) ) .frame(height: 200) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
ImageView.swift
import SwiftUI struct ImageView<Placeholder: View>: View { @ObservedObject private var imageloader: ImageLoader private let placeholder: Placeholder? init(url: URL, cache: ImageCache? = nil, placeholder: Placeholder? = nil) { imageloader = ImageLoader(url: url, cache: cache) self.placeholder = placeholder } var body: some View { Group { if imageloader.image != nil { Image(uiImage: imageloader.image!) .resizable() .scaledToFit() } else { placeholder } } .onAppear(perform: imageloader.load) } }
ImageLoader.swift
import UIKit import Combine class ImageLoader: ObservableObject { @Published var image: UIImage? private let url: URL private var cache: ImageCache? private var cancellables = Set<AnyCancellable>() private(set) var isLoading = false init(url: URL, cache: ImageCache? = nil) { self.url = url self.cache = cache self.image = cache?[url as AnyObject] } private func onStart() { isLoading = true } private func onFinish() { isLoading = false } private func fetchImage(url: URL) -> Future<UIImage, Never> { return Future<UIImage, Never> { promise in URLSession.shared.dataTaskPublisher(for: url) .map { UIImage(data: $0.data) } .replaceError(with: nil) .handleEvents( receiveSubscription: { [weak self] _ in self?.onStart() }, receiveCompletion: { [weak self] _ in self?.onFinish() } ) .sink(receiveValue: { promise(.success($0!)) }) .store(in: &self.cancellables) } } private func addCache(_ image: UIImage?) { image.map { cache?[url as AnyObject] = $0 } } func load() { guard !isLoading else { return } if cache?[url as AnyObject] != nil { return } fetchImage(url: url) .receive(on: DispatchQueue.main) .sink(receiveValue: { self.image = $0 self.addCache($0) }) .store(in: &cancellables) } }
ImageCache.swift
import UIKit import SwiftUI protocol ImageCache { var cache: NSCache<AnyObject, UIImage> { get set } subscript(key: AnyObject) -> UIImage? { get set } } struct DefaultImageCache: ImageCache { var cache = NSCache<AnyObject, UIImage>() subscript(key: AnyObject) -> UIImage? { get { cache.object(forKey: key) } set(image) { cache.setObject(image!, forKey: key) } } } struct ImageCacheKey: EnvironmentKey { static let defaultValue: ImageCache = DefaultImageCache() } extension EnvironmentValues { var imageCache: ImageCache { get { self[ImageCacheKey.self] } set(image) { self[ImageCacheKey.self] = image } } }
IndicatorView.swift
import SwiftUI struct IndicatorView: UIViewRepresentable { let isAnimating: Bool func makeUIView(context: Context) -> UIActivityIndicatorView { let indicatorView = UIActivityIndicatorView(style: .medium) indicatorView.hidesWhenStopped = true return indicatorView } func updateUIView(_ indicatorView: UIActivityIndicatorView, context: Context) { isAnimating ? indicatorView.startAnimating() : indicatorView.stopAnimating() } }
おわりに
参考にしたリポジトリの実装を読んで、表題の実装を試すだけでなくアーキテクチャの勉強にもなったので、もっと他の人のコードを読む数を増やしていきたいと思いました。
参考
-
https://developer.apple.com/documentation/foundation/nscache
- https://developer.apple.com/documentation/swiftui/environmentvalues
-
https://developer.apple.com/documentation/foundation/urlsession/3329708-datataskpublisher
-
https://developer.apple.com/documentation/combine/publisher/sink(receivevalue:)
- https://tech.dely.jp/entry/2019/12/11/103000