277 lines
9.1 KiB
Swift
277 lines
9.1 KiB
Swift
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
|
||
}
|
||
}
|
||
}
|
||
}
|