import Foundation // MARK: - App Group /// Единая точка правды об App Group: идентификатор и пути к общему хранилищу. /// Идентификатор обязан совпадать со значением в entitlements обоих таргетов. /// /// `nonisolated` — инфраструктура не привязана к UI: в app-таргете включена /// изоляция MainActor по умолчанию, а сюда ходят и акторы, и провайдер виджета. nonisolated enum AppGroup { static let id = "group.robonen.rapira-market-widget.crypto" /// UserDefaults, видимые и приложению, и виджету. static var defaults: UserDefaults? { UserDefaults(suiteName: id) } /// Каталог дисковых кэшей в контейнере группы. /// nil — если entitlements не настроены; кэширование тогда просто отключается. static var cachesURL: URL? { FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: id)? .appendingPathComponent("Library/Caches", isDirectory: true) } } // MARK: - Выбор пар /// Пары, выбранные пользователем для виджета. Порядок сохраняется: /// первая пара — «герой» маленького и большого виджетов. nonisolated struct PairSelectionStore: Sendable { static let shared = PairSelectionStore() /// Стартовый набор до первой настройки. static let defaultPairs = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] /// Максимум пар: столько вмещает большой виджет (1 крупная + 4 строки). static let maxSelection = 5 private let key = "selectedPairs" func load() -> [String] { AppGroup.defaults?.stringArray(forKey: key) ?? Self.defaultPairs } func save(_ pairs: [String]) { AppGroup.defaults?.set(pairs, forKey: key) } }