diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/Contents.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/Contents.swift new file mode 100644 index 00000000..e3e00528 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/Contents.swift @@ -0,0 +1,16 @@ +// +//import Network +// +//let monitor = NWPathMonitor() +//monitor.pathUpdateHandler = { path in +// print(path.status) +//} +//monitor.start(queue: .main) +// + +import Combine + +ImmediateScheduler.shared + .schedule(after: ImmediateScheduler.shared.now.advanced(by: 100)) { + print("hi") + } diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/contents.xcplayground b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/contents.xcplayground new file mode 100644 index 00000000..63b6dd8d --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.pbxproj b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.pbxproj new file mode 100644 index 00000000..e6d81b74 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.pbxproj @@ -0,0 +1,909 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 2A55622624C8972A00955510 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A55622524C8972A00955510 /* AppDelegate.swift */; }; + 2A55622824C8972A00955510 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A55622724C8972A00955510 /* SceneDelegate.swift */; }; + 2A55622C24C8972B00955510 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A55622B24C8972B00955510 /* Assets.xcassets */; }; + 2A55622F24C8972B00955510 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A55622E24C8972B00955510 /* Preview Assets.xcassets */; }; + 2A55623224C8972B00955510 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A55623024C8972B00955510 /* LaunchScreen.storyboard */; }; + 2A55623D24C8972B00955510 /* DesigningDependenciesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A55623C24C8972B00955510 /* DesigningDependenciesTests.swift */; }; + 2A55625724C8B82200955510 /* WeatherFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A55624E24C8B82200955510 /* WeatherFeature.framework */; }; + 2A55625E24C8B82200955510 /* WeatherFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A55625D24C8B82200955510 /* WeatherFeatureTests.swift */; }; + 2A55626024C8B82200955510 /* WeatherFeature.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A55625024C8B82200955510 /* WeatherFeature.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2A55626324C8B82200955510 /* WeatherFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A55624E24C8B82200955510 /* WeatherFeature.framework */; }; + 2A55626524C8B82200955510 /* WeatherFeature.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2A55624E24C8B82200955510 /* WeatherFeature.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 2A55626C24C8B83300955510 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A55622924C8972A00955510 /* ContentView.swift */; }; + 2A55F5A924C8BCB600ED4286 /* WeatherClient in Frameworks */ = {isa = PBXBuildFile; productRef = 2A55F5A824C8BCB600ED4286 /* WeatherClient */; }; + 2A55F5AB24C8BCB600ED4286 /* WeatherClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2A55F5A824C8BCB600ED4286 /* WeatherClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2A99D92424D3157D00008F0B /* LocationClient in Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92324D3157D00008F0B /* LocationClient */; }; + 2A99D92624D3157D00008F0B /* LocationClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92324D3157D00008F0B /* LocationClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2A99D92824D3157D00008F0B /* PathMonitorClient in Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92724D3157D00008F0B /* PathMonitorClient */; }; + 2A99D92924D3157D00008F0B /* PathMonitorClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92724D3157D00008F0B /* PathMonitorClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2A99D92B24D3157D00008F0B /* WeatherClient in Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92A24D3157D00008F0B /* WeatherClient */; }; + 2A99D92C24D3157D00008F0B /* WeatherClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2A99D92A24D3157D00008F0B /* WeatherClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2AA6C8B4260BC61A00ED8F56 /* CombineSchedulers in Frameworks */ = {isa = PBXBuildFile; productRef = 2AA6C8B3260BC61A00ED8F56 /* CombineSchedulers */; }; + 2ACAD44C24CA04F000352F1E /* PathMonitorClient in Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD44B24CA04F000352F1E /* PathMonitorClient */; }; + 2ACAD44D24CA04F000352F1E /* PathMonitorClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD44B24CA04F000352F1E /* PathMonitorClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2ACAD44F24CA04F800352F1E /* PathMonitorClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD44E24CA04F800352F1E /* PathMonitorClientLive */; }; + 2ACAD45024CA04F800352F1E /* PathMonitorClientLive in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD44E24CA04F800352F1E /* PathMonitorClientLive */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 2ACAD45224CA051D00352F1E /* WeatherClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD45124CA051D00352F1E /* WeatherClientLive */; }; + 2ACAD45324CA051D00352F1E /* WeatherClientLive in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 2ACAD45124CA051D00352F1E /* WeatherClientLive */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 4B28C0E824CA40A5001381C9 /* LocationClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 4B28C0E724CA40A5001381C9 /* LocationClientLive */; }; + 4B28C0E924CA40A5001381C9 /* LocationClientLive in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 4B28C0E724CA40A5001381C9 /* LocationClientLive */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 4B28C0EB24CA40AA001381C9 /* LocationClient in Frameworks */ = {isa = PBXBuildFile; productRef = 4B28C0EA24CA40AA001381C9 /* LocationClient */; }; + 4B28C0EC24CA40AA001381C9 /* LocationClient in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 4B28C0EA24CA40AA001381C9 /* LocationClient */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A55623924C8972B00955510 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A55621A24C8972A00955510 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A55622124C8972A00955510; + remoteInfo = DesigningDependencies; + }; + 2A55625824C8B82200955510 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A55621A24C8972A00955510 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A55624D24C8B82200955510; + remoteInfo = WeatherFeature; + }; + 2A55626124C8B82200955510 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A55621A24C8972A00955510 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A55624D24C8B82200955510; + remoteInfo = WeatherFeature; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2A55626424C8B82200955510 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 2A55626524C8B82200955510 /* WeatherFeature.framework in Embed Frameworks */, + 2ACAD45024CA04F800352F1E /* PathMonitorClientLive in Embed Frameworks */, + 4B28C0E924CA40A5001381C9 /* LocationClientLive in Embed Frameworks */, + 2ACAD45324CA051D00352F1E /* WeatherClientLive in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55F5AA24C8BCB600ED4286 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 4B28C0EC24CA40AA001381C9 /* LocationClient in Embed Frameworks */, + 2A55F5AB24C8BCB600ED4286 /* WeatherClient in Embed Frameworks */, + 2ACAD44D24CA04F000352F1E /* PathMonitorClient in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 2A99D92524D3157D00008F0B /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 2A99D92624D3157D00008F0B /* LocationClient in Embed Frameworks */, + 2A99D92C24D3157D00008F0B /* WeatherClient in Embed Frameworks */, + 2A99D92924D3157D00008F0B /* PathMonitorClient in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2A55622224C8972A00955510 /* DesigningDependencies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DesigningDependencies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A55622524C8972A00955510 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2A55622724C8972A00955510 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2A55622924C8972A00955510 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2A55622B24C8972B00955510 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A55622E24C8972B00955510 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A55623124C8972B00955510 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2A55623324C8972B00955510 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A55623824C8972B00955510 /* DesigningDependenciesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DesigningDependenciesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A55623C24C8972B00955510 /* DesigningDependenciesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesigningDependenciesTests.swift; sourceTree = ""; }; + 2A55623E24C8972B00955510 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A55624E24C8B82200955510 /* WeatherFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WeatherFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A55625024C8B82200955510 /* WeatherFeature.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WeatherFeature.h; sourceTree = ""; }; + 2A55625124C8B82200955510 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A55625624C8B82200955510 /* WeatherFeatureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherFeatureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A55625D24C8B82200955510 /* WeatherFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherFeatureTests.swift; sourceTree = ""; }; + 2A55625F24C8B82200955510 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2ACAD44A24CA041E00352F1E /* PathMonitorClient */ = {isa = PBXFileReference; lastKnownFileType = folder; path = PathMonitorClient; sourceTree = ""; }; + 4B16222124C8B141007FE7AD /* WeatherClient */ = {isa = PBXFileReference; lastKnownFileType = folder; path = WeatherClient; sourceTree = ""; }; + 4B28C0E624CA3FA8001381C9 /* LocationClient */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LocationClient; path = DesigningDependencies/LocationClient; sourceTree = ""; }; + 4B67DA2324C9EF880024EFB1 /* DesigningDependencies.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DesigningDependencies.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A55621F24C8972A00955510 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2ACAD44F24CA04F800352F1E /* PathMonitorClientLive in Frameworks */, + 2A55626324C8B82200955510 /* WeatherFeature.framework in Frameworks */, + 4B28C0E824CA40A5001381C9 /* LocationClientLive in Frameworks */, + 2ACAD45224CA051D00352F1E /* WeatherClientLive in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55623524C8972B00955510 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55624B24C8B82200955510 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2AA6C8B4260BC61A00ED8F56 /* CombineSchedulers in Frameworks */, + 4B28C0EB24CA40AA001381C9 /* LocationClient in Frameworks */, + 2A55F5A924C8BCB600ED4286 /* WeatherClient in Frameworks */, + 2ACAD44C24CA04F000352F1E /* PathMonitorClient in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55625324C8B82200955510 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A99D92B24D3157D00008F0B /* WeatherClient in Frameworks */, + 2A99D92824D3157D00008F0B /* PathMonitorClient in Frameworks */, + 2A99D92424D3157D00008F0B /* LocationClient in Frameworks */, + 2A55625724C8B82200955510 /* WeatherFeature.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A55621924C8972A00955510 = { + isa = PBXGroup; + children = ( + 4B16222124C8B141007FE7AD /* WeatherClient */, + 2ACAD44A24CA041E00352F1E /* PathMonitorClient */, + 4B28C0E624CA3FA8001381C9 /* LocationClient */, + 4B67DA2324C9EF880024EFB1 /* DesigningDependencies.playground */, + 2A55622424C8972A00955510 /* DesigningDependencies */, + 2A55623B24C8972B00955510 /* DesigningDependenciesTests */, + 2A55624F24C8B82200955510 /* WeatherFeature */, + 2A55625C24C8B82200955510 /* WeatherFeatureTests */, + 2A55622324C8972A00955510 /* Products */, + 4B16222224C8B212007FE7AD /* Frameworks */, + ); + sourceTree = ""; + }; + 2A55622324C8972A00955510 /* Products */ = { + isa = PBXGroup; + children = ( + 2A55622224C8972A00955510 /* DesigningDependencies.app */, + 2A55623824C8972B00955510 /* DesigningDependenciesTests.xctest */, + 2A55624E24C8B82200955510 /* WeatherFeature.framework */, + 2A55625624C8B82200955510 /* WeatherFeatureTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2A55622424C8972A00955510 /* DesigningDependencies */ = { + isa = PBXGroup; + children = ( + 2A55622524C8972A00955510 /* AppDelegate.swift */, + 2A55622724C8972A00955510 /* SceneDelegate.swift */, + 2A55622B24C8972B00955510 /* Assets.xcassets */, + 2A55623024C8972B00955510 /* LaunchScreen.storyboard */, + 2A55623324C8972B00955510 /* Info.plist */, + 2A55622D24C8972B00955510 /* Preview Content */, + ); + path = DesigningDependencies; + sourceTree = ""; + }; + 2A55622D24C8972B00955510 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2A55622E24C8972B00955510 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2A55623B24C8972B00955510 /* DesigningDependenciesTests */ = { + isa = PBXGroup; + children = ( + 2A55623C24C8972B00955510 /* DesigningDependenciesTests.swift */, + 2A55623E24C8972B00955510 /* Info.plist */, + ); + path = DesigningDependenciesTests; + sourceTree = ""; + }; + 2A55624F24C8B82200955510 /* WeatherFeature */ = { + isa = PBXGroup; + children = ( + 2A55622924C8972A00955510 /* ContentView.swift */, + 2A55625024C8B82200955510 /* WeatherFeature.h */, + 2A55625124C8B82200955510 /* Info.plist */, + ); + path = WeatherFeature; + sourceTree = ""; + }; + 2A55625C24C8B82200955510 /* WeatherFeatureTests */ = { + isa = PBXGroup; + children = ( + 2A55625D24C8B82200955510 /* WeatherFeatureTests.swift */, + 2A55625F24C8B82200955510 /* Info.plist */, + ); + path = WeatherFeatureTests; + sourceTree = ""; + }; + 4B16222224C8B212007FE7AD /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 2A55624924C8B82200955510 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55626024C8B82200955510 /* WeatherFeature.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 2A55622124C8972A00955510 /* DesigningDependencies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A55624124C8972B00955510 /* Build configuration list for PBXNativeTarget "DesigningDependencies" */; + buildPhases = ( + 2A55621E24C8972A00955510 /* Sources */, + 2A55621F24C8972A00955510 /* Frameworks */, + 2A55622024C8972A00955510 /* Resources */, + 2A55626424C8B82200955510 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 2A55626224C8B82200955510 /* PBXTargetDependency */, + ); + name = DesigningDependencies; + packageProductDependencies = ( + 2ACAD44E24CA04F800352F1E /* PathMonitorClientLive */, + 2ACAD45124CA051D00352F1E /* WeatherClientLive */, + 4B28C0E724CA40A5001381C9 /* LocationClientLive */, + ); + productName = DesigningDependencies; + productReference = 2A55622224C8972A00955510 /* DesigningDependencies.app */; + productType = "com.apple.product-type.application"; + }; + 2A55623724C8972B00955510 /* DesigningDependenciesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A55624424C8972B00955510 /* Build configuration list for PBXNativeTarget "DesigningDependenciesTests" */; + buildPhases = ( + 2A55623424C8972B00955510 /* Sources */, + 2A55623524C8972B00955510 /* Frameworks */, + 2A55623624C8972B00955510 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A55623A24C8972B00955510 /* PBXTargetDependency */, + ); + name = DesigningDependenciesTests; + productName = DesigningDependenciesTests; + productReference = 2A55623824C8972B00955510 /* DesigningDependenciesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2A55624D24C8B82200955510 /* WeatherFeature */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A55626624C8B82200955510 /* Build configuration list for PBXNativeTarget "WeatherFeature" */; + buildPhases = ( + 2A55624924C8B82200955510 /* Headers */, + 2A55624A24C8B82200955510 /* Sources */, + 2A55624B24C8B82200955510 /* Frameworks */, + 2A55624C24C8B82200955510 /* Resources */, + 2A55F5AA24C8BCB600ED4286 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WeatherFeature; + packageProductDependencies = ( + 2A55F5A824C8BCB600ED4286 /* WeatherClient */, + 2ACAD44B24CA04F000352F1E /* PathMonitorClient */, + 4B28C0EA24CA40AA001381C9 /* LocationClient */, + 2AA6C8B3260BC61A00ED8F56 /* CombineSchedulers */, + ); + productName = WeatherFeature; + productReference = 2A55624E24C8B82200955510 /* WeatherFeature.framework */; + productType = "com.apple.product-type.framework"; + }; + 2A55625524C8B82200955510 /* WeatherFeatureTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A55626924C8B82200955510 /* Build configuration list for PBXNativeTarget "WeatherFeatureTests" */; + buildPhases = ( + 2A55625224C8B82200955510 /* Sources */, + 2A55625324C8B82200955510 /* Frameworks */, + 2A55625424C8B82200955510 /* Resources */, + 2A99D92524D3157D00008F0B /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 2A55625924C8B82200955510 /* PBXTargetDependency */, + ); + name = WeatherFeatureTests; + packageProductDependencies = ( + 2A99D92324D3157D00008F0B /* LocationClient */, + 2A99D92724D3157D00008F0B /* PathMonitorClient */, + 2A99D92A24D3157D00008F0B /* WeatherClient */, + ); + productName = WeatherFeatureTests; + productReference = 2A55625624C8B82200955510 /* WeatherFeatureTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A55621A24C8972A00955510 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1160; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Point-Free"; + TargetAttributes = { + 2A55622124C8972A00955510 = { + CreatedOnToolsVersion = 11.6; + }; + 2A55623724C8972B00955510 = { + CreatedOnToolsVersion = 11.6; + TestTargetID = 2A55622124C8972A00955510; + }; + 2A55624D24C8B82200955510 = { + CreatedOnToolsVersion = 11.6; + }; + 2A55625524C8B82200955510 = { + CreatedOnToolsVersion = 11.6; + }; + }; + }; + buildConfigurationList = 2A55621D24C8972A00955510 /* Build configuration list for PBXProject "DesigningDependencies" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A55621924C8972A00955510; + packageReferences = ( + 2AA6C8B2260BC61A00ED8F56 /* XCRemoteSwiftPackageReference "combine-schedulers" */, + ); + productRefGroup = 2A55622324C8972A00955510 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A55622124C8972A00955510 /* DesigningDependencies */, + 2A55623724C8972B00955510 /* DesigningDependenciesTests */, + 2A55624D24C8B82200955510 /* WeatherFeature */, + 2A55625524C8B82200955510 /* WeatherFeatureTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A55622024C8972A00955510 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55623224C8972B00955510 /* LaunchScreen.storyboard in Resources */, + 2A55622F24C8972B00955510 /* Preview Assets.xcassets in Resources */, + 2A55622C24C8972B00955510 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55623624C8972B00955510 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55624C24C8B82200955510 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55625424C8B82200955510 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A55621E24C8972A00955510 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55622624C8972A00955510 /* AppDelegate.swift in Sources */, + 2A55622824C8972A00955510 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55623424C8972B00955510 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55623D24C8972B00955510 /* DesigningDependenciesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55624A24C8B82200955510 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55626C24C8B83300955510 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A55625224C8B82200955510 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A55625E24C8B82200955510 /* WeatherFeatureTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A55623A24C8972B00955510 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A55622124C8972A00955510 /* DesigningDependencies */; + targetProxy = 2A55623924C8972B00955510 /* PBXContainerItemProxy */; + }; + 2A55625924C8B82200955510 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A55624D24C8B82200955510 /* WeatherFeature */; + targetProxy = 2A55625824C8B82200955510 /* PBXContainerItemProxy */; + }; + 2A55626224C8B82200955510 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A55624D24C8B82200955510 /* WeatherFeature */; + targetProxy = 2A55626124C8B82200955510 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 2A55623024C8972B00955510 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2A55623124C8972B00955510 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 2A55623F24C8972B00955510 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2A55624024C8972B00955510 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A55624224C8972B00955510 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"DesigningDependencies/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = DesigningDependencies/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DesigningDependencies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A55624324C8972B00955510 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"DesigningDependencies/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = DesigningDependencies/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DesigningDependencies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A55624524C8972B00955510 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DesigningDependenciesTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DesigningDependenciesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DesigningDependencies.app/DesigningDependencies"; + }; + name = Debug; + }; + 2A55624624C8972B00955510 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DesigningDependenciesTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DesigningDependenciesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DesigningDependencies.app/DesigningDependencies"; + }; + name = Release; + }; + 2A55626724C8B82200955510 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = WeatherFeature/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.WeatherFeature; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 2A55626824C8B82200955510 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = WeatherFeature/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.WeatherFeature; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 2A55626A24C8B82200955510 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = WeatherFeatureTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.WeatherFeatureTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A55626B24C8B82200955510 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = WeatherFeatureTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.WeatherFeatureTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A55621D24C8972A00955510 /* Build configuration list for PBXProject "DesigningDependencies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A55623F24C8972B00955510 /* Debug */, + 2A55624024C8972B00955510 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A55624124C8972B00955510 /* Build configuration list for PBXNativeTarget "DesigningDependencies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A55624224C8972B00955510 /* Debug */, + 2A55624324C8972B00955510 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A55624424C8972B00955510 /* Build configuration list for PBXNativeTarget "DesigningDependenciesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A55624524C8972B00955510 /* Debug */, + 2A55624624C8972B00955510 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A55626624C8B82200955510 /* Build configuration list for PBXNativeTarget "WeatherFeature" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A55626724C8B82200955510 /* Debug */, + 2A55626824C8B82200955510 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A55626924C8B82200955510 /* Build configuration list for PBXNativeTarget "WeatherFeatureTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A55626A24C8B82200955510 /* Debug */, + 2A55626B24C8B82200955510 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2AA6C8B2260BC61A00ED8F56 /* XCRemoteSwiftPackageReference "combine-schedulers" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/combine-schedulers"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A55F5A824C8BCB600ED4286 /* WeatherClient */ = { + isa = XCSwiftPackageProductDependency; + productName = WeatherClient; + }; + 2A99D92324D3157D00008F0B /* LocationClient */ = { + isa = XCSwiftPackageProductDependency; + productName = LocationClient; + }; + 2A99D92724D3157D00008F0B /* PathMonitorClient */ = { + isa = XCSwiftPackageProductDependency; + productName = PathMonitorClient; + }; + 2A99D92A24D3157D00008F0B /* WeatherClient */ = { + isa = XCSwiftPackageProductDependency; + productName = WeatherClient; + }; + 2AA6C8B3260BC61A00ED8F56 /* CombineSchedulers */ = { + isa = XCSwiftPackageProductDependency; + package = 2AA6C8B2260BC61A00ED8F56 /* XCRemoteSwiftPackageReference "combine-schedulers" */; + productName = CombineSchedulers; + }; + 2ACAD44B24CA04F000352F1E /* PathMonitorClient */ = { + isa = XCSwiftPackageProductDependency; + productName = PathMonitorClient; + }; + 2ACAD44E24CA04F800352F1E /* PathMonitorClientLive */ = { + isa = XCSwiftPackageProductDependency; + productName = PathMonitorClientLive; + }; + 2ACAD45124CA051D00352F1E /* WeatherClientLive */ = { + isa = XCSwiftPackageProductDependency; + productName = WeatherClientLive; + }; + 4B28C0E724CA40A5001381C9 /* LocationClientLive */ = { + isa = XCSwiftPackageProductDependency; + productName = LocationClientLive; + }; + 4B28C0EA24CA40AA001381C9 /* LocationClient */ = { + isa = XCSwiftPackageProductDependency; + productName = LocationClient; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A55621A24C8972A00955510 /* Project object */; +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/xcshareddata/xcschemes/WeatherFeature.xcscheme b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/xcshareddata/xcschemes/WeatherFeature.xcscheme new file mode 100644 index 00000000..b1a20ec2 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies.xcodeproj/xcshareddata/xcschemes/WeatherFeature.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/AppDelegate.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/AppDelegate.swift new file mode 100644 index 00000000..1cc418c1 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/AppDelegate.swift @@ -0,0 +1,8 @@ +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/AppIcon.appiconset/Contents.json b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/Contents.json b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Base.lproj/LaunchScreen.storyboard b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Info.plist b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Info.plist new file mode 100644 index 00000000..52d8d998 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Info.plist @@ -0,0 +1,62 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSLocationWhenInUseUsageDescription + To fetch weather near you. + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/.gitignore b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/.gitignore new file mode 100644 index 00000000..95c43209 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Package.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Package.swift new file mode 100644 index 00000000..eb247b1f --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.3 + +import PackageDescription + +let package = Package( + name: "LocationClient", + platforms: [.iOS(.v13)], + products: [ + .library( + name: "LocationClient", + type: .dynamic, + targets: ["LocationClient"]), + .library( + name: "LocationClientLive", + type: .dynamic, + targets: ["LocationClientLive"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "LocationClient", + dependencies: []), + .testTarget( + name: "LocationClientTests", + dependencies: ["LocationClient"]), + .target( + name: "LocationClientLive", + dependencies: ["LocationClient"]), + ] +) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/README.md b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/README.md new file mode 100644 index 00000000..d7b0d85c --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/README.md @@ -0,0 +1,3 @@ +# LocationClient + +A description of this package. diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Interface.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Interface.swift new file mode 100644 index 00000000..1e84c025 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Interface.swift @@ -0,0 +1,27 @@ +import Combine +import CoreLocation + +public struct LocationClient { + public var authorizationStatus: () -> CLAuthorizationStatus + public var requestWhenInUseAuthorization: () -> Void + public var requestLocation: () -> Void + public var delegate: AnyPublisher + + public init( + authorizationStatus: @escaping () -> CLAuthorizationStatus, + requestWhenInUseAuthorization: @escaping () -> Void, + requestLocation: @escaping () -> Void, + delegate: AnyPublisher + ) { + self.authorizationStatus = authorizationStatus + self.requestWhenInUseAuthorization = requestWhenInUseAuthorization + self.requestLocation = requestLocation + self.delegate = delegate + } + + public enum DelegateEvent { + case didChangeAuthorization(CLAuthorizationStatus) + case didUpdateLocations([CLLocation]) + case didFailWithError(Error) + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Mocks.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Mocks.swift new file mode 100644 index 00000000..62b4b1d2 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClient/Mocks.swift @@ -0,0 +1,34 @@ +import Combine +import CoreLocation + +extension LocationClient { + public static var authorizedWhenInUse: Self { + let subject = PassthroughSubject() + + return Self( + authorizationStatus: { .authorizedWhenInUse }, + requestWhenInUseAuthorization: { }, + requestLocation: { + subject.send(.didUpdateLocations([CLLocation()])) + }, + delegate: subject.eraseToAnyPublisher() + ) + } + + public static var notDetermined: Self { + var status = CLAuthorizationStatus.notDetermined + let subject = PassthroughSubject() + + return Self( + authorizationStatus: { status }, + requestWhenInUseAuthorization: { + status = .authorizedWhenInUse + subject.send(.didChangeAuthorization(status)) + }, + requestLocation: { + subject.send(.didUpdateLocations([CLLocation()])) + }, + delegate: subject.eraseToAnyPublisher() + ) + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClientLive/Live.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClientLive/Live.swift new file mode 100644 index 00000000..3016405e --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Sources/LocationClientLive/Live.swift @@ -0,0 +1,41 @@ +import Combine +import CoreLocation +import LocationClient + +extension LocationClient { + public static var live: Self { + class Delegate: NSObject, CLLocationManagerDelegate { + let subject: PassthroughSubject + + init(subject: PassthroughSubject) { + self.subject = subject + } + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + self.subject.send(.didChangeAuthorization(status)) + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + self.subject.send(.didUpdateLocations(locations)) + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + self.subject.send(.didFailWithError(error)) + } + } + + let locationManager = CLLocationManager() + let subject = PassthroughSubject() + var delegate: Delegate? = Delegate(subject: subject) + locationManager.delegate = delegate + + return Self( + authorizationStatus: CLLocationManager.authorizationStatus, + requestWhenInUseAuthorization: locationManager.requestWhenInUseAuthorization, + requestLocation: locationManager.requestLocation, + delegate: subject + .handleEvents(receiveCancel: { delegate = nil }) + .eraseToAnyPublisher() + ) + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LinuxMain.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LinuxMain.swift new file mode 100644 index 00000000..651c3134 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import LocationClientTests + +var tests = [XCTestCaseEntry]() +tests += LocationClientTests.allTests() +XCTMain(tests) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/LocationClientTests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/LocationClientTests.swift new file mode 100644 index 00000000..ee92ba5b --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/LocationClientTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import LocationClient + +final class LocationClientTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(LocationClient().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/XCTestManifests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/XCTestManifests.swift new file mode 100644 index 00000000..3ec93b1d --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/LocationClient/Tests/LocationClientTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(LocationClientTests.allTests), + ] +} +#endif diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Preview Content/Preview Assets.xcassets/Contents.json b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/SceneDelegate.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/SceneDelegate.swift new file mode 100644 index 00000000..44c60782 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependencies/SceneDelegate.swift @@ -0,0 +1,27 @@ +import SwiftUI +import UIKit +import LocationClientLive +import PathMonitorClientLive +import WeatherClientLive +import WeatherFeature + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + let contentView = ContentView( + viewModel: AppViewModel( + locationClient: .live, + pathMonitorClient: .live(queue: .main), + weatherClient: .live + ) + ) + + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + window.makeKeyAndVisible() + } + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/DesigningDependenciesTests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/DesigningDependenciesTests.swift new file mode 100644 index 00000000..bb8f23f7 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/DesigningDependenciesTests.swift @@ -0,0 +1,34 @@ +// +// DesigningDependenciesTests.swift +// DesigningDependenciesTests +// +// Created by Point-Free on 7/22/20. +// Copyright © 2020 Point-Free. All rights reserved. +// + +import XCTest +@testable import DesigningDependencies + +class DesigningDependenciesTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/Info.plist b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/DesigningDependenciesTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/.gitignore b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/.gitignore new file mode 100644 index 00000000..95c43209 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Package.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Package.swift new file mode 100644 index 00000000..11c1a1cf --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.3 + +import PackageDescription + +let package = Package( + name: "PathMonitorClient", + platforms: [.iOS(.v13)], + products: [ + .library( + name: "PathMonitorClient", + type: .dynamic, + targets: ["PathMonitorClient"]), + .library( + name: "PathMonitorClientLive", + type: .dynamic, + targets: ["PathMonitorClientLive"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "PathMonitorClient", + dependencies: []), + .testTarget( + name: "PathMonitorClientTests", + dependencies: ["PathMonitorClient"]), + .target( + name: "PathMonitorClientLive", + dependencies: ["PathMonitorClient"]), + ] +) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/README.md b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/README.md new file mode 100644 index 00000000..3e71c586 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/README.md @@ -0,0 +1,3 @@ +# PathMonitorClient + +A description of this package. diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Interface.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Interface.swift new file mode 100644 index 00000000..95238247 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Interface.swift @@ -0,0 +1,26 @@ +import Combine +import Network + +public struct NetworkPath { + public var status: NWPath.Status + + public init(status: NWPath.Status) { + self.status = status + } +} + +extension NetworkPath { + public init(rawValue: NWPath) { + self.status = rawValue.status + } +} + +public struct PathMonitorClient { + public var networkPathPublisher: AnyPublisher + + public init( + networkPathPublisher: AnyPublisher + ) { + self.networkPathPublisher = networkPathPublisher + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Mocks.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Mocks.swift new file mode 100644 index 00000000..54cec93f --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClient/Mocks.swift @@ -0,0 +1,25 @@ +import Combine +import Foundation +import Network + +extension PathMonitorClient { + public static let satisfied = Self( + networkPathPublisher: Just(NetworkPath(status: .satisfied)) + .eraseToAnyPublisher() + ) + + public static let unsatisfied = Self( + networkPathPublisher: Just(NetworkPath(status: .unsatisfied)) + .eraseToAnyPublisher() + ) + + public static let flakey = Self( + networkPathPublisher: Timer.publish(every: 2, on: .main, in: .default) + .autoconnect() + .scan(.satisfied) { status, _ in + status == .satisfied ? .unsatisfied : .satisfied + } + .map { NetworkPath(status: $0) } + .eraseToAnyPublisher() + ) +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClientLive/Live.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClientLive/Live.swift new file mode 100644 index 00000000..c5f412d6 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Sources/PathMonitorClientLive/Live.swift @@ -0,0 +1,28 @@ +import Combine +import Network +import PathMonitorClient + +extension PathMonitorClient { + public static func live(queue: DispatchQueue) -> Self { + let monitor = NWPathMonitor() + let subject = PassthroughSubject() + monitor.pathUpdateHandler = subject.send + + return Self( +// cancel: { monitor.cancel() }, +// setPathUpdateHandler: { callback in +// monitor.pathUpdateHandler = { path in +// callback(NetworkPath(rawValue: path)) +// } +// }, +// start: monitor.start(queue:) + networkPathPublisher: subject + .handleEvents( + receiveSubscription: { _ in monitor.start(queue: queue) }, + receiveCancel: monitor.cancel + ) + .map(NetworkPath.init(rawValue:)) + .eraseToAnyPublisher() + ) + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/LinuxMain.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/LinuxMain.swift new file mode 100644 index 00000000..4e31d0d4 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import PathMonitorClientTests + +var tests = [XCTestCaseEntry]() +tests += PathMonitorClientTests.allTests() +XCTMain(tests) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/PathMonitorClientTests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/PathMonitorClientTests.swift new file mode 100644 index 00000000..679072e3 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/PathMonitorClientTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import PathMonitorClient + +final class PathMonitorClientTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(PathMonitorClient().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/XCTestManifests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/XCTestManifests.swift new file mode 100644 index 00000000..8620e05e --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/PathMonitorClient/Tests/PathMonitorClientTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(PathMonitorClientTests.allTests), + ] +} +#endif diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/.gitignore b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/.gitignore new file mode 100644 index 00000000..95c43209 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Package.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Package.swift new file mode 100644 index 00000000..144be0d6 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "WeatherClient", + platforms: [.iOS(.v13)], + products: [ + .library( + name: "WeatherClient", + type: .dynamic, + targets: ["WeatherClient"]), + .library( + name: "WeatherClientLive", + type: .dynamic, + targets: ["WeatherClientLive"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "WeatherClient", + dependencies: []), + .testTarget( + name: "WeatherClientTests", + dependencies: ["WeatherClient"]), + + .target( + name: "WeatherClientLive", + dependencies: ["WeatherClient"]), + ] +) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/README.md b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/README.md new file mode 100644 index 00000000..a0e41739 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/README.md @@ -0,0 +1,3 @@ +# WeatherClient + +A description of this package. diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Interface.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Interface.swift new file mode 100644 index 00000000..1fb1f6b5 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Interface.swift @@ -0,0 +1,34 @@ +import Combine +import CoreLocation +import Foundation + +/// A client for accessing weather data for locations. +public struct WeatherClient { + public var weather: (Int) -> AnyPublisher + public var searchLocations: (CLLocationCoordinate2D) -> AnyPublisher<[Location], Error> + + public init( + weather: @escaping (Int) -> AnyPublisher, + searchLocations: @escaping (CLLocationCoordinate2D) -> AnyPublisher<[Location], Error> + ) { + self.weather = weather + self.searchLocations = searchLocations + } +} + +public struct WeatherResponse: Decodable, Equatable { + public var consolidatedWeather: [ConsolidatedWeather] + + public struct ConsolidatedWeather: Decodable, Equatable { + public var applicableDate: Date + public var id: Int + public var maxTemp: Double + public var minTemp: Double + public var theTemp: Double + } +} + +public struct Location: Decodable, Equatable { + public var title: String + public var woeid: Int +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Mocks.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Mocks.swift new file mode 100644 index 00000000..604d2649 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClient/Mocks.swift @@ -0,0 +1,44 @@ +import Combine +import Foundation + +extension WeatherClient { + public static let empty = Self( + weather: { _ in + Just(WeatherResponse(consolidatedWeather: [])) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + }, + searchLocations: { _ in + Just([]).setFailureType(to: Error.self) + .eraseToAnyPublisher() + }) + + public static let happyPath = Self( + weather: { _ in + Just( + WeatherResponse( + consolidatedWeather: [ + .init(applicableDate: Date(), id: 1, maxTemp: 30, minTemp: 10, theTemp: 20), + .init(applicableDate: Date().addingTimeInterval(86400), id: 2, maxTemp: -10, minTemp: -30, theTemp: -20) + ] + ) + ) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + }, searchLocations: { _ in + Just([Location(title: "New York", woeid: 1)]) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + }) + + public static let failed = Self( + weather: { _ in + Fail(error: NSError(domain: "", code: 1)) + .eraseToAnyPublisher() + }, searchLocations: { _ in + Just([]) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + }) +} + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClientLive/Live.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClientLive/Live.swift new file mode 100644 index 00000000..cc30fe09 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Sources/WeatherClientLive/Live.swift @@ -0,0 +1,42 @@ +import Combine +import Foundation +import WeatherClient + +extension WeatherClient { + public static let live = Self( + weather: { id in + URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.metaweather.com/api/location/\(id)")!) + .map { data, _ in data } + .decode(type: WeatherResponse.self, decoder: weatherJsonDecoder) +// .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + }, + searchLocations: { coordinate in + URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.metaweather.com/api/location/search?lattlong=\(coordinate.latitude),\(coordinate.longitude)")!) + .map { data, _ in data } + .decode(type: [Location].self, decoder: weatherJsonDecoder) +// .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + }) +} + +private let weatherJsonDecoder: JSONDecoder = { + let jsonDecoder = JSONDecoder() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + jsonDecoder.dateDecodingStrategy = .formatted(formatter) + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + return jsonDecoder +}() + + +public let __tmp0 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp1 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp2 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp3 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp4 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp5 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp6 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp7 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp8 = 2 * 2 * 2 * 2.0 / 2 + 2 +public let __tmp9 = 2 * 2 * 2 * 2.0 / 2 + 2 diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/LinuxMain.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/LinuxMain.swift new file mode 100644 index 00000000..28159025 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import WeatherClientTests + +var tests = [XCTestCaseEntry]() +tests += WeatherClientTests.allTests() +XCTMain(tests) diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/WeatherClientTests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/WeatherClientTests.swift new file mode 100644 index 00000000..6eeb29b2 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/WeatherClientTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import WeatherClient + +final class WeatherClientTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(WeatherClient().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/XCTestManifests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/XCTestManifests.swift new file mode 100644 index 00000000..ed485f4b --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherClient/Tests/WeatherClientTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(WeatherClientTests.allTests), + ] +} +#endif diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/ContentView.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/ContentView.swift new file mode 100644 index 00000000..cc8b1fca --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/ContentView.swift @@ -0,0 +1,230 @@ +import Combine +import CombineSchedulers +import CoreLocation +import SwiftUI +import Network +import PathMonitorClient +import WeatherClient +import LocationClient + +public class AppViewModel: ObservableObject { + @Published var currentLocation: Location? + @Published var isConnected = true + @Published var weatherResults: [WeatherResponse.ConsolidatedWeather] = [] + + var weatherRequestCancellable: AnyCancellable? + var pathUpdateCancellable: AnyCancellable? + var searchLocationsCancellable: AnyCancellable? + var locationDelegateCancellable: AnyCancellable? + + let weatherClient: WeatherClient + let pathMonitorClient: PathMonitorClient + let locationClient: LocationClient + let mainQueue: AnySchedulerOf + // AnyScheduler + // any Scheduler + + public init( + locationClient: LocationClient, + pathMonitorClient: PathMonitorClient, + weatherClient: WeatherClient, + mainQueue: AnySchedulerOf + ) { + self.weatherClient = weatherClient + self.locationClient = locationClient + self.pathMonitorClient = pathMonitorClient + self.mainQueue = mainQueue + + self.pathUpdateCancellable = self.pathMonitorClient.networkPathPublisher + .map { $0.status == .satisfied } + .removeDuplicates() + .sink(receiveValue: { [weak self] isConnected in + guard let self = self else { return } + self.isConnected = isConnected + if self.isConnected { + self.refreshWeather() + } else { + self.weatherResults = [] + } + }) + + self.locationDelegateCancellable = self.locationClient.delegate + .sink { event in + switch event { + case let .didChangeAuthorization(status): + switch status { + case .notDetermined: + break + + case .restricted: + // TODO: show an alert + break + case .denied: + // TODO: show an alert + break + + case .authorizedAlways, .authorizedWhenInUse: + self.locationClient.requestLocation() + + @unknown default: + break + } + + case let .didUpdateLocations(locations): + guard self.isConnected, let location = locations.first else { return } + + self.searchLocationsCancellable = self.weatherClient + .searchLocations(location.coordinate) + .receive(on: self.mainQueue) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] locations in + self?.currentLocation = locations.first + self?.refreshWeather() + } + ) + + case .didFailWithError: + break + } + } + + if self.locationClient.authorizationStatus() == .authorizedWhenInUse { + self.locationClient.requestLocation() + } + } + + func refreshWeather() { + guard let location = self.currentLocation else { return } + + self.weatherResults = [] + + self.weatherRequestCancellable = self.weatherClient + .weather(location.woeid) + .receive(on: self.mainQueue) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] response in + self?.weatherResults = response.consolidatedWeather + }) + } + + func cancelButtonTapped() { + self.searchLocationsCancellable = nil + self.weatherRequestCancellable = nil + } + + func locationButtonTapped() { + switch self.locationClient.authorizationStatus() { + case .notDetermined: + self.locationClient.requestWhenInUseAuthorization() + + case .restricted: + // TODO: show an alert + break + case .denied: + // TODO: show an alert + break + + case .authorizedAlways, .authorizedWhenInUse: + self.locationClient.requestLocation() + + @unknown default: + break + } + } +} + +public struct ContentView: View { + @ObservedObject var viewModel: AppViewModel + + public init(viewModel: AppViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + NavigationView { + ZStack(alignment: .bottom) { + ZStack(alignment: .bottomTrailing) { + List { + ForEach(self.viewModel.weatherResults, id: \.id) { weather in + VStack(alignment: .leading) { + Text(dayOfWeekFormatter.string(from: weather.applicableDate).capitalized) + .font(.title) + + Text("Current temp: \(weather.theTemp, specifier: "%.1f")°C") + .bold() + Text("Max temp: \(weather.maxTemp, specifier: "%.1f")°C") + Text("Min temp: \(weather.minTemp, specifier: "%.1f")°C") + } + } + } + + Button( + action: { self.viewModel.locationButtonTapped() } + ) { + Image(systemName: "location.fill") + .foregroundColor(.white) + .frame(width: 60, height: 60) + } + .background(Color.black) + .clipShape(Circle()) + .padding() + } + + if !self.viewModel.isConnected { + HStack { + Image(systemName: "exclamationmark.octagon.fill") + + Text("Not connected to internet") + } + .foregroundColor(.white) + .padding() + .background(Color.red) + } + } + .navigationBarTitle(self.viewModel.currentLocation?.title ?? "Weather") + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + return ContentView( + viewModel: AppViewModel( + locationClient: .notDetermined, + pathMonitorClient: .satisfied, + weatherClient: .happyPath, + mainQueue: DispatchQueue.main.eraseToAnyScheduler() + ) + ) + } +} + +let dayOfWeekFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE" + return formatter +}() + +extension Scheduler { + static func immediate(now: SchedulerTimeType) -> AnySchedulerOf { + .init( + minimumTolerance: { .zero }, + now: { now }, + scheduleImmediately: { _, action in action() }, + delayed: { _, _, _, action in action() }, + interval: { _, _, _, _, action in action(); return AnyCancellable {} } + ) + } +} + +extension Scheduler +where + SchedulerTimeType == DispatchQueue.SchedulerTimeType, + SchedulerOptions == DispatchQueue.SchedulerOptions +{ + static var immediate: AnySchedulerOf { + .immediate(now: .init(.now())) + } +} diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/Info.plist b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/Info.plist new file mode 100644 index 00000000..9bcb2444 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/WeatherFeature.h b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/WeatherFeature.h new file mode 100644 index 00000000..7bfb1a65 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeature/WeatherFeature.h @@ -0,0 +1,19 @@ +// +// WeatherFeature.h +// WeatherFeature +// +// Created by Point-Free on 7/22/20. +// Copyright © 2020 Point-Free. All rights reserved. +// + +#import + +//! Project version number for WeatherFeature. +FOUNDATION_EXPORT double WeatherFeatureVersionNumber; + +//! Project version string for WeatherFeature. +FOUNDATION_EXPORT const unsigned char WeatherFeatureVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/Info.plist b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/WeatherFeatureTests.swift b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/WeatherFeatureTests.swift new file mode 100644 index 00000000..9953d4b1 --- /dev/null +++ b/0140-better-test-dependencies-pt3/DesigningDependencies/WeatherFeatureTests/WeatherFeatureTests.swift @@ -0,0 +1,207 @@ +import Combine +import CoreLocation +import LocationClient +import PathMonitorClient +@testable import WeatherClient +import XCTest +@testable import WeatherFeature + +extension WeatherResponse { + static let moderateWeather = WeatherResponse( + consolidatedWeather: [ + .init( + applicableDate: Date(timeIntervalSinceReferenceDate: 0), + id: 1, + maxTemp: 30, + minTemp: 20, + theTemp: 25 + ), + ] + ) +} + +extension Location { + static let brooklyn = Location(title: "Brooklyn", woeid: 1) +} + +extension AnyPublisher { + init(_ value: Output) { + self = Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() + } +} + +extension WeatherClient { + static let unimplemented = Self( + weather: { _ in fatalError() }, + searchLocations: { _ in fatalError() } + ) +} + +class WeatherFeatureTests: XCTestCase { + let mainQueue = DispatchQueue.testScheduler + + func testBasics() { + let viewModel = AppViewModel( + locationClient: .authorizedWhenInUse, + pathMonitorClient: .satisfied, + weatherClient: WeatherClient( + weather: { _ in .init(.moderateWeather) }, + searchLocations: { _ in .init([.brooklyn]) } + ), + mainQueue: .immediate + //ImmediateScheduler.shared.eraseToAnyScheduler() + //mainQueue.eraseToAnyScheduler() + ) + +// ImmediateScheduler + +// _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.01) + +// mainQueue.advance() + + XCTAssertEqual(viewModel.currentLocation, .brooklyn) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather) + } + + func testCancellation() { + let viewModel = AppViewModel( + locationClient: .authorizedWhenInUse, + pathMonitorClient: .satisfied, + weatherClient: WeatherClient( + weather: { _ in .init(.moderateWeather) }, + searchLocations: { _ in .init([.brooklyn]) } + ), + mainQueue: mainQueue.eraseToAnyScheduler() + // .immediate + //ImmediateScheduler.shared.eraseToAnyScheduler() + //mainQueue.eraseToAnyScheduler() + ) + + viewModel.cancelButtonTapped() + self.mainQueue.run() + + XCTAssertEqual(viewModel.currentLocation, nil) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, []) + } + + func testDisconnected() { + let viewModel = AppViewModel( + locationClient: .authorizedWhenInUse, + pathMonitorClient: .unsatisfied, + weatherClient: .unimplemented, + mainQueue: .failing + ) + + XCTAssertEqual(viewModel.currentLocation, nil) + XCTAssertEqual(viewModel.isConnected, false) + XCTAssertEqual(viewModel.weatherResults, []) + } + + func testPathUpdates() { + let pathUpdateSubject = PassthroughSubject() + let viewModel = AppViewModel( + locationClient: .authorizedWhenInUse, + pathMonitorClient: PathMonitorClient( + networkPathPublisher: pathUpdateSubject + .eraseToAnyPublisher() + ), + weatherClient: WeatherClient( + weather: { _ in .init(.moderateWeather) }, + searchLocations: { _ in .init([.brooklyn]) } + ), + mainQueue: .immediate // self.mainQueue.eraseToAnyScheduler() + ) + pathUpdateSubject.send(.init(status: .satisfied)) +// self.mainQueue.advance() + + XCTAssertEqual(viewModel.currentLocation, .brooklyn) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather) + + pathUpdateSubject.send(.init(status: .unsatisfied)) + + XCTAssertEqual(viewModel.currentLocation, .brooklyn) + XCTAssertEqual(viewModel.isConnected, false) + XCTAssertEqual(viewModel.weatherResults, []) + + pathUpdateSubject.send(.init(status: .satisfied)) +// self.mainQueue.advance() + + XCTAssertEqual(viewModel.currentLocation, .brooklyn) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather) + } + + func testLocationAuthorization() { + var authorizationStatus = CLAuthorizationStatus.notDetermined + let locationDelegateSubject = PassthroughSubject() + + let viewModel = AppViewModel( + locationClient: LocationClient( + authorizationStatus: { authorizationStatus }, + requestWhenInUseAuthorization: { + authorizationStatus = .authorizedWhenInUse + locationDelegateSubject.send(.didChangeAuthorization(authorizationStatus)) + }, + requestLocation: { + locationDelegateSubject.send(.didUpdateLocations([CLLocation()])) + }, + delegate: locationDelegateSubject.eraseToAnyPublisher() + ), + pathMonitorClient: .satisfied, + weatherClient: WeatherClient( + weather: { _ in .init(.moderateWeather) }, + searchLocations: { _ in .init([.brooklyn]) } + ), + mainQueue: .immediate //self.mainQueue.eraseToAnyScheduler() + ) + + XCTAssertEqual(viewModel.currentLocation, nil) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, []) + + viewModel.locationButtonTapped() +// self.mainQueue.advance() + + XCTAssertEqual(viewModel.currentLocation, .brooklyn) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather) + } + + func testLocationAuthorizationDenied() { + var authorizationStatus = CLAuthorizationStatus.notDetermined + let locationDelegateSubject = PassthroughSubject() + + let viewModel = AppViewModel( + locationClient: LocationClient( + authorizationStatus: { authorizationStatus }, + requestWhenInUseAuthorization: { + authorizationStatus = .denied + locationDelegateSubject.send(.didChangeAuthorization(authorizationStatus)) + }, + requestLocation: { fatalError() }, + delegate: locationDelegateSubject.eraseToAnyPublisher() + ), + pathMonitorClient: .satisfied, + weatherClient: WeatherClient( + weather: { _ in .init(.moderateWeather) }, + searchLocations: { _ in .init([.brooklyn]) } + ), + mainQueue: .failing + ) + + XCTAssertEqual(viewModel.currentLocation, nil) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, []) + //XCTAssertEqual(viewModel.authorizationAlert, nil) + + viewModel.locationButtonTapped() + + XCTAssertEqual(viewModel.currentLocation, nil) + XCTAssertEqual(viewModel.isConnected, true) + XCTAssertEqual(viewModel.weatherResults, []) + //XCTAssertEqual(viewModel.authorizationAlert, "Please give us location access.") + } +} diff --git a/0140-better-test-dependencies-pt3/README.md b/0140-better-test-dependencies-pt3/README.md new file mode 100644 index 00000000..bf418cc1 --- /dev/null +++ b/0140-better-test-dependencies-pt3/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Better Test Dependencies: Immediacy](https://www.pointfree.co/episodes/ep140-better-test-dependencies-immediacy) +> +> A major source of complexity in our applications is asynchrony. It is a side effect that is easy to overlook and can make testing more difficult and less reliable. We will explore the problem and come to a solution using Combine schedulers. diff --git a/README.md b/README.md index 2defc5eb..40c9c270 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [SwiftUI Animation: The Point](0137-swiftui-animation-pt3) 1. [Better Test Dependencies: Exhaustivity](0138-better-test-dependencies-pt1) 1. [Better Test Dependencies: Failability](0139-better-test-dependencies-pt2) +1. [Better Test Dependencies: Immediacy](0140-better-test-dependencies-pt3)