Merge pull request #1143 from omnivore-app/chore/use-swiftui-async-image

SwiftUI AsyncImage Adoption
This commit is contained in:
Jackson Harper 2022-08-26 13:19:27 +08:00 committed by GitHub
commit b79f75c170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 21 additions and 183 deletions

View file

@ -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

View file

@ -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

View file

@ -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<NSString, PlatformImage>()
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
}
}

View file

@ -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<Content: View>: 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")
}
}

View file

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

View file

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

View file

@ -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)