Implement iOS settings/preferences store
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
- Created Settings directory with core store files - Implemented SettingsStore with UserDefaults/App Group support - Created AppSettings for app-wide configuration - Created UserPreferences for unified preferences access - Added enableAll/disableAll methods to ReadingPreferences - Added enableAll/disableAll methods to NotificationPreferences - Created SettingsMigration framework for version migrations This implements the core settings infrastructure for iOS. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -39,6 +39,24 @@ struct NotificationPreferences: Codable, Equatable {
|
||||
newArticles || episodeReleases || customAlerts || badgeCount || sound || vibration
|
||||
}
|
||||
|
||||
mutating func enableAll() {
|
||||
newArticles = true
|
||||
episodeReleases = true
|
||||
customAlerts = true
|
||||
badgeCount = true
|
||||
sound = true
|
||||
vibration = true
|
||||
}
|
||||
|
||||
mutating func disableAll() {
|
||||
newArticles = false
|
||||
episodeReleases = false
|
||||
customAlerts = false
|
||||
badgeCount = false
|
||||
sound = false
|
||||
vibration = false
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
NotificationPreferences(
|
||||
|
||||
@@ -61,6 +61,20 @@ struct ReadingPreferences: Codable, Equatable {
|
||||
self.showDate = showDate
|
||||
}
|
||||
|
||||
mutating func enableAll() {
|
||||
showTableOfContents = true
|
||||
showReadingTime = true
|
||||
showAuthor = true
|
||||
showDate = true
|
||||
}
|
||||
|
||||
mutating func disableAll() {
|
||||
showTableOfContents = false
|
||||
showReadingTime = false
|
||||
showAuthor = false
|
||||
showDate = false
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
ReadingPreferences(
|
||||
|
||||
65
iOS/RSSuper/Settings/AppSettings.swift
Normal file
65
iOS/RSSuper/Settings/AppSettings.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// AppSettings.swift
|
||||
// RSSuper
|
||||
//
|
||||
// App-wide settings configuration
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AppSettings: Codable, Equatable {
|
||||
var appVersion: String
|
||||
var buildNumber: String
|
||||
var lastMigrationVersion: String?
|
||||
var firstLaunchAt: Date?
|
||||
var lastLaunchAt: Date?
|
||||
var launchCount: Int
|
||||
|
||||
init(
|
||||
appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
|
||||
buildNumber: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1",
|
||||
lastMigrationVersion: String? = nil,
|
||||
firstLaunchAt: Date? = nil,
|
||||
lastLaunchAt: Date? = nil,
|
||||
launchCount: Int = 0
|
||||
) {
|
||||
self.appVersion = appVersion
|
||||
self.buildNumber = buildNumber
|
||||
self.lastMigrationVersion = lastMigrationVersion
|
||||
self.firstLaunchAt = firstLaunchAt
|
||||
self.lastLaunchAt = lastLaunchAt
|
||||
self.launchCount = launchCount
|
||||
}
|
||||
|
||||
var isFirstLaunch: Bool {
|
||||
firstLaunchAt == nil
|
||||
}
|
||||
|
||||
func incrementLaunchCount() -> AppSettings {
|
||||
var copy = self
|
||||
copy.launchCount += 1
|
||||
copy.lastLaunchAt = Date()
|
||||
if firstLaunchAt == nil {
|
||||
copy.firstLaunchAt = Date()
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func withMigrationComplete(version: String) -> AppSettings {
|
||||
var copy = self
|
||||
copy.lastMigrationVersion = version
|
||||
return copy
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
AppSettings(
|
||||
appVersion: \(appVersion),
|
||||
buildNumber: \(buildNumber),
|
||||
lastMigrationVersion: \(lastMigrationVersion ?? "none"),
|
||||
isFirstLaunch: \(isFirstLaunch),
|
||||
launchCount: \(launchCount)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
77
iOS/RSSuper/Settings/SettingsMigration.swift
Normal file
77
iOS/RSSuper/Settings/SettingsMigration.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// SettingsMigration.swift
|
||||
// RSSuper
|
||||
//
|
||||
// Settings migration support between versions
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SettingsMigratable {
|
||||
associatedtype SettingsType: Codable
|
||||
|
||||
var fromVersion: String { get }
|
||||
var toVersion: String { get }
|
||||
|
||||
func migrate(_ settings: SettingsType) -> SettingsType
|
||||
}
|
||||
|
||||
struct SettingsMigrationManager {
|
||||
private(set) var migrations: [String: SettingsMigratable]
|
||||
|
||||
init() {
|
||||
migrations = [:]
|
||||
}
|
||||
|
||||
func registerMigration(_ migration: some SettingsMigratable) {
|
||||
migrations[migration.fromVersion] = migration
|
||||
}
|
||||
|
||||
func migrateSettings<T: Codable>(_ settings: T, fromVersion: String, toVersion: String) -> T? {
|
||||
var currentVersion = fromVersion
|
||||
var currentSettings = settings
|
||||
|
||||
while currentVersion != toVersion,
|
||||
let migration = migrations[currentVersion],
|
||||
let migrated = try? JSONDecoder().decode(T.self, from: JSONEncoder().encode(migration.migrate(currentSettings))) {
|
||||
currentSettings = migrated
|
||||
currentVersion = migration.toVersion
|
||||
}
|
||||
|
||||
return currentSettings == settings ? nil : currentSettings
|
||||
}
|
||||
|
||||
func getAvailableVersions() -> [String] {
|
||||
migrations.keys.sorted()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Migration Implementations
|
||||
|
||||
struct V1ToV2AppSettingsMigration: SettingsMigratable {
|
||||
let fromVersion = "1.0.0"
|
||||
let toVersion = "1.1.0"
|
||||
|
||||
func migrate(_ settings: AppSettings) -> AppSettings {
|
||||
var copy = settings
|
||||
// Add new settings for v1.1.0
|
||||
if copy.lastMigrationVersion == nil {
|
||||
copy.lastMigrationVersion = fromVersion
|
||||
}
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
struct V2ToV3AppSettingsMigration: SettingsMigratable {
|
||||
let fromVersion = "1.1.0"
|
||||
let toVersion = "1.2.0"
|
||||
|
||||
func migrate(_ settings: AppSettings) -> AppSettings {
|
||||
var copy = settings
|
||||
// Add new settings for v1.2.0
|
||||
if copy.launchCount == 0 {
|
||||
copy.launchCount = 1
|
||||
}
|
||||
return copy
|
||||
}
|
||||
}
|
||||
160
iOS/RSSuper/Settings/SettingsStore.swift
Normal file
160
iOS/RSSuper/Settings/SettingsStore.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// SettingsStore.swift
|
||||
// RSSuper
|
||||
//
|
||||
// Main settings store with UserDefaults/App Group
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class SettingsStore {
|
||||
static let shared = SettingsStore()
|
||||
|
||||
private let userDefaults: UserDefaults
|
||||
private let appGroupDefaults: UserDefaults?
|
||||
|
||||
private init() {
|
||||
// Main app defaults
|
||||
userDefaults = UserDefaults.standard
|
||||
|
||||
// App Group defaults for widget/shared content
|
||||
if let groupID = Bundle.main.object(forInfoDictionaryKey: "AppGroupID") as? String {
|
||||
appGroupDefaults = UserDefaults(suiteName: groupID)
|
||||
} else {
|
||||
appGroupDefaults = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Settings
|
||||
|
||||
private enum AppSettingsKey: String {
|
||||
case appVersion = "appVersion"
|
||||
case buildNumber = "buildNumber"
|
||||
case lastMigrationVersion = "lastMigrationVersion"
|
||||
case firstLaunchAt = "firstLaunchAt"
|
||||
case lastLaunchAt = "lastLaunchAt"
|
||||
case launchCount = "launchCount"
|
||||
}
|
||||
|
||||
func getAppSettings() -> AppSettings {
|
||||
guard let data = userDefaults.data(forKey: AppSettingsKey.appVersion.rawValue),
|
||||
let settings = try? JSONDecoder().decode(AppSettings.self, from: data) else {
|
||||
return AppSettings()
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
func save(_ settings: AppSettings) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(settings)
|
||||
userDefaults.set(data, forKey: AppSettingsKey.appVersion.rawValue)
|
||||
} catch {
|
||||
print("Failed to save app settings: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Preferences
|
||||
|
||||
private enum UserPreferencesKey: String {
|
||||
case reading = "readingPreferences"
|
||||
case notification = "notificationPreferences"
|
||||
}
|
||||
|
||||
func getReadingPreferences() -> ReadingPreferences {
|
||||
guard let data = userDefaults.data(forKey: UserPreferencesKey.reading),
|
||||
let prefs = try? JSONDecoder().decode(ReadingPreferences.self, from: data) else {
|
||||
return ReadingPreferences()
|
||||
}
|
||||
return prefs
|
||||
}
|
||||
|
||||
func save(_ reading: ReadingPreferences) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(reading)
|
||||
userDefaults.set(data, forKey: UserPreferencesKey.reading)
|
||||
} catch {
|
||||
print("Failed to save reading preferences: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getNotificationPreferences() -> NotificationPreferences {
|
||||
guard let data = userDefaults.data(forKey: UserPreferencesKey.notification),
|
||||
let prefs = try? JSONDecoder().decode(NotificationPreferences.self, from: data) else {
|
||||
return NotificationPreferences()
|
||||
}
|
||||
return prefs
|
||||
}
|
||||
|
||||
func save(_ notification: NotificationPreferences) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(notification)
|
||||
userDefaults.set(data, forKey: UserPreferencesKey.notification)
|
||||
} catch {
|
||||
print("Failed to save notification preferences: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getUserPreferences() -> UserPreferences {
|
||||
UserPreferences(
|
||||
reading: getReadingPreferences(),
|
||||
notification: getNotificationPreferences()
|
||||
)
|
||||
}
|
||||
|
||||
func save(_ preferences: UserPreferences) {
|
||||
save(preferences.reading)
|
||||
save(preferences.notification)
|
||||
}
|
||||
|
||||
// MARK: - App Group Sync
|
||||
|
||||
func syncToAppGroup() {
|
||||
guard let groupDefaults = appGroupDefaults else { return }
|
||||
|
||||
if let appSettingsData = userDefaults.data(forKey: AppSettingsKey.appVersion.rawValue) {
|
||||
groupDefaults.set(appSettingsData, forKey: AppSettingsKey.appVersion.rawValue)
|
||||
}
|
||||
|
||||
if let readingData = userDefaults.data(forKey: UserPreferencesKey.reading) {
|
||||
groupDefaults.set(readingData, forKey: UserPreferencesKey.reading)
|
||||
}
|
||||
|
||||
if let notificationData = userDefaults.data(forKey: UserPreferencesKey.notification) {
|
||||
groupDefaults.set(notificationData, forKey: UserPreferencesKey.notification)
|
||||
}
|
||||
}
|
||||
|
||||
func syncFromAppGroup() {
|
||||
guard let groupDefaults = appGroupDefaults else { return }
|
||||
|
||||
if let appSettingsData = groupDefaults.data(forKey: AppSettingsKey.appVersion.rawValue) {
|
||||
userDefaults.set(appSettingsData, forKey: AppSettingsKey.appVersion.rawValue)
|
||||
}
|
||||
|
||||
if let readingData = groupDefaults.data(forKey: UserPreferencesKey.reading) {
|
||||
userDefaults.set(readingData, forKey: UserPreferencesKey.reading)
|
||||
}
|
||||
|
||||
if let notificationData = groupDefaults.data(forKey: UserPreferencesKey.notification) {
|
||||
userDefaults.set(notificationData, forKey: UserPreferencesKey.notification)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
private let settingsChangedNotification = NotificationCenter.default
|
||||
|
||||
func startObservingSettingsChanges() {
|
||||
settingsChangedNotification.addObserver(
|
||||
forName: UserDefaults.didChangeNotification,
|
||||
object: userDefaults,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.syncToAppGroup()
|
||||
}
|
||||
}
|
||||
|
||||
func stopObservingSettingsChanges() {
|
||||
settingsChangedNotification.removeObserver(self)
|
||||
}
|
||||
}
|
||||
32
iOS/RSSuper/Settings/UserPreferences.swift
Normal file
32
iOS/RSSuper/Settings/UserPreferences.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// UserPreferences.swift
|
||||
// RSSuper
|
||||
//
|
||||
// User preferences keys and defaults
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UserPreferences: Codable, Equatable {
|
||||
var reading: ReadingPreferences
|
||||
var notification: NotificationPreferences
|
||||
|
||||
init(
|
||||
reading: ReadingPreferences = ReadingPreferences(),
|
||||
notification: NotificationPreferences = NotificationPreferences()
|
||||
) {
|
||||
self.reading = reading
|
||||
self.notification = notification
|
||||
}
|
||||
|
||||
static let `default': UserPreferences = UserPreferences()
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
UserPreferences(
|
||||
reading: \(reading.debugDescription),
|
||||
notification: \(notification.debugDescription)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user