ios_app_v2/yobble/Services/KeychainService.swift
2025-12-10 00:11:53 +03:00

277 lines
9.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import SwiftUICore
import Security
import UIKit
import Combine
//let username = "user1"
// Сохраняем токены
//KeychainService.shared.save("access_token_value", forKey: "access_token", service: username)
//KeychainService.shared.save("refresh_token_value", forKey: "refresh_token", service: username)
// Получаем токены
//let accessToken = KeychainService.shared.get(forKey: "access_token", service: username)
//let refreshToken = KeychainService.shared.get(forKey: "refresh_token", service: username)
// получение всех пользователей
//let users = KeychainService.shared.getAllServices()
//print("Все пользователи: \(users)")
// Удаление всех пользователей
//KeychainService.shared.deleteAll()
// удаление по одному
//KeychainService.shared.delete(forKey: "access_token", service: username)
//KeychainService.shared.delete(forKey: "refresh_token", service: username)
class KeychainService {
static let shared = KeychainService()
private init() {}
func save(_ value: String, forKey key: String, service: String) {
guard let data = value.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service, // ключ группировки
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
print("Error saving to Keychain: \(status)")
}
}
func get(forKey key: String, service: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess,
let data = dataTypeRef as? Data,
let value = String(data: data, encoding: .utf8) {
return value
}
return nil
}
func getAllServices() -> [String] {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let items = result as? [[String: Any]] else {
return []
}
// Собираем все уникальные service (username)
var services = Set<String>()
for item in items {
if let service = item[kSecAttrService as String] as? String {
services.insert(service)
}
}
return Array(services)
}
func delete(forKey key: String, service: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
/// Удалить все записи Keychain, сохранённые этим приложением
func deleteAll() {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
print("Error deleting all Keychain items: \(status)")
}
}
}
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 to delete files inside first, ignoring errors
if let fileUrls = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) {
for fileUrl in fileUrls {
try? fileManager.removeItem(at: fileUrl)
}
}
// Then try to delete the directory itself
try? fileManager.removeItem(at: directory)
}
func clearAllCache() {
guard let directory = baseCacheDirectory else { return }
try? fileManager.removeItem(at: directory)
}
func getAllCachedUserIds() -> [String] {
guard let baseDir = baseCacheDirectory else { return [] }
do {
let directoryContents = try fileManager.contentsOfDirectory(at: baseDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
return directoryContents.map { $0.lastPathComponent }
} catch {
// This can happen if the directory doesn't exist yet, which is not an error.
return []
}
}
func sizeOfCache(forUserId userId: String) -> Int64 {
guard let directory = cacheDirectory(for: userId) else { return 0 }
return directorySize(url: directory)
}
func sizeOfAllCache() -> Int64 {
guard let directory = baseCacheDirectory else { return 0 }
return directorySize(url: directory)
}
private func directorySize(url: URL) -> Int64 {
let contents: [URL]
do {
contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey], options: .skipsHiddenFiles)
} catch {
return 0
}
var totalSize: Int64 = 0
for url in contents {
let fileSize = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0
totalSize += Int64(fileSize)
}
return totalSize
}
}
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<Placeholder: View>: 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
}
}
}
}