diff --git a/AsyncImageDemo/App/ContentView.swift b/AsyncImageDemo/App/ContentView.swift index 15a1532..db3df18 100644 --- a/AsyncImageDemo/App/ContentView.swift +++ b/AsyncImageDemo/App/ContentView.swift @@ -11,15 +11,8 @@ import AsyncImage struct ContentView: View { @State private var showSlide = false - private let imageCache: ImageCache - private let publisherCache: PublisherCache private let images: [ImageItem] = (1...100).map { _ in ImageItem() } - init(imageCache: ImageCache) { - self.imageCache = imageCache - self.publisherCache = PublisherCacheFactory.makeTemporaryCache() - } - var body: some View { NavigationView { ZStack { @@ -28,21 +21,13 @@ struct ContentView: View { ForEach(self.images) { item in NavigationLink( - destination: DetailView( - imageCache: self.imageCache, - publisherCache: self.publisherCache, - item: item - )) { + destination: DetailView(item: item)) { HStack(alignment: .center) { Spacer(minLength: 0) - AsyncImage( - request: item.image.urlRequest, - imageCache: self.imageCache, - publisherCache: self.publisherCache - ) - .frame(width: 200) - .cornerRadius(10) + AsyncImage(item.image.urlRequest) + .frame(width: 200) + .cornerRadius(10) Spacer(minLength: 0) } @@ -57,21 +42,13 @@ struct ContentView: View { Spacer(minLength: 90) HStack { - AsyncImage( - request: self.images[self.images.count - 1].image.urlRequest, - imageCache: self.imageCache, - publisherCache: self.publisherCache - ) - .frame(width: 200) - .clipShape(Circle()) + AsyncImage(self.images[self.images.count - 1].image.urlRequest) + .frame(width: 200) + .clipShape(Circle()) - AsyncImage( - request: self.images[self.images.count - 2].image.urlRequest, - imageCache: self.imageCache, - publisherCache: self.publisherCache - ) - .frame(width: 200) - .clipShape(Circle()) + AsyncImage(self.images[self.images.count - 2].image.urlRequest) + .frame(width: 200) + .clipShape(Circle()) } } } @@ -86,23 +63,15 @@ struct ContentView: View { } struct DetailView: View { - private let imageCache: ImageCache - private let publisherCache: PublisherCache private let item: ImageItem - init(imageCache: ImageCache, publisherCache: PublisherCache, item: ImageItem) { - self.imageCache = imageCache - self.publisherCache = publisherCache + init(item: ImageItem) { self.item = item } var body: some View { - AsyncImage( - request: self.item.image.urlRequest, - imageCache: self.imageCache, - publisherCache: self.publisherCache - ) - .aspectRatio(contentMode: .fit) + AsyncImage(self.item.image.urlRequest) + .aspectRatio(contentMode: .fit) } } @@ -118,6 +87,6 @@ struct ImageItem: Identifiable { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(imageCache: ImageCacheFactory.makeTemporaryCache()) + ContentView() } } diff --git a/AsyncImageDemo/SceneDelegate.swift b/AsyncImageDemo/SceneDelegate.swift index 8dc5092..9f89022 100644 --- a/AsyncImageDemo/SceneDelegate.swift +++ b/AsyncImageDemo/SceneDelegate.swift @@ -11,13 +11,9 @@ import AsyncImage class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - let globalImageCache = ImageCacheFactory.makeTemporaryCache() - + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Create the SwiftUI view that provides the window contents. - let contentView = ContentView( - imageCache: globalImageCache - ) + let contentView = ContentView() // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { diff --git a/README.md b/README.md index 2b47601..9493d70 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ iOS >= 13 AsyncImage with the following characteristics: ## Getting started You can clone this repository and run the `AsyncImageDemo` App. +```swift +AsyncImage("https://picsum.photos/200/300".urlRequest) + .frame(width: 200, height: 200) +``` + +You can optionally use the `configuration` to apply modifiers to the Image rather than directly to the View returned by AsyncImage. +If not specified, default ones applied are: `resizable().renderingMode(.original)` + +### Controlling the cache > Warning: Make sure to create an ImageCache and PublisherCache object somewhere in your app/module and injected to the AsyncImage object. Do not create a new one per each image, otherwise it will not work as intended. ```swift @@ -25,9 +34,6 @@ AsyncImage( .frame(width: 200, height: 200) ``` -You can optionally use the `configuration` to apply modifiers to the Image rather than directly to the View returned by AsyncImage. -If not specified, default ones applied are: `resizable().renderingMode(.original)` - ## Install ### Swift Package Manager In Xcode, right click your project and `Add Packages`, then enter the repository URL. diff --git a/SimpleAsyncImage.podspec b/SimpleAsyncImage.podspec index d0bc019..78b8f79 100644 --- a/SimpleAsyncImage.podspec +++ b/SimpleAsyncImage.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.platform = :ios, "13.0" s.swift_version = '5.0' s.name = "SimpleAsyncImage" - s.version = "1.0.2" + s.version = "1.0.3" s.summary = "Simple iOS 13 AsyncImage" s.description = <<-DESC iOS >= 13 AsyncImage with the following characteristics: @@ -15,9 +15,9 @@ Pod::Spec.new do |s| - Automatically reload when connectivity is regained - Automatic retry when URLRequest fails DESC - s.homepage = "https://github.com/MarcBiosca" + s.homepage = "https://github.com/MarcBiosca/AsyncImage.git" s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "MarcBiosca" => "marc@dostresdos.com" } s.source = { :git => "https://github.com/MarcBiosca/AsyncImage.git", :tag => "#{s.version}" } - s.source_files = 'Sources/AsyncImage/**/*' -end \ No newline at end of file + s.source_files = 'Sources/AsyncImage/**/*' +end diff --git a/Sources/AsyncImage/Feature/AsyncImage/AsyncImage.swift b/Sources/AsyncImage/Feature/AsyncImage/AsyncImage.swift index e286855..4dd0feb 100644 --- a/Sources/AsyncImage/Feature/AsyncImage/AsyncImage.swift +++ b/Sources/AsyncImage/Feature/AsyncImage/AsyncImage.swift @@ -14,7 +14,7 @@ public struct AsyncImage: View { public init( request: URLRequest?, - imageCache: ImageCache?, + imageCache: ImageCache, publisherCache: PublisherCache, configuration: @escaping (Image) -> Image = { $0.resizable().renderingMode(.original) @@ -27,6 +27,23 @@ public struct AsyncImage: View { publisherCache: publisherCache ) ) + + self.configuration = configuration + } + + public init( + _ request: URLRequest?, + configuration: @escaping (Image) -> Image = { + $0.resizable().renderingMode(.original) + } + ) { + self._viewModel = ObservedObject( + wrappedValue: AsyncImageVM( + request: request, + imageCache: TemporaryImageCache.shared, + publisherCache: TemporaryPublisherCache.shared + ) + ) self.configuration = configuration } diff --git a/Sources/AsyncImage/Feature/AsyncImage/AsyncImageVM.swift b/Sources/AsyncImage/Feature/AsyncImage/AsyncImageVM.swift index 98e5876..6188331 100644 --- a/Sources/AsyncImage/Feature/AsyncImage/AsyncImageVM.swift +++ b/Sources/AsyncImage/Feature/AsyncImage/AsyncImageVM.swift @@ -17,7 +17,7 @@ final class AsyncImageVM: ObservableObject { private let request: URLRequest? private let networkAgent: NetworkAgentProtocol - private var images: ImageCache? + private var images: ImageCache private var publishers: PublisherCache private var imagePublisher: AnyCancellable? private var networkPublisher: AnyCancellable? @@ -26,7 +26,7 @@ final class AsyncImageVM: ObservableObject { init( request: URLRequest?, networkAgent: NetworkAgentProtocol = NetworkAgent(), - imageCache: ImageCache?, + imageCache: ImageCache, publisherCache: PublisherCache ) { self.request = request @@ -40,7 +40,7 @@ final class AsyncImageVM: ObservableObject { func load() { guard !self.isLoading, let url = self.request?.url else { return } - if let image = self.images?[url] { + if let image = self.images[url] { self.imageState = .success(image) return } @@ -81,11 +81,6 @@ private extension AsyncImageVM { let publisher = self.networkAgent.download(from: self.request) .subscribe(on: self.imageProcessingQueue) - .handleEvents(receiveCompletion: { [weak self] _ in - self?.onCancel(url: url) - }, receiveCancel: { [weak self] in - self?.onCancel(url: url) - }) .share() .eraseToAnyPublisher() @@ -116,15 +111,11 @@ private extension AsyncImageVM { return Empty().eraseToAnyPublisher() } - func onCancel(url: URL) { - self.publishers.set(nil, for: url.absoluteString) - self.isLoading = false - } - func onOutput(url: URL, image: UIImage) { - self.images?[url] = image + self.images[url] = image self.set(.success(image)) + self.publishers.set(nil, for: url.absoluteString) self.networkPublisher = nil } diff --git a/Sources/AsyncImage/Feature/Cache/Image/TemporaryImageCache.swift b/Sources/AsyncImage/Feature/Cache/Image/TemporaryImageCache.swift index 4f26ab8..a43e424 100644 --- a/Sources/AsyncImage/Feature/Cache/Image/TemporaryImageCache.swift +++ b/Sources/AsyncImage/Feature/Cache/Image/TemporaryImageCache.swift @@ -9,6 +9,9 @@ import Foundation import UIKit final class TemporaryImageCache: ImageCache { + // To facilitate integrations + static let shared = TemporaryImageCache() + private let cache = NSCache() init() { diff --git a/Sources/AsyncImage/Feature/Cache/Publisher/PublisherCache.swift b/Sources/AsyncImage/Feature/Cache/Publisher/PublisherCache.swift index 105323f..00fa43a 100644 --- a/Sources/AsyncImage/Feature/Cache/Publisher/PublisherCache.swift +++ b/Sources/AsyncImage/Feature/Cache/Publisher/PublisherCache.swift @@ -7,6 +7,5 @@ public protocol PublisherCache { func set(_ value: UIImageErrorPublisher?, for key: String) - func get(_ key: String) -> UIImageErrorPublisher? } diff --git a/Sources/AsyncImage/Feature/Cache/Publisher/TemporaryPublisherCache.swift b/Sources/AsyncImage/Feature/Cache/Publisher/TemporaryPublisherCache.swift index fe69368..683c4af 100644 --- a/Sources/AsyncImage/Feature/Cache/Publisher/TemporaryPublisherCache.swift +++ b/Sources/AsyncImage/Feature/Cache/Publisher/TemporaryPublisherCache.swift @@ -9,6 +9,9 @@ import Combine import UIKit final class TemporaryPublisherCache: PublisherCache { + // To facilitate integrations + static let shared = TemporaryPublisherCache() + private var cache = [String: UIImageErrorPublisher]() private let concurrentQueue = DispatchQueue(label: "temporary-publisher", attributes: .concurrent)