Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【SwiftUI】画像URLから取得した画像を表示する

 

はじめに

以下のリポジトリを参考に、画像URLから取得した画像を表示し、画像をキャッシュする実装を試したので記事に残します。

※参考にしたリポジトリ:https://github.com/V8tr/ModernMVVM

 

サンプルイメージ

f:id:hfoasi8fje3:20210521205119p:plain

 

開発環境

 

実装

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()
    }
}

 

おわりに

参考にしたリポジトリの実装を読んで、表題の実装を試すだけでなくアーキテクチャの勉強にもなったので、もっと他の人のコードを読む数を増やしていきたいと思いました。

 

参考