diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 035f01a..4c61dbb 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,13 +5,13 @@ on: - 'develop' jobs: build: - runs-on: macos-12 + runs-on: macos-14 steps: - - uses: swift-actions/setup-swift@v1 + - uses: swift-actions/setup-swift@v2 - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.0.1' - - uses: actions/checkout@v3 + xcode-version: latest-stable + - uses: actions/checkout@v4 - uses: maierj/fastlane-action@v2.2.0 with: lane: 'tests' diff --git a/Package.swift b/Package.swift index 76bc612..1a1162f 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tca: Target.Dependency = .product(name: "ComposableArchitecture", package: "swift-composable-architecture") -let tcaCoreLocation: Target.Dependency = .product(name: "ComposableCoreLocation", package: "composable-core-location") +let perception: Target.Dependency = .product(name: "Perception", package: "swift-perception") +let dependencies: Target.Dependency = .product(name: "Dependencies", package: "swift-dependencies") let package = Package( name: "uv-today-ios", @@ -16,8 +16,8 @@ let package = Package( .library(name: "UVClient", targets: ["UVClient"]) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "0.43.0"), - .package(url: "https://github.com/pointfreeco/composable-core-location", exact: "0.2.0"), + .package(url: "https://github.com/pointfreeco/swift-perception", exact: "1.1.5"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", exact: "1.2.2") ], targets: [ .target( @@ -26,14 +26,14 @@ let package = Package( "LocationManager", "Models", "UVClient", - tca, - tcaCoreLocation + perception, + dependencies ] ), .target( name: "LocationManager", dependencies: [ - tcaCoreLocation + dependencies ] ), .target(name: "Models"), @@ -41,16 +41,7 @@ let package = Package( name: "UVClient", dependencies: [ "Models", - tca - ] - ), - .testTarget( - name: "AppFeatureTests", - dependencies: [ - "AppFeature", - "Models", - "UVClient", - tca + dependencies ] ), .testTarget( diff --git a/Sources/AppFeature/AppReducer.swift b/Sources/AppFeature/AppReducer.swift deleted file mode 100644 index 5b86444..0000000 --- a/Sources/AppFeature/AppReducer.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// AppReducer.swift -// swiftUV -// -// Created by Thomas Guilleminot on 03/08/2022. -// Copyright © 2022 Thomas Guilleminot. All rights reserved. -// - -import ComposableArchitecture -import ComposableCoreLocation -import LocationManager -import Models -import UVClient - -public struct AppReducer: ReducerProtocol { - public struct State: Equatable { - public var uvIndex: Index - public var cityName: String - public var weatherRequestInFlight: Bool - public var getCityNameRequestInFlight: Bool - public var attributionLogo: URL? - public var attributionLink: URL? - public var errorText: String - public var userLocation: Models.Location? - public var isRequestingCurrentLocation: Bool - public var hasAlreadyRequestLocation: Bool - public var isLocationRefused: Bool - - @BindableState public var shouldShowErrorPopup: Bool - - public init( - uvIndex: Index = 0, - cityName: String = "loading", - weatherRequestInFlight: Bool = false, - getCityNameRequestInFlight: Bool = false, - errorText: String = "", - userLocation: Models.Location? = nil, - isRequestingCurrentLocation: Bool = false, - hasAlreadyRequestLocation: Bool = false, - isLocationRefused: Bool = false, - shouldShowErrorPopup: Bool = false - ) { - self.uvIndex = uvIndex - self.cityName = cityName - self.weatherRequestInFlight = weatherRequestInFlight - self.getCityNameRequestInFlight = getCityNameRequestInFlight - self.errorText = errorText - self.userLocation = userLocation - self.isRequestingCurrentLocation = isRequestingCurrentLocation - self.hasAlreadyRequestLocation = hasAlreadyRequestLocation - self.isLocationRefused = isLocationRefused - self.shouldShowErrorPopup = shouldShowErrorPopup - } - } - - public enum Action: Equatable, BindableAction { - case getUVRequest - case getUVResponse(TaskResult) - case getCityNameResponse(TaskResult) - case getAttribution - case getAttributionResponse(TaskResult) - - case onAppear - case onDisappear - case locationManager(LocationManager.Action) - case binding(BindingAction) - } - - @Dependency(\.uvClient) public var uvClient: UVClient - @Dependency(\.locationManager) public var locationManager: LocationManager - - public init() {} - - public var body: some ReducerProtocol { - BindingReducer() - - Reduce { state, action in - switch action { - case .onAppear: - state.weatherRequestInFlight = true - state.getCityNameRequestInFlight = true - state.isRequestingCurrentLocation = true - state.isLocationRefused = false - - switch locationManager.authorizationStatus() { - case .notDetermined: - return .merge( - locationManager - .delegate() - .map(AppReducer.Action.locationManager), - - locationManager - .requestWhenInUseAuthorization() - .fireAndForget() - ) - - case .authorizedAlways, .authorizedWhenInUse: - return .merge( - locationManager - .delegate() - .map(AppReducer.Action.locationManager), - - locationManager - .requestLocation() - .fireAndForget() - ) - - case .restricted, .denied: - state.shouldShowErrorPopup = true - state.errorText = "app.error.localisationDisabled".localized - state.isLocationRefused = true - return .none - - @unknown default: - return .none - } - - case .onDisappear: - state.hasAlreadyRequestLocation = false - return .none - - case .getUVRequest: - state.weatherRequestInFlight = true - state.getCityNameRequestInFlight = true - - guard let location = state.userLocation else { - state.shouldShowErrorPopup = true - state.errorText = "app.error.couldNotLocalise".localized - return .none - } - - return .run { send in - async let fetchUV: Void = send( - .getUVResponse(TaskResult { try await uvClient.fetchUVIndex(UVClientRequest(lat: location.latitude, long: location.longitude)) }) - ) - - async let fetchCityName: Void = send( - .getCityNameResponse(TaskResult { try await uvClient.fetchCityName(location) }) - ) - - _ = await [fetchUV, fetchCityName] - } - - case .getUVResponse(.success(let index)): - state.weatherRequestInFlight = false - state.uvIndex = index - return .none - - case .getUVResponse(.failure(let error)): - state.weatherRequestInFlight = false - state.shouldShowErrorPopup = true - state.errorText = error.localizedDescription - state.uvIndex = 0 - return .none - - case .getCityNameResponse(.success(let city)): - state.getCityNameRequestInFlight = false - state.cityName = city - return .none - - case .getCityNameResponse(.failure): - state.getCityNameRequestInFlight = false - state.cityName = "app.label.unknown".localized - return .none - - case .getAttribution: - return .task { - await .getAttributionResponse(TaskResult { try await uvClient.fetchWeatherKitAttribution() }) - } - - case .getAttributionResponse(.success(let attribution)): - state.attributionLogo = attribution.logo - state.attributionLink = attribution.link - return .none - - case .getAttributionResponse(.failure): - return .none - - case .binding(\.$shouldShowErrorPopup): - state.shouldShowErrorPopup = false - return .none - - case .binding: - return .none - - case .locationManager: - return .none - } - } - ._printChanges() - - LocationReducer() - } -} diff --git a/Sources/AppFeature/ContentVIew.swift b/Sources/AppFeature/ContentVIew.swift index 357f747..595d151 100644 --- a/Sources/AppFeature/ContentVIew.swift +++ b/Sources/AppFeature/ContentVIew.swift @@ -6,23 +6,24 @@ // Copyright © 2022 Thomas Guilleminot. All rights reserved. // -import ComposableArchitecture -import ComposableCoreLocation +import Perception import SwiftUI public struct ContentView: View { - let store: StoreOf + @Environment(\.scenePhase) var scenePhase - public init(store: StoreOf) { - self.store = store + @Perception.Bindable private var viewModel: UVViewModel + + public init(viewModel: UVViewModel) { + self.viewModel = viewModel } public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in + WithPerceptionTracking { ZStack { Rectangle() - .animation(.easeIn(duration: 0.5), value: viewStore.uvIndex.associatedColor) - .foregroundColor(Color(viewStore.uvIndex.associatedColor)) + .animation(.easeIn(duration: 0.5), value: viewModel.uvIndex.associatedColor) + .foregroundColor(Color(viewModel.uvIndex.associatedColor)) .edgesIgnoringSafeArea(.all) VStack { @@ -30,7 +31,9 @@ public struct ContentView: View { Spacer() Button { - viewStore.send(.getUVRequest) + Task { + await viewModel.getUVRequest() + } } label: { Image(systemName: "arrow.clockwise") .resizable() @@ -39,31 +42,31 @@ public struct ContentView: View { .foregroundColor(.white) .padding(.trailing, 20) } - .disabled(viewStore.isLocationRefused) + .disabled(viewModel.isLocationRefused) } HStack { - Text(viewStore.cityName) + Text(viewModel.cityName) .padding(.top, 33) .font(.system(size: 38, weight: .bold, design: .rounded)) .foregroundColor(.white) .padding(.horizontal, 20) .lineLimit(1) .minimumScaleFactor(0.2) - .redacted(reason: viewStore.getCityNameRequestInFlight ? .placeholder : []) + .redacted(reason: viewModel.getCityNameRequestInFlight ? .placeholder : []) Spacer() } Spacer() - Text(String(viewStore.uvIndex)) + Text(String(viewModel.uvIndex)) .foregroundColor(.white) .font(.system(size: 80, weight: .semibold, design: .rounded)) - .redacted(reason: viewStore.weatherRequestInFlight ? .placeholder : []) + .redacted(reason: viewModel.weatherRequestInFlight ? .placeholder : []) Spacer() - if viewStore.isLocationRefused { + if viewModel.isLocationRefused { Button { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) } label: { @@ -75,13 +78,13 @@ public struct ContentView: View { .cornerRadius(8) } - Text(viewStore.uvIndex.associatedDescription) + Text(viewModel.uvIndex.associatedDescription) .padding(20) .foregroundColor(.white) .font(.system(size: 12)) - .redacted(reason: viewStore.weatherRequestInFlight ? .placeholder : []) + .redacted(reason: viewModel.weatherRequestInFlight ? .placeholder : []) - if let attributionLogo = viewStore.attributionLogo { + if let attributionLogo = viewModel.attributionLogo { AsyncImage(url: attributionLogo, content: { image in image .resizable() @@ -92,44 +95,31 @@ public struct ContentView: View { }) } - if let attributionLink = viewStore.attributionLink { + if let attributionLink = viewModel.attributionLink { Link(destination: attributionLink, label: { Text("Other data sources")}) .foregroundColor(.white) } } } - .alert(isPresented: viewStore.binding(\.$shouldShowErrorPopup)) { - Alert(title: Text("app.label.error"), message: Text(viewStore.errorText)) - } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in - viewStore.send(.onAppear) + .alert(isPresented: $viewModel.shouldShowErrorPopup) { + Alert(title: Text("app.label.error"), message: Text(viewModel.errorText)) } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in - viewStore.send(.onDisappear) + .onChange(of: scenePhase) { value in + switch value { + case .active: viewModel.onAppear() + case .inactive, .background: viewModel.onDisappear() + @unknown default: break + } } .task { if #available(iOS 16.0, *) { - viewStore.send(.getAttribution) + await viewModel.getAtribution() } } } } } -#if DEBUG -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView( - store: Store( - initialState: AppReducer.State( - uvIndex: 6, - cityName: "Gueugnon", - weatherRequestInFlight: false, - getCityNameRequestInFlight: false - ), - reducer: AppReducer() - ) - ) - } +#Preview { + ContentView(viewModel: UVViewModel()) } -#endif diff --git a/Sources/AppFeature/LocationReducer.swift b/Sources/AppFeature/LocationReducer.swift deleted file mode 100644 index baa4ab7..0000000 --- a/Sources/AppFeature/LocationReducer.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// LocationReducer.swift -// swiftUV -// -// Created by Thomas Guilleminot on 31/07/2022. -// Copyright © 2022 Thomas Guilleminot. All rights reserved. -// - -import ComposableArchitecture -import ComposableCoreLocation -import Foundation -import Models - -struct LocationReducer: ReducerProtocol { - typealias State = AppReducer.State - typealias Action = AppReducer.Action - - func reduce(into state: inout AppReducer.State, action: AppReducer.Action) -> Effect { - switch action { - case let .locationManager(.didUpdateLocations(locations)): - guard state.hasAlreadyRequestLocation == false else { - return .none - } - - state.isRequestingCurrentLocation = false - guard let location = locations.first else { return .none } - state.userLocation = Models.Location(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) - state.hasAlreadyRequestLocation = true - return .task { .getUVRequest } - - default: - return .none - } - } -} diff --git a/Sources/AppFeature/UVViewModel.swift b/Sources/AppFeature/UVViewModel.swift new file mode 100644 index 0000000..94c5140 --- /dev/null +++ b/Sources/AppFeature/UVViewModel.swift @@ -0,0 +1,132 @@ +// +// AppReducer.swift +// swiftUV +// +// Created by Thomas Guilleminot on 03/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import Combine +import Dependencies +import Foundation +import LocationManager +import Models +import Perception +import UVClient + +@Perceptible +public class UVViewModel { + @PerceptionIgnored @Dependency(\.uvClient) public var uvClient: UVClient + @PerceptionIgnored @Dependency(\.locationManager) public var locationManager: LocationManager + + public var uvIndex: Index = 0 + public var cityName = "loading" + public var weatherRequestInFlight = false + public var getCityNameRequestInFlight = false + public var errorText = "" + public var userLocation: Models.Location? = nil + public var isLocationRefused = false + public var shouldShowErrorPopup = false + + public var attributionLogo: URL? = nil + public var attributionLink: URL? = nil + + private var cancellables = Set() + + public init() { + locationManager.location.sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] location in + self?.userLocation = Models.Location( + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude + ) + + Task { [self] in + await self?.getUVRequest() + } + } + ) + .store(in: &cancellables) + + locationManager.authorizationStatus + .sink(receiveValue: { [weak self] result in + switch result { + case .success(let status): + switch status { + case .authorizedAlways, .authorizedWhenInUse: + self?.locationManager.getLocation() + self?.isLocationRefused = false + case .denied, .restricted: + self?.shouldShowErrorPopup = true + self?.errorText = "app.error.localisationDisabled".localized + self?.isLocationRefused = true + case .notDetermined: + self?.weatherRequestInFlight = false + self?.getCityNameRequestInFlight = false + @unknown default: break + } + case .failure: + break + } + }) + .store(in: &cancellables) + } + + public func onAppear() { + guard weatherRequestInFlight == false && getCityNameRequestInFlight == false else { return } + + showLoading() + + isLocationRefused = false + shouldShowErrorPopup = false + + locationManager.requestAuthorisation() + } + + public func onDisappear() { + locationManager.stop() + } + + public func getUVRequest() async { + showLoading() + + guard let location = userLocation else { + shouldShowErrorPopup = true + errorText = "app.error.couldNotLocalise".localized + return + } + + async let fetchUV = uvClient.fetchUVIndex(UVClientRequest(lat: location.latitude, long: location.longitude)) + async let fetchCityName = uvClient.fetchCityName(location) + + do { + let (index, name) = try await (fetchUV, fetchCityName) + + weatherRequestInFlight = false + getCityNameRequestInFlight = false + uvIndex = index + cityName = name + } catch let error { + weatherRequestInFlight = false + getCityNameRequestInFlight = false + shouldShowErrorPopup = true + errorText = error.localizedDescription + cityName = "app.label.unknown".localized + uvIndex = 0 + } + } + + public func getAtribution() async { + do { + let attribution = try await uvClient.fetchWeatherKitAttribution() + attributionLogo = attribution.logo + attributionLink = attribution.link + } catch {} + } + + private func showLoading() { + weatherRequestInFlight = true + getCityNameRequestInFlight = true + } +} diff --git a/Sources/LocationManager/LocationManager.swift b/Sources/LocationManager/LocationManager.swift new file mode 100644 index 0000000..04a6dd2 --- /dev/null +++ b/Sources/LocationManager/LocationManager.swift @@ -0,0 +1,62 @@ +// +// File.swift +// +// +// Created by Thomas Guilleminot on 20/04/2024. +// + +import Combine +import CoreLocation +import Foundation + +public enum LocationManagerError: Error { + case notAuthorised +} + +public class LocationManager: NSObject, CLLocationManagerDelegate { + public var authorizationStatus = PassthroughSubject, Never>() + public var location = PassthroughSubject() + + private let locationManager = CLLocationManager() + + public override init() { + super.init() + authorizationStatus.send(.success(locationManager.authorizationStatus)) + locationManager.delegate = self + } + + public func requestAuthorisation() { + switch locationManager.authorizationStatus { + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + case .authorizedAlways, .authorizedWhenInUse, .denied, .restricted: + authorizationStatus.send(.success(locationManager.authorizationStatus)) + @unknown default: + break + } + } + + public func getLocation() { + locationManager.requestLocation() + } + + public func stop() { + locationManager.stopUpdatingLocation() + } +} + +extension LocationManager { + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let loc = locations.first { + location.send(loc) + } + } + + public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + authorizationStatus.send(.success(manager.authorizationStatus)) + } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { + + } +} diff --git a/Sources/LocationManager/LocationManagerDependency.swift b/Sources/LocationManager/LocationManagerDependency.swift index a23ca62..f0e4b8d 100644 --- a/Sources/LocationManager/LocationManagerDependency.swift +++ b/Sources/LocationManager/LocationManagerDependency.swift @@ -5,12 +5,11 @@ // Created by Thomas Guilleminot on 16/10/2022. // -import ComposableCoreLocation +import Dependencies import Foundation private enum LocationManagerKey: DependencyKey { - static var liveValue = LocationManager.live - static var testValue = LocationManager.failing + static var liveValue = LocationManager() } public extension DependencyValues { diff --git a/Sources/UVClient/UVClient+Mock.swift b/Sources/UVClient/UVClient+Mock.swift deleted file mode 100644 index 35beebc..0000000 --- a/Sources/UVClient/UVClient+Mock.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Mock.swift -// swiftUV -// -// Created by Thomas Guilleminot on 03/08/2022. -// Copyright © 2022 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import ComposableArchitecture -import XCTestDynamicOverlay -import Models - -#if DEBUG -extension UVClient { - public static let mock = Self( - fetchUVIndex: { _ in - 5 - }, - fetchCityName: { _ in - "Gueugnon" - }, - fetchWeatherKitAttribution: { - AttributionResponse( - logo: URL(string:"https://www.logo.com")!, - link: URL(string: "https://www.link.com")! - ) - } - ) -} - -extension UVClientKey { - static let testValue = UVClient.unimplemented - static let previewValue = UVClient.mock -} - -extension UVClient { - public static let unimplemented = Self( - fetchUVIndex: XCTUnimplemented("\(Self.self).fetchUVIndex)"), - fetchCityName: XCTUnimplemented("\(Self.self).fetchCityName"), - fetchWeatherKitAttribution: XCTUnimplemented("\(Self.self).fetchWeatherKitAttribution") - ) -} -#endif diff --git a/Sources/UVClient/UVClient.swift b/Sources/UVClient/UVClient.swift index 930f27d..0f3bb36 100644 --- a/Sources/UVClient/UVClient.swift +++ b/Sources/UVClient/UVClient.swift @@ -6,7 +6,6 @@ // Copyright © 2022 Thomas Guilleminot. All rights reserved. // -import ComposableArchitecture import Models public struct UVClientRequest { diff --git a/Tests/AppFeatureTests/AppReducerTests.swift b/Tests/AppFeatureTests/AppReducerTests.swift deleted file mode 100644 index 7d0defd..0000000 --- a/Tests/AppFeatureTests/AppReducerTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// AppReducerTests.swift -// swiftUVTests -// -// Created by Thomas Guilleminot on 04/08/2022. -// Copyright © 2022 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import ComposableArchitecture -import XCTest -import AppFeature -import Models - -@MainActor -class AppReducerTests: XCTestCase { - func testGetUVRequestSuccess() async { - let store = TestStore( - initialState: - AppReducer.State( - getCityNameRequestInFlight: true, - userLocation: Location(latitude: 12.0, longitude: 13.0) - ), - reducer: AppReducer() - ) - - store.dependencies.uvClient.fetchUVIndex = { _ in 5 } - store.dependencies.uvClient.fetchCityName = { _ in "Gueugnon" } - - await store.send(.getUVRequest) { - $0.weatherRequestInFlight = true - $0.getCityNameRequestInFlight = true - } - - await store.receive(.getUVResponse(.success(5))) { - $0.weatherRequestInFlight = false - $0.uvIndex = 5 - } - - await store.receive(.getCityNameResponse(.success("Gueugnon"))) { - $0.getCityNameRequestInFlight = false - $0.cityName = "Gueugnon" - } - } - - func testGetUVRequestFailure() async { - let store = TestStore( - initialState: - AppReducer.State( - getCityNameRequestInFlight: true, - userLocation: Location(latitude: 12.0, longitude: 13.0) - ), - reducer: AppReducer() - ) - - store.dependencies.uvClient.fetchUVIndex = { _ in throw "test" } - store.dependencies.uvClient.fetchCityName = { _ in throw "no city" } - - await store.send(.getUVRequest) { - $0.weatherRequestInFlight = true - $0.getCityNameRequestInFlight = true - } - - await store.receive(.getUVResponse(.failure("test"))) { - $0.weatherRequestInFlight = false - $0.shouldShowErrorPopup = true - $0.errorText = "test" - $0.uvIndex = 0 - } - - await store.receive(.getCityNameResponse(.failure("no city"))) { - $0.getCityNameRequestInFlight = false - $0.cityName = "app.label.unknown".localized - } - } - - func testGetUVRequestFailureNoLocation() { - let store = TestStore( - initialState: - AppReducer.State( - getCityNameRequestInFlight: true - ), - reducer: AppReducer() - ) - - store.send(.getUVRequest) { - $0.weatherRequestInFlight = true - $0.shouldShowErrorPopup = true - $0.errorText = "app.error.couldNotLocalise".localized - } - } - - func testGetAttributionSuccess() async { - let store = TestStore( - initialState: .init(), - reducer: AppReducer() - ) - - let url1 = URL(string: "https://www.url1.com")! - let url2 = URL(string: "https://www.url2.com")! - let attribution = AttributionResponse(logo: url1, link: url2) - - store.dependencies.uvClient.fetchWeatherKitAttribution = { attribution } - - await store.send(.getAttribution) - await store.receive(.getAttributionResponse(.success(attribution))) { - $0.attributionLogo = attribution.logo - $0.attributionLink = attribution.link - } - } - - func testGetAttributionFailure() async { - let store = TestStore( - initialState: .init(), - reducer: AppReducer() - ) - - store.dependencies.uvClient.fetchWeatherKitAttribution = { throw UVError.noAttributionAvailable } - - await store.send(.getAttribution) - await store.receive(.getAttributionResponse(.failure(UVError.noAttributionAvailable))) - } - - func testDismissErrorPopup() { - let store = TestStore( - initialState: - AppReducer.State( - shouldShowErrorPopup: true - ), - reducer: AppReducer() - ) - - store.send(.set(\.$shouldShowErrorPopup, true)) { - $0.shouldShowErrorPopup = false - } - } - - func testOnDisappear() { - let store = TestStore( - initialState: AppReducer.State(hasAlreadyRequestLocation: true), - reducer: AppReducer() - ) - - store.send(.onDisappear) { - $0.hasAlreadyRequestLocation = false - } - } -} - -extension String: Error {} -extension String: LocalizedError { - public var errorDescription: String? { self } -} diff --git a/swiftUV/fastlane/Fastfile b/swiftUV/fastlane/Fastfile index 77d80c1..6efe15f 100644 --- a/swiftUV/fastlane/Fastfile +++ b/swiftUV/fastlane/Fastfile @@ -18,9 +18,10 @@ default_platform(:ios) platform :ios do desc "Run tests" lane :tests do - run_tests( + scan( devices: "iPhone 13 Pro", - scheme: "swiftUV" + scheme: "swiftUV", + xcargs: "-skipMacroValidation" ) end end diff --git a/swiftUV/swiftUV.xcodeproj/project.pbxproj b/swiftUV/swiftUV.xcodeproj/project.pbxproj index 62aebfb..d6eaee7 100644 --- a/swiftUV/swiftUV.xcodeproj/project.pbxproj +++ b/swiftUV/swiftUV.xcodeproj/project.pbxproj @@ -3,19 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ E989314D28C14F2C00F973FD /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = E989314C28C14F2C00F973FD /* Models */; }; E989314F28C14F3300F973FD /* UVClient in Frameworks */ = {isa = PBXBuildFile; productRef = E989314E28C14F3300F973FD /* UVClient */; }; E989315128C1541700F973FD /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = E989315028C1541700F973FD /* AppFeature */; }; - E9A22D3D2896C7A3006DC054 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */; }; E9AF34A3289C606D00C0F763 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AF34A2289C606D00C0F763 /* App.swift */; }; E9CB9A32291AEA71008A44DB /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = E9CB9A31291AEA71008A44DB /* FirebaseAnalyticsWithoutAdIdSupport */; }; E9CB9A34291AEA71008A44DB /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = E9CB9A33291AEA71008A44DB /* FirebaseCrashlytics */; }; E9CB9A36291AEAB3008A44DB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9CB9A35291AEAB3008A44DB /* GoogleService-Info.plist */; }; - E9E29EC42896E0A7000DE660 /* ComposableCoreLocation in Frameworks */ = {isa = PBXBuildFile; productRef = E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */; }; EBCAA0451E7C30F000DC2E9D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = EBCAA0471E7C30F000DC2E9D /* Localizable.strings */; }; EBF4B7571E7951D400B7A616 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EBF4B7561E7951D400B7A616 /* Assets.xcassets */; }; EBF4B75A1E7951D400B7A616 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EBF4B7581E7951D400B7A616 /* LaunchScreen.storyboard */; }; @@ -60,9 +58,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E9E29EC42896E0A7000DE660 /* ComposableCoreLocation in Frameworks */, E989315128C1541700F973FD /* AppFeature in Frameworks */, - E9A22D3D2896C7A3006DC054 /* ComposableArchitecture in Frameworks */, E989314D28C14F2C00F973FD /* Models in Frameworks */, E9CB9A32291AEA71008A44DB /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */, E9CB9A34291AEA71008A44DB /* FirebaseCrashlytics in Frameworks */, @@ -182,8 +178,6 @@ ); name = swiftUV; packageProductDependencies = ( - E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */, - E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */, E989314C28C14F2C00F973FD /* Models */, E989314E28C14F3300F973FD /* UVClient */, E989315028C1541700F973FD /* AppFeature */, @@ -200,8 +194,9 @@ EBF4B7441E7951D300B7A616 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0910; - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1530; ORGANIZATIONNAME = "Thomas Guilleminot"; TargetAttributes = { EBDA88921FAE5C89009514AB = { @@ -228,8 +223,6 @@ ); mainGroup = EBF4B7431E7951D300B7A616; packageReferences = ( - E9A22D3B2896C7A3006DC054 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, - E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */, E9CB9A30291AEA71008A44DB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); productRefGroup = EBF4B74D1E7951D400B7A616 /* Products */; @@ -266,6 +259,7 @@ /* Begin PBXShellScriptBuildPhase section */ EBC31C9F1FAF7FC6004DC13C /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 12; files = ( ); @@ -530,7 +524,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.zlatan.swiftUV; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -558,7 +552,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.zlatan.swiftUV; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -605,28 +599,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - E9A22D3B2896C7A3006DC054 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; - requirement = { - kind = exactVersion; - version = 0.43.0; - }; - }; E9CB9A30291AEA71008A44DB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = exactVersion; - version = 10.1.0; - }; - }; - E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/composable-core-location"; - requirement = { - kind = exactVersion; - version = 0.2.0; + version = 10.24.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -644,11 +622,6 @@ isa = XCSwiftPackageProductDependency; productName = AppFeature; }; - E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */ = { - isa = XCSwiftPackageProductDependency; - package = E9A22D3B2896C7A3006DC054 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = ComposableArchitecture; - }; E9CB9A31291AEA71008A44DB /* FirebaseAnalyticsWithoutAdIdSupport */ = { isa = XCSwiftPackageProductDependency; package = E9CB9A30291AEA71008A44DB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; @@ -659,11 +632,6 @@ package = E9CB9A30291AEA71008A44DB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; - E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */ = { - isa = XCSwiftPackageProductDependency; - package = E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */; - productName = ComposableCoreLocation; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = EBF4B7441E7951D300B7A616 /* Project object */; diff --git a/swiftUV/swiftUV.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swiftUV/swiftUV.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 29bdf3d..61d511f 100644 --- a/swiftUV/swiftUV.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/swiftUV/swiftUV.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "abseil-cpp-swiftpm", + "identity" : "abseil-cpp-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", + "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", - "version" : "0.20220203.2" + "revision" : "748c7837511d0e6a507737353af268484e1745e2", + "version" : "1.2024011601.1" } }, { - "identity" : "boringssl-swiftpm", + "identity" : "app-check", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/boringssl-SwiftPM.git", + "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", - "version" : "0.9.1" + "revision" : "7d2688de038d5484866d835acb47b379722d610e", + "version" : "10.19.0" } }, { @@ -23,17 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "aa3e575929f2bcc5bad012bd2575eae716cbcdf7", - "version" : "0.8.0" - } - }, - { - "identity" : "composable-core-location", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/composable-core-location", - "state" : { - "revision" : "95d45d71a4e9c21bd97b02189d2d8342d41ea527", - "version" : "0.2.0" + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } }, { @@ -41,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "f0926478fda955aef0dfbf99263b5c24b2ca5db4", - "version" : "10.1.0" + "revision" : "42eae77a0af79e9c3f41df04a23c76f05cfdda77", + "version" : "10.24.0" } }, { @@ -50,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "71eb6700dd53a851473c48d392f00a3ab26699a6", - "version" : "10.1.0" + "revision" : "51ba746a9d51a4bd0774b68499b0c73ef6e8570d", + "version" : "10.24.0" } }, { @@ -59,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "5056b15c5acbb90cd214fe4d6138bdf5a740e5a8", - "version" : "9.2.0" + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" } }, { @@ -68,17 +59,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "68ea347bdb1a69e2d2ae2e25cd085b6ef92f64cb", - "version" : "7.9.0" + "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", + "version" : "7.13.1" } }, { - "identity" : "grpc-ios", + "identity" : "grpc-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/grpc/grpc-ios.git", + "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", - "version" : "1.44.3-grpc" + "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", + "version" : "1.62.2" } }, { @@ -90,13 +81,22 @@ "version" : "2.3.0" } }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, { "identity" : "leveldb", "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { - "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", - "version" : "1.22.2" + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" } }, { @@ -113,17 +113,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb", - "version" : "2.1.1" + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" } }, { - "identity" : "swift-case-paths", + "identity" : "swift-clocks", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", + "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "a09839348486db8866f85a727b8550be1d671c50", - "version" : "0.9.1" + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" } }, { @@ -131,35 +131,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", - "version" : "1.0.2" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { - "identity" : "swift-composable-architecture", + "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "5bd450a8ac6a802f82d485bac219cbfacffa69fb", - "version" : "0.43.0" + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" } }, { - "identity" : "swift-custom-dump", + "identity" : "swift-dependencies", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", + "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7", - "version" : "0.5.0" + "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", + "version" : "1.2.2" } }, { - "identity" : "swift-identified-collections", + "identity" : "swift-perception", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", + "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "2d6b7ffcc67afd9077fac5e5a29bcd6d39b71076", - "version" : "0.4.0" + "revision" : "052bd30a2079d9eb2400f0bc66707cc93c015152", + "version" : "1.1.5" } }, { @@ -167,8 +167,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "ab3a58b7209a17d781c0d1dbb3e1ff3da306bae8", - "version" : "1.20.3" + "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" } }, { @@ -176,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260", - "version" : "0.4.0" + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" } } ], diff --git a/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme b/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme index 7aeaad2..d7187bf 100644 --- a/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme +++ b/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme @@ -1,6 +1,6 @@ diff --git a/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUVTests.xcscheme b/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUVTests.xcscheme index b852088..0f2dd7c 100644 --- a/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUVTests.xcscheme +++ b/swiftUV/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUVTests.xcscheme @@ -1,6 +1,6 @@