Adding tests for location picker

This commit is contained in:
Selim Mustafaev 2024-08-11 10:49:24 +03:00
parent 299ee23992
commit 9a1c05cb93
16 changed files with 398 additions and 79 deletions

View File

@ -39,6 +39,8 @@
7A1CF80529A41C66007962DA /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80429A41C66007962DA /* RealmSwift */; };
7A1CF81629A42117007962DA /* Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1CF81529A42117007962DA /* Realm.swift */; };
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */; };
7A22B6ED2C67FDEA00E60173 /* SwiftLocationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */; };
7A22B6EE2C67FDEA00E60173 /* GeocoderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */; };
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADC6249D43210035F39E /* RegionsController.swift */; };
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
@ -130,6 +132,8 @@
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */; };
7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABB1F1267E9CC800D7AB32 /* SwiftDate */; };
7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; };
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */; };
7AB0EF892C5D307600291EE6 /* LocationServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF882C5D307600291EE6 /* LocationServiceStub.swift */; };
7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; };
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; };
7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */; };
@ -274,6 +278,8 @@
7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISegmentedControl.swift; sourceTree = "<group>"; };
7A1CF81529A42117007962DA /* Realm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Realm.swift; sourceTree = "<group>"; };
7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBarButtonItem.swift; sourceTree = "<group>"; };
7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeocoderMock.swift; sourceTree = "<group>"; };
7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationMock.swift; sourceTree = "<group>"; };
7A27ADC6249D43210035F39E /* RegionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsController.swift; sourceTree = "<group>"; };
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
@ -374,6 +380,8 @@
7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = "<group>"; };
7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = "<group>"; };
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = "<group>"; };
7AB0EF882C5D307600291EE6 /* LocationServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServiceStub.swift; sourceTree = "<group>"; };
7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = "<group>"; };
7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = "<group>"; };
@ -641,6 +649,15 @@
path = NotesScreen;
sourceTree = "<group>";
};
7A22B6EC2C67FDEA00E60173 /* Mocks */ = {
isa = PBXGroup;
children = (
7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */,
7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
7A3F07A924360D9100E59687 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -718,6 +735,7 @@
7A60D24C2C5A9D4900D13F7B /* LocationService.swift */,
7A60D24E2C5A9DA800D13F7B /* LocationServiceProtocol.swift */,
7A60D2502C5A9E4200D13F7B /* GeocoderProtocol.swift */,
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */,
);
path = LocationService;
sourceTree = "<group>";
@ -857,6 +875,7 @@
children = (
7AB587362C42E3EC00FA7B66 /* StorageServiceStub.swift */,
7A176DB12C43071A00999D6B /* ApiServiceStub.swift */,
7AB0EF882C5D307600291EE6 /* LocationServiceStub.swift */,
);
path = Preview;
sourceTree = "<group>";
@ -910,6 +929,7 @@
7AF6D2292677C3950086EA64 /* Extensions */,
7A11474523FF2A9000B424AF /* Models */,
7AF6D20D2677C0C30086EA64 /* Utils */,
7A22B6EC2C67FDEA00E60173 /* Mocks */,
7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */,
7AF6D1F22677C03B0086EA64 /* Info.plist */,
);
@ -1269,6 +1289,7 @@
7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */,
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */,
7AB0EF892C5D307600291EE6 /* LocationServiceStub.swift in Sources */,
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */,
7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */,
7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */,
@ -1294,11 +1315,14 @@
buildActionMask = 2147483647;
files = (
7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */,
7A22B6ED2C67FDEA00E60173 /* SwiftLocationMock.swift in Sources */,
7A22B6EE2C67FDEA00E60173 /* GeocoderMock.swift in Sources */,
7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */,
7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */,
7A761C08267E8EA20005F28F /* JWT.swift in Sources */,
7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */,
7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */,
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */,
7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */,
7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */,
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */,
@ -1543,16 +1567,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 132;
CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1569,16 +1594,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 132;
CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1601,8 +1627,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1625,8 +1652,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1649,8 +1677,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1675,8 +1704,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1700,12 +1730,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AutoCatCore/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@ -1730,12 +1761,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AutoCatCore/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;

View File

@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kolos65/Mockable",
"state" : {
"revision" : "81ccaead99a3c038c09345caa2888ae74b644ee9",
"version" : "0.0.9"
"revision" : "da977ecb20974c4b1cf185f5fd38771b2d4674fb",
"version" : "0.0.10"
}
},
{
@ -51,8 +51,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-core.git",
"state" : {
"revision" : "f3d7ae5f9f31d90b327a64536bb7801cc69fd85b",
"version" : "14.9.0"
"revision" : "c2552e1d36867cb42b28130e894a81fc17081062",
"version" : "14.12.0"
}
},
{
@ -60,17 +60,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-swift.git",
"state" : {
"revision" : "4c4413abd0cd2221f59318f800960fe5bddc1494",
"version" : "10.51.0"
"revision" : "5221a83dc720823e3017493394d00d49ccf13446",
"version" : "10.52.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
"version" : "510.0.2"
"revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82",
"version" : "510.0.3"
}
},
{

View File

@ -25,8 +25,10 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell {
self.componentsFormatter.allowedUnits = [.minute, .second]
self.componentsFormatter.zeroFormattingBehavior = .pad
DispatchQueue.main.async {
self.progressView.progress = 0
}
}
override func prepareForReuse() {
super.prepareForReuse()

View File

@ -10,9 +10,12 @@ class VehicleNoteCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
DispatchQueue.main.async {
self.dateFormatter.dateStyle = .medium
self.dateFormatter.timeStyle = .medium
}
}
func configure(with note: VehicleNoteDto) {
self.noteText.text = note.text

View File

@ -2,6 +2,7 @@ import UIKit
import Kingfisher
import AutoCatCore
@MainActor
class VehiclePhotoCell: UICollectionViewCell {
@IBOutlet weak var photo: UIImageView!
@IBOutlet weak var model: UILabel!
@ -11,9 +12,12 @@ class VehiclePhotoCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
DispatchQueue.main.async {
self.layer.cornerRadius = 8
formatter.timeStyle = .none
formatter.dateStyle = .medium
self.formatter.timeStyle = .none
self.formatter.dateStyle = .medium
}
}
override func prepareForReuse() {

View File

@ -0,0 +1,27 @@
//
// LocationServiceStub.swift
// AutoCat
//
// Created by Selim Mustafaev on 02.08.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Foundation
import AutoCatCore
final class LocationServiceStub: LocationServiceProtocol {
let event: VehicleEventDto
init(event: VehicleEventDto) {
self.event = event
}
func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String {
event.address ?? ""
}
func requestCurrentLocation() async throws -> AutoCatCore.VehicleEventDto {
event
}
}

View File

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

View File

@ -18,15 +18,22 @@ struct LocationPickerScreen: View {
var body: some View {
ZStack {
Map(coordinateRegion: $viewModel.region)
Map(initialPosition: viewModel.position)
.mapControls {
MapUserLocationButton()
}
.onMapCameraChange(frequency: .onEnd) { context in
Task { await viewModel.updateEvent(center: context.region.center) }
}
Image(systemName: "mappin.and.ellipse")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 48)
.offset(.init(width: 0, height: -10))
.offset(.init(width: 0, height: -16))
.foregroundColor(.blue)
}
.ignoresSafeArea()
//.ignoresSafeArea()
.navigationTitle(viewModel.event.location)
.toolbar {
ToolbarItem(placement: .primaryAction) {
@ -44,5 +51,9 @@ struct LocationPickerScreen: View {
var event = VehicleEventDto(lat: 47.250049, lon: 39.711821)
event.address = "Ул. Ленина, 123"
return LocationPickerScreen(viewModel: .init(event: event))
let locationService = LocationServiceStub(event: event)
let viewModel = LocationPickerViewModel(event: event,
locationService: locationService)
return LocationPickerScreen(viewModel: viewModel)
}

View File

@ -14,58 +14,30 @@ import SwiftUI
@MainActor
final class LocationPickerViewModel: ObservableObject {
let locationService: LocationServiceProtocol
@Published var event: VehicleEventDto
@Published var region: MKCoordinateRegion {
didSet {
Task { await updateEvent(region: region) }
}
}
@Published var position: MapCameraPosition
var result: VehicleEventDto?
var geocodingTask: Task<Void, Error>?
init(event: VehicleEventDto) {
init(event: VehicleEventDto, locationService: LocationServiceProtocol) {
self.event = event
let center = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)
self.region = MKCoordinateRegion(center: center,
latitudinalMeters: 1000,
longitudinalMeters: 1000)
self.locationService = locationService
if event.latitude == 0 && event.longitude == 0 {
Task { await moveToCurrentLocation() }
self.position = .userLocation(fallback: .automatic)
} else {
let center = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)
self.position = .region(.init(center: center, latitudinalMeters: 1000, longitudinalMeters: 1000))
}
}
func moveToCurrentLocation() async {
do {
let currentEvent = try await RxLocationManager.requestCurrentLocation()
let center = CLLocationCoordinate2D(latitude: currentEvent.latitude,
longitude: currentEvent.longitude)
self.region = MKCoordinateRegion(center: center,
latitudinalMeters: 1000,
longitudinalMeters: 1000)
event.latitude = currentEvent.latitude
event.longitude = currentEvent.longitude
event.address = try? await RxLocationManager.getAddressForLocation(latitude: region.center.latitude,
longitude: region.center.longitude)
} catch {
print(error)
}
}
func updateEvent(region: MKCoordinateRegion) async {
geocodingTask?.cancel()
geocodingTask = Task {
event.latitude = region.center.latitude
event.longitude = region.center.longitude
try await Task.sleep(nanoseconds: 500_000_000)
event.address = try? await RxLocationManager.getAddressForLocation(latitude: region.center.latitude,
longitude: region.center.longitude)
geocodingTask = nil
}
func updateEvent(center: CLLocationCoordinate2D) async {
event.latitude = center.latitude
event.longitude = center.longitude
event.address = try? await locationService.getAddressForLocation(latitude: center.latitude,
longitude: center.longitude)
}
func done() {

View File

@ -0,0 +1,45 @@
//
// SwiftLocationMock.swift
// AutoCatCoreTests
//
// Created by Selim Mustafaev on 02.08.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import AutoCatCore
import CoreLocation
import SwiftLocation
final class SwiftLocationMock {
var authorizationStatus: CLAuthorizationStatus = .notDetermined
var requestedStatus: CLAuthorizationStatus = .notDetermined
var location: CLLocation?
var requestLocationTime: TimeInterval = 0
var requestLocationCount = 0
}
extension SwiftLocationMock: SwiftLocationProtocol {
func requestPermission(_ permission: LocationPermission) async throws -> CLAuthorizationStatus {
authorizationStatus = requestedStatus
return requestedStatus
}
func requestLocation(accuracy filters: AccuracyFilters?,
timeout: TimeInterval?) async throws -> Tasks.ContinuousUpdateLocation.StreamEvent {
requestLocationCount += 1
if requestLocationTime > 0 {
try await Task.sleep(nanoseconds: UInt64(requestLocationTime*1_000_000_000))
}
if let location {
return .didUpdateLocations([location])
} else {
return .didUpdateLocations([])
}
}
}

View File

@ -7,16 +7,53 @@
//
import CoreLocation
import SwiftLocation
@MainActor
public final class LocationService: LocationServiceProtocol {
public final class LocationService {
private var geocoder: GeocoderProtocol
public static let shared = LocationService(geocoder: CLGeocoder(),
locationManager: Location())
public init(geocoder: GeocoderProtocol) {
private let geocoder: GeocoderProtocol
private var locationManager: SwiftLocationProtocol
private var eventTask: Task<VehicleEventDto,Error>?
public init(geocoder: GeocoderProtocol, locationManager: SwiftLocationProtocol) {
self.geocoder = geocoder
self.locationManager = locationManager
}
private func checkPermissions() async throws {
switch locationManager.authorizationStatus {
case .authorizedWhenInUse, .authorizedAlways:
break
case .notDetermined:
_ = try await locationManager.requestPermission(.always)
try await checkPermissions()
case .denied:
throw CLError(.denied)
default:
throw LocationError.permission
}
}
private func requestLocation() async throws -> VehicleEventDto {
try await checkPermissions()
let locationEvent = try await locationManager.requestLocation(accuracy: nil, timeout: 20)
guard let coordinate = locationEvent.location?.coordinate else {
throw LocationError.generic
}
return VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude)
}
}
extension LocationService: LocationServiceProtocol {
public func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String {
let location = CLLocation(latitude: latitude, longitude: longitude)
@ -28,4 +65,20 @@ public final class LocationService: LocationServiceProtocol {
throw LocationError.reverseGeocode
}
}
@discardableResult
public func requestCurrentLocation() async throws -> VehicleEventDto {
if let eventTask {
return try await eventTask.value
} else {
let task = Task {
let location = try await requestLocation()
eventTask = nil
return location
}
eventTask = task
return try await task.value
}
}
}

View File

@ -6,9 +6,9 @@
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Foundation
@MainActor
public protocol LocationServiceProtocol {
func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String
func requestCurrentLocation() async throws -> VehicleEventDto
}

View File

@ -0,0 +1,21 @@
//
// SwiftLocationProtocol.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 02.08.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import SwiftLocation
import CoreLocation
public protocol SwiftLocationProtocol {
var authorizationStatus: CLAuthorizationStatus { get }
func requestPermission(_ permission: LocationPermission) async throws -> CLAuthorizationStatus
func requestLocation(accuracy filters: AccuracyFilters?,
timeout: TimeInterval?) async throws -> Tasks.ContinuousUpdateLocation.StreamEvent
}
extension Location: SwiftLocationProtocol { }

View File

@ -8,7 +8,7 @@
import Testing
import CoreLocation
import AutoCatCore
@testable import AutoCatCore
@MainActor
struct LocationServiceTests {
@ -18,10 +18,12 @@ struct LocationServiceTests {
let address = "Test Address"
let geocoder = GeocoderMock()
let locationManager = SwiftLocationMock()
let locationService: LocationService
init() {
self.locationService = LocationService(geocoder: geocoder)
self.locationService = LocationService(geocoder: geocoder,
locationManager: locationManager)
}
@Test
@ -58,4 +60,85 @@ struct LocationServiceTests {
longitude: longitude)
}
}
@Test("Get location: denied")
func getLocationDenied() async throws {
locationManager.authorizationStatus = .denied
await #expect(throws: CLError(.denied)) {
_ = try await locationService.requestCurrentLocation()
}
}
@Test("Get location: not determined -> denied")
func getLocationNotDeterminedDenied() async throws {
locationManager.authorizationStatus = .notDetermined
locationManager.requestedStatus = .denied
await #expect(throws: CLError(.denied)) {
_ = try await locationService.requestCurrentLocation()
}
}
@Test("Get location: not determined -> allow")
func getLocationNotDeterminedAllow() async throws {
locationManager.authorizationStatus = .notDetermined
locationManager.requestedStatus = .authorizedWhenInUse
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
let event = try await locationService.requestCurrentLocation()
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
}
@Test("Get location: normal")
func getLocationNormal() async throws {
locationManager.authorizationStatus = .authorizedWhenInUse
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
let event = try await locationService.requestCurrentLocation()
#expect(event.latitude == latitude)
#expect(event.longitude == longitude)
}
@Test("Get location: no location")
func getLocationNone() async throws {
locationManager.authorizationStatus = .authorizedWhenInUse
await #expect(throws: LocationError.generic) {
_ = try await locationService.requestCurrentLocation()
}
}
@Test("Get location: parallel requests")
func getLocationParallel() async throws {
locationManager.authorizationStatus = .authorizedWhenInUse
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
locationManager.requestLocationTime = 1
async let task1 = locationService.requestCurrentLocation()
async let task2 = locationService.requestCurrentLocation()
try await Task.sleep(nanoseconds: 1_500_000_000)
async let task3 = locationService.requestCurrentLocation()
let (event1, event2, event3) = try await (task1, task2, task3)
#expect(locationManager.requestLocationCount == 2)
#expect(event1.latitude == latitude)
#expect(event1.longitude == longitude)
#expect(event2.latitude == latitude)
#expect(event2.longitude == longitude)
#expect(event3.latitude == latitude)
#expect(event3.longitude == longitude)
}
}

View File

@ -0,0 +1,66 @@
//
// LocationPickerTests.swift
// AutoCatTests
//
// Created by Selim Mustafaev on 10.08.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Testing
import CoreLocation
@testable import AutoCat
@testable import AutoCatCore
@MainActor
struct LocationPickerTests {
let latitude: CLLocationDegrees = 10
let longitude: CLLocationDegrees = 10
let address = "Test Address"
let geocoder = GeocoderMock()
let locationManager = SwiftLocationMock()
let locationService: LocationService
init() {
self.locationService = LocationService(geocoder: geocoder,
locationManager: locationManager)
}
@Test("Set initial location (user)")
func setInitialLocationUser() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0),
locationService: locationService)
#expect(viewModel.position == .userLocation(fallback: .automatic))
}
@Test("Set initial location (custom)")
func setInitialLocationCustom() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: latitude, lon: longitude),
locationService: locationService)
#expect(viewModel.position.region?.center.latitude == latitude)
#expect(viewModel.position.region?.center.longitude == longitude)
}
@Test("Update event")
func updateEvent() async throws {
let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0),
locationService: locationService)
geocoder.addLocation(latitude: latitude,
longitude: longitude,
address: address)
await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude))
#expect(viewModel.event.latitude == latitude)
#expect(viewModel.event.longitude == longitude)
#expect(viewModel.event.address == address)
}
}