From aa157031a102a35db88bf6fac34210919889204e Mon Sep 17 00:00:00 2001 From: cheykrym Date: Tue, 9 Dec 2025 23:17:34 +0300 Subject: [PATCH] add cache photo --- yobble/Services/KeychainService.swift | 117 ++++++++++++++++++++++++++ yobble/Views/Tab/ChatsTab.swift | 25 ++---- 2 files changed, 122 insertions(+), 20 deletions(-) diff --git a/yobble/Services/KeychainService.swift b/yobble/Services/KeychainService.swift index 14f7503..aba246a 100644 --- a/yobble/Services/KeychainService.swift +++ b/yobble/Services/KeychainService.swift @@ -1,5 +1,8 @@ import Foundation +import SwiftUICore import Security +import UIKit +import Combine //let username = "user1" @@ -111,3 +114,117 @@ class KeychainService { } } } + +class AvatarCacheService { + static let shared = AvatarCacheService() + private let fileManager = FileManager.default + private var baseCacheDirectory: URL? { + fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("avatar_cache") + } + + private init() {} + + private func cacheDirectory(for userId: String) -> URL? { + baseCacheDirectory?.appendingPathComponent(userId) + } + + private func filePath(forKey key: String, userId: String) -> URL? { + cacheDirectory(for: userId)?.appendingPathComponent(key) + } + + func getImage(forKey key: String, userId: String) -> UIImage? { + guard let url = filePath(forKey: key, userId: userId), + fileManager.fileExists(atPath: url.path), + let data = try? Data(contentsOf: url) else { + return nil + } + return UIImage(data: data) + } + + func saveImage(_ image: UIImage, forKey key: String, userId: String) { + guard let directory = cacheDirectory(for: userId), + let url = filePath(forKey: key, userId: userId), + let data = image.jpegData(compressionQuality: 0.8) else { + return + } + + if !fileManager.fileExists(atPath: directory.path) { + try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) + } + + try? data.write(to: url) + } + + func clearCache(forUserId userId: String) { + guard let directory = cacheDirectory(for: userId) else { return } + try? fileManager.removeItem(at: directory) + } + + func clearAllCache() { + guard let directory = baseCacheDirectory else { return } + try? fileManager.removeItem(at: directory) + } +} + +class ImageLoader: ObservableObject { + @Published var image: UIImage? + + private let url: URL + private let fileId: String + private let userId: String + private var cancellable: AnyCancellable? + private let cache = AvatarCacheService.shared + + init(url: URL, fileId: String, userId: String) { + self.url = url + self.fileId = fileId + self.userId = userId + } + + deinit { + cancellable?.cancel() + } + + func load() { + if let cachedImage = cache.getImage(forKey: fileId, userId: userId) { + self.image = cachedImage + return + } + + cancellable = URLSession.shared.dataTaskPublisher(for: url) + .map { UIImage(data: $0.data) } + .replaceError(with: nil) + .receive(on: DispatchQueue.main) + .sink { [weak self] loadedImage in + guard let self = self, let image = loadedImage else { return } + self.image = image + self.cache.saveImage(image, forKey: self.fileId, userId: self.userId) + } + } +} + +struct CachedAvatarView: View { + @StateObject private var loader: ImageLoader + private let placeholder: Placeholder + + init(url: URL, fileId: String, userId: String, @ViewBuilder placeholder: () -> Placeholder) { + self.placeholder = placeholder() + _loader = StateObject(wrappedValue: ImageLoader(url: url, fileId: fileId, userId: userId)) + } + + var body: some View { + content + .onAppear(perform: loader.load) + } + + private var content: some View { + Group { + if let image = loader.image { + Image(uiImage: image) + .resizable() + } else { + placeholder + } + } + } +} diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index 308800c..2a3f4c6 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -1035,28 +1035,13 @@ private struct ChatRowView: View { var body: some View { HStack(spacing: 12) { - if #available(iOS 15.0, *) { - if let url = avatarUrl { - AsyncImage(url: url) { phase in - switch phase { - case .empty: - placeholderAvatar - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - case .failure: - placeholderAvatar - @unknown default: - placeholderAvatar - } - } - .frame(width: avatarSize, height: avatarSize) - } else { + if let url = avatarUrl, let fileId = chat.chatData?.avatars?.current?.fileId, let loggedInUserId = currentUserId { + CachedAvatarView(url: url, fileId: fileId, userId: loggedInUserId) { placeholderAvatar } + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) } else { placeholderAvatar }