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

- 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:
2026-03-30 17:07:42 -04:00
parent c2e1622bd8
commit 6191458730
10 changed files with 1303 additions and 0 deletions

View File

@@ -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(

View File

@@ -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(

View 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)
)
"""
}
}

View 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
}
}

View 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)
}
}

View 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)
)
"""
}
}