From a34963985f5404df9022bc518859fbbce770bf35 Mon Sep 17 00:00:00 2001 From: Alexander Ignatov Date: Sat, 27 May 2023 01:40:29 +0300 Subject: [PATCH] Make location refresh possible --- .../LocationService+Dependency.swift | 6 +- .../LocationService/LocationService.swift | 5 +- .../LocationService+Live.swift | 11 ++++ .../SearchStationReducer.swift | 38 ++++++----- .../SearchStationView/SearchStationView.swift | 14 +++-- .../SearchStationDomainTests.swift | 63 ++++++++++++++++++- 6 files changed, 113 insertions(+), 24 deletions(-) diff --git a/BDZDelays/bdz-delays/Sources/LocationService/LocationService+Dependency.swift b/BDZDelays/bdz-delays/Sources/LocationService/LocationService+Dependency.swift index 737ef94..0d05e39 100644 --- a/BDZDelays/bdz-delays/Sources/LocationService/LocationService+Dependency.swift +++ b/BDZDelays/bdz-delays/Sources/LocationService/LocationService+Dependency.swift @@ -13,7 +13,8 @@ import Dependencies extension LocationService: TestDependencyKey { public static var testValue = Self( statusStream: unimplemented("LocationService.statusStream"), - requestAuthorization: unimplemented("LocationService.requestAuthorization") + requestAuthorization: unimplemented("LocationService.requestAuthorization"), + manuallyRefreshStatus: unimplemented("LocationService.manuallyRefreshStatus") ) public static let previewValue: Self = { @@ -27,6 +28,9 @@ extension LocationService: TestDependencyKey { }, requestAuthorization: { continuation?.yield(.authorized(nearestStation: .dobrich)) + }, + manuallyRefreshStatus: { + continuation?.yield(.authorized(nearestStation: .varna)) } ) }() diff --git a/BDZDelays/bdz-delays/Sources/LocationService/LocationService.swift b/BDZDelays/bdz-delays/Sources/LocationService/LocationService.swift index 8db27a2..4b0c7f4 100644 --- a/BDZDelays/bdz-delays/Sources/LocationService/LocationService.swift +++ b/BDZDelays/bdz-delays/Sources/LocationService/LocationService.swift @@ -11,12 +11,15 @@ import SharedModels public struct LocationService { public var statusStream: () async -> AsyncStream public var requestAuthorization: () async -> Void + public var manuallyRefreshStatus: () async -> Void public init( statusStream: @escaping () async -> AsyncStream, - requestAuthorization: @escaping () async -> Void + requestAuthorization: @escaping () async -> Void, + manuallyRefreshStatus: @escaping () async -> Void ) { self.statusStream = statusStream self.requestAuthorization = requestAuthorization + self.manuallyRefreshStatus = manuallyRefreshStatus } } diff --git a/BDZDelays/bdz-delays/Sources/LocationServiceLive/LocationService+Live.swift b/BDZDelays/bdz-delays/Sources/LocationServiceLive/LocationService+Live.swift index 04bef49..a194e32 100644 --- a/BDZDelays/bdz-delays/Sources/LocationServiceLive/LocationService+Live.swift +++ b/BDZDelays/bdz-delays/Sources/LocationServiceLive/LocationService+Live.swift @@ -66,6 +66,9 @@ extension LocationService: DependencyKey { }, requestAuthorization: { await LocationManagerActor.shared.requestAuth() + }, + manuallyRefreshStatus: { + await LocationManagerActor.shared.refresh() } ) }() @@ -127,6 +130,14 @@ private struct LocationManagerActor { guard case .notDetermined = manager.authorizationStatus else { return } manager.requestWhenInUseAuthorization() } + + func refresh() { + let location = authorizedStatuses.contains(manager.authorizationStatus) + ? manager.location?.coordinate + : nil + + delegate.onLocationUpdate?(location) + } } extension LocationManagerActor { diff --git a/BDZDelays/bdz-delays/Sources/SearchStationDomain/SearchStationReducer.swift b/BDZDelays/bdz-delays/Sources/SearchStationDomain/SearchStationReducer.swift index 2134d52..1d5de5a 100644 --- a/BDZDelays/bdz-delays/Sources/SearchStationDomain/SearchStationReducer.swift +++ b/BDZDelays/bdz-delays/Sources/SearchStationDomain/SearchStationReducer.swift @@ -66,8 +66,7 @@ public struct SearchStationReducer: ReducerProtocol { case moveFavorite(from: IndexSet, to: Int) case locationStatusUpdate(LocationStatus) - case askForLocationPersmission - case locationSettings + case locationAction /// To be send from the `.task` view modifier. /// Used for executing a long-running effect for @@ -134,11 +133,11 @@ public struct SearchStationReducer: ReducerProtocol { state.stationState = .init(station: new) return .send(.stationAction(.refresh)) - + case .loadSavedStations(let statons): state.favoriteStations = statons return .none - + case .toggleSaveStation(let station): if state.isStationFavorite(station) { state.favoriteStations.removeAll { $0 == station } @@ -151,7 +150,7 @@ public struct SearchStationReducer: ReducerProtocol { } catch: { [favorites = state.favoriteStations] error, _ in log.error(error, ".toggleSaveStation: Coud not save favorites=\(favorites)") } - + case let .moveFavorite(from: from, to: to): state.favoriteStations.move(fromOffsets: from, toOffset: to) return .run { [favorites = state.favoriteStations] _ in @@ -160,15 +159,26 @@ public struct SearchStationReducer: ReducerProtocol { log.error(error, ".moveFavorite: Could not save favorites=\(favorites)") } - case .askForLocationPersmission: - state.locationStatus = .determining - return .fireAndForget { - await locationService.requestAuthorization() - } - - case .locationSettings: - return .fireAndForget { - await settingsService.openSettings() + case .locationAction: + switch state.locationStatus { + case .notYetAskedForAuthorization: + state.locationStatus = .determining + return .fireAndForget { + await locationService.requestAuthorization() + } + case .denied: + return .fireAndForget { + await settingsService.openSettings() + } + case .authorized(nearestStation: .some(let station)): + return .send(.selectStation(station)) + case .authorized(nearestStation: .none): + state.locationStatus = .determining + return .fireAndForget { + await locationService.manuallyRefreshStatus() + } + case .determining, .unableToUseLocation: + return .none } case .stationAction(let childAction): diff --git a/BDZDelays/bdz-delays/Sources/SearchStationView/SearchStationView.swift b/BDZDelays/bdz-delays/Sources/SearchStationView/SearchStationView.swift index 258625e..8201f63 100644 --- a/BDZDelays/bdz-delays/Sources/SearchStationView/SearchStationView.swift +++ b/BDZDelays/bdz-delays/Sources/SearchStationView/SearchStationView.swift @@ -130,7 +130,7 @@ private struct NearestStationView: View { Image(systemName: "location.fill") .foregroundColor(.accentColor) Button { - vs.send(.selectStation(station)) + vs.send(.locationAction) } label: { Text(station.name) }.favoritable(station: station, vs: vs) @@ -138,7 +138,7 @@ private struct NearestStationView: View { Image(systemName: "location") .foregroundColor(.accentColor) Button { - vs.send(.askForLocationPersmission) + vs.send(.locationAction) } label: { Text("Позволи достъп до локацията") } @@ -146,7 +146,7 @@ private struct NearestStationView: View { Image(systemName: "location") .foregroundColor(.red) Button { - vs.send(.locationSettings) + vs.send(.locationAction) } label: { Text("Достъпът до локацията е отказан") } @@ -159,8 +159,12 @@ private struct NearestStationView: View { case .authorized(nearestStation: .none): Image(systemName: "wifi.exclamationmark") .foregroundColor(.gray) - Text("Неуспех при опит за връзка") - .foregroundColor(.gray) + Button { + vs.send(.locationAction) + } label: { + Text("Неуспех при опит за връзка") + .foregroundColor(.gray) + } case .unableToUseLocation: EmptyView() } diff --git a/BDZDelays/bdz-delays/Tests/SearchStationDomainTests/SearchStationDomainTests.swift b/BDZDelays/bdz-delays/Tests/SearchStationDomainTests/SearchStationDomainTests.swift index b1be200..2702afb 100644 --- a/BDZDelays/bdz-delays/Tests/SearchStationDomainTests/SearchStationDomainTests.swift +++ b/BDZDelays/bdz-delays/Tests/SearchStationDomainTests/SearchStationDomainTests.swift @@ -47,10 +47,12 @@ final class SearchStationDomainTests: XCTestCase { await store.send(.updateQuery(query)) // no modification expected } - func test_askForLocation_callsService() async throws { + func test_locationAction_whenNotYetAsked_asksForPermisson() async throws { let serviceSpy = Spy() let store = TestStore( - initialState: SearchStationReducer.State(), + initialState: SearchStationReducer.State( + locationStatus: .notYetAskedForAuthorization + ), reducer: SearchStationReducer() ) { $0.locationService.requestAuthorization = { @@ -58,7 +60,7 @@ final class SearchStationDomainTests: XCTestCase { } } - await store.send(.askForLocationPersmission) { + await store.send(.locationAction) { $0.locationStatus = .determining } @@ -66,6 +68,61 @@ final class SearchStationDomainTests: XCTestCase { XCTAssertEqual(calls, 1) } + func test_locationAction_whenAvailable_opensStationInfo() async throws { + let station = BGStation.dobrich + let store = TestStore( + initialState: SearchStationReducer.State( + locationStatus: .authorized(nearestStation: station) + ), + reducer: SearchStationReducer() + ) + + store.exhaustivity = .off + + await store.send(.locationAction) + await store.receive(.selectStation(station)) + } + + func test_locationAction_whenConnectionFailed_refreshes() async throws { + let serviceSpy = Spy() + let store = TestStore( + initialState: SearchStationReducer.State( + locationStatus: .authorized(nearestStation: nil) + ), + reducer: SearchStationReducer() + ) { + $0.locationService.manuallyRefreshStatus = { + await serviceSpy.call() + } + } + + await store.send(.locationAction) { + $0.locationStatus = .determining + } + + let calls = await serviceSpy.calls + XCTAssertEqual(calls, 1) + } + + func test_locationAction_whenDenied_showsSettings() async throws { + let serviceSpy = Spy() + let store = TestStore( + initialState: SearchStationReducer.State( + locationStatus: .denied + ), + reducer: SearchStationReducer() + ) { + $0.settingsService.openSettings = { + await serviceSpy.call() + } + } + + await store.send(.locationAction) + + let calls = await serviceSpy.calls + XCTAssertEqual(calls, 1) + } + func test_task_observesLocation() async throws { let statuses: [LocationStatus] = [ .notYetAskedForAuthorization,