Using ObservableDefaults library for settings

This commit is contained in:
Selim Mustafaev 2025-04-06 23:40:45 +03:00
parent 6b003990b8
commit 0a6426b7f1
6 changed files with 63 additions and 180 deletions

View File

@ -46,7 +46,6 @@
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; };
7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; };
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; };
7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */; };
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */; };
7A4322932CB2CCAA00085CF6 /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */; };
@ -115,7 +114,9 @@
7A761C0B267E8FF90005F28F /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A761C0A267E8FF90005F28F /* Error.swift */; };
7A7AA2C42DA2A3CB00276D83 /* LocationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AA2C32DA2A3CB00276D83 /* LocationError.swift */; };
7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C62DA2A45600276D83 /* RealmSwift */; };
7A7AA2C82DA2A45600276D83 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C62DA2A45600276D83 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
7A7AA2CA2DA2C85100276D83 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C92DA2C85100276D83 /* RealmSwift */; };
7A7AA2CB2DA2C85100276D83 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C92DA2C85100276D83 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
7A7AA2CE2DA3120500276D83 /* ObservableDefaults in Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2CD2DA3120500276D83 /* ObservableDefaults */; };
7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7DADAB2D99738300F52F6C /* AudioRecordView.swift */; };
7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A809F382D66755B00CF1B3C /* Error+Canceled.swift */; };
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */; };
@ -254,7 +255,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
7A7AA2C82DA2A45600276D83 /* RealmSwift in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -265,6 +265,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
7A7AA2CB2DA2C85100276D83 /* RealmSwift in Embed Frameworks */,
7AF6D2052677C03B0086EA64 /* AutoCatCore.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
@ -325,7 +326,6 @@
7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = "<group>"; };
7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Settings.swift"; sourceTree = "<group>"; };
7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = "<group>"; };
7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersScreen.swift; sourceTree = "<group>"; };
7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
@ -516,6 +516,7 @@
files = (
7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */,
7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */,
7A7AA2CA2DA2C85100276D83 /* RealmSwift in Frameworks */,
7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */,
7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */,
7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */,
@ -547,6 +548,7 @@
files = (
7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */,
7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */,
7A7AA2CE2DA3120500276D83 /* ObservableDefaults in Frameworks */,
7AF8606E2CB9B86300954D2F /* Mockable in Frameworks */,
7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */,
);
@ -570,7 +572,6 @@
children = (
7A06E0B22C707E13005731AC /* SettingsServiceProtocol.swift */,
7A06E0B42C707E2B005731AC /* SettingsService.swift */,
7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */,
);
path = SettingsService;
sourceTree = "<group>";
@ -1238,6 +1239,7 @@
7AABDE1C2532F3EB0041AFC6 /* PKHUD */,
7AC355492969652F00889457 /* SwiftEntryKit */,
7ACBB91D2CB9B155005A5168 /* Mockable */,
7A7AA2C92DA2C85100276D83 /* RealmSwift */,
);
productName = AutoCat;
productReference = 7A1146FD23FDE7E500B424AF /* AutoCat.app */;
@ -1312,6 +1314,7 @@
7A6C4D9D2C56BCA600982597 /* SwiftLocation */,
7AF8606D2CB9B86300954D2F /* Mockable */,
7A7AA2C62DA2A45600276D83 /* RealmSwift */,
7A7AA2CD2DA3120500276D83 /* ObservableDefaults */,
);
productName = AutoCatCore;
productReference = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */;
@ -1360,6 +1363,7 @@
7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */,
7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */,
7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */,
7A7AA2CC2DA3120500276D83 /* XCRemoteSwiftPackageReference "ObservableDefaults" */,
);
productRefGroup = 7A1146FE23FDE7E500B424AF /* Products */;
projectDirPath = "";
@ -1597,7 +1601,6 @@
7AB4E4332D3C21C00006D052 /* FileManagerExt.swift in Sources */,
7A9519792D80B3E800E69883 /* AudioRecordService.swift in Sources */,
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */,
7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */,
7AB4E43B2D3D3F4F0006D052 /* VehicleServiceProtocol.swift in Sources */,
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */,
7A64A2222C19E99E00284124 /* DebugInfoDto.swift in Sources */,
@ -2115,6 +2118,14 @@
minimumVersion = 6.0.0;
};
};
7A7AA2CC2DA3120500276D83 /* XCRemoteSwiftPackageReference "ObservableDefaults" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/fatbobman/ObservableDefaults";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.6.2;
};
};
7A813DBF2508C4D900CC93B9 /* XCRemoteSwiftPackageReference "ExceptionCatcher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/ExceptionCatcher";
@ -2160,6 +2171,16 @@
package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
7A7AA2C92DA2C85100276D83 /* RealmSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
7A7AA2CD2DA3120500276D83 /* ObservableDefaults */ = {
isa = XCSwiftPackageProductDependency;
package = 7A7AA2CC2DA3120500276D83 /* XCRemoteSwiftPackageReference "ObservableDefaults" */;
productName = ObservableDefaults;
};
7A813DC02508C4D900CC93B9 /* ExceptionCatcher */ = {
isa = XCSwiftPackageProductDependency;
package = 7A813DBF2508C4D900CC93B9 /* XCRemoteSwiftPackageReference "ExceptionCatcher" */;

View File

@ -1,5 +1,5 @@
{
"originHash" : "6fccb9fdc0d29647d4f0b927aef60f375302d72b5b724992eab52ac0d8ec71c3",
"originHash" : "3cc3aec63412867029bf57f8fff11744df284387a47994473d82e7aab44a4293",
"pins" : [
{
"identity" : "exceptioncatcher",
@ -19,6 +19,15 @@
"version" : "0.3.1"
}
},
{
"identity" : "observabledefaults",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fatbobman/ObservableDefaults",
"state" : {
"revision" : "4740e2c39043b7daff946aa3bbec22e6a68850d3",
"version" : "0.6.2"
}
},
{
"identity" : "pkhud",
"kind" : "remoteSourceControl",

View File

@ -38,7 +38,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let container = ServiceContainer.shared
let settingsService = SettingsService(defaults: .standard)
let settingsService = SettingsService()
container.register(SettingsServiceProtocol.self, instance: settingsService)

View File

@ -6,21 +6,20 @@
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import Foundation
import ObservableDefaults
@Observable
@ObservableDefaults
public final class SettingsService: SettingsServiceProtocol {
let defaults: UserDefaults
var observations: [NSKeyValueObservation] = []
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
@Ignore let jsonEncoder = JSONEncoder()
@Ignore let jsonDecoder = JSONDecoder()
@ObservableOnly
public var user: User {
get {
if let data = userData {
let result = try? jsonDecoder.decode(User.self, from: data)
if userData.count > 0 {
let result = try? jsonDecoder.decode(User.self, from: userData)
return result ?? User()
} else {
return User()
@ -34,92 +33,20 @@ public final class SettingsService: SettingsServiceProtocol {
}
}
public var userData: Data? = nil {
didSet {
defaults.user = userData
}
}
@DefaultsKey(userDefaultsKey: "user")
public var userData: Data = .init()
public var recognizeAlternativeOrder: Bool = false {
didSet {
defaults.recognizeAlternativeOrder = recognizeAlternativeOrder
}
}
public var recognizeShortenedNumbers: Bool = false {
didSet {
defaults.recognizeShortenedNumbers = recognizeShortenedNumbers
}
}
public var defaultRegion: String = "161" {
didSet {
defaults.defaultRegion = defaultRegion
}
}
public var recordBeep: Bool = false {
didSet {
defaults.recordBeep = recordBeep
}
}
public var showDebugInfo: Bool = false {
didSet {
defaults.showDebugInfo = showDebugInfo
}
}
public var recognizeAlternativeOrder: Bool = false
public var recognizeShortenedNumbers: Bool = false
public var defaultRegion: String = "161"
public var recordBeep: Bool = false
public var showDebugInfo: Bool = false
@ObservableOnly
public var backend: Constants.Backend {
get { Constants.Backend(rawValue: backendString) ?? .de }
set { backendString = newValue.rawValue }
}
public var backendString: String = Constants.Backend.de.rawValue {
didSet {
defaults.backendString = backendString
}
}
public init(defaults: UserDefaults) {
self.defaults = defaults
observe(key: \.recognizeAlternativeOrder, for: \.recognizeAlternativeOrder)
observe(key: \.recognizeShortenedNumbers, for: \.recognizeShortenedNumbers)
observe(key: \.defaultRegion, for: \.defaultRegion)
observe(key: \.recordBeep, for: \.recordBeep)
observe(key: \.showDebugInfo, for: \.showDebugInfo)
observe(key: \.backendString, for: \.backendString)
observe(key: \.user, for: \.userData)
register(defaultValues: [
.recognizeAlternativeOrder: false,
.recognizeShortenedNumbers: false,
.defaultRegion: "761",
.recordBeep: false,
.showDebugInfo: false,
.backendString: Constants.Backend.de.rawValue
])
}
deinit {
observations.forEach { $0.invalidate() }
}
func observe<T>(key userDefaultsKey: KeyPath<UserDefaults,T>, for settingsKey: ReferenceWritableKeyPath<SettingsService,T>) where T: Equatable {
let observation = defaults.observe(userDefaultsKey, options: [.initial, .new]) { [weak self] _, change in
guard let self else { return }
if let new = change.newValue, self[keyPath: settingsKey] != new {
self[keyPath: settingsKey] = new
}
}
observations.append(observation)
}
func register(defaultValues: [SettingsKey: Any]) {
defaults.register(defaults: defaultValues.reduce(into: [:], { (partialResult: inout [String: Any], tuple) in
partialResult[tuple.key.rawValue] = tuple.value
}))
}
public var backendString: String = Constants.Backend.de.rawValue
}

View File

@ -1,74 +0,0 @@
//
// UserDefaults+Settings.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 25.08.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Foundation
enum SettingsKey: String {
case user
case recognizeAlternativeOrder
case recognizeShortenedNumbers
case defaultRegion
case recordBeep
case showDebugInfo
case backendString
}
extension UserDefaults {
func bool(for key: SettingsKey) -> Bool {
bool(forKey: key.rawValue)
}
func string(for key: SettingsKey, defaultValue: String = "") -> String {
string(forKey: key.rawValue) ?? defaultValue
}
func data(for key: SettingsKey) -> Data? {
data(forKey: key.rawValue)
}
func set<T>(value: T, for key: SettingsKey) {
setValue(value, forKey: key.rawValue)
}
@objc dynamic var recognizeAlternativeOrder: Bool {
get { bool(for: .recognizeAlternativeOrder) }
set { set(value: newValue, for: .recognizeAlternativeOrder) }
}
@objc dynamic var recognizeShortenedNumbers: Bool {
get { bool(for: .recognizeShortenedNumbers) }
set { set(value: newValue, for: .recognizeShortenedNumbers) }
}
@objc dynamic var defaultRegion: String {
get { string(for: .defaultRegion) }
set { set(value: newValue, for: .defaultRegion) }
}
@objc dynamic var recordBeep: Bool {
get { bool(for: .recordBeep) }
set { set(value: newValue, for: .recordBeep) }
}
@objc dynamic var showDebugInfo: Bool {
get { bool(for: .showDebugInfo) }
set { set(value: newValue, for: .showDebugInfo) }
}
@objc dynamic var backendString: String {
get { string(for: .backendString, defaultValue: Constants.Backend.de.rawValue) }
set { set(value: newValue, for: .backendString) }
}
@objc dynamic var user: Data? {
get { data(for: .user) }
set { set(value: newValue, for: .user) }
}
}

View File

@ -34,7 +34,7 @@ struct SettingsServiceTests {
self.testUser = User(email: testEmail, token: testToken)
self.defaults = UserDefaults(suiteName: dbName)!
self.defaults.removePersistentDomain(forName: dbName)
self.settingsService = SettingsService(defaults: defaults)
self.settingsService = SettingsService(userDefaults: self.defaults)
}
@Test("Creating default user")
@ -50,7 +50,7 @@ struct SettingsServiceTests {
let data = try #require(testUser.data)
defaults.setPersistentDomain(["user": data], forName: dbName)
let service = SettingsService(defaults: defaults)
let service = SettingsService(userDefaults: defaults)
#expect(service.user.email == testEmail)
#expect(service.user.token == testToken)
@ -73,7 +73,7 @@ struct SettingsServiceTests {
settingsService.backend = value
#expect(defaults.string(forKey: SettingsKey.backendString.rawValue) == value.rawValue)
#expect(defaults.string(forKey: "backendString") == value.rawValue)
}
@Test("Save settings", .serialized, arguments: [true, false])
@ -85,10 +85,10 @@ struct SettingsServiceTests {
settingsService.recordBeep = value
settingsService.showDebugInfo = value
#expect(defaults.bool(forKey: SettingsKey.recognizeAlternativeOrder.rawValue) == value)
#expect(defaults.bool(forKey: SettingsKey.recognizeShortenedNumbers.rawValue) == value)
#expect(defaults.string(forKey: SettingsKey.defaultRegion.rawValue) == testRegion)
#expect(defaults.bool(forKey: SettingsKey.recordBeep.rawValue) == value)
#expect(defaults.bool(forKey: SettingsKey.showDebugInfo.rawValue) == value)
#expect(defaults.bool(forKey: "recognizeAlternativeOrder") == value)
#expect(defaults.bool(forKey: "recognizeShortenedNumbers") == value)
#expect(defaults.string(forKey: "defaultRegion") == testRegion)
#expect(defaults.bool(forKey: "recordBeep") == value)
#expect(defaults.bool(forKey: "showDebugInfo") == value)
}
}