From e91fd89eff2110cc8a6a62117c57cf6875b99a10 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Thu, 12 Jun 2025 19:09:48 +0300 Subject: [PATCH] Implementing SwiftData storage service --- AutoCat.xcodeproj/project.pbxproj | 28 ++++++ .../Models/SwiftData/SDVehicleNote.swift | 10 ++ ...SwiftDataStorageService+AudioRecords.swift | 53 +++++++++++ .../SwiftDataStorageService+Events.swift | 57 ++++++++++++ .../SwiftDataStorageService+Notes.swift | 59 ++++++++++++ .../SwiftDataStorageService+Utils.swift | 49 ++++++++++ .../SwiftDataStorageService.swift | 93 +++++++++++++++++++ 7 files changed, 349 insertions(+) create mode 100644 AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+AudioRecords.swift create mode 100644 AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Events.swift create mode 100644 AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Notes.swift create mode 100644 AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Utils.swift create mode 100644 AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 7bcccd8..deed1c4 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -28,6 +28,10 @@ 7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441652C297EDE00E79018 /* NotesScreen.swift */; }; 7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441672C297EFD00E79018 /* NotesViewModel.swift */; }; 7A17ADA02DC9F4A5002BA02A /* ScreenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17AD9F2DC9F4A5002BA02A /* ScreenInput.swift */; }; + 7A1AE64F2DFB22EC00ECFC4D /* SwiftDataStorageService+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1AE64E2DFB22EC00ECFC4D /* SwiftDataStorageService+Utils.swift */; }; + 7A1AE6512DFB235300ECFC4D /* SwiftDataStorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1AE6502DFB235300ECFC4D /* SwiftDataStorageService+Notes.swift */; }; + 7A1AE6532DFB2AD900ECFC4D /* SwiftDataStorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1AE6522DFB2AD900ECFC4D /* SwiftDataStorageService+Events.swift */; }; + 7A1AE6552DFB2E9D00ECFC4D /* SwiftDataStorageService+AudioRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1AE6542DFB2E9D00ECFC4D /* SwiftDataStorageService+AudioRecords.swift */; }; 7A1CF81629A42117007962DA /* Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1CF81529A42117007962DA /* Realm.swift */; }; 7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F52CE900330004B740 /* ReportScreen.swift */; }; 7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F72CE900440004B740 /* ReportViewModel.swift */; }; @@ -167,6 +171,7 @@ 7AD857262DF876C9009E4B72 /* SDVehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD857252DF876C9009E4B72 /* SDVehicleNote.swift */; }; 7AD857282DF87733009E4B72 /* SDDebugInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD857272DF87733009E4B72 /* SDDebugInfo.swift */; }; 7AD8572A2DF87928009E4B72 /* SDAudioRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD857292DF87928009E4B72 /* SDAudioRecord.swift */; }; + 7AD8572D2DF95F72009E4B72 /* SwiftDataStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8572C2DF95F72009E4B72 /* SwiftDataStorageService.swift */; }; 7ADCBC572DB51739002522C0 /* AutoCatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCBC562DB51739002522C0 /* AutoCatApp.swift */; }; 7ADF6C99250F872C00F237B2 /* RoadNumbers.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */; }; 7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6CA02512244400F237B2 /* MapExt.swift */; }; @@ -289,6 +294,10 @@ 7A1441672C297EFD00E79018 /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = ""; }; 7A15051124DB3E3000F39631 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; 7A17AD9F2DC9F4A5002BA02A /* ScreenInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenInput.swift; sourceTree = ""; }; + 7A1AE64E2DFB22EC00ECFC4D /* SwiftDataStorageService+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftDataStorageService+Utils.swift"; sourceTree = ""; }; + 7A1AE6502DFB235300ECFC4D /* SwiftDataStorageService+Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftDataStorageService+Notes.swift"; sourceTree = ""; }; + 7A1AE6522DFB2AD900ECFC4D /* SwiftDataStorageService+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftDataStorageService+Events.swift"; sourceTree = ""; }; + 7A1AE6542DFB2E9D00ECFC4D /* SwiftDataStorageService+AudioRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftDataStorageService+AudioRecords.swift"; sourceTree = ""; }; 7A1CF81529A42117007962DA /* Realm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Realm.swift; sourceTree = ""; }; 7A1E78F52CE900330004B740 /* ReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportScreen.swift; sourceTree = ""; }; 7A1E78F72CE900440004B740 /* ReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; @@ -432,6 +441,7 @@ 7AD857252DF876C9009E4B72 /* SDVehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDVehicleNote.swift; sourceTree = ""; }; 7AD857272DF87733009E4B72 /* SDDebugInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDDebugInfo.swift; sourceTree = ""; }; 7AD857292DF87928009E4B72 /* SDAudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDAudioRecord.swift; sourceTree = ""; }; + 7AD8572C2DF95F72009E4B72 /* SwiftDataStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDataStorageService.swift; sourceTree = ""; }; 7ADCBC562DB51739002522C0 /* AutoCatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCatApp.swift; sourceTree = ""; }; 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers.otf; sourceTree = ""; }; 7ADF6CA02512244400F237B2 /* MapExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapExt.swift; sourceTree = ""; }; @@ -766,6 +776,7 @@ 7A60D24B2C5A9D2700D13F7B /* LocationService */, 7AB5873D2C42FF4000FA7B66 /* ApiService */, 7AB587302C42D35900FA7B66 /* StorageService */, + 7AD8572B2DF95F1E009E4B72 /* SwiftDataStorageService */, ); path = Services; sourceTree = ""; @@ -1053,6 +1064,18 @@ path = NumberEditView; sourceTree = ""; }; + 7AD8572B2DF95F1E009E4B72 /* SwiftDataStorageService */ = { + isa = PBXGroup; + children = ( + 7AD8572C2DF95F72009E4B72 /* SwiftDataStorageService.swift */, + 7A1AE64E2DFB22EC00ECFC4D /* SwiftDataStorageService+Utils.swift */, + 7A1AE6502DFB235300ECFC4D /* SwiftDataStorageService+Notes.swift */, + 7A1AE6522DFB2AD900ECFC4D /* SwiftDataStorageService+Events.swift */, + 7A1AE6542DFB2E9D00ECFC4D /* SwiftDataStorageService+AudioRecords.swift */, + ); + path = SwiftDataStorageService; + sourceTree = ""; + }; 7ADFC9552DAD026C001A43E3 /* GoogleAuthScreen */ = { isa = PBXGroup; children = ( @@ -1490,7 +1513,9 @@ 7A64A21E2C19E8D500284124 /* VehicleAdDto.swift in Sources */, 7AD857202DF874F8009E4B72 /* SDVehicleEvent.swift in Sources */, 7AF6D2202677C1680086EA64 /* Filter.swift in Sources */, + 7A1AE6552DFB2E9D00ECFC4D /* SwiftDataStorageService+AudioRecords.swift in Sources */, 7A761C042677F18E0005F28F /* ApiService.swift in Sources */, + 7A1AE6532DFB2AD900ECFC4D /* SwiftDataStorageService+Events.swift in Sources */, 7A95197B2D80B41600E69883 /* AudioRecordServiceProtocol.swift in Sources */, 7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */, 7A0818762DF83DB4000219FE /* SDVehicleName.swift in Sources */, @@ -1502,6 +1527,7 @@ 7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */, 7AF6D2172677C1680086EA64 /* VehicleRegion.swift in Sources */, 7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */, + 7AD8572D2DF95F72009E4B72 /* SwiftDataStorageService.swift in Sources */, 7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */, 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */, 7AD8572A2DF87928009E4B72 /* SDAudioRecord.swift in Sources */, @@ -1542,12 +1568,14 @@ 7AD857222DF875B2009E4B72 /* SDOsago.swift in Sources */, 7ABDA80F2D8723F90083C715 /* StorageService+AudioRecords.swift in Sources */, 7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */, + 7A1AE64F2DFB22EC00ECFC4D /* SwiftDataStorageService+Utils.swift in Sources */, 7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */, 7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */, 7A64A21A2C19E6B300284124 /* VehicleEventDto.swift in Sources */, 7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */, 7AF6D21B2677C1680086EA64 /* Vehicle.swift in Sources */, 7AB587412C42FFE200FA7B66 /* ApiServiceProtocol.swift in Sources */, + 7A1AE6512DFB235300ECFC4D /* SwiftDataStorageService+Notes.swift in Sources */, 7A5D84C42C1AE65C00C2209B /* VehicleBrand.swift in Sources */, 7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */, 7A45FB382C27073700618694 /* StorageService.swift in Sources */, diff --git a/AutoCatCore/Models/SwiftData/SDVehicleNote.swift b/AutoCatCore/Models/SwiftData/SDVehicleNote.swift index eef3eef..d514381 100644 --- a/AutoCatCore/Models/SwiftData/SDVehicleNote.swift +++ b/AutoCatCore/Models/SwiftData/SDVehicleNote.swift @@ -30,6 +30,16 @@ final class SDVehicleNote { self.date = date self.text = text } + + convenience init(text: String, user: String) { + + self.init( + id: UUID().uuidString, + user: user, + date: Date().timeIntervalSince1970, + text: text + ) + } } extension SDVehicleNote: DtoConvertible { diff --git a/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+AudioRecords.swift b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+AudioRecords.swift new file mode 100644 index 0000000..843aff5 --- /dev/null +++ b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+AudioRecords.swift @@ -0,0 +1,53 @@ +// +// SwiftDataStorageService+AudioRecords.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftData + +extension SwiftDataStorageService { + + public func add(record: AudioRecordDto) async throws { + + context.insert(SDAudioRecord(dto: record)) + try context.save() + } + + public func loadRecords() async throws -> [AudioRecordDto] { + + do { + return try context.fetch( + FetchDescriptor( + sortBy: [SortDescriptor(\.addedDate, order: .reverse)] + ) + ) + .map(\.dto) + } catch { + return [] + } + } + + public func deleteRecord(id: String) async throws { + guard let record = try? fetchAudioRecord(id: id) else { + throw StorageError.recordNotFound + } + + context.delete(record) + try context.save() + } + + public func updateRecord(id: String, number: String) async throws -> AudioRecordDto { + guard let record = try? fetchAudioRecord(id: id) else { + throw StorageError.recordNotFound + } + + record.number = number + try context.save() + + return record.dto + } +} diff --git a/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Events.swift b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Events.swift new file mode 100644 index 0000000..d06b592 --- /dev/null +++ b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Events.swift @@ -0,0 +1,57 @@ +// +// SwiftDataStorageService+Events.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftData + +extension SwiftDataStorageService { + + public func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + vehicle.events.append(SDVehicleEvent(dto: event)) + vehicle.updatedDate = Date().timeIntervalSince1970 + try context.save() + + return vehicle.dto + } + + public func remove(event id: String, from number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + if let index = vehicle.events.firstIndex(where: { $0.id == id }) { + vehicle.events.remove(at: index) + vehicle.updatedDate = Date().timeIntervalSince1970 + } else { + throw StorageError.eventNotFound + } + + try context.save() + return vehicle.dto + } + + public func edit(event: VehicleEventDto, for number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + if let index = vehicle.events.firstIndex(where: { $0.id == event.id }) { + vehicle.events[index] = SDVehicleEvent(dto: event) + vehicle.updatedDate = Date().timeIntervalSince1970 + try context.save() + } else { + throw StorageError.eventNotFound + } + + return vehicle.dto + } +} diff --git a/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Notes.swift b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Notes.swift new file mode 100644 index 0000000..4325a4d --- /dev/null +++ b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Notes.swift @@ -0,0 +1,59 @@ +// +// SwiftDataStorageService+Notes.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftData + +extension SwiftDataStorageService { + + public func addNote(text: String, to number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + let note = SDVehicleNote(text: text, user: settingsService.user.email) + + vehicle.notes.append(note) + vehicle.updatedDate = Date().timeIntervalSince1970 + try context.save() + + return vehicle.dto + } + + public func deleteNote(id: String, for number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + guard let note = try? fetchNote(id: id) else { + throw StorageError.noteNotFound + } + + vehicle.updatedDate = Date().timeIntervalSince1970 + context.delete(note) + try context.save() + + return vehicle.dto + } + + public func editNote(id: String, text: String, for number: String) async throws -> VehicleDto { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + guard let note = try? fetchNote(id: id) else { + throw StorageError.noteNotFound + } + + note.text = text + vehicle.updatedDate = Date().timeIntervalSince1970 + try context.save() + + return vehicle.dto + } +} diff --git a/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Utils.swift b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Utils.swift new file mode 100644 index 0000000..3af432d --- /dev/null +++ b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService+Utils.swift @@ -0,0 +1,49 @@ +// +// SwiftDataStorageService+Utils.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftData + +extension SwiftDataStorageService { + + func vehicleExists(number: String) throws -> Bool { + + let count = try context.fetchCount( + FetchDescriptor(predicate: #Predicate { $0.number == number }) + ) + + return count > 0 + } + + func fetchVehicle(number: String) throws -> SDVehicle? { + + let vehicles = try context.fetch( + FetchDescriptor(predicate: #Predicate { $0.number == number }) + ) + + return vehicles.first + } + + func fetchNote(id: String) throws -> SDVehicleNote? { + + let notes = try context.fetch( + FetchDescriptor(predicate: #Predicate { $0.id == id }) + ) + + return notes.first + } + + func fetchAudioRecord(id: String) throws -> SDAudioRecord? { + + let records = try context.fetch( + FetchDescriptor(predicate: #Predicate { $0.path == id }) + ) + + return records.first + } +} diff --git a/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService.swift b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService.swift new file mode 100644 index 0000000..fb2f6dc --- /dev/null +++ b/AutoCatCore/Services/SwiftDataStorageService/SwiftDataStorageService.swift @@ -0,0 +1,93 @@ +// +// SwiftDataStorageService.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 11.06.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftData + +@MainActor +public final class SwiftDataStorageService: StorageServiceProtocol { + + let settingsService: SettingsServiceProtocol + let container: ModelContainer + let context: ModelContext + + public init(settingsService: SettingsServiceProtocol, isTest: Bool = false) throws { + + self.settingsService = settingsService + + self.container = try ModelContainer( + for: SDVehicle.self, SDVehicleNote.self, SDAudioRecord.self, + configurations: .init(isStoredInMemoryOnly: isTest) + ) + + self.context = container.mainContext + } + + public var dbFileURL: URL? { + + // TODO: Set specific URL in ModelConfiguration + get async { nil } + } + + public func deleteAll() async throws { + + container.deleteAllData() + } + + @discardableResult + public func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool { + + let shouldUpdate = switch policy { + case .always: + true + case .ifExists: + try vehicleExists(number: dto.number) + } + + guard shouldUpdate else { + return false + } + + context.insert(SDVehicle(dto: dto)) + try context.save() + + return true + } + + public func loadVehicle(number: String) async throws -> VehicleDto { + + if let vehicle = try fetchVehicle(number: number) { + return vehicle.dto + } else { + throw StorageError.vehicleNotFound + } + } + + public func loadVehicles() async -> [VehicleDto] { + + do { + return try context.fetch( + FetchDescriptor( + sortBy: [SortDescriptor(\.updatedDate, order: .reverse)] + ) + ) + .map(\.shallowDto) + } catch { + return [] + } + } + + public func deleteVehicle(number: String) async throws { + guard let vehicle = try? fetchVehicle(number: number) else { + throw StorageError.vehicleNotFound + } + + context.delete(vehicle) + try context.save() + } +}