From 357ed7a8ec0333003c9ef74343ee088bd8c45aae Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 25 Aug 2022 21:36:58 -0700 Subject: [PATCH 1/3] use async image for profile card and linked item cards --- .../Sources/Views/FeedItem/GridCard.swift | 10 +++++----- .../Views/FeedItem/HomeFeedCardView.swift | 10 +++++----- .../OmnivoreKit/Sources/Views/ProfileCard.swift | 17 +++++------------ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 1e8e46254..2cb04ca89 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -133,19 +133,19 @@ public struct GridCard: View { Spacer() if let imageURL = item.imageURL { - AsyncLoadingImage(url: imageURL) { imageStatus in - if case let AsyncImageStatus.loaded(image) = imageStatus { + AsyncImage(url: imageURL) { phase in + if let image = phase.image { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: geo.size.width / 3, height: (geo.size.width * 2) / 9) .cornerRadius(3) - } else if case AsyncImageStatus.loading = imageStatus { + } else if phase.error != nil { + EmptyView() + } else { Color.appButtonBackground .frame(width: geo.size.width / 3, height: (geo.size.width * 2) / 9) .cornerRadius(3) - } else { - EmptyView() } } } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift index f7515393c..daac1fc81 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift @@ -47,19 +47,19 @@ public struct FeedCard: View { Group { if let imageURL = item.imageURL { - AsyncLoadingImage(url: imageURL) { imageStatus in - if case let AsyncImageStatus.loaded(image) = imageStatus { + AsyncImage(url: imageURL) { phase in + if let image = phase.image { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .cornerRadius(6) - } else if case AsyncImageStatus.loading = imageStatus { + } else if phase.error != nil { + EmptyView().frame(width: 80, height: 80, alignment: .top) + } else { Color.appButtonBackground .frame(width: 80, height: 80) .cornerRadius(6) - } else { - EmptyView().frame(width: 80, height: 80, alignment: .top) } } } diff --git a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift index 61583e480..9e923a560 100644 --- a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift +++ b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift @@ -22,18 +22,11 @@ public struct ProfileCard: View { public var body: some View { HStack(alignment: .center) { Group { - if let url = data.imageURL { - AsyncLoadingImage(url: url) { imageStatus in - if case let AsyncImageStatus.loaded(image) = imageStatus { - image.resizable() - } else { - Image(systemName: "person.crop.circle").resizable() - } - } - } else { - Image(systemName: "person.crop.circle") - .resizable() - } + AsyncImage( + url: data.imageURL, + content: { $0.resizable() }, + placeholder: { Image(systemName: "person.crop.circle").resizable() } + ) } .aspectRatio(contentMode: .fill) .frame(width: 70, height: 70, alignment: .center) From b70a8a8585978634360ccf015b040d1fa460eb2b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 25 Aug 2022 21:45:33 -0700 Subject: [PATCH 2/3] remove AsyncLoadingImage struct --- .../Share/Views/ShareExtensionView.swift | 10 ++- .../Sources/Views/AsyncLoadingImage.swift | 81 ------------------- 2 files changed, 6 insertions(+), 85 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index ecba3393b..b7b7d39f0 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -73,19 +73,21 @@ public struct ShareExtensionView: View { HStack { if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) { if !iconURL.isFileURL { - AsyncLoadingImage(url: iconURL) { imageStatus in - if case let AsyncImageStatus.loaded(image) = imageStatus { + AsyncImage( + url: iconURL, + content: { image in image .resizable() .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) .clipped() - } else { + }, + placeholder: { Color.appButtonBackground .aspectRatio(contentMode: .fill) .frame(width: 61, height: 61) } - } + ) } else { if let localImage = localImage(from: iconURL) { localImage diff --git a/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift b/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift deleted file mode 100644 index b25e732aa..000000000 --- a/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import Models -import SwiftUI -import Utils - -public enum AsyncImageStatus { - case loading - case loaded(image: Image) - case error -} - -public struct AsyncLoadingImage: View { - let viewBuilder: (AsyncImageStatus) -> Content - let url: URL - @StateObject private var imageLoader = ImageLoader() - - public init(url: URL, @ViewBuilder viewBuilder: @escaping (AsyncImageStatus) -> Content) { - self.url = url - self.viewBuilder = viewBuilder - } - - public var body: some View { - viewBuilder(imageLoader.status) - .task { await imageLoader.load(fromUrl: url) } - } -} - -@MainActor private final class ImageLoader: ObservableObject { - @Published var status: AsyncImageStatus = .loading - var loadStarted = false - - func load(fromUrl url: URL) async { - guard !loadStarted else { return } - loadStarted = true - - if let cachedImage = ImageCache.shared[url] { - #if os(iOS) - status = .loaded(image: Image(uiImage: cachedImage)) - #else - status = .loaded(image: Image(nsImage: cachedImage)) - #endif - return - } - - if let imageData = try? await fetchImageData(url: url) { - #if os(iOS) - let fetchedImage = UIImage(data: imageData) - #else - let fetchedImage = NSImage(data: imageData) - #endif - - guard let fetchedImage = fetchedImage else { - status = .error - return - } - ImageCache.shared[url] = fetchedImage - - #if os(iOS) - status = .loaded(image: Image(uiImage: fetchedImage)) - #else - status = .loaded(image: Image(nsImage: fetchedImage)) - #endif - } else { - status = .error - } - } -} - -private func fetchImageData(url: URL) async throws -> Data { - do { - let (data, response) = try await URLSession.shared.data(from: url) - - if let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode { - return data - } else { - throw BasicError.message(messageText: "failed") - } - } catch { - throw BasicError.message(messageText: "failed") - } -} From 32711ef657a53ff0970bd606207281a74a62d9c1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 25 Aug 2022 21:52:13 -0700 Subject: [PATCH 3/3] remove image cache --- .../Services/AudioSession/AudioSession.swift | 13 ---- .../Sources/Utils/ImageCache.swift | 63 ------------------- 2 files changed, 76 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Utils/ImageCache.swift diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift index 781c481fd..6300379cd 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift @@ -303,19 +303,6 @@ public class AudioSession: NSObject, ObservableObject, AVAudioPlayerDelegate { ] } -// if let imageURL = item?.imageURL, let cachedImage = ImageCache.shared[imageURL] { - //// #if os(iOS) - //// status = .loaded(image: Image(uiImage: cachedImage)) - //// #else - //// status = .loaded(image: Image(nsImage: cachedImage)) - //// #endif -// MPNowPlayingInfoCenter.default().nowPlayingInfo = [ -// // MPMediaItemPropertyArtwork: cachedImage, -// MPMediaItemPropertyArtist: item?.author ?? "Omnivore", -// MPMediaItemPropertyTitle: item?.title ?? "Your Omnivore Article" -// ] -// } - let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true diff --git a/apple/OmnivoreKit/Sources/Utils/ImageCache.swift b/apple/OmnivoreKit/Sources/Utils/ImageCache.swift deleted file mode 100644 index f05003969..000000000 --- a/apple/OmnivoreKit/Sources/Utils/ImageCache.swift +++ /dev/null @@ -1,63 +0,0 @@ -#if os(iOS) - import UIKit - - public typealias PlatformImage = UIImage -#elseif os(macOS) - import AppKit - - public typealias PlatformImage = NSImage -#endif - -/// Reference: https://www.onswiftwings.com/posts/reusable-image-cache/ - -public final class ImageCache { - public static let shared = ImageCache() - - public subscript(_ key: URL) -> PlatformImage? { - get { - image(key) - } - set { - insertImage(newValue, url: key) - } - } - - public func removeAllObjects() { - cache.removeAllObjects() - } - - private let queue = DispatchQueue(label: "app.omnivore.image.cache.queue", attributes: .concurrent) - private let cache = NSCache() - - private init() { - cache.totalCostLimit = 1024 * 1024 * 1024 * 50 // 50 MB - } - - private func image(_ url: URL) -> PlatformImage? { - var cachedImage: PlatformImage? - queue.sync { - cachedImage = cache.object(forKey: NSString(string: url.absoluteString)) - } - return cachedImage - } - - private func insertImage(_ image: PlatformImage?, url: URL) { - guard let image = image else { return } - queue.async(flags: .barrier) { - self.cache.setObject(image, forKey: NSString(string: url.absoluteString), cost: 1) - } - } -} - -private extension PlatformImage { - var diskSize: Int { - #if os(iOS) - guard let cgImage = cgImage else { return 0 } - return cgImage.bytesPerRow * cgImage.height - #elseif os(macOS) - // Instead of calculating the nsimage size just assume 250k - // which will allow for up to 200 images in the cache - (1024 * 1024) / 4 - #endif - } -}