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)