From 133be4a85eed6965614a1fa002ecbf5e4476799f Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Thu, 2 Jan 2025 17:53:56 -0500 Subject: [PATCH] fixes the issue where WebImage is constantly rerendered --- Nos/Views/Components/AvatarView.swift | 114 +++++++++++++++++++++----- 1 file changed, 94 insertions(+), 20 deletions(-) diff --git a/Nos/Views/Components/AvatarView.swift b/Nos/Views/Components/AvatarView.swift index fc95fe6a7..14c833dae 100644 --- a/Nos/Views/Components/AvatarView.swift +++ b/Nos/Views/Components/AvatarView.swift @@ -1,30 +1,104 @@ import SwiftUI +import Logger import SDWebImageSwiftUI struct AvatarView: View { + let imageUrl: URL? + let size: CGFloat + private let id: String - var imageUrl: URL? - var size: CGFloat + init(imageUrl: URL?, size: CGFloat) { + self.imageUrl = imageUrl + self.size = size + self.id = imageUrl?.absoluteString ?? "empty-avatar-\(UUID())" + } var body: some View { - WebImage( - url: imageUrl, - content: { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: size, height: size) - .clipShape(Circle()) - }, - placeholder: { - Image.emptyAvatar + ManagedImageView(imageUrl: imageUrl, size: size, id: id) + } +} + +private struct ManagedImageView: View { + let imageUrl: URL? + let size: CGFloat + let id: String + + @StateObject private var loader = ImageLoader() + + var body: some View { + Group { + if let image = loader.image { + Image(uiImage: image) .resizable() - .renderingMode(.original) .aspectRatio(contentMode: .fill) - .frame(width: size, height: size) - .clipShape(Circle()) + } else { + Color.gray } - ) + } + .frame(width: size, height: size) + .clipShape(Circle()) + .id(id) + .task(id: imageUrl?.absoluteString) { + await loader.load(url: imageUrl) + } + } +} + +private class ImageLoader: ObservableObject { + @Published var image: UIImage? + private var cancellable: SDWebImageCombinedOperation? + + func load(url: URL?) async { + await MainActor.run { + cancel() + } + + guard let url = url else { + await MainActor.run { + image = nil + } + return + } + + // Check cache first + if let cachedImage = SDImageCache.shared.imageFromCache(forKey: url.absoluteString) { + await MainActor.run { + image = cachedImage + } + return + } + + // Load from network + await withCheckedContinuation { continuation in + Task { @MainActor in + cancellable = SDWebImageManager.shared.loadImage( + with: url, + options: [.retryFailed, .refreshCached], + progress: nil + ) { [weak self] image, _, _, _, _, _ in + Task { @MainActor in + self?.image = image + continuation.resume() + } + } + } + } + } + + func cancel() { + cancellable?.cancel() + cancellable = nil + } + + deinit { + cancel() + } +} + +// Make view identity stable +extension ManagedImageView: Equatable { + static func == (lhs: ManagedImageView, rhs: ManagedImageView) -> Bool { + lhs.id == rhs.id && lhs.size == rhs.size && lhs.imageUrl == rhs.imageUrl } } @@ -39,9 +113,9 @@ struct AvatarView_Previews: PreviewProvider { AvatarView(imageUrl: avatarURL, size: 87) } VStack { - AvatarView(size: 24) - AvatarView(size: 45) - AvatarView(size: 87) + AvatarView(imageUrl: nil, size: 24) + AvatarView(imageUrl: nil, size: 45) + AvatarView(imageUrl: nil, size: 87) } } }