diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index af6002b..48a463c 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -64,6 +64,9 @@ 7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C12C1AE5C900C2209B /* VehicleModel.swift */; }; 7A5D84C42C1AE65C00C2209B /* VehicleBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C32C1AE65C00C2209B /* VehicleBrand.swift */; }; 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C52C1AE72E00C2209B /* VehicleName.swift */; }; + 7A60D24D2C5A9D4900D13F7B /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A60D24C2C5A9D4900D13F7B /* LocationService.swift */; }; + 7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A60D24E2C5A9DA800D13F7B /* LocationServiceProtocol.swift */; }; + 7A60D2512C5A9E4200D13F7B /* GeocoderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A60D2502C5A9E4200D13F7B /* GeocoderProtocol.swift */; }; 7A61FF8B2575A2CD00D905D5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FF892575A2CD00D905D5 /* Localizable.strings */; }; 7A61FF912575A5B300D905D5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FF8F2575A5B300D905D5 /* InfoPlist.strings */; }; 7A61FFA0257D3CFC00D905D5 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FFA2257D3CFC00D905D5 /* Localizable.stringsdict */; }; @@ -301,6 +304,9 @@ 7A5D84C12C1AE5C900C2209B /* VehicleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleModel.swift; sourceTree = ""; }; 7A5D84C32C1AE65C00C2209B /* VehicleBrand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleBrand.swift; sourceTree = ""; }; 7A5D84C52C1AE72E00C2209B /* VehicleName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleName.swift; sourceTree = ""; }; + 7A60D24C2C5A9D4900D13F7B /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = ""; }; + 7A60D24E2C5A9DA800D13F7B /* LocationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServiceProtocol.swift; sourceTree = ""; }; + 7A60D2502C5A9E4200D13F7B /* GeocoderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeocoderProtocol.swift; sourceTree = ""; }; 7A61FF8325759DE700D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 7A61FF8A2575A2CD00D905D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 7A61FF8D2575A2F900D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -657,6 +663,7 @@ 7A45FB362C2706D000618694 /* Services */ = { isa = PBXGroup; children = ( + 7A60D24B2C5A9D2700D13F7B /* LocationService */, 7AB5873D2C42FF4000FA7B66 /* ApiService */, 7AB587302C42D35900FA7B66 /* StorageService */, ); @@ -705,6 +712,16 @@ path = Realm; sourceTree = ""; }; + 7A60D24B2C5A9D2700D13F7B /* LocationService */ = { + isa = PBXGroup; + children = ( + 7A60D24C2C5A9D4900D13F7B /* LocationService.swift */, + 7A60D24E2C5A9DA800D13F7B /* LocationServiceProtocol.swift */, + 7A60D2502C5A9E4200D13F7B /* GeocoderProtocol.swift */, + ); + path = LocationService; + sourceTree = ""; + }; 7A64A2012C19D99D00284124 /* DTO */ = { isa = PBXGroup; children = ( @@ -1303,6 +1320,8 @@ 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */, 7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */, 7AF6D21F2677C1680086EA64 /* Response.swift in Sources */, + 7A60D24D2C5A9D4900D13F7B /* LocationService.swift in Sources */, + 7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */, 7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */, 7A64A2032C19DA1000284124 /* VehicleDto.swift in Sources */, 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */, @@ -1310,6 +1329,7 @@ 7A5D84BC2C1AD81000C2209B /* VehicleOwnershipPeriod.swift in Sources */, 7A64A2202C19E93500284124 /* VehicleNoteDto.swift in Sources */, 7AF6D21A2677C1680086EA64 /* User.swift in Sources */, + 7A60D2512C5A9E4200D13F7B /* GeocoderProtocol.swift in Sources */, 7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */, 7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */, 7AF6D2152677C1680086EA64 /* Settings.swift in Sources */, diff --git a/AutoCatCore/Services/LocationService/GeocoderProtocol.swift b/AutoCatCore/Services/LocationService/GeocoderProtocol.swift new file mode 100644 index 0000000..a12cf0e --- /dev/null +++ b/AutoCatCore/Services/LocationService/GeocoderProtocol.swift @@ -0,0 +1,16 @@ +// +// GeocoderProtocol.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 31.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import CoreLocation + +public protocol GeocoderProtocol { + + func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark] +} + +extension CLGeocoder: GeocoderProtocol { } diff --git a/AutoCatCore/Services/LocationService/LocationService.swift b/AutoCatCore/Services/LocationService/LocationService.swift new file mode 100644 index 0000000..b38fd12 --- /dev/null +++ b/AutoCatCore/Services/LocationService/LocationService.swift @@ -0,0 +1,31 @@ +// +// LocationService.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 31.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import CoreLocation + +@MainActor +public final class LocationService: LocationServiceProtocol { + + private var geocoder: GeocoderProtocol + + public init(geocoder: GeocoderProtocol) { + self.geocoder = geocoder + } + + public func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String { + + let location = CLLocation(latitude: latitude, longitude: longitude) + let placemarks = try await geocoder.reverseGeocodeLocation(location) + + if let name = placemarks.first?.name { + return name + } else { + throw LocationError.reverseGeocode + } + } +} diff --git a/AutoCatCore/Services/LocationService/LocationServiceProtocol.swift b/AutoCatCore/Services/LocationService/LocationServiceProtocol.swift new file mode 100644 index 0000000..51be8d5 --- /dev/null +++ b/AutoCatCore/Services/LocationService/LocationServiceProtocol.swift @@ -0,0 +1,14 @@ +// +// LocationServiceProtocol.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 31.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public protocol LocationServiceProtocol { + + func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String +} diff --git a/AutoCatCore/Utils/Location.swift b/AutoCatCore/Utils/Location.swift index f848953..28fc5cf 100644 --- a/AutoCatCore/Utils/Location.swift +++ b/AutoCatCore/Utils/Location.swift @@ -2,13 +2,13 @@ import Foundation import CoreLocation import SwiftLocation -enum LocationError: LocalizedError { +public enum LocationError: LocalizedError { case generic case permission case reverseGeocode - var errorDescription: String? { + public var errorDescription: String? { switch self { case .generic: "Location error" case .permission: "Location permission error" diff --git a/AutoCatCoreTests/LocationServiceTests.swift b/AutoCatCoreTests/LocationServiceTests.swift new file mode 100644 index 0000000..e9ba3e5 --- /dev/null +++ b/AutoCatCoreTests/LocationServiceTests.swift @@ -0,0 +1,61 @@ +// +// LocationServiceTests.swift +// AutoCatCoreTests +// +// Created by Selim Mustafaev on 01.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Testing +import CoreLocation +import AutoCatCore + +@MainActor +struct LocationServiceTests { + + let latitude: CLLocationDegrees = 10 + let longitude: CLLocationDegrees = 10 + let address = "Test Address" + + let geocoder = GeocoderMock() + let locationService: LocationService + + init() { + self.locationService = LocationService(geocoder: geocoder) + } + + @Test + func getValidAddress() async throws { + + geocoder.addLocation(latitude: latitude, + longitude: longitude, + address: address) + + let result = try await locationService.getAddressForLocation(latitude: latitude, + longitude: longitude) + + #expect(result == address) + } + + @Test + func getNilAddress() async throws { + + geocoder.addLocation(latitude: latitude, + longitude: longitude, + address: nil) + + await #expect(throws: LocationError.reverseGeocode) { + _ = try await locationService.getAddressForLocation(latitude: latitude, + longitude: longitude) + } + } + + @Test + func addressNotFound() async throws { + + await #expect(throws: LocationError.reverseGeocode) { + _ = try await locationService.getAddressForLocation(latitude: latitude, + longitude: longitude) + } + } +} diff --git a/AutoCatCoreTests/Mocks/GeocoderMock.swift b/AutoCatCoreTests/Mocks/GeocoderMock.swift new file mode 100644 index 0000000..72c24f7 --- /dev/null +++ b/AutoCatCoreTests/Mocks/GeocoderMock.swift @@ -0,0 +1,51 @@ +// +// GeocoderMock.swift +// AutoCatCoreTests +// +// Created by Selim Mustafaev on 31.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import CoreLocation +import AutoCatCore +import Intents +import Contacts + +final class GeocoderMock { + + struct Location { + + let latitude: CLLocationDegrees + let longitude: CLLocationDegrees + let address: String? + } + + var locations: [Location] = [] + + func addLocation(latitude: CLLocationDegrees, longitude: CLLocationDegrees, address: String?) { + + locations.append(Location(latitude: latitude, + longitude: longitude, + address: address)) + } +} + +extension GeocoderMock: GeocoderProtocol { + + func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark] { + + let first = locations.first { + $0.latitude == location.coordinate.latitude && $0.longitude == location.coordinate.longitude + } + + guard let first else { + return [] + } + + let placemark = CLPlacemark(location: CLLocation(latitude: first.latitude, longitude: first.longitude), + name: first.address, + postalAddress: nil) + + return [placemark] + } +}