Adding dependency injection with property wrappers
This commit is contained in:
parent
17d00fbf65
commit
2a8f1d921a
@ -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 */,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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?
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import Foundation
|
|||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class AdsViewModel: ObservableObject {
|
@Observable
|
||||||
|
class AdsViewModel {
|
||||||
|
|
||||||
let ads: [VehicleAdDto]
|
let ads: [VehicleAdDto]
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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?
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -83,5 +83,5 @@ struct SettingsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsScreen(viewModel: .init(settingService: SettingsService(defaults: .standard)))
|
SettingsScreen(viewModel: .init())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
64
AutoCatCore/DependencyInjection/ServiceContainer.swift
Normal file
64
AutoCatCore/DependencyInjection/ServiceContainer.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift
Normal file
24
AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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] = []
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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])
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user