Removing ObservableDefaults, back to old dumb UserDefaults wrapper

This commit is contained in:
Selim Mustafaev 2025-04-09 18:33:29 +03:00
parent 0a6426b7f1
commit dc6281ac46
15 changed files with 195 additions and 76 deletions

View File

@ -116,7 +116,6 @@
7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A7AA2C62DA2A45600276D83 /* RealmSwift */; };
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 */; };
@ -548,7 +547,6 @@
files = (
7A7AA2C72DA2A45600276D83 /* RealmSwift in Frameworks */,
7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */,
7A7AA2CE2DA3120500276D83 /* ObservableDefaults in Frameworks */,
7AF8606E2CB9B86300954D2F /* Mockable in Frameworks */,
7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */,
);
@ -1314,7 +1312,6 @@
7A6C4D9D2C56BCA600982597 /* SwiftLocation */,
7AF8606D2CB9B86300954D2F /* Mockable */,
7A7AA2C62DA2A45600276D83 /* RealmSwift */,
7A7AA2CD2DA3120500276D83 /* ObservableDefaults */,
);
productName = AutoCatCore;
productReference = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */;
@ -1363,7 +1360,6 @@
7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */,
7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */,
7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */,
7A7AA2CC2DA3120500276D83 /* XCRemoteSwiftPackageReference "ObservableDefaults" */,
);
productRefGroup = 7A1146FE23FDE7E500B424AF /* Products */;
projectDirPath = "";
@ -2118,14 +2114,6 @@
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";
@ -2176,11 +2164,6 @@
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" : "3cc3aec63412867029bf57f8fff11744df284387a47994473d82e7aab44a4293",
"originHash" : "6fccb9fdc0d29647d4f0b927aef60f375302d72b5b724992eab52ac0d8ec71c3",
"pins" : [
{
"identity" : "exceptioncatcher",
@ -19,15 +19,6 @@
"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()
let settingsService = await SettingsService()
container.register(SettingsServiceProtocol.self, instance: settingsService)

View File

@ -60,6 +60,8 @@ class EventsViewModel: ACHudContainer {
func updateEvents() {
let email = settingsService.user.email
events = vehicle.events.sorted { $0.date > $1.date }.map { event in
let date = Date(timeIntervalSince1970: event.date)
@ -71,7 +73,7 @@ class EventsViewModel: ACHudContainer {
date: dateString,
coordinate: coordinate,
address: event.address ?? "Lat: \(event.latitude), Lon: \(event.longitude)",
isMe: event.addedBy == settingsService.user.email
isMe: event.addedBy == email
)
}
}

View File

@ -39,7 +39,7 @@ struct SettingsScreen: View {
}
Section {
Picker("Server", selection: $viewModel.settingService.backend) {
Picker("Server", selection: $viewModel.backend) {
ForEach(Constants.Backend.allCases, id: \.self) { backend in
Text(backend.name)
}
@ -49,26 +49,26 @@ struct SettingsScreen: View {
Section("Plate number recognition") {
ToggleRowView(title: "Alternative order",
description: "Recognize plate numbers in alternative form. For example 'ЕВА 123 777' instead of 'Е123ВА 777'",
toggle: $viewModel.settingService.recognizeAlternativeOrder)
toggle: $viewModel.recognizeAlternativeOrder)
ToggleRowView(title: "Shortened numbers",
description: "If enabled, app will try to recognize shortened plate numbers (without region) and add default region",
toggle: $viewModel.settingService.recognizeShortenedNumbers)
if viewModel.settingService.recognizeShortenedNumbers {
toggle: $viewModel.recognizeShortenedNumbers)
if viewModel.recognizeShortenedNumbers {
LabeledContent("Default region") {
TextField("", text: $viewModel.settingService.defaultRegion)
TextField("", text: $viewModel.defaultRegion)
.frame(width: 50)
.multilineTextAlignment(.trailing)
}
}
ToggleRowView(title: "Beep before record",
description: "When enabled, you will hear short sound before starting audio recording. This will only work when audio record is started via Siri",
toggle: $viewModel.settingService.recordBeep)
toggle: $viewModel.recordBeep)
}
Section("Debug") {
ToggleRowView(title: "Show debug info",
description: nil,
toggle: $viewModel.settingService.showDebugInfo)
toggle: $viewModel.showDebugInfo)
}
}
.navigationTitle("Settings")

View File

@ -42,6 +42,36 @@ class SettingsViewModel {
return jwt.payload.email
}
var recognizeAlternativeOrder: Bool {
get { settingService.recognizeAlternativeOrder }
set { settingService.recognizeAlternativeOrder = newValue }
}
var recognizeShortenedNumbers: Bool {
get { settingService.recognizeShortenedNumbers }
set { settingService.recognizeShortenedNumbers = newValue }
}
var backend: Constants.Backend {
get { settingService.backend }
set { settingService.backend = newValue }
}
var recordBeep: Bool {
get { settingService.recordBeep }
set { settingService.recordBeep = newValue }
}
var showDebugInfo: Bool {
get { settingService.showDebugInfo }
set { settingService.showDebugInfo = newValue }
}
var defaultRegion: String {
get { settingService.defaultRegion }
set { settingService.defaultRegion = newValue }
}
init(settingsService: SettingsServiceProtocol) {
self.settingService = settingsService

View File

@ -1,5 +1,6 @@
import Foundation
@MainActor
public struct User: Codable, Sendable {
public let email: String
public var token: String

View File

@ -20,7 +20,7 @@ public actor ApiService: ApiServiceProtocol {
private func createRequest<B,P>(api: String, method: String, body: B? = nil, params: [String:P]? = nil) async -> URLRequest? where B: Encodable, P: LosslessStringConvertible {
let baseUrl = settingsService.backend.baseUrl
let baseUrl = await settingsService.backend.baseUrl
guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil }
@ -32,7 +32,7 @@ public actor ApiService: ApiServiceProtocol {
request.httpMethod = method
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Bearer " + settingsService.user.token, forHTTPHeaderField: "Authorization")
request.addValue("Bearer " + (await settingsService.user.token), forHTTPHeaderField: "Authorization")
if let body = body, method.uppercased() != "GET" {
let encoder = JSONEncoder()
@ -109,8 +109,8 @@ public actor ApiService: ApiServiceProtocol {
// MARK: - Firebase API
public func refreshFbToken() async throws {
guard let token = settingsService.user.firebaseIdToken,
let refreshToken = settingsService.user.firebaseRefreshToken,
guard let token = await settingsService.user.firebaseIdToken,
let refreshToken = await settingsService.user.firebaseRefreshToken,
let jwt = JWT<FirebasePayload>(string: token), jwt.expired else {
return
}
@ -138,11 +138,11 @@ public actor ApiService: ApiServiceProtocol {
let model = try JSONDecoder().decode(FbRefreshTokenModel.self, from: data)
if let idToken = model.id_token {
await settingsService.user.firebaseIdToken = idToken
await settingsService.setFirebaseIdToken(idToken)
}
if let refreshToken = model.refresh_token {
await settingsService.user.firebaseRefreshToken = refreshToken
await settingsService.setFirebaseRefreshToken(refreshToken)
}
}
@ -181,11 +181,11 @@ public actor ApiService: ApiServiceProtocol {
let model = try JSONDecoder().decode(FbVerifyTokenModel.self, from: data)
if let idToken = model.idToken {
await settingsService.user.firebaseIdToken = idToken
await settingsService.setFirebaseIdToken(idToken)
}
if let refreshToken = model.refreshToken {
await settingsService.user.firebaseRefreshToken = refreshToken
await settingsService.setFirebaseRefreshToken(refreshToken)
}
} catch {
@ -237,7 +237,7 @@ public actor ApiService: ApiServiceProtocol {
"forceUpdate": AnyEncodable(force)
]
if let token = settingsService.user.firebaseIdToken {
if let token = await settingsService.user.firebaseIdToken {
body["googleIdToken"] = AnyEncodable(token)
}
@ -324,7 +324,7 @@ public actor ApiService: ApiServiceProtocol {
var body = ["number": number]
if let token = settingsService.user.firebaseIdToken {
if let token = await settingsService.user.firebaseIdToken {
body["token"] = token
}

View File

@ -52,7 +52,11 @@ public final class LocationService {
throw LocationError.generic
}
return VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude, addedBy: settingsService.user.email)
return VehicleEventDto(
lat: coordinate.latitude,
lon: coordinate.longitude,
addedBy: await settingsService.user.email
)
}
func setLastEvent(_ event: VehicleEventDto) {

View File

@ -7,18 +7,37 @@
//
import Foundation
import ObservableDefaults
@ObservableDefaults
enum SettingsKey: String {
case user
case recognizeAlternativeOrder
case recognizeShortenedNumbers
case defaultRegion
case recordBeep
case showDebugInfo
case backendString
}
@MainActor
public final class SettingsService: SettingsServiceProtocol {
@Ignore let jsonEncoder = JSONEncoder()
@Ignore let jsonDecoder = JSONDecoder()
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
let suiteName: String?
var defaults: UserDefaults {
if let suiteName {
return UserDefaults(suiteName: suiteName) ?? .standard
} else {
return .standard
}
}
@ObservableOnly
public var user: User {
get {
if userData.count > 0 {
if let userData, userData.count > 0 {
let result = try? jsonDecoder.decode(User.self, from: userData)
return result ?? User()
} else {
@ -33,20 +52,102 @@ public final class SettingsService: SettingsServiceProtocol {
}
}
@DefaultsKey(userDefaultsKey: "user")
public var userData: Data = .init()
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
public var userData: Data? {
didSet {
set(value: userData, for: .user)
}
}
public var recognizeAlternativeOrder: Bool = false {
didSet {
set(value: recognizeAlternativeOrder, for: .recognizeAlternativeOrder)
}
}
public var recognizeShortenedNumbers: Bool = false {
didSet {
set(value: recognizeShortenedNumbers, for: .recognizeShortenedNumbers)
}
}
public var defaultRegion: String = "161" {
didSet {
set(value: defaultRegion, for: .defaultRegion)
}
}
public var recordBeep: Bool = false {
didSet {
set(value: recordBeep, for: .recordBeep)
}
}
public var showDebugInfo: Bool = false {
didSet {
set(value: showDebugInfo, for: .showDebugInfo)
}
}
@ObservableOnly
public var backend: Constants.Backend {
get { Constants.Backend(rawValue: backendString) ?? .de }
set { backendString = newValue.rawValue }
}
public var backendString: String = Constants.Backend.de.rawValue
public var backendString: String = Constants.Backend.de.rawValue {
didSet {
set(value: backendString, for: .backendString)
}
}
public init(suiteName: String? = nil) async {
self.suiteName = suiteName
register(defaultValues: [
.recognizeAlternativeOrder: false,
.recognizeShortenedNumbers: false,
.defaultRegion: "761",
.recordBeep: false,
.showDebugInfo: false,
.backendString: Constants.Backend.de.rawValue
])
userData = data(for: .user)
recognizeAlternativeOrder = bool(for: .recognizeAlternativeOrder)
recognizeShortenedNumbers = bool(for: .recognizeShortenedNumbers)
defaultRegion = string(for: .defaultRegion)
recordBeep = bool(for: .recordBeep)
showDebugInfo = bool(for: .showDebugInfo)
backendString = string(for: .backendString)
}
func register(defaultValues: [SettingsKey: Any]) {
defaults.register(defaults: defaultValues.reduce(into: [:], { (partialResult: inout [String: Any], tuple) in
partialResult[tuple.key.rawValue] = tuple.value
}))
}
func set<T>(value: T, for key: SettingsKey) {
defaults.setValue(value, forKey: key.rawValue)
}
func bool(for key: SettingsKey) -> Bool {
defaults.bool(forKey: key.rawValue)
}
func string(for key: SettingsKey, defaultValue: String = "") -> String {
defaults.string(forKey: key.rawValue) ?? defaultValue
}
func data(for key: SettingsKey) -> Data? {
defaults.data(forKey: key.rawValue)
}
public func setFirebaseIdToken(_ idToken: String?) {
user.firebaseIdToken = idToken
}
public func setFirebaseRefreshToken(_ refreshToken: String?) {
user.firebaseRefreshToken = refreshToken
}
}

View File

@ -8,15 +8,18 @@
import Mockable
@MainActor
@Mockable
public protocol SettingsServiceProtocol: Sendable {
var user: User { get set }
var recognizeAlternativeOrder: Bool { get set }
var recognizeShortenedNumbers: Bool { get set }
var defaultRegion: String { get set }
var recordBeep: Bool { get set }
var showDebugInfo: Bool { get set }
var backend: Constants.Backend { get set }
func setFirebaseIdToken(_ idToken: String?)
func setFirebaseRefreshToken(_ refreshToken: String?)
}

View File

@ -16,7 +16,7 @@ extension StorageService {
throw StorageError.vehicleNotFound
}
let note = VehicleNote(text: text, user: settingsService.user.email)
let note = VehicleNote(text: text, user: await settingsService.user.email)
try await realm.asyncWrite {
vehicle.notes.append(note)

View File

@ -33,7 +33,7 @@ public actor VehicleRecordService {
self.settingsService = settingsService
}
func getPlateNumber(from recognizedText: String?) -> String? {
func getPlateNumber(from recognizedText: String?) async -> String? {
guard let recognizedText else {
return nil
}
@ -45,17 +45,21 @@ public actor VehicleRecordService {
.replacingOccurrences(of: "НОЛЬ", with: "0")
.replacingOccurrences(of: "Э", with: "")
let recognizeAlternativeOrder = await settingsService.recognizeAlternativeOrder
let recognizeShortenedNumbers = await settingsService.recognizeShortenedNumbers
let defaultRegion = await settingsService.defaultRegion
var result = ""
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
result = String(trimmed[range])
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), settingsService.recognizeAlternativeOrder {
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), recognizeAlternativeOrder {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), settingsService.recognizeShortenedNumbers {
result = String(trimmed[range]) + settingsService.defaultRegion
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), settingsService.recognizeAlternativeOrder && settingsService.recognizeShortenedNumbers {
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), recognizeShortenedNumbers {
result = String(trimmed[range]) + defaultRegion
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), recognizeAlternativeOrder && recognizeShortenedNumbers {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + settingsService.defaultRegion
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + defaultRegion
}
if !result.isEmpty && valid(number: result) {
@ -123,7 +127,7 @@ extension VehicleRecordService: VehicleRecordServiceProtocol {
let record = AudioRecordDto(
path: url.lastPathComponent,
number: getPlateNumber(from: text),
number: await getPlateNumber(from: text),
raw: text ?? "",
duration: duration ?? 0,
event: location

View File

@ -30,11 +30,11 @@ struct SettingsServiceTests {
let defaults: UserDefaults
var settingsService: SettingsService
init() {
init() async {
self.testUser = User(email: testEmail, token: testToken)
self.defaults = UserDefaults(suiteName: dbName)!
self.defaults.removePersistentDomain(forName: dbName)
self.settingsService = SettingsService(userDefaults: self.defaults)
self.settingsService = await SettingsService(suiteName: dbName)
}
@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(userDefaults: defaults)
let service = await SettingsService(suiteName: dbName)
#expect(service.user.email == testEmail)
#expect(service.user.token == testToken)

View File

@ -34,7 +34,7 @@ struct StorageServiceTests {
try addTestVehicle(config: realmConfig)
given(settingsServiceMock)
await given(settingsServiceMock)
.user.willReturn(User())
}