diff --git a/.gitignore b/.gitignore index 3444a8e..076e240 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ swiftUVTests/GeneratedMocks.swift # Secrets .env.default +.DS_Store \ No newline at end of file diff --git a/Podfile b/Podfile index 0a490e8..8c25960 100644 --- a/Podfile +++ b/Podfile @@ -8,16 +8,7 @@ target 'swiftUV' do use_frameworks! # Pods for swiftUV - pod 'SwiftLint', '= 0.40.3' - pod 'ZLogger', '= 1.1.0' - pod 'Resolver', '= 1.1.4' - pod 'ExytePopupView', '= 0.0.10' - pod 'Bugsnag' -end - -target 'swiftUVTests' do - use_frameworks! - pod 'Cuckoo', '= 1.4.1' + pod 'SwiftLint', '= 0.48.0' end plugin 'cocoapods-keys', { diff --git a/Podfile.lock b/Podfile.lock index 233ba4b..47a67a8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,45 +1,23 @@ PODS: - - Bugsnag (5.22.2) - - Cuckoo (1.4.1): - - Cuckoo/Swift (= 1.4.1) - - Cuckoo/Swift (1.4.1) - - ExytePopupView (0.0.10) - Keys (1.0.1) - - Resolver (1.1.4) - - SwiftLint (0.40.3) - - ZLogger (1.1.0) + - SwiftLint (0.48.0) DEPENDENCIES: - - Bugsnag - - Cuckoo (= 1.4.1) - - ExytePopupView (= 0.0.10) - Keys (from `Pods/CocoaPodsKeys`) - - Resolver (= 1.1.4) - - SwiftLint (= 0.40.3) - - ZLogger (= 1.1.0) + - SwiftLint (= 0.48.0) SPEC REPOS: trunk: - - Bugsnag - - Cuckoo - - ExytePopupView - - Resolver - SwiftLint - - ZLogger EXTERNAL SOURCES: Keys: :path: Pods/CocoaPodsKeys SPEC CHECKSUMS: - Bugsnag: 3e9e2b563f1e99ce447de123c65e851bd109fa9f - Cuckoo: 4625f7f54d9bb880123270e8969898d6c1d036b5 - ExytePopupView: 14a98f924a7201bc5492e2a961da11045b17eca8 Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 - Resolver: 1f4c046255cbadfa97094692e16e940d4329779f - SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 - ZLogger: d53f23ce00f0f94a97271495f9f2957e5255938f + SwiftLint: 284cea64b6187c5d6bd83e9a548a64104d546447 -PODFILE CHECKSUM: f40c331718a023a0e25457206562975d769fd478 +PODFILE CHECKSUM: 05532806187603e2d3d823648e92d6ec2366b8e7 -COCOAPODS: 1.9.3 +COCOAPODS: 1.11.3 diff --git a/swiftUV.xcodeproj/project.pbxproj b/swiftUV.xcodeproj/project.pbxproj index 3395bfa..5726781 100644 --- a/swiftUV.xcodeproj/project.pbxproj +++ b/swiftUV.xcodeproj/project.pbxproj @@ -3,38 +3,29 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ - 5F98968E285F31ECA77CF6F1 /* Pods_swiftUVTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFBB7043C945108FD94D44AF /* Pods_swiftUVTests.framework */; }; 6538403F53A4E29309F2F084 /* Pods_swiftUV.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59AC0D333DE20DF5F86F5B5C /* Pods_swiftUV.framework */; }; - E96F2B682542D62C00C38427 /* AppComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96F2B672542D62C00C38427 /* AppComponents.swift */; }; - E9805F31254731EF009E196C /* UVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F2E254731EF009E196C /* UVView.swift */; }; - E9805F32254731EF009E196C /* UVViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F2F254731EF009E196C /* UVViewFactory.swift */; }; - E9805F33254731EF009E196C /* ViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F30254731EF009E196C /* ViewFactory.swift */; }; - E9805F37254731F9009E196C /* UVViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F36254731F9009E196C /* UVViewModel.swift */; }; - E9805F3B25473206009E196C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F3A25473206009E196C /* SceneDelegate.swift */; }; - E9805F4525473302009E196C /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9805F4425473302009E196C /* GeneratedMocks.swift */; }; - E9833665253C966500EF7510 /* URLFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9833664253C966500EF7510 /* URLFactoryTests.swift */; }; + E931727F2896C5E1003EC842 /* ContentVIew.swift in Sources */ = {isa = PBXBuildFile; fileRef = E931727E2896C5E1003EC842 /* ContentVIew.swift */; }; + E938D4E1289B0C7000B8C968 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E938D4E0289B0C7000B8C968 /* AppReducer.swift */; }; + E938D4E7289B0DCD00B8C968 /* Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = E938D4E6289B0DCD00B8C968 /* Live.swift */; }; + E938D4E9289B0E0000B8C968 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E938D4E8289B0E0000B8C968 /* Mock.swift */; }; E9A116352322E96C004DC9FB /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E9A116342322E96C004DC9FB /* Colors.xcassets */; }; - E9C726CE22C577920040E5BC /* APIWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C726CD22C577920040E5BC /* APIWorker.swift */; }; - E9C726D022C577B80040E5BC /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C726CF22C577B80040E5BC /* NetworkSession.swift */; }; - E9C726D222C5786C0040E5BC /* APIWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C726D122C5786C0040E5BC /* APIWorkerTests.swift */; }; - E9FF14F6253D8F100098BE0C /* UVServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF14F5253D8F100098BE0C /* UVServiceTests.swift */; }; - E9FF14FC253D90E70098BE0C /* URLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF14FB253D90E70098BE0C /* URLFactory.swift */; }; - E9FF1504253D9A8B0098BE0C /* TaskExecutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF1503253D9A8B0098BE0C /* TaskExecutable.swift */; }; + E9A22D3D2896C7A3006DC054 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */; }; + E9AF349C289C4FE600C0F763 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AF349B289C4FE600C0F763 /* AppReducerTests.swift */; }; + E9AF349F289C58DD00C0F763 /* Bugsnag in Frameworks */ = {isa = PBXBuildFile; productRef = E9AF349E289C58DD00C0F763 /* Bugsnag */; }; + E9AF34A3289C606D00C0F763 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AF34A2289C606D00C0F763 /* App.swift */; }; + E9E29EC02896CD7C000DE660 /* UVClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E29EBF2896CD7C000DE660 /* UVClient.swift */; }; + E9E29EC42896E0A7000DE660 /* ComposableCoreLocation in Frameworks */ = {isa = PBXBuildFile; productRef = E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */; }; + E9E29EC62896E346000DE660 /* LocationReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E29EC52896E346000DE660 /* LocationReducer.swift */; }; EBB0615B21A1CF9A0084E975 /* IndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB0615A21A1CF9A0084E975 /* IndexTests.swift */; }; - EBB061CF21A1DC8B0084E975 /* UVService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061CD21A1DC8B0084E975 /* UVService.swift */; }; - EBB061D021A1DC8B0084E975 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061CE21A1DC8B0084E975 /* LocationService.swift */; }; EBB061D821A1DC980084E975 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D121A1DC980084E975 /* Constants.swift */; }; EBB061D921A1DC980084E975 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D221A1DC980084E975 /* String.swift */; }; - EBB061DA21A1DC980084E975 /* UVError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D321A1DC980084E975 /* UVError.swift */; }; EBB061DB21A1DC980084E975 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D421A1DC980084E975 /* Location.swift */; }; EBB061DC21A1DC980084E975 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D521A1DC980084E975 /* Forecast.swift */; }; EBB061DD21A1DC980084E975 /* Index.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061D621A1DC980084E975 /* Index.swift */; }; - EBB061E221A1DCA60084E975 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB061E021A1DCA60084E975 /* AppDelegate.swift */; }; - EBB061EF21A1DCD60084E975 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EBB061E521A1DCD50084E975 /* OpenSans-Semibold.ttf */; }; 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 */; }; @@ -51,37 +42,24 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 091ACF6A8F16618D0F5A2B41 /* Pods-swiftUVTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swiftUVTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-swiftUVTests/Pods-swiftUVTests.debug.xcconfig"; sourceTree = ""; }; 4F32DAD95650411C27D7704A /* Pods-swiftUV.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swiftUV.debug.xcconfig"; path = "Pods/Target Support Files/Pods-swiftUV/Pods-swiftUV.debug.xcconfig"; sourceTree = ""; }; 59AC0D333DE20DF5F86F5B5C /* Pods_swiftUV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_swiftUV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 84B83E859B0ADFAB9D8946E1 /* Pods-swiftUV.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swiftUV.release.xcconfig"; path = "Pods/Target Support Files/Pods-swiftUV/Pods-swiftUV.release.xcconfig"; sourceTree = ""; }; - 8B91A999F78A3A6A33A70B5A /* Pods-swiftUVTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-swiftUVTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-swiftUVTests/Pods-swiftUVTests.release.xcconfig"; sourceTree = ""; }; - E96F2B672542D62C00C38427 /* AppComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppComponents.swift; path = app/AppComponents.swift; sourceTree = ""; }; - E9805F2E254731EF009E196C /* UVView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UVView.swift; path = app/Views/UVView.swift; sourceTree = ""; }; - E9805F2F254731EF009E196C /* UVViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UVViewFactory.swift; path = app/Views/UVViewFactory.swift; sourceTree = ""; }; - E9805F30254731EF009E196C /* ViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewFactory.swift; path = app/Views/ViewFactory.swift; sourceTree = ""; }; - E9805F36254731F9009E196C /* UVViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UVViewModel.swift; path = app/ViewModels/UVViewModel.swift; sourceTree = ""; }; - E9805F3A25473206009E196C /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = app/SceneDelegate.swift; sourceTree = ""; }; - E9805F4425473302009E196C /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; - E9833664253C966500EF7510 /* URLFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFactoryTests.swift; sourceTree = ""; }; + E931727E2896C5E1003EC842 /* ContentVIew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContentVIew.swift; path = app/Views/ContentVIew.swift; sourceTree = ""; }; + E938D4E0289B0C7000B8C968 /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppReducer.swift; path = app/Views/AppReducer.swift; sourceTree = ""; }; + E938D4E6289B0DCD00B8C968 /* Live.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Live.swift; sourceTree = ""; }; + E938D4E8289B0E0000B8C968 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; E9A116342322E96C004DC9FB /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; - E9C726CD22C577920040E5BC /* APIWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = APIWorker.swift; path = app/Workers/APIWorker.swift; sourceTree = ""; }; - E9C726CF22C577B80040E5BC /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NetworkSession.swift; path = app/Workers/NetworkSession.swift; sourceTree = ""; }; - E9C726D122C5786C0040E5BC /* APIWorkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIWorkerTests.swift; sourceTree = ""; }; - E9FF14F5253D8F100098BE0C /* UVServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UVServiceTests.swift; sourceTree = ""; }; - E9FF14FB253D90E70098BE0C /* URLFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLFactory.swift; path = app/Factories/URLFactory.swift; sourceTree = ""; }; - E9FF1503253D9A8B0098BE0C /* TaskExecutable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TaskExecutable.swift; path = app/Models/TaskExecutable.swift; sourceTree = ""; }; + E9AF349B289C4FE600C0F763 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; + E9AF34A2289C606D00C0F763 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + E9E29EBF2896CD7C000DE660 /* UVClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UVClient.swift; sourceTree = ""; }; + E9E29EC52896E346000DE660 /* LocationReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LocationReducer.swift; path = app/Views/LocationReducer.swift; sourceTree = ""; }; EBB0615A21A1CF9A0084E975 /* IndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexTests.swift; sourceTree = ""; }; - EBB061CD21A1DC8B0084E975 /* UVService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UVService.swift; path = app/Services/UVService.swift; sourceTree = ""; }; - EBB061CE21A1DC8B0084E975 /* LocationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LocationService.swift; path = app/Services/LocationService.swift; sourceTree = ""; }; EBB061D121A1DC980084E975 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = app/Models/Constants.swift; sourceTree = ""; }; EBB061D221A1DC980084E975 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = String.swift; path = app/Models/String.swift; sourceTree = ""; }; - EBB061D321A1DC980084E975 /* UVError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UVError.swift; path = app/Models/UVError.swift; sourceTree = ""; }; EBB061D421A1DC980084E975 /* Location.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Location.swift; path = app/Models/Location.swift; sourceTree = ""; }; EBB061D521A1DC980084E975 /* Forecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Forecast.swift; path = app/Models/Forecast.swift; sourceTree = ""; }; EBB061D621A1DC980084E975 /* Index.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Index.swift; path = app/Models/Index.swift; sourceTree = ""; }; - EBB061E021A1DCA60084E975 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = app/AppDelegate.swift; sourceTree = ""; }; - EBB061E521A1DCD50084E975 /* OpenSans-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Semibold.ttf"; path = "Resources/Fonts/OpenSans-Semibold.ttf"; sourceTree = ""; }; EBC1904E1E8EEAB600B9F2EC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchScreen.strings; sourceTree = ""; }; EBC1904F1E8EEAB600B9F2EC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; EBCAA0461E7C30F000DC2E9D /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; @@ -92,7 +70,6 @@ EBF4B7561E7951D400B7A616 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; EBF4B7591E7951D400B7A616 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; EBF4B75B1E7951D400B7A616 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EFBB7043C945108FD94D44AF /* Pods_swiftUVTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_swiftUVTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -100,7 +77,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5F98968E285F31ECA77CF6F1 /* Pods_swiftUVTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +84,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E9E29EC42896E0A7000DE660 /* ComposableCoreLocation in Frameworks */, + E9A22D3D2896C7A3006DC054 /* ComposableArchitecture in Frameworks */, + E9AF349F289C58DD00C0F763 /* Bugsnag in Frameworks */, 6538403F53A4E29309F2F084 /* Pods_swiftUV.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -119,7 +98,6 @@ isa = PBXGroup; children = ( 59AC0D333DE20DF5F86F5B5C /* Pods_swiftUV.framework */, - EFBB7043C945108FD94D44AF /* Pods_swiftUVTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -129,56 +107,31 @@ children = ( 4F32DAD95650411C27D7704A /* Pods-swiftUV.debug.xcconfig */, 84B83E859B0ADFAB9D8946E1 /* Pods-swiftUV.release.xcconfig */, - 091ACF6A8F16618D0F5A2B41 /* Pods-swiftUVTests.debug.xcconfig */, - 8B91A999F78A3A6A33A70B5A /* Pods-swiftUVTests.release.xcconfig */, ); name = Pods; sourceTree = ""; }; - E9C726CC22C576B30040E5BC /* Workers */ = { + E938D4E5289B0D0500B8C968 /* UVClient */ = { isa = PBXGroup; children = ( - E9C726CD22C577920040E5BC /* APIWorker.swift */, - E9C726CF22C577B80040E5BC /* NetworkSession.swift */, + E9E29EBF2896CD7C000DE660 /* UVClient.swift */, + E938D4E6289B0DCD00B8C968 /* Live.swift */, + E938D4E8289B0E0000B8C968 /* Mock.swift */, ); - name = Workers; - sourceTree = ""; - }; - E9F8B50A253C4616004CEDF2 /* Factories */ = { - isa = PBXGroup; - children = ( - E9FF14FB253D90E70098BE0C /* URLFactory.swift */, - ); - name = Factories; + name = UVClient; + path = app/UVClient; sourceTree = ""; }; EBB061462198956B0084E975 /* Views */ = { isa = PBXGroup; children = ( - E9805F2E254731EF009E196C /* UVView.swift */, - E9805F2F254731EF009E196C /* UVViewFactory.swift */, - E9805F30254731EF009E196C /* ViewFactory.swift */, + E931727E2896C5E1003EC842 /* ContentVIew.swift */, + E938D4E0289B0C7000B8C968 /* AppReducer.swift */, + E9E29EC52896E346000DE660 /* LocationReducer.swift */, ); name = Views; sourceTree = ""; }; - EBB06147219895700084E975 /* ViewModels */ = { - isa = PBXGroup; - children = ( - E9805F36254731F9009E196C /* UVViewModel.swift */, - ); - name = ViewModels; - sourceTree = ""; - }; - EBB0614A219897140084E975 /* Services */ = { - isa = PBXGroup; - children = ( - EBB061CE21A1DC8B0084E975 /* LocationService.swift */, - EBB061CD21A1DC8B0084E975 /* UVService.swift */, - ); - name = Services; - sourceTree = ""; - }; EBCAA0421E7C30CE00DC2E9D /* Translation */ = { isa = PBXGroup; children = ( @@ -190,12 +143,9 @@ EBDA88941FAE5C89009514AB /* swiftUVTests */ = { isa = PBXGroup; children = ( - E9805F4425473302009E196C /* GeneratedMocks.swift */, EBDA88971FAE5C89009514AB /* Info.plist */, EBB0615A21A1CF9A0084E975 /* IndexTests.swift */, - E9FF14F5253D8F100098BE0C /* UVServiceTests.swift */, - E9C726D122C5786C0040E5BC /* APIWorkerTests.swift */, - E9833664253C966500EF7510 /* URLFactoryTests.swift */, + E9AF349B289C4FE600C0F763 /* AppReducerTests.swift */, ); path = swiftUVTests; sourceTree = ""; @@ -236,15 +186,10 @@ EBF4B7631E79917000B7A616 /* app */ = { isa = PBXGroup; children = ( + E938D4E5289B0D0500B8C968 /* UVClient */, EBB061462198956B0084E975 /* Views */, - EBB06147219895700084E975 /* ViewModels */, - EBB0614A219897140084E975 /* Services */, - E9C726CC22C576B30040E5BC /* Workers */, EBF4B77A1E799BC600B7A616 /* Models */, - E9F8B50A253C4616004CEDF2 /* Factories */, - EBB061E021A1DCA60084E975 /* AppDelegate.swift */, - E9805F3A25473206009E196C /* SceneDelegate.swift */, - E96F2B672542D62C00C38427 /* AppComponents.swift */, + E9AF34A2289C606D00C0F763 /* App.swift */, ); name = app; sourceTree = ""; @@ -253,19 +198,10 @@ isa = PBXGroup; children = ( EBCAA0421E7C30CE00DC2E9D /* Translation */, - EBF4B7651E79919100B7A616 /* Fonts */, ); name = Resources; sourceTree = ""; }; - EBF4B7651E79919100B7A616 /* Fonts */ = { - isa = PBXGroup; - children = ( - EBB061E521A1DCD50084E975 /* OpenSans-Semibold.ttf */, - ); - name = Fonts; - sourceTree = ""; - }; EBF4B77A1E799BC600B7A616 /* Models */ = { isa = PBXGroup; children = ( @@ -273,9 +209,7 @@ EBB061D521A1DC980084E975 /* Forecast.swift */, EBB061D621A1DC980084E975 /* Index.swift */, EBB061D421A1DC980084E975 /* Location.swift */, - E9FF1503253D9A8B0098BE0C /* TaskExecutable.swift */, EBB061D221A1DC980084E975 /* String.swift */, - EBB061D321A1DC980084E975 /* UVError.swift */, ); name = Models; sourceTree = ""; @@ -287,11 +221,9 @@ isa = PBXNativeTarget; buildConfigurationList = EBDA889A1FAE5C89009514AB /* Build configuration list for PBXNativeTarget "swiftUVTests" */; buildPhases = ( - 6E12D50FD8431F6DF4E052E4 /* [CP] Check Pods Manifest.lock */, EBDA888F1FAE5C89009514AB /* Sources */, EBDA88901FAE5C89009514AB /* Frameworks */, EBDA88911FAE5C89009514AB /* Resources */, - 9724060F794018FE06B0FACD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -299,6 +231,8 @@ EBDA88991FAE5C89009514AB /* PBXTargetDependency */, ); name = swiftUVTests; + packageProductDependencies = ( + ); productName = swiftUVTests; productReference = EBDA88931FAE5C89009514AB /* swiftUVTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -313,13 +247,17 @@ EBF4B74A1E7951D300B7A616 /* Resources */, 8DAFA6781280E0C278DF29AA /* [CP] Embed Pods Frameworks */, EBC31C9F1FAF7FC6004DC13C /* SwiftLint */, - EBB061F721A1DDFA0084E975 /* Cuckoo */, ); buildRules = ( ); dependencies = ( ); name = swiftUV; + packageProductDependencies = ( + E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */, + E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */, + E9AF349E289C58DD00C0F763 /* Bugsnag */, + ); productName = swiftUV; productReference = EBF4B74C1E7951D400B7A616 /* swiftUV.app */; productType = "com.apple.product-type.application"; @@ -336,9 +274,7 @@ TargetAttributes = { EBDA88921FAE5C89009514AB = { CreatedOnToolsVersion = 9.1; - DevelopmentTeam = XDX42S5M9N; LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; TestTargetID = EBF4B74B1E7951D300B7A616; }; EBF4B74B1E7951D300B7A616 = { @@ -359,6 +295,11 @@ fr, ); mainGroup = EBF4B7431E7951D300B7A616; + packageReferences = ( + E9A22D3B2896C7A3006DC054 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */, + E9AF349D289C58DD00C0F763 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */, + ); productRefGroup = EBF4B74D1E7951D400B7A616 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -385,31 +326,12 @@ EBCAA0451E7C30F000DC2E9D /* Localizable.strings in Resources */, EBF4B7571E7951D400B7A616 /* Assets.xcassets in Resources */, E9A116352322E96C004DC9FB /* Colors.xcassets in Resources */, - EBB061EF21A1DCD60084E975 /* OpenSans-Semibold.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 6E12D50FD8431F6DF4E052E4 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-swiftUVTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 8DAFA6781280E0C278DF29AA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -418,62 +340,16 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-swiftUV/Pods-swiftUV-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Keys-framework/Keys.framework", - "${BUILT_PRODUCTS_DIR}/Bugsnag/Bugsnag.framework", - "${BUILT_PRODUCTS_DIR}/ExytePopupView/ExytePopupView.framework", - "${BUILT_PRODUCTS_DIR}/Resolver/Resolver.framework", - "${BUILT_PRODUCTS_DIR}/ZLogger/ZLogger.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Keys.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Bugsnag.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExytePopupView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Resolver.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZLogger.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-swiftUV/Pods-swiftUV-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 9724060F794018FE06B0FACD /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-swiftUVTests/Pods-swiftUVTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Keys-framework/Keys.framework", - "${BUILT_PRODUCTS_DIR}/Cuckoo/Cuckoo.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Keys.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cuckoo.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-swiftUVTests/Pods-swiftUVTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - EBB061F721A1DDFA0084E975 /* Cuckoo */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 12; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = Cuckoo; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Define output file. Change \"$PROJECT_DIR/Tests\" to your test's root source folder, if it's not the default name.\nOUTPUT_FILE=\"$PROJECT_DIR/swiftUVTests/GeneratedMocks.swift\"\necho \"Generated Mocks File = $OUTPUT_FILE\"\n\n# Define input directory. Change \"$PROJECT_DIR\" to your project's root source folder, if it's not the default name.\nINPUT_DIR=\"$PROJECT_DIR\"\necho \"Mocks Input Directory = $INPUT_DIR\"\n\n# Generate mock files, include as many input files as you'd like to create mocks for.\n${PODS_ROOT}/Cuckoo/run generate --testable \"$PROJECT_NAME\" \\\n--output \"${OUTPUT_FILE}\" \\\n\"$INPUT_DIR/swiftUV/app/Presenters/UVPresenterImpl.swift\" \\\n\"$INPUT_DIR/swiftUV/app/Services/LocationService.swift\" \\\n\"$INPUT_DIR/swiftUV/app/Services/UVService.swift\" \\\n\"$INPUT_DIR/swiftUV/app/Workers/APIWorker.swift\" \\\n\"$INPUT_DIR/swiftUV/app/Workers/NetworkSession.swift\" \\\n\"$INPUT_DIR/swiftUV/app/Factories/URLFactory.swift\" \\\n\n# ... and so forth\n\n# After running once, locate `GeneratedMocks.swift` and drag it into your Xcode test target group.\n"; - }; EBC31C9F1FAF7FC6004DC13C /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 12; @@ -513,11 +389,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E9AF349C289C4FE600C0F763 /* AppReducerTests.swift in Sources */, EBB0615B21A1CF9A0084E975 /* IndexTests.swift in Sources */, - E9C726D222C5786C0040E5BC /* APIWorkerTests.swift in Sources */, - E9FF14F6253D8F100098BE0C /* UVServiceTests.swift in Sources */, - E9833665253C966500EF7510 /* URLFactoryTests.swift in Sources */, - E9805F4525473302009E196C /* GeneratedMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -525,25 +398,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EBB061CF21A1DC8B0084E975 /* UVService.swift in Sources */, - E9805F31254731EF009E196C /* UVView.swift in Sources */, + E9E29EC02896CD7C000DE660 /* UVClient.swift in Sources */, EBB061DC21A1DC980084E975 /* Forecast.swift in Sources */, - E9FF1504253D9A8B0098BE0C /* TaskExecutable.swift in Sources */, - EBB061E221A1DCA60084E975 /* AppDelegate.swift in Sources */, - E9805F32254731EF009E196C /* UVViewFactory.swift in Sources */, + E938D4E1289B0C7000B8C968 /* AppReducer.swift in Sources */, + E938D4E7289B0DCD00B8C968 /* Live.swift in Sources */, + E938D4E9289B0E0000B8C968 /* Mock.swift in Sources */, + E9AF34A3289C606D00C0F763 /* App.swift in Sources */, EBB061DB21A1DC980084E975 /* Location.swift in Sources */, - E9C726CE22C577920040E5BC /* APIWorker.swift in Sources */, - E9FF14FC253D90E70098BE0C /* URLFactory.swift in Sources */, - E9C726D022C577B80040E5BC /* NetworkSession.swift in Sources */, - E9805F37254731F9009E196C /* UVViewModel.swift in Sources */, - EBB061D021A1DC8B0084E975 /* LocationService.swift in Sources */, EBB061D821A1DC980084E975 /* Constants.swift in Sources */, - E96F2B682542D62C00C38427 /* AppComponents.swift in Sources */, EBB061D921A1DC980084E975 /* String.swift in Sources */, - E9805F33254731EF009E196C /* ViewFactory.swift in Sources */, - E9805F3B25473206009E196C /* SceneDelegate.swift in Sources */, - EBB061DA21A1DC980084E975 /* UVError.swift in Sources */, + E931727F2896C5E1003EC842 /* ContentVIew.swift in Sources */, EBB061DD21A1DC980084E975 /* Index.swift in Sources */, + E9E29EC62896E346000DE660 /* LocationReducer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -582,7 +448,6 @@ /* Begin XCBuildConfiguration section */ EBDA889B1FAE5C89009514AB /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 091ACF6A8F16618D0F5A2B41 /* Pods-swiftUVTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -596,14 +461,20 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = XDX42S5M9N; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = swiftUVTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.swiftUVTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -613,7 +484,6 @@ }; EBDA889C1FAE5C89009514AB /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8B91A999F78A3A6A33A70B5A /* Pods-swiftUVTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -627,14 +497,20 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = XDX42S5M9N; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = swiftUVTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.swiftUVTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/swiftUV.app/swiftUV"; @@ -747,7 +623,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; @@ -760,13 +637,16 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = XDX42S5M9N; INFOPLIST_FILE = swiftUV/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.0.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.zlatan.swiftUV; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "ff2f93fb-7946-4ffc-a9c8-b1a55133e36e"; @@ -783,12 +663,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = XDX42S5M9N; INFOPLIST_FILE = swiftUV/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.0.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.zlatan.swiftUV; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "f7c749cb-6205-4c81-895e-2b74e79d6421"; @@ -831,6 +714,51 @@ defaultConfigurationName = Release; }; /* 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.38.3; + }; + }; + E9AF349D289C58DD00C0F763 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/bugsnag/bugsnag-cocoa"; + requirement = { + kind = exactVersion; + version = 6.21.0; + }; + }; + E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/composable-core-location"; + requirement = { + kind = exactVersion; + version = 0.2.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + E9A22D3C2896C7A3006DC054 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = E9A22D3B2896C7A3006DC054 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; + E9AF349E289C58DD00C0F763 /* Bugsnag */ = { + isa = XCSwiftPackageProductDependency; + package = E9AF349D289C58DD00C0F763 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */; + productName = Bugsnag; + }; + E9E29EC32896E0A7000DE660 /* ComposableCoreLocation */ = { + isa = XCSwiftPackageProductDependency; + package = E9E29EC22896E0A7000DE660 /* XCRemoteSwiftPackageReference "composable-core-location" */; + productName = ComposableCoreLocation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = EBF4B7441E7951D300B7A616 /* Project object */; } diff --git a/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme b/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme index 986a9a7..fab2342 100644 --- a/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme +++ b/swiftUV.xcodeproj/xcshareddata/xcschemes/swiftUV.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/swiftUV.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/swiftUV.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/swiftUV.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/swiftUV.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swiftUV.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..ad118c9 --- /dev/null +++ b/swiftUV.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,86 @@ +{ + "pins" : [ + { + "identity" : "bugsnag-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bugsnag/bugsnag-cocoa", + "state" : { + "revision" : "1a5afefae616dbafcab3ad93c8bed0db1c841bc0", + "version" : "6.21.0" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "0ba3a562716efabb585ccb169450c5389107286b", + "version" : "0.6.0" + } + }, + { + "identity" : "composable-core-location", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/composable-core-location", + "state" : { + "revision" : "95d45d71a4e9c21bd97b02189d2d8342d41ea527", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "a09839348486db8866f85a727b8550be1d671c50", + "version" : "0.9.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "0a38f2c860a64a005afeb8a6fe186eb2818c9a3f", + "version" : "0.38.3" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7", + "version" : "0.5.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "2d6b7ffcc67afd9077fac5e5a29bcd6d39b71076", + "version" : "0.4.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "ef8e14e7ce1c0c304c644c6ba365d06c468ded6b", + "version" : "0.3.3" + } + } + ], + "version" : 2 +} diff --git a/swiftUV/App.swift b/swiftUV/App.swift new file mode 100644 index 0000000..bd8db3a --- /dev/null +++ b/swiftUV/App.swift @@ -0,0 +1,42 @@ +// +// App.swift +// swiftUV +// +// Created by Thomas Guilleminot on 04/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture +import Bugsnag +import Keys + +@main +struct SwiftUVApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + ContentView( + store: Store( + initialState: AppState(), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .live, + dispatchQueue: .main, + locationManager: .live + ) + ) + ) + } + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + #if RELEASE + Bugsnag.start(withApiKey: SwiftUVKeys().bugsnagApiKey) + #endif + return true + } +} diff --git a/swiftUV/Base.lproj/Localizable.strings b/swiftUV/Base.lproj/Localizable.strings index eb554cc..b5ad037 100644 --- a/swiftUV/Base.lproj/Localizable.strings +++ b/swiftUV/Base.lproj/Localizable.strings @@ -10,6 +10,7 @@ "app.label.city" = "Ville"; "app.label.unknown" = "Inconnue"; "app.label.error" = "Erreur"; +"app.error.openSettings" = "Authorisez la localisation dans les paramètres"; // Messages "app.message.downloading" = "Téléchargement des données en cours"; diff --git a/swiftUV/Info.plist b/swiftUV/Info.plist index 0ebb15a..de7173f 100644 --- a/swiftUV/Info.plist +++ b/swiftUV/Info.plist @@ -46,38 +46,6 @@ IndexUV would like to access your location to give you the UV index from your city NSLocationWhenInUseUsageDescription IndexUV would like to access your location to give you the UV index from your city - UIAppFonts - - OpenSans-Bold.ttf - OpenSans-BoldItalic.ttf - OpenSans-ExtraBold.ttf - OpenSans-ExtraBoldItalic.ttf - OpenSans-Italic.ttf - OpenSans-Light.ttf - OpenSans-LightItalic.ttf - OpenSans-Regular.ttf - OpenSans-Semibold.ttf - OpenSans-SemiboldItalic.ttf - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UILaunchStoryboardName - LaunchScreen - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/swiftUV/Resources/Fonts/OpenSans-Semibold.ttf b/swiftUV/Resources/Fonts/OpenSans-Semibold.ttf deleted file mode 100755 index 1a7679e..0000000 Binary files a/swiftUV/Resources/Fonts/OpenSans-Semibold.ttf and /dev/null differ diff --git a/swiftUV/app/AppComponents.swift b/swiftUV/app/AppComponents.swift deleted file mode 100644 index 8b85ee2..0000000 --- a/swiftUV/app/AppComponents.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AppComponent+Injection.swift -// swiftUV -// -// Created by Thomas Guilleminot on 23/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import Resolver -import Keys -import CoreLocation - -extension Resolver: ResolverRegistering { - public static func registerAllServices() { - registerViewModule() - registerViewModelModule() - registerNetworkModule() - registerServiceModule() - registerAppModule() - registerLocationModule() - } -} - -extension Resolver { - public static func registerViewModule() { - register(UVViewFactory.self) { - UVViewFactory(with: resolve(UVViewModel.self)) - } - } -} - -extension Resolver { - public static func registerViewModelModule() { - register(UVViewModel.self) { - UVViewModel( - locationService: resolve(LocationService.self), - uvService: resolve(UVService.self) - ) - } - } -} - -extension Resolver { - public static func registerNetworkModule() { - register(NetworkSession.self) { - let session = URLSessionConfiguration.default - session.timeoutIntervalForRequest = 10.0 - return URLSession(configuration: session) - } - - register(APIWorker.self) { APIWorkerImpl(with: resolve(NetworkSession.self)) } - } -} - -extension Resolver { - public static func registerServiceModule() { - register(UVService.self) { - UVServiceImpl(apiExecutor: resolve(APIWorker.self), urlFactory: resolve(URLFactory.self)) - } - } -} - -extension Resolver { - public static func registerLocationModule() { - register(CLLocationManager.self) { CLLocationManager() } - register(LocationService.self) { - LocationService(locationManager: resolve(CLLocationManager.self)) - } - } -} - -extension Resolver { - public static func registerAppModule() { - register(URLFactory.self) { URLFactory(with: SwiftUVKeys().openWeatherMapApiKey) } - } -} diff --git a/swiftUV/app/AppDelegate.swift b/swiftUV/app/AppDelegate.swift deleted file mode 100644 index 06c0159..0000000 --- a/swiftUV/app/AppDelegate.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AppDelegate.swift -// swiftUV -// -// Created by Thomas Guilleminot on 15/03/2017. -// Copyright © 2017 Thomas Guilleminot. All rights reserved. -// - -import UIKit -import ZLogger -import Keys -import Bugsnag - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - #if RELEASE - Bugsnag.start(withApiKey: SwiftUVKeys().bugsnagApiKey) - #endif - - return true - } - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - - } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions - // (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information - // to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - -} diff --git a/swiftUV/app/Factories/URLFactory.swift b/swiftUV/app/Factories/URLFactory.swift deleted file mode 100644 index a1c3d8e..0000000 --- a/swiftUV/app/Factories/URLFactory.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// URLFactory.swift -// swiftUV -// -// Created by Thomas Guilleminot on 18/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation - -class URLFactory { - - private let apiKey: String - - init(with apiKey: String) { - self.apiKey = apiKey - } - - func createUVURL(lat: Double, lon: Double) -> URL { - URL(string: K.Api.baseURL + String(format: K.Api.Endpoints.getUV, arguments: [lat, lon, self.apiKey]))! - } - -} diff --git a/swiftUV/app/Models/Constants.swift b/swiftUV/app/Models/Constants.swift index eaf407e..aa991e9 100644 --- a/swiftUV/app/Models/Constants.swift +++ b/swiftUV/app/Models/Constants.swift @@ -7,14 +7,11 @@ // struct K { - struct Api { static let baseURL = "https://api.openweathermap.org/data/2.5/" struct Endpoints { static let getUV = "uvi?lat=%.4f&lon=%.4f&appid=%@" } - } - } diff --git a/swiftUV/app/Models/Forecast.swift b/swiftUV/app/Models/Forecast.swift index e462f79..fc7e797 100644 --- a/swiftUV/app/Models/Forecast.swift +++ b/swiftUV/app/Models/Forecast.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Thomas Guilleminot. All rights reserved. // -struct Forecast: Codable, TaskExecutable, Equatable { +struct Forecast: Codable, Equatable { let lat: Double let lon: Double let dateIso: String diff --git a/swiftUV/app/Models/Index.swift b/swiftUV/app/Models/Index.swift index dac6f9a..f74b3ee 100644 --- a/swiftUV/app/Models/Index.swift +++ b/swiftUV/app/Models/Index.swift @@ -17,12 +17,7 @@ extension Index { case 0: return UIColor.systemBlue case 1, 2: return UIColor.systemGreen case 3, 4, 5: return UIColor.systemYellow - case 6, 7: - if #available(iOS 11.0, *) { - return UIColor(named: "LightRed")! - } else { - return UIColor.systemRed - } + case 6, 7: return UIColor(named: "LightRed")! case 8, 9, 10: return UIColor.systemRed case 11, 12, 13, 14: return UIColor.systemPurple default : return UIColor.black diff --git a/swiftUV/app/Models/Location.swift b/swiftUV/app/Models/Location.swift index 573b32f..06312a9 100644 --- a/swiftUV/app/Models/Location.swift +++ b/swiftUV/app/Models/Location.swift @@ -6,12 +6,7 @@ // Copyright © 2018 Thomas Guilleminot. All rights reserved. // -struct Location { - +struct Location: Equatable { let latitude: Double let longitude: Double - let city: String - } - -extension Location: Equatable {} diff --git a/swiftUV/app/Models/TaskExecutable.swift b/swiftUV/app/Models/TaskExecutable.swift deleted file mode 100644 index 12606d4..0000000 --- a/swiftUV/app/Models/TaskExecutable.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// TaskExecutable.swift -// swiftUV -// -// Created by Thomas Guilleminot on 19/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation - -protocol TaskExecutable: Codable {} diff --git a/swiftUV/app/Models/UVError.swift b/swiftUV/app/Models/UVError.swift deleted file mode 100644 index 7fb8149..0000000 --- a/swiftUV/app/Models/UVError.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UVError.swift -// swiftUV -// -// Created by Zlatan on 16/11/2018. -// Copyright © 2018 Thomas Guilleminot. All rights reserved. -// - -enum UVError: Error, Equatable { - - case urlNotValid - case noData(String) - case couldNotDecodeJSON - case customError(String) - - var localizedDescription: String { - switch self { - case .urlNotValid: - return "Url is invalid" - case .noData(let message): - return message - case .couldNotDecodeJSON: - return "Could not parse JSON" - case .customError(let message): - return message - } - } - -} diff --git a/swiftUV/app/SceneDelegate.swift b/swiftUV/app/SceneDelegate.swift deleted file mode 100644 index abb3143..0000000 --- a/swiftUV/app/SceneDelegate.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SceneDelegate.swift -// swiftUV -// -// Created by Thomas Guilleminot on 26/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import SwiftUI -import Resolver - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - - // Use a UIHostingController as window root view controller. - if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: Resolver.resolve(UVViewFactory.self).make()) - self.window = window - window.makeKeyAndVisible() - } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - UIApplication.shared.applicationIconBadgeNumber = 0 - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - - } - -} diff --git a/swiftUV/app/Services/LocationService.swift b/swiftUV/app/Services/LocationService.swift deleted file mode 100644 index 18c7996..0000000 --- a/swiftUV/app/Services/LocationService.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// LocationService.swift -// swiftUV -// -// Created by Zlatan on 11/11/2018. -// Copyright © 2018 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import CoreLocation -import ZLogger -import Resolver - -protocol LocationServiceDelegate: class { - func didUpdateLocation(_ location: Location) - func didFailUpdateLocation() - - func didAcceptLocationService() - func didRefuseLocationService() -} - -class LocationService: NSObject { - - private let locationManager: CLLocationManager - weak var delegate: LocationServiceDelegate? - - private var authorizationStatus = CLAuthorizationStatus.notDetermined - - init(locationManager: CLLocationManager) { - self.locationManager = locationManager - super.init() - self.requestAuthorization() - } - - private func requestAuthorization() { - self.locationManager.delegate = self - self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters - self.locationManager.requestWhenInUseAuthorization() - } - - func searchLocation() { - guard CLLocationManager.locationServicesEnabled() else { - self.delegate?.didRefuseLocationService() - return - } - - guard self.authorizationStatus != .notDetermined else { return } - - guard self.authorizationStatus == .authorizedAlways || - self.authorizationStatus == .authorizedWhenInUse else { - self.delegate?.didRefuseLocationService() - return - } - - self.locationManager.requestLocation() - } - -} - -extension LocationService: CLLocationManagerDelegate { - - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - self.locationManager.stopUpdatingLocation() - - guard let latitude = manager.location?.coordinate.latitude, - let longitude = manager.location?.coordinate.longitude else { - return - } - - let location = CLLocation(latitude: latitude, longitude: longitude) - let geoCoder = CLGeocoder() - geoCoder.reverseGeocodeLocation(location) { placemarks, error in - guard error == nil else { - self.delegate?.didFailUpdateLocation() - return - } - - let customLocation = Location(latitude: latitude, longitude: longitude, city: placemarks?.first?.locality ?? "app.label.unknown") - self.delegate?.didUpdateLocation(customLocation) - } - - ZLogger.info(message: "location = \(location)") - } - - func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - self.delegate?.didFailUpdateLocation() - } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - self.authorizationStatus = manager.authorizationStatus - - switch manager.authorizationStatus { - case .authorizedAlways, .authorizedWhenInUse: - self.delegate?.didAcceptLocationService() - case .denied, .restricted: - self.delegate?.didRefuseLocationService() - default: () - } - } - -} diff --git a/swiftUV/app/Services/UVService.swift b/swiftUV/app/Services/UVService.swift deleted file mode 100644 index 211a98a..0000000 --- a/swiftUV/app/Services/UVService.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// UVService.swift -// swiftUV -// -// Created by Thomas Guilleminot on 16/11/2018. -// Copyright © 2018 Thomas Guilleminot. All rights reserved. -// - -import ZLogger -import Combine -import Resolver - -protocol UVService { - func getUVIndex(from location: Location) -> AnyPublisher -} - -class UVServiceImpl: UVService { - - private let apiExecutor: APIWorker - private let urlFactory: URLFactory - - init(apiExecutor: APIWorker, urlFactory: URLFactory) { - self.apiExecutor = apiExecutor - self.urlFactory = urlFactory - } - - func getUVIndex(from location: Location) -> AnyPublisher { - let url = self.urlFactory.createUVURL(lat: location.latitude, lon: location.longitude) - - ZLogger.info(message: "\(url)") - - return self.apiExecutor.request(for: Forecast.self, at: url, method: .get, parameters: [:]) - .map { forecast in - Int(forecast.value.rounded()) - } - .eraseToAnyPublisher() - } -} diff --git a/swiftUV/app/UVClient/Live.swift b/swiftUV/app/UVClient/Live.swift new file mode 100644 index 0000000..39d1b9e --- /dev/null +++ b/swiftUV/app/UVClient/Live.swift @@ -0,0 +1,43 @@ +// +// Live.swift +// swiftUV +// +// Created by Thomas Guilleminot on 03/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import Foundation +import ComposableArchitecture +import CoreLocation +import Keys + +extension UVClient { + static let live = UVClient( + fetchUVIndex: { request in + var request = URLRequest(url: URL(string: K.Api.baseURL + String(format: K.Api.Endpoints.getUV, arguments: [request.lat, request.long, SwiftUVKeys().openWeatherMapApiKey]))!) + request.httpMethod = "GET" + request.httpBody = try? JSONSerialization.data(withJSONObject: [:], options: []) + + return URLSession.shared.dataTaskPublisher(for: request.url!) + .map { data, _ in data } + .decode(type: Forecast.self, decoder: JSONDecoder()) + .mapError { error in Failure(errorDescription: error.localizedDescription) } + .eraseToEffect() + }, + fetchCityName: { location in + Effect.future { callback in + let geocoder = CLGeocoder() + let clLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) + geocoder.reverseGeocodeLocation(clLocation) { placemarks, error in + guard error == nil else { + callback(.failure(Failure(errorDescription: error!.localizedDescription))) + return + } + + let cityName = placemarks?.first?.locality ?? "Unknown" + callback(.success(cityName)) + } + } + } + ) +} diff --git a/swiftUV/app/UVClient/Mock.swift b/swiftUV/app/UVClient/Mock.swift new file mode 100644 index 0000000..783c951 --- /dev/null +++ b/swiftUV/app/UVClient/Mock.swift @@ -0,0 +1,23 @@ +// +// Mock.swift +// swiftUV +// +// Created by Thomas Guilleminot on 03/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import Foundation +import ComposableArchitecture + +#if DEBUG +extension UVClient { + static let mock = Self( + fetchUVIndex: { _ in + Effect(value: Forecast(lat: 12.0, lon: 13.0, dateIso: "32323", date: 1234, value: 5)) + }, + fetchCityName: { _ in + Effect(value: "Gueugnon") + } + ) +} +#endif diff --git a/swiftUV/app/UVClient/UVClient.swift b/swiftUV/app/UVClient/UVClient.swift new file mode 100644 index 0000000..2e7d4e2 --- /dev/null +++ b/swiftUV/app/UVClient/UVClient.swift @@ -0,0 +1,23 @@ +// +// WeatherClient.swift +// swiftUV +// +// Created by Thomas Guilleminot on 31/07/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import ComposableArchitecture + +struct UVClientRequest { + let lat: Double + let long: Double +} + +struct UVClient { + var fetchUVIndex: (UVClientRequest) -> Effect + var fetchCityName: (Location) -> Effect + + struct Failure: Error, Equatable { + let errorDescription: String + } +} diff --git a/swiftUV/app/ViewModels/UVViewModel.swift b/swiftUV/app/ViewModels/UVViewModel.swift deleted file mode 100644 index 5f58395..0000000 --- a/swiftUV/app/ViewModels/UVViewModel.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// UVViewModel.swift -// swiftUV -// -// Created by Thomas Guilleminot on 26/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import Combine -import ZLogger - -class UVViewModel: ObservableObject { - - @Published var cityLabel = "Loading" - @Published var index: Index = 0 - @Published var showLoading = false - @Published var showErrorPopup = false - @Published var errorText = "" - - private let locationService: LocationService - private let uvService: UVService - - private var location: Location? - private var cancelable: AnyCancellable? - - init(locationService: LocationService, uvService: UVService) { - self.locationService = locationService - self.uvService = uvService - - self.locationService.delegate = self - } - - func searchLocation() { - self.showLoading = true - self.locationService.searchLocation() - } - - func getUVIndex() { - guard let location = self.location else { - self.searchLocation() - return - } - - self.cancelable = self.uvService.getUVIndex(from: location) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .finished: break - case .failure(let error): - self.showLoading = false - self.showErrorPopup = true - self.errorText = error.localizedDescription - } - } receiveValue: { value in - ZLogger.info(message: "Did receive index with value : \(value)") - self.index = value - self.showLoading = false - } - } - - private func showError(message: String) { - self.showLoading = false - self.showErrorPopup = true - self.errorText = message - self.index = Index(-1) - } - -} - -extension UVViewModel: LocationServiceDelegate { - - func didUpdateLocation(_ location: Location) { - self.location = location - self.cityLabel = location.city - - self.getUVIndex() - } - - func didFailUpdateLocation() { - self.showError(message: "app.error.couldNotLocalise".localized) - } - - func didAcceptLocationService() { - self.searchLocation() - } - - func didRefuseLocationService() { - self.showError(message: "app.error.localisationDisabled".localized) - } - -} diff --git a/swiftUV/app/Views/AppReducer.swift b/swiftUV/app/Views/AppReducer.swift new file mode 100644 index 0000000..c781081 --- /dev/null +++ b/swiftUV/app/Views/AppReducer.swift @@ -0,0 +1,144 @@ +// +// AppReducer.swift +// swiftUV +// +// Created by Thomas Guilleminot on 03/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import ComposableArchitecture +import ComposableCoreLocation + +struct AppState: Equatable { + var uvIndex: Index = 0 + var cityName = "loading" + var weatherRequestInFlight = false + var getCityNameRequestInFlight = false + var shouldShowErrorPopup = false + var errorText = "" + + var userLocation: Location? + var isRequestingCurrentLocation = false + var isLocationRefused = false +} + +enum AppAction: Equatable { + case getUVRequest + case getUVResponse(Result) + case getCityNameResponse(Result) + case dismissErrorPopup + + case onAppear + case locationManager(LocationManager.Action) +} + +struct AppEnvironment { + var uvClient: UVClient + var dispatchQueue: AnySchedulerOf + var locationManager: LocationManager +} + +let appReducer = Reducer { state, action, environment in + switch action { + case .onAppear: + state.weatherRequestInFlight = true + state.getCityNameRequestInFlight = true + state.isRequestingCurrentLocation = true + state.isLocationRefused = false + + switch environment.locationManager.authorizationStatus() { + case .notDetermined: + state.isRequestingCurrentLocation = true + + return .merge( + environment.locationManager + .delegate() + .map(AppAction.locationManager), + + environment.locationManager + .requestWhenInUseAuthorization() + .fireAndForget() + ) + + case .authorizedAlways, .authorizedWhenInUse: + return .merge( + environment.locationManager + .delegate() + .map(AppAction.locationManager), + + environment.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 .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 environment.uvClient + .fetchUVIndex(UVClientRequest(lat: location.latitude, long: location.longitude)) + .receive(on: environment.dispatchQueue) + .catchToEffect() + .map { .getUVResponse($0) } + + case .getUVResponse(.success(let forecast)): + state.weatherRequestInFlight = false + state.uvIndex = Int(forecast.value) + + guard let location = state.userLocation else { + state.cityName = "app.label.unknown".localized + return .none + } + + return environment.uvClient + .fetchCityName(location) + .receive(on: environment.dispatchQueue) + .catchToEffect() + .map { .getCityNameResponse($0) } + + case .getUVResponse(.failure(let error)): + state.weatherRequestInFlight = false + state.shouldShowErrorPopup = true + state.errorText = error.errorDescription + state.uvIndex = 0 + return .none + + case .getCityNameResponse(.success(let city)): + state.getCityNameRequestInFlight = false + state.cityName = city + return .none + + case .getCityNameResponse(.failure(let error)): + state.getCityNameRequestInFlight = false + state.cityName = "app.label.unknown".localized + return .none + + case .dismissErrorPopup: + state.shouldShowErrorPopup = false + return .none + + case .locationManager: + return .none + } +} +.combined( + with: + locationManagerReducer + .pullback(state: \.self, action: /AppAction.self, environment: { $0 }) +) diff --git a/swiftUV/app/Views/ContentVIew.swift b/swiftUV/app/Views/ContentVIew.swift new file mode 100644 index 0000000..15b4b5f --- /dev/null +++ b/swiftUV/app/Views/ContentVIew.swift @@ -0,0 +1,112 @@ +// +// ContentVIew.swift +// swiftUV +// +// Created by Thomas Guilleminot on 31/07/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import ComposableArchitecture +import ComposableCoreLocation +import SwiftUI + +struct ContentView: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + ZStack { + Rectangle() + .animation(Animation.easeIn(duration: 1.0)) + .foregroundColor(Color(viewStore.uvIndex.associatedColor)) + .edgesIgnoringSafeArea(.all) + + VStack { + HStack { + Spacer() + + Button { + viewStore.send(.getUVRequest) + } label: { + Image(systemName: "arrow.clockwise") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .font(Font.title.weight(Font.Weight.thin)) + .foregroundColor(.white) + .padding(.trailing, 20) + } + .disabled(viewStore.isLocationRefused) + } + + HStack { + Text(viewStore.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 : []) + Spacer() + } + + Spacer() + + Text(String(viewStore.uvIndex)) + .foregroundColor(.white) + .font(.system(size: 80, weight: .semibold, design: .rounded)) + .redacted(reason: viewStore.weatherRequestInFlight ? .placeholder : []) + + Spacer() + + if viewStore.isLocationRefused { + Button { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + } label: { + Label("app.error.openSettings".localized, systemImage: "location") + } + .foregroundColor(.black) + .padding(20) + .background(Color.white) + .cornerRadius(8) + } + + Text(viewStore.uvIndex.associatedDescription) + .padding(20) + .foregroundColor(.white) + .font(.system(size: 12)) + .redacted(reason: viewStore.weatherRequestInFlight ? .placeholder : []) + } + } + .alert(isPresented: viewStore.binding(get: \.shouldShowErrorPopup, send: .dismissErrorPopup)) { + Alert(title: Text("app.label.error"), message: Text(viewStore.errorText)) + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + viewStore.send(.onAppear) + } + } + } +} + +#if DEBUG +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView( + store: Store( + initialState: AppState( + uvIndex: 1, + cityName: "Gueugnon", + weatherRequestInFlight: false, + getCityNameRequestInFlight: false + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .live + ) + ) + ) + } +} +#endif diff --git a/swiftUV/app/Views/LocationReducer.swift b/swiftUV/app/Views/LocationReducer.swift new file mode 100644 index 0000000..31f6ba4 --- /dev/null +++ b/swiftUV/app/Views/LocationReducer.swift @@ -0,0 +1,42 @@ +// +// LocationReducer.swift +// swiftUV +// +// Created by Thomas Guilleminot on 31/07/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import ComposableArchitecture +import ComposableCoreLocation +import Foundation + +let locationManagerReducer = Reducer { state, action, environment in + + switch action { + case .locationManager(.didChangeAuthorization(.authorizedAlways)), + .locationManager(.didChangeAuthorization(.authorizedWhenInUse)): + if state.isRequestingCurrentLocation { + return environment.locationManager + .requestLocation() + .fireAndForget() + } + return .none + + case .locationManager(.didChangeAuthorization(.denied)): + if state.isRequestingCurrentLocation { + state.errorText = "app.error.localisationDisabled".localized + state.shouldShowErrorPopup = true + state.isRequestingCurrentLocation = false + state.isLocationRefused = true + } + return .none + case let .locationManager(.didUpdateLocations(locations)): + state.isRequestingCurrentLocation = false + guard let location = locations.first else { return .none } + state.userLocation = Location(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) + return Effect(value: .getUVRequest) + + default: + return .none + } +} diff --git a/swiftUV/app/Views/UVView.swift b/swiftUV/app/Views/UVView.swift deleted file mode 100644 index 2d75eb5..0000000 --- a/swiftUV/app/Views/UVView.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// UVView.swift -// swiftUV -// -// Created by Thomas Guilleminot on 26/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import SwiftUI -import CoreLocation -import Resolver -import ExytePopupView - -struct UVView: View { - - @ObservedObject var viewModel: UVViewModel - - var body: some View { - ZStack { - Rectangle() - .animation(Animation.easeIn(duration: 1.0)) - .foregroundColor(Color(self.viewModel.index.associatedColor)) - .edgesIgnoringSafeArea(.all) - - VStack { - HStack { - Spacer() - Image(systemName: "arrow.clockwise") - .resizable() - .frame(width: 20, height: 20, alignment: .center) - .font(Font.title.weight(Font.Weight.thin)) - .foregroundColor(.white) - .padding(.trailing, 20) - .onTapGesture { - self.viewModel.searchLocation() - } - } - - HStack { - Text(self.viewModel.cityLabel) - .padding(.top, 33) - .font(.system(size: 38, weight: .bold, design: .rounded)) - .foregroundColor(.white) - .padding(.horizontal, 20) - .lineLimit(1) - .minimumScaleFactor(0.2) - .redacted(reason: self.viewModel.showLoading ? .placeholder : []) - Spacer() - } - - Spacer() - - Text(String(self.viewModel.index)) - .foregroundColor(.white) - .font(.custom("OpenSans-Semibold", size: 80)) - .redacted(reason: self.viewModel.showLoading ? .placeholder : []) - - Spacer() - - Text(self.viewModel.index.associatedDescription) - .padding(20) - .foregroundColor(.white) - .font(.custom("OpenSans", size: 12)) - .redacted(reason: self.viewModel.showLoading ? .placeholder : []) - } - } - .blur(radius: self.viewModel.showErrorPopup ? 3 : 0) - .popup(isPresented: self.$viewModel.showErrorPopup) { - VStack { - Text(self.viewModel.errorText) - .foregroundColor(.gray) - .padding(.bottom, 20) - Button("Retry") { - self.viewModel.getUVIndex() - } - } - .padding(20) - .background(Color.white) - .cornerRadius(20) - } - .onAppear { - self.viewModel.getUVIndex() - } - } -} - -struct UVView_Previews: PreviewProvider { - static var previews: some View { - UVView(viewModel: Resolver.resolve(UVViewModel.self)) - } -} diff --git a/swiftUV/app/Views/UVViewFactory.swift b/swiftUV/app/Views/UVViewFactory.swift deleted file mode 100644 index ccd14b5..0000000 --- a/swiftUV/app/Views/UVViewFactory.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UVViewFactory.swift -// swiftUV -// -// Created by Thomas Guilleminot on 26/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import SwiftUI - -struct UVViewFactory: ViewFactory { - - private let viewModel: UVViewModel - - init(with viewModel: UVViewModel) { - self.viewModel = viewModel - } - - func make() -> AnyView { - AnyView(UVView(viewModel: self.viewModel)) - } - -} diff --git a/swiftUV/app/Views/ViewFactory.swift b/swiftUV/app/Views/ViewFactory.swift deleted file mode 100644 index c06b9b5..0000000 --- a/swiftUV/app/Views/ViewFactory.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ViewFactory.swift -// swiftUV -// -// Created by Thomas Guilleminot on 26/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import SwiftUI - -protocol ViewFactory { - func make() -> AnyView -} diff --git a/swiftUV/app/Workers/APIWorker.swift b/swiftUV/app/Workers/APIWorker.swift deleted file mode 100644 index aa40821..0000000 --- a/swiftUV/app/Workers/APIWorker.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// APIExecutor.swift -// swiftUV -// -// Created by Thomas Guilleminot on 27/06/2019. -// Copyright © 2019 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import Combine -import Resolver - -enum HTTPMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" -} - -protocol APIWorker { - func request(for type: T.Type, at url: URL, method: HTTPMethod, parameters: [String: Any]) -> AnyPublisher -} - -class APIWorkerImpl: APIWorker { - - private let session: NetworkSession - - init(with session: NetworkSession) { - self.session = session - } - - func request(for type: T.Type, at url: URL, method: HTTPMethod, parameters: [String: Any]) -> AnyPublisher { - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) - - return self.session.loadData(from: url) - .tryMap { output in - guard let response = output.response as? HTTPURLResponse else { - throw UVError.urlNotValid - } - - switch response.statusCode { - case 200: - return output.data - default: - throw UVError.noData("No data for HTTP Code \(response.statusCode)") - } - } - .decode(type: type, decoder: JSONDecoder()) - .mapError { error in - switch error { - case is UVError: - return (error as? UVError) ?? .customError(error.localizedDescription) - case is Swift.DecodingError: - return .couldNotDecodeJSON - default: - return .customError(error.localizedDescription) - } - } - .eraseToAnyPublisher() - } - -} diff --git a/swiftUV/app/Workers/NetworkSession.swift b/swiftUV/app/Workers/NetworkSession.swift deleted file mode 100644 index 5a1ecd2..0000000 --- a/swiftUV/app/Workers/NetworkSession.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// NetworkSession.swift -// swiftUV -// -// Created by Thomas Guilleminot on 28/06/2019. -// Copyright © 2019 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import Combine - -protocol NetworkSession { - func loadData(from url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError> -} - -extension URLSession: NetworkSession { - func loadData(from url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { - dataTaskPublisher(for: url).eraseToAnyPublisher() - } -} diff --git a/swiftUV/en.lproj/Localizable.strings b/swiftUV/en.lproj/Localizable.strings index 8c217ef..0730766 100644 --- a/swiftUV/en.lproj/Localizable.strings +++ b/swiftUV/en.lproj/Localizable.strings @@ -11,13 +11,11 @@ "app.label.unknown" = "Unknown"; "app.label.error" = "Error"; -// Messages -"app.message.downloading" = "Fetching data"; - // Errros "app.error.localisationDisabled" = "You disabled location service"; "app.error.couldNotLocalise" = "Could not find your location"; "app.error.noData" = "No data available"; +"app.error.openSettings" = "Authorize location in settings"; // Description diff --git a/swiftUV/fr.lproj/Localizable.strings b/swiftUV/fr.lproj/Localizable.strings index eb554cc..901d5cb 100644 --- a/swiftUV/fr.lproj/Localizable.strings +++ b/swiftUV/fr.lproj/Localizable.strings @@ -10,9 +10,7 @@ "app.label.city" = "Ville"; "app.label.unknown" = "Inconnue"; "app.label.error" = "Erreur"; - -// Messages -"app.message.downloading" = "Téléchargement des données en cours"; +"app.error.openSettings" = "Authorisez la localisation dans les paramètres"; // Errors "app.error.localisationDisabled" = "Vous avez désactivé la location"; diff --git a/swiftUVTests/APIWorkerTests.swift b/swiftUVTests/APIWorkerTests.swift deleted file mode 100644 index 3880513..0000000 --- a/swiftUVTests/APIWorkerTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// APIWorkerTests.swift -// swiftUVTests -// -// Created by Thomas Guilleminot on 28/06/2019. -// Copyright © 2019 Thomas Guilleminot. All rights reserved. -// - -import XCTest -import Cuckoo -import Combine - -@testable import swiftUV - -class APIWorkerTests: XCTestCase { - - var sessionMock: MockNetworkSession! - var apiWorker: APIWorker! - - private let expectedURL = URL(string: "https://www.fake-url.com")! - private let expectedForecast = Forecast(lat: 10.212, lon: 43.232, dateIso: "12154215", date: 1234566, value: 1) - - private var cancelable: AnyCancellable? - - override func setUp() { - sessionMock = MockNetworkSession() - apiWorker = APIWorkerImpl(with: sessionMock) - } - - func testLoadDataSuccess() { - let forecastData = try! JSONEncoder().encode(expectedForecast) - let expectedURLResponse = HTTPURLResponse(url: expectedURL, statusCode: 200, httpVersion: nil, headerFields: nil)! - let expectedPublisher = Just((data: forecastData, response: expectedURLResponse as URLResponse)).setFailureType(to: URLError.self).eraseToAnyPublisher() - - stub(sessionMock) { stub in - when(stub).loadData(from: any()).thenReturn(expectedPublisher) - } - - cancelable = apiWorker.request(for: Forecast.self, at: expectedURL, method: .get, parameters: [:]).sink { completion in - switch completion { - case .finished: break - case .failure: XCTFail("Should not trigger failure") - } - } receiveValue: { forecast in - XCTAssertEqual(forecast, self.expectedForecast) - } - - verify(sessionMock).loadData(from: any()) - verifyNoMoreInteractions(sessionMock) - } - - func testLoadDataFailureNoResponse() { - let forecastData = try! JSONEncoder().encode(expectedForecast) - let expectedURLResponse = URLResponse() - let expectedPublisher = Just((data: forecastData, response: expectedURLResponse)).setFailureType(to: URLError.self).eraseToAnyPublisher() - - stub(sessionMock) { stub in - when(stub).loadData(from: any()).thenReturn(expectedPublisher) - } - - cancelable = apiWorker.request(for: Forecast.self, at: expectedURL, method: .get, parameters: [:]).sink { completion in - switch completion { - case .finished: break - case .failure(let error): XCTAssertEqual(error, UVError.urlNotValid) - } - } receiveValue: { forecast in - XCTFail("Should not trigger success") - } - - verify(sessionMock).loadData(from: any()) - verifyNoMoreInteractions(sessionMock) - } - - func testLoadDataFailure404() { - let forecastData = try! JSONEncoder().encode(expectedForecast) - let expectedURLResponse = HTTPURLResponse(url: expectedURL, statusCode: 404, httpVersion: nil, headerFields: nil)! - let expectedPublisher = Just((data: forecastData, response: expectedURLResponse as URLResponse)).setFailureType(to: URLError.self).eraseToAnyPublisher() - - stub(sessionMock) { stub in - when(stub).loadData(from: any()).thenReturn(expectedPublisher) - } - - cancelable = apiWorker.request(for: Forecast.self, at: expectedURL, method: .get, parameters: [:]).sink { completion in - switch completion { - case .finished: break - case .failure(let error): XCTAssertEqual(error, UVError.noData("No data for HTTP Code 404")) - } - } receiveValue: { forecast in - XCTFail("Should not trigger success") - } - - verify(sessionMock).loadData(from: any()) - verifyNoMoreInteractions(sessionMock) - } - - func testLoadDataFailureJSONDecode() { - let wrongData = "{id: 12}".data(using: .utf8)! - let expectedURLResponse = HTTPURLResponse(url: expectedURL, statusCode: 200, httpVersion: nil, headerFields: nil)! - let expectedPublisher = Just((data: wrongData, response: expectedURLResponse as URLResponse)).setFailureType(to: URLError.self).eraseToAnyPublisher() - - stub(sessionMock) { stub in - when(stub).loadData(from: any()).thenReturn(expectedPublisher) - } - - cancelable = apiWorker.request(for: Forecast.self, at: expectedURL, method: .get, parameters: [:]).sink { completion in - switch completion { - case .finished: break - case .failure(let error): XCTAssertEqual(error, UVError.couldNotDecodeJSON) - } - } receiveValue: { forecast in - XCTFail("Should not trigger success") - } - - verify(sessionMock).loadData(from: any()) - verifyNoMoreInteractions(sessionMock) - } - -} - diff --git a/swiftUVTests/AppReducerTests.swift b/swiftUVTests/AppReducerTests.swift new file mode 100644 index 0000000..cd3c607 --- /dev/null +++ b/swiftUVTests/AppReducerTests.swift @@ -0,0 +1,238 @@ +// +// AppReducerTests.swift +// swiftUVTests +// +// Created by Thomas Guilleminot on 04/08/2022. +// Copyright © 2022 Thomas Guilleminot. All rights reserved. +// + +import Foundation +import ComposableArchitecture +import XCTest + +@testable import swiftUV + +class AppReducerTests: XCTestCase { + + func testGetUVRequestSuccess() { + let mainQueue = DispatchQueue.test + let expectedForecast = Forecast(lat: 12.0, lon: 13.0, dateIso: "32323", date: 1234, value: 5) + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true, + userLocation: Location(latitude: 12.0, longitude: 13.0) + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: mainQueue.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVRequest) { + $0.weatherRequestInFlight = true + $0.getCityNameRequestInFlight = true + } + + mainQueue.advance() + + store.receive(.getUVResponse(.success(expectedForecast))) { + $0.weatherRequestInFlight = false + $0.uvIndex = 5 + } + + mainQueue.advance() + + store.receive(.getCityNameResponse(.success("Gueugnon"))) { + $0.getCityNameRequestInFlight = false + $0.cityName = "Gueugnon" + } + } + + func testGetUVRequestFailure() { + let mainQueue = DispatchQueue.test + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true, + userLocation: Location(latitude: 12.0, longitude: 13.0) + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: UVClient(fetchUVIndex: { _ in return Effect(error: UVClient.Failure(errorDescription: "test"))}, fetchCityName: { _ in fatalError()}), + dispatchQueue: mainQueue.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVRequest) { + $0.weatherRequestInFlight = true + $0.getCityNameRequestInFlight = true + } + + mainQueue.advance() + + store.receive(.getUVResponse(.failure(UVClient.Failure(errorDescription: "test")))) { + $0.weatherRequestInFlight = false + $0.shouldShowErrorPopup = true + $0.errorText = "test" + $0.uvIndex = 0 + } + } + + func testGetUVRequestFailureNoLocation() { + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.main.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVRequest) { + $0.weatherRequestInFlight = true + $0.shouldShowErrorPopup = true + $0.errorText = "app.error.couldNotLocalise".localized + } + } + + func testGetUVResponseSuccess() { + let mainQueue = DispatchQueue.test + let expectedForecast = Forecast(lat: 12.0, lon: 13.0, dateIso: "32323", date: 1234, value: 5) + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true, + userLocation: Location(latitude: 12.0, longitude: 13.0) + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: mainQueue.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVResponse(.success(expectedForecast))) { + $0.weatherRequestInFlight = false + $0.uvIndex = 5 + } + + mainQueue.advance() + + store.receive(.getCityNameResponse(.success("Gueugnon"))) { + $0.cityName = "Gueugnon" + $0.getCityNameRequestInFlight = false + } + } + + func testGetUVResponseSuccessNoLocation() { + let expectedForecast = Forecast(lat: 12.0, lon: 13.0, dateIso: "32323", date: 1234, value: 5) + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVResponse(.success(expectedForecast))) { + $0.weatherRequestInFlight = false + $0.uvIndex = 5 + $0.cityName = "app.label.unknown".localized + } + } + + func testGetUVResponseFailure() { + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getUVResponse(.failure(UVClient.Failure(errorDescription: "test")))) { + $0.weatherRequestInFlight = false + $0.shouldShowErrorPopup = true + $0.errorText = "test" + $0.uvIndex = 0 + } + } + + func testGetCityNameResponseSuccess() { + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getCityNameResponse(.success("Gueugnon"))) { + $0.getCityNameRequestInFlight = false + $0.cityName = "Gueugnon" + } + } + + func testGetCityNameResponseFailure() { + let store = TestStore( + initialState: + AppState( + getCityNameRequestInFlight: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.getCityNameResponse(.failure(UVClient.Failure(errorDescription: "test")))) { + $0.getCityNameRequestInFlight = false + $0.cityName = "app.label.unknown".localized + } + } + + func testDismissErrorPopup() { + let store = TestStore( + initialState: + AppState( + shouldShowErrorPopup: true + ), + reducer: appReducer, + environment: AppEnvironment( + uvClient: .mock, + dispatchQueue: DispatchQueue.test.eraseToAnyScheduler(), + locationManager: .failing + ) + ) + + store.send(.dismissErrorPopup) { + $0.shouldShowErrorPopup = false + } + } +} diff --git a/swiftUVTests/URLFactoryTests.swift b/swiftUVTests/URLFactoryTests.swift deleted file mode 100644 index d7019ee..0000000 --- a/swiftUVTests/URLFactoryTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// URLFactoryTests.swift -// swiftUVTests -// -// Created by Thomas Guilleminot on 18/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import XCTest - -@testable import swiftUV - -class URLFactoryTests: XCTestCase { - - func testCreateURL() { - let expectedURL = "https://api.openweathermap.org/data/2.5/uvi?lat=48.8534&lon=2.3488&appid=123456" - let expectedLat = 48.8534 - let expectedLon = 2.3488 - let expectedAppId = "123456" - - let factory = URLFactory(with: expectedAppId) - - XCTAssertEqual(factory.createUVURL(lat: expectedLat, lon: expectedLon), URL(string: expectedURL)!) - } - -} diff --git a/swiftUVTests/UVServiceTests.swift b/swiftUVTests/UVServiceTests.swift deleted file mode 100644 index fa443b2..0000000 --- a/swiftUVTests/UVServiceTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// UVServiceTests.swift -// swiftUVTests -// -// Created by Thomas Guilleminot on 19/10/2020. -// Copyright © 2020 Thomas Guilleminot. All rights reserved. -// - -import Foundation -import XCTest -import Cuckoo -import Combine - -@testable import swiftUV - -class UVServiceTests: XCTestCase { - - private var mockApiWorker: MockAPIWorker! - private var mockURLFactory: MockURLFactory! - - private var uvService: UVService! - private var cancelable: AnyCancellable? - - private let expectedForecast = Forecast(lat: 12, lon: 13, dateIso: "sdksja", date: 21345, value: 4.3) - private let expectedLocation = Location(latitude: 12, longitude: 13, city: "Paris") - - override func setUp() { - self.mockApiWorker = MockAPIWorker() - self.mockURLFactory = MockURLFactory(with: "123456") - - self.uvService = UVServiceImpl(apiExecutor: mockApiWorker, urlFactory: mockURLFactory) - } - - func testGetUVSuccess() { - let expectedValue = 4 - let expectedPublisher = Just(expectedForecast).setFailureType(to: UVError.self).eraseToAnyPublisher() - - stub(mockURLFactory) { stub in - when(stub).createUVURL(lat: any(), lon: any()).thenReturn(URL(string: "https://fake.com")!) - } - - stub(mockApiWorker) { stub in - when(stub).request(for: any(), at: any(), method: any(), parameters: any()).thenReturn(expectedPublisher) - } - - cancelable = uvService.getUVIndex(from: expectedLocation).sink { completion in - switch completion { - case .finished: break - case .failure: XCTFail("Should not fail") - } - } receiveValue: { value in - XCTAssertEqual(value, expectedValue) - } - } - - func testGetUVFailure() { - let expectedError = UVError.couldNotDecodeJSON - let expectedPublisher: AnyPublisher = Result.failure(expectedError).publisher.eraseToAnyPublisher() - - stub(mockURLFactory) { stub in - when(stub).createUVURL(lat: any(), lon: any()).thenReturn(URL(string: "https://fake.com")!) - } - - stub(mockApiWorker) { stub in - when(stub).request(for: any(), at: any(), method: any(), parameters: any()).thenReturn(expectedPublisher) - } - - cancelable = uvService.getUVIndex(from: expectedLocation).sink{ completion in - switch completion { - case .finished: break - case .failure(let error): XCTAssertEqual(error, expectedError) - } - } receiveValue: { _ in - XCTFail("Should not succeed") - } - } - -}