Adding dependency injection with property wrappers

This commit is contained in:
Selim Mustafaev 2024-09-22 13:18:53 +03:00
parent 17d00fbf65
commit 2a8f1d921a
26 changed files with 215 additions and 90 deletions

View File

@ -104,6 +104,8 @@
7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; }; 7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; };
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; }; 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; };
7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095F26DBF588003A965D /* VehicleNote.swift */; }; 7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095F26DBF588003A965D /* VehicleNote.swift */; };
7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */; };
7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */; };
7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */; }; 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */; };
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */; }; 7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */; };
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158062C44085600852088 /* OsagoScreen.swift */; }; 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158062C44085600852088 /* OsagoScreen.swift */; };
@ -361,6 +363,8 @@
7A6DD90B24335A6D009DE740 /* FlagLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagLayer.swift; sourceTree = "<group>"; }; 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagLayer.swift; sourceTree = "<group>"; };
7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = "<group>"; }; 7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = "<group>"; };
7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = "<group>"; }; 7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = "<group>"; };
7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = "<group>"; };
7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicePropertyWrapper.swift; sourceTree = "<group>"; };
7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = "<group>"; }; 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = "<group>"; };
7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = "<group>"; }; 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = "<group>"; };
7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = "<group>"; }; 7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = "<group>"; };
@ -836,6 +840,15 @@
path = Fonts; path = Fonts;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7A7097C02C9EC113007CFDCA /* DependencyInjection */ = {
isa = PBXGroup;
children = (
7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */,
7A7097C52C9EC77A007CFDCA /* ServicePropertyWrapper.swift */,
);
path = DependencyInjection;
sourceTree = "<group>";
};
7A7157FE2C43EA5200852088 /* OwnersScreen */ = { 7A7157FE2C43EA5200852088 /* OwnersScreen */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -957,6 +970,7 @@
7AF6D1F02677C03B0086EA64 /* AutoCatCore */ = { 7AF6D1F02677C03B0086EA64 /* AutoCatCore */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A7097C02C9EC113007CFDCA /* DependencyInjection */,
7A45FB362C2706D000618694 /* Services */, 7A45FB362C2706D000618694 /* Services */,
7A761C06267E8E720005F28F /* ThirdParty */, 7A761C06267E8E720005F28F /* ThirdParty */,
7AF6D2292677C3950086EA64 /* Extensions */, 7AF6D2292677C3950086EA64 /* Extensions */,
@ -1364,6 +1378,8 @@
7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */, 7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */,
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */, 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */,
7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */, 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */,
7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */,
7A7097C62C9EC77A007CFDCA /* ServicePropertyWrapper.swift in Sources */,
7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */, 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */,
7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */, 7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */,
7A761C09267E8EE40005F28F /* Base64FS.swift in Sources */, 7A761C09267E8EE40005F28F /* Base64FS.swift in Sources */,

View File

@ -33,5 +33,21 @@
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "4C063CE0-2872-4A19-8434-06F8B142D421"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "../../../../../var/folders/g9/lb6r30c97kq6yx2kcl8h66500000gn/T/swift-generated-sources/@__swiftmacro_7AutoCat17SettingsViewModelC14settingService18ObservationTrackedfMa_.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "1"
endingLineNumber = "1"
landmarkName = "unknown"
landmarkType = "0">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints> </Breakpoints>
</Bucket> </Bucket>

View File

@ -2,6 +2,8 @@ import UIKit
import RealmSwift import RealmSwift
import PKHUD import PKHUD
import AutoCatCore import AutoCatCore
import SwiftLocation
import CoreLocation
enum QuickAction { enum QuickAction {
case none case none
@ -70,10 +72,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
HUD.dimsBackground = true HUD.dimsBackground = true
HUD.allowsInteraction = false HUD.allowsInteraction = false
Task {
try! await registerServices()
}
//Logging.URLRequests = { _ in false }; //Logging.URLRequests = { _ in false };
return true return true
} }
func registerServices() async throws {
let container = ServiceContainer.shared
container.register(SettingsServiceProtocol.self, instance: SettingsService(defaults: .standard))
container.register(StorageServiceProtocol.self, instance: try await StorageService())
container.register(ApiServiceProtocol.self, instance: ApiService())
container.register(GeocoderProtocol.self, instance: CLGeocoder())
container.register(SwiftLocationProtocol.self, instance: Location())
container.register(LocationServiceProtocol.self, instance: LocationService())
}
// MARK: UISceneSession Lifecycle // MARK: UISceneSession Lifecycle

View File

@ -10,7 +10,7 @@ import SwiftUI
struct AdsScreen: View { struct AdsScreen: View {
@StateObject var viewModel: AdsViewModel @State var viewModel: AdsViewModel
@State var galleryModel: ACImageSliderModel? @State var galleryModel: ACImageSliderModel?

View File

@ -10,7 +10,8 @@ import Foundation
import AutoCatCore import AutoCatCore
@MainActor @MainActor
class AdsViewModel: ObservableObject { @Observable
class AdsViewModel {
let ads: [VehicleAdDto] let ads: [VehicleAdDto]

View File

@ -13,7 +13,7 @@ struct LocationEditScreen: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@StateObject var viewModel: LocationEditViewModel @State var viewModel: LocationEditViewModel
var body: some View { var body: some View {
List { List {

View File

@ -10,12 +10,13 @@ import AutoCatCore
import SwiftUI import SwiftUI
@MainActor @MainActor
final class LocationEditViewModel: ObservableObject { @Observable
final class LocationEditViewModel {
weak var coordinator: LocationEditCoordinator? weak var coordinator: LocationEditCoordinator?
@Published var event: VehicleEventDto var event: VehicleEventDto
@Published var date: Date var date: Date
var result: VehicleEventDto? var result: VehicleEventDto?

View File

@ -23,7 +23,7 @@ final class LocationPickerCoordinator: Coordinator {
func start() async throws -> VehicleEventDto? { func start() async throws -> VehicleEventDto? {
let viewModel = LocationPickerViewModel(event: event, locationService: LocationService.shared) let viewModel = LocationPickerViewModel(event: event)
let screen = LocationPickerScreen(viewModel: viewModel) let screen = LocationPickerScreen(viewModel: viewModel)
let controller = CustomHostingController(rootView: screen) let controller = CustomHostingController(rootView: screen)
viewController?.pushViewController(controller, animated: true) viewController?.pushViewController(controller, animated: true)

View File

@ -14,7 +14,7 @@ struct LocationPickerScreen: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@StateObject var viewModel: LocationPickerViewModel @State var viewModel: LocationPickerViewModel
var body: some View { var body: some View {
ZStack { ZStack {
@ -52,8 +52,7 @@ struct LocationPickerScreen: View {
event.address = "Ул. Ленина, 123" event.address = "Ул. Ленина, 123"
let locationService = LocationServiceStub(event: event) let locationService = LocationServiceStub(event: event)
let viewModel = LocationPickerViewModel(event: event, let viewModel = LocationPickerViewModel(event: event)
locationService: locationService)
return LocationPickerScreen(viewModel: viewModel) return LocationPickerScreen(viewModel: viewModel)
} }

View File

@ -12,18 +12,18 @@ import MapKit
import SwiftUI import SwiftUI
@MainActor @MainActor
final class LocationPickerViewModel: ObservableObject { @Observable
final class LocationPickerViewModel {
let locationService: LocationServiceProtocol @ObservationIgnored @Service var locationService: LocationServiceProtocol
@Published var event: VehicleEventDto var event: VehicleEventDto
@Published var position: MapCameraPosition var position: MapCameraPosition
var result: VehicleEventDto? var result: VehicleEventDto?
init(event: VehicleEventDto, locationService: LocationServiceProtocol) { init(event: VehicleEventDto) {
self.event = event self.event = event
self.locationService = locationService
if event.latitude == 0 && event.longitude == 0 { if event.latitude == 0 && event.longitude == 0 {
self.position = .userLocation(fallback: .automatic) self.position = .userLocation(fallback: .automatic)

View File

@ -10,6 +10,7 @@ import Foundation
import SwiftUI import SwiftUI
import AutoCatCore import AutoCatCore
@MainActor
class NotesCoordinator: Coordinator { class NotesCoordinator: Coordinator {
let viewController: UINavigationController? let viewController: UINavigationController?
@ -23,10 +24,8 @@ class NotesCoordinator: Coordinator {
func start() async throws { func start() async throws {
let viewModel = await NotesViewModel(vehicle: vehicle, let viewModel = NotesViewModel(vehicle: vehicle)
storageService: try await StorageService.shared, let controller = UIHostingController(rootView: NotesScreen(viewModel: viewModel))
apiService: ApiService.shared) viewController?.pushViewController(controller, animated: true)
let controller = await UIHostingController(rootView: NotesScreen(viewModel: viewModel))
await viewController?.pushViewController(controller, animated: true)
} }
} }

View File

@ -11,7 +11,7 @@ import AutoCatCore
struct NotesScreen: View { struct NotesScreen: View {
@StateObject var viewModel: NotesViewModel @State var viewModel: NotesViewModel
@State var showNewNoteAlert = false @State var showNewNoteAlert = false
@State var showEditNoteAlert = false @State var showEditNoteAlert = false
@ -96,9 +96,7 @@ struct NotesScreen: View {
.init(text: "zxcv") .init(text: "zxcv")
] ]
let vm = NotesViewModel(vehicle: vehicle, let vm = NotesViewModel(vehicle: vehicle)
storageService: StorageServiceStub(),
apiService: ApiServiceStub())
return NotesScreen(viewModel: vm) return NotesScreen(viewModel: vm)
} }

View File

@ -12,19 +12,18 @@ import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
@MainActor @MainActor
class NotesViewModel: ObservableObject, ACHudContainer { @Observable
class NotesViewModel: ACHudContainer {
let storageService: StorageServiceProtocol @ObservationIgnored @Service var storageService: StorageServiceProtocol
let apiService: ApiServiceProtocol @ObservationIgnored @Service var apiService: ApiServiceProtocol
@Published var vehicle: VehicleDto var vehicle: VehicleDto
@Published var hud: ACHud? var hud: ACHud?
init(vehicle: VehicleDto, storageService: StorageServiceProtocol, apiService: ApiServiceProtocol) { init(vehicle: VehicleDto) {
self.vehicle = vehicle self.vehicle = vehicle
self.storageService = storageService
self.apiService = apiService
} }
func addNote(text: String) async { func addNote(text: String) async {

View File

@ -23,7 +23,7 @@ class SettingsCoordinator: Coordinator {
func start() async throws { func start() async throws {
let viewModel = SettingsViewModel(settingService: SettingsService.shared) let viewModel = SettingsViewModel()
viewModel.coordinator = self viewModel.coordinator = self
let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel)) let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel))
settingsController = controller settingsController = controller

View File

@ -83,5 +83,5 @@ struct SettingsScreen: View {
} }
#Preview { #Preview {
SettingsScreen(viewModel: .init(settingService: SettingsService(defaults: .standard))) SettingsScreen(viewModel: .init())
} }

View File

@ -42,8 +42,8 @@ class SettingsViewModel {
return jwt.payload.email return jwt.payload.email
} }
init(settingService: SettingsServiceProtocol) { init() {
self.settingService = settingService self.settingService = try! ServiceContainer.shared.resolve(SettingsServiceProtocol.self)
} }
func signOut() { func signOut() {

View File

@ -0,0 +1,64 @@
//
// ServiceContainer.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 21.09.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
public enum DIError: Error {
case wrongServiceType(String)
case serviceNotFound(String)
var localizedDescription: String {
switch self {
case .wrongServiceType(let type): "Wrong service type for service: \(type)"
case .serviceNotFound(let type): "Service \(type) not found"
}
}
}
@MainActor
public class ServiceContainer {
public static let shared: ServiceContainer = .init()
private var cache: [String: Any] = [:]
private var factories: [String: () -> Any] = [:]
public func register<Service>(_ service: Service.Type,
factory: @autoclosure @escaping () -> Service) {
factories[String(describing: service.self)] = factory
}
public func register<Service>(_ service: Service.Type,
instance: Service) {
cache[String(describing: service.self)] = instance
}
public func resolve<Service>(_ service: Service.Type) throws -> Service {
let type = String(describing: service.self)
if let cachedService = cache[type] {
if let service = cachedService as? Service {
return service
} else {
throw DIError.wrongServiceType(type)
}
}
guard let factory = factories[type] else {
throw DIError.serviceNotFound(type)
}
if let service = factory() as? Service {
return service
} else {
throw DIError.wrongServiceType(type)
}
}
}

View File

@ -0,0 +1,24 @@
//
// ServicePropertyWrapper.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 21.09.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
@MainActor
@propertyWrapper
public struct Service<Service> {
public var service: Service
public init() {
self.service = try! ServiceContainer.shared.resolve(Service.self)
}
public var wrappedValue: Service {
get { service }
set { service = newValue }
}
}

View File

@ -2,12 +2,11 @@ import Foundation
public actor ApiService: ApiServiceProtocol { public actor ApiService: ApiServiceProtocol {
public static let shared = ApiService(settingsService: SettingsService.shared) public static let shared = ApiService()
let settingsService: SettingsServiceProtocol @Service var settingsService: SettingsServiceProtocol
init(settingsService: SettingsServiceProtocol) { public init() {
self.settingsService = settingsService
} }
private let session: URLSession = { private let session: URLSession = {
@ -19,9 +18,9 @@ public actor ApiService: ApiServiceProtocol {
// MARK: - Private wrappres // MARK: - Private wrappres
private func createRequest<B,P>(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible { 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.useTestBackend ? Constants.baseUrlDebug : Constants.baseUrl let baseUrl = await settingsService.useTestBackend ? Constants.baseUrlDebug : Constants.baseUrl
guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil } guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil }
@ -51,7 +50,7 @@ public actor ApiService: ApiServiceProtocol {
body: B?, body: B?,
params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible { params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible {
guard let request = self.createRequest(api: api, method: method, body: body, params: params) else { guard let request = await self.createRequest(api: api, method: method, body: body, params: params) else {
throw ApiError.generic throw ApiError.generic
} }

View File

@ -12,17 +12,13 @@ import SwiftLocation
@MainActor @MainActor
public final class LocationService { public final class LocationService {
public static let shared = LocationService(geocoder: CLGeocoder(), @Service var geocoder: GeocoderProtocol
locationManager: Location()) @Service var locationManager: SwiftLocationProtocol
private let geocoder: GeocoderProtocol
private var locationManager: SwiftLocationProtocol
private var eventTask: Task<VehicleEventDto,Error>? private var eventTask: Task<VehicleEventDto,Error>?
public init(geocoder: GeocoderProtocol, locationManager: SwiftLocationProtocol) { public init() {
self.geocoder = geocoder
self.locationManager = locationManager
} }
private func checkPermissions() async throws { private func checkPermissions() async throws {

View File

@ -10,8 +10,6 @@ import SwiftUI
@Observable @Observable
public final class SettingsService: SettingsServiceProtocol { public final class SettingsService: SettingsServiceProtocol {
public static let shared = SettingsService(defaults: .standard)
let defaults: UserDefaults let defaults: UserDefaults
var observations: [NSKeyValueObservation] = [] var observations: [NSKeyValueObservation] = []

View File

@ -24,20 +24,6 @@ public enum StorageError: LocalizedError {
public actor StorageService: StorageServiceProtocol { public actor StorageService: StorageServiceProtocol {
private static var instance: StorageService?
public static var shared: StorageService {
get async throws {
if let instance {
return instance
} else {
let service = try await StorageService()
instance = service
return service
}
}
}
var realm: Realm! var realm: Realm!
public init(config: Realm.Configuration = .defaultConfiguration) async throws { public init(config: Realm.Configuration = .defaultConfiguration) async throws {

View File

@ -22,8 +22,11 @@ struct LocationServiceTests {
let locationService: LocationService let locationService: LocationService
init() { init() {
self.locationService = LocationService(geocoder: geocoder,
locationManager: locationManager) ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder)
ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: locationManager)
self.locationService = LocationService()
} }
@Test @Test

View File

@ -18,13 +18,14 @@ extension Encodable {
} }
} }
@MainActor
struct SettingsServiceTests { struct SettingsServiceTests {
let testRegion = "xxx" let testRegion = "xxx"
let testEmail = "test@test.test" let testEmail = "test@test.test"
let testToken = "testToken" let testToken = "testToken"
let testUser: User let testUser: User
let dbName = "testUserDefaults" let dbName = UUID().uuidString
let defaults: UserDefaults let defaults: UserDefaults
var settingsService: SettingsService var settingsService: SettingsService
@ -35,9 +36,9 @@ struct SettingsServiceTests {
self.defaults.removePersistentDomain(forName: dbName) self.defaults.removePersistentDomain(forName: dbName)
self.settingsService = SettingsService(defaults: defaults) self.settingsService = SettingsService(defaults: defaults)
} }
@Test("Creating default user") @Test("Creating default user")
mutating func defaultUser() async throws { func defaultUser() async throws {
#expect(settingsService.user.email == "") #expect(settingsService.user.email == "")
#expect(settingsService.user.token == "") #expect(settingsService.user.token == "")
@ -60,8 +61,11 @@ struct SettingsServiceTests {
settingsService.user = testUser settingsService.user = testUser
let userData = defaults.data(forKey: "user") let userData = try #require(defaults.data(forKey: "user"))
#expect(userData == testUser.data) let user = try JSONDecoder().decode(User.self, from: userData)
#expect(user.email == testUser.email)
#expect(user.token == testUser.token)
} }
@Test("Save settings", .serialized, arguments: [true, false]) @Test("Save settings", .serialized, arguments: [true, false])

View File

@ -20,19 +20,18 @@ struct LocationPickerTests {
let address = "Test Address" let address = "Test Address"
let geocoder = GeocoderMock() let geocoder = GeocoderMock()
let locationManager = SwiftLocationMock()
let locationService: LocationService
init() { init() {
self.locationService = LocationService(geocoder: geocoder,
locationManager: locationManager) ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder)
ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: SwiftLocationMock())
ServiceContainer.shared.register(LocationServiceProtocol.self, instance: LocationService())
} }
@Test("Set initial location (user)") @Test("Set initial location (user)")
func setInitialLocationUser() async throws { func setInitialLocationUser() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0), let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0))
locationService: locationService)
#expect(viewModel.position == .userLocation(fallback: .automatic)) #expect(viewModel.position == .userLocation(fallback: .automatic))
} }
@ -40,8 +39,7 @@ struct LocationPickerTests {
@Test("Set initial location (custom)") @Test("Set initial location (custom)")
func setInitialLocationCustom() async throws { func setInitialLocationCustom() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: latitude, lon: longitude), let viewModel = LocationPickerViewModel(event: .init(lat: latitude, lon: longitude))
locationService: locationService)
#expect(viewModel.position.region?.center.latitude == latitude) #expect(viewModel.position.region?.center.latitude == latitude)
#expect(viewModel.position.region?.center.longitude == longitude) #expect(viewModel.position.region?.center.longitude == longitude)
@ -50,8 +48,7 @@ struct LocationPickerTests {
@Test("Update event") @Test("Update event")
func updateEvent() async throws { func updateEvent() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0), let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0))
locationService: locationService)
geocoder.addLocation(latitude: latitude, geocoder.addLocation(latitude: latitude,
longitude: longitude, longitude: longitude,

View File

@ -14,12 +14,10 @@ import MockableTest
@MainActor @MainActor
final class NotesTests { final class NotesTests {
var storageServiceMock = StorageServiceMock() var storageServiceMock: StorageServiceMock
var apiServiceMock = ApiServiceMock() var apiServiceMock: ApiServiceMock
lazy var viewModel = NotesViewModel(vehicle: VehicleDto(), var viewModel: NotesViewModel
storageService: storageServiceMock,
apiService: apiServiceMock)
let noteText = "Test note text" let noteText = "Test note text"
let noteTextModified = "Test note text modified" let noteTextModified = "Test note text modified"
@ -27,6 +25,15 @@ final class NotesTests {
lazy var vehicleWithNote: VehicleDto = .normal.addNote(text: noteText) lazy var vehicleWithNote: VehicleDto = .normal.addNote(text: noteText)
lazy var unrecognizedVehicleWithNote: VehicleDto = .unrecognized.addNote(text: noteText) lazy var unrecognizedVehicleWithNote: VehicleDto = .unrecognized.addNote(text: noteText)
init() {
storageServiceMock = StorageServiceMock()
apiServiceMock = ApiServiceMock()
ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock)
ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock)
viewModel = NotesViewModel(vehicle: VehicleDto())
}
@Test("Add note (normal vehicle)") @Test("Add note (normal vehicle)")
func addNote() async throws { func addNote() async throws {