From 5124f0e606c0d8a6f205bb610162120f1c3deab6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Jun 2020 17:38:55 -0400 Subject: [PATCH] Fix #64 --- .../project.pbxproj | 200 +++++++------- .../xcschemes/CombineSchedulers.xcscheme | 88 ++++++ .../CombineSchedulers/AppDelegate.swift | 2 +- .../CombineSchedulers/ContentView.swift | 166 +++++++---- .../CombineSchedulers/SceneDelegate.swift | 62 +---- .../CombineSchedulers/TestScheduler.swift | 113 ++++++++ .../CombineSchedulersTests.swift | 259 +++++++++++++++++- .../Playground.playground/Contents.swift | 60 ++++ .../contents.xcplayground | 4 + .../project.pbxproj | 194 ++++++------- .../CombineSchedulers/AppDelegate.swift | 2 +- .../CombineSchedulers/ContentView.swift | 163 +++++++---- .../CombineSchedulers/SceneDelegate.swift | 62 +---- .../CombineSchedulersTests.swift | 76 ++++- 14 files changed, 1027 insertions(+), 424 deletions(-) create mode 100644 0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme create mode 100644 0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/TestScheduler.swift create mode 100644 0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/Contents.swift create mode 100644 0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/contents.xcplayground diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj index 92eaac09..2fa733e3 100644 --- a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj @@ -7,48 +7,51 @@ objects = { /* Begin PBXBuildFile section */ - 2A6F2AC924817B1E00ED215A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */; }; - 2A6F2ACB24817B1E00ED215A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */; }; - 2A6F2ACD24817B1E00ED215A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACC24817B1E00ED215A /* ContentView.swift */; }; - 2A6F2ACF24817B1F00ED215A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */; }; - 2A6F2AD224817B1F00ED215A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */; }; - 2A6F2AD524817B1F00ED215A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */; }; - 2A6F2AE024817B1F00ED215A /* CombineSchedulersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */; }; + 2A3CEE872486B4EA00C69C3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */; }; + 2A3CEE892486B4EA00C69C3C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */; }; + 2A3CEE8B2486B4EA00C69C3C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */; }; + 2A3CEE8D2486B4EC00C69C3C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */; }; + 2A3CEE902486B4EC00C69C3C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */; }; + 2A3CEE932486B4EC00C69C3C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */; }; + 2A3CEE9E2486B4EC00C69C3C /* CombineSchedulersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */; }; + 4B648A612486E87B009448FB /* TestScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B648A602486E87B009448FB /* TestScheduler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 2A6F2ADC24817B1F00ED215A /* PBXContainerItemProxy */ = { + 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 2A6F2ABD24817B1E00ED215A /* Project object */; + containerPortal = 2A3CEE7B2486B4EA00C69C3C /* Project object */; proxyType = 1; - remoteGlobalIDString = 2A6F2AC424817B1E00ED215A; + remoteGlobalIDString = 2A3CEE822486B4EA00C69C3C; remoteInfo = CombineSchedulers; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CombineSchedulers.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 2A6F2ACC24817B1E00ED215A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2A6F2AD424817B1F00ED215A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 2A6F2AD624817B1F00ED215A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CombineSchedulersTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSchedulersTests.swift; sourceTree = ""; }; - 2A6F2AE124817B1F00ED215A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CombineSchedulers.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A3CEE922486B4EC00C69C3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2A3CEE942486B4EC00C69C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CombineSchedulersTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSchedulersTests.swift; sourceTree = ""; }; + 2A3CEE9F2486B4EC00C69C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A3CEEA82486E3C200C69C3C /* Playground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Playground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 4B648A602486E87B009448FB /* TestScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScheduler.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 2A6F2AC224817B1E00ED215A /* Frameworks */ = { + 2A3CEE802486B4EA00C69C3C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD824817B1F00ED215A /* Frameworks */ = { + 2A3CEE962486B4EC00C69C3C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -58,51 +61,53 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2A6F2ABC24817B1E00ED215A = { + 2A3CEE7A2486B4EA00C69C3C = { isa = PBXGroup; children = ( - 2A6F2AC724817B1E00ED215A /* CombineSchedulers */, - 2A6F2ADE24817B1F00ED215A /* CombineSchedulersTests */, - 2A6F2AC624817B1E00ED215A /* Products */, + 2A3CEEA82486E3C200C69C3C /* Playground.playground */, + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */, + 2A3CEE842486B4EA00C69C3C /* Products */, ); sourceTree = ""; }; - 2A6F2AC624817B1E00ED215A /* Products */ = { + 2A3CEE842486B4EA00C69C3C /* Products */ = { isa = PBXGroup; children = ( - 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */, - 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */, + 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */, + 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */, ); name = Products; sourceTree = ""; }; - 2A6F2AC724817B1E00ED215A /* CombineSchedulers */ = { + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */ = { isa = PBXGroup; children = ( - 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */, - 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */, - 2A6F2ACC24817B1E00ED215A /* ContentView.swift */, - 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */, - 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */, - 2A6F2AD624817B1F00ED215A /* Info.plist */, - 2A6F2AD024817B1F00ED215A /* Preview Content */, + 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */, + 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */, + 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */, + 4B648A602486E87B009448FB /* TestScheduler.swift */, + 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */, + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */, + 2A3CEE942486B4EC00C69C3C /* Info.plist */, + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */, ); path = CombineSchedulers; sourceTree = ""; }; - 2A6F2AD024817B1F00ED215A /* Preview Content */ = { + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */ = { isa = PBXGroup; children = ( - 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */, + 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; - 2A6F2ADE24817B1F00ED215A /* CombineSchedulersTests */ = { + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */ = { isa = PBXGroup; children = ( - 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */, - 2A6F2AE124817B1F00ED215A /* Info.plist */, + 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */, + 2A3CEE9F2486B4EC00C69C3C /* Info.plist */, ); path = CombineSchedulersTests; sourceTree = ""; @@ -110,13 +115,13 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2A6F2AC424817B1E00ED215A /* CombineSchedulers */ = { + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A6F2AE424817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulers" */; + buildConfigurationList = 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */; buildPhases = ( - 2A6F2AC124817B1E00ED215A /* Sources */, - 2A6F2AC224817B1E00ED215A /* Frameworks */, - 2A6F2AC324817B1E00ED215A /* Resources */, + 2A3CEE7F2486B4EA00C69C3C /* Sources */, + 2A3CEE802486B4EA00C69C3C /* Frameworks */, + 2A3CEE812486B4EA00C69C3C /* Resources */, ); buildRules = ( ); @@ -124,47 +129,47 @@ ); name = CombineSchedulers; productName = CombineSchedulers; - productReference = 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */; + productReference = 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */; productType = "com.apple.product-type.application"; }; - 2A6F2ADA24817B1F00ED215A /* CombineSchedulersTests */ = { + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A6F2AE724817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */; + buildConfigurationList = 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */; buildPhases = ( - 2A6F2AD724817B1F00ED215A /* Sources */, - 2A6F2AD824817B1F00ED215A /* Frameworks */, - 2A6F2AD924817B1F00ED215A /* Resources */, + 2A3CEE952486B4EC00C69C3C /* Sources */, + 2A3CEE962486B4EC00C69C3C /* Frameworks */, + 2A3CEE972486B4EC00C69C3C /* Resources */, ); buildRules = ( ); dependencies = ( - 2A6F2ADD24817B1F00ED215A /* PBXTargetDependency */, + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */, ); name = CombineSchedulersTests; productName = CombineSchedulersTests; - productReference = 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */; + productReference = 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 2A6F2ABD24817B1E00ED215A /* Project object */ = { + 2A3CEE7B2486B4EA00C69C3C /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; LastUpgradeCheck = 1150; ORGANIZATIONNAME = "Point-Free"; TargetAttributes = { - 2A6F2AC424817B1E00ED215A = { + 2A3CEE822486B4EA00C69C3C = { CreatedOnToolsVersion = 11.5; }; - 2A6F2ADA24817B1F00ED215A = { + 2A3CEE982486B4EC00C69C3C = { CreatedOnToolsVersion = 11.5; - TestTargetID = 2A6F2AC424817B1E00ED215A; + TestTargetID = 2A3CEE822486B4EA00C69C3C; }; }; }; - buildConfigurationList = 2A6F2AC024817B1E00ED215A /* Build configuration list for PBXProject "CombineSchedulers" */; + buildConfigurationList = 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -172,29 +177,29 @@ en, Base, ); - mainGroup = 2A6F2ABC24817B1E00ED215A; - productRefGroup = 2A6F2AC624817B1E00ED215A /* Products */; + mainGroup = 2A3CEE7A2486B4EA00C69C3C; + productRefGroup = 2A3CEE842486B4EA00C69C3C /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 2A6F2AC424817B1E00ED215A /* CombineSchedulers */, - 2A6F2ADA24817B1F00ED215A /* CombineSchedulersTests */, + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 2A6F2AC324817B1E00ED215A /* Resources */ = { + 2A3CEE812486B4EA00C69C3C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AD524817B1F00ED215A /* LaunchScreen.storyboard in Resources */, - 2A6F2AD224817B1F00ED215A /* Preview Assets.xcassets in Resources */, - 2A6F2ACF24817B1F00ED215A /* Assets.xcassets in Resources */, + 2A3CEE932486B4EC00C69C3C /* LaunchScreen.storyboard in Resources */, + 2A3CEE902486B4EC00C69C3C /* Preview Assets.xcassets in Resources */, + 2A3CEE8D2486B4EC00C69C3C /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD924817B1F00ED215A /* Resources */ = { + 2A3CEE972486B4EC00C69C3C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -204,39 +209,40 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 2A6F2AC124817B1E00ED215A /* Sources */ = { + 2A3CEE7F2486B4EA00C69C3C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AC924817B1E00ED215A /* AppDelegate.swift in Sources */, - 2A6F2ACB24817B1E00ED215A /* SceneDelegate.swift in Sources */, - 2A6F2ACD24817B1E00ED215A /* ContentView.swift in Sources */, + 2A3CEE872486B4EA00C69C3C /* AppDelegate.swift in Sources */, + 4B648A612486E87B009448FB /* TestScheduler.swift in Sources */, + 2A3CEE892486B4EA00C69C3C /* SceneDelegate.swift in Sources */, + 2A3CEE8B2486B4EA00C69C3C /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD724817B1F00ED215A /* Sources */ = { + 2A3CEE952486B4EC00C69C3C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AE024817B1F00ED215A /* CombineSchedulersTests.swift in Sources */, + 2A3CEE9E2486B4EC00C69C3C /* CombineSchedulersTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 2A6F2ADD24817B1F00ED215A /* PBXTargetDependency */ = { + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2A6F2AC424817B1E00ED215A /* CombineSchedulers */; - targetProxy = 2A6F2ADC24817B1F00ED215A /* PBXContainerItemProxy */; + target = 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */; + targetProxy = 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */ = { + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - 2A6F2AD424817B1F00ED215A /* Base */, + 2A3CEE922486B4EC00C69C3C /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -244,7 +250,7 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 2A6F2AE224817B1F00ED215A /* Debug */ = { + 2A3CEEA02486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -304,7 +310,7 @@ }; name = Debug; }; - 2A6F2AE324817B1F00ED215A /* Release */ = { + 2A3CEEA12486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -358,7 +364,7 @@ }; name = Release; }; - 2A6F2AE524817B1F00ED215A /* Debug */ = { + 2A3CEEA32486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -377,7 +383,7 @@ }; name = Debug; }; - 2A6F2AE624817B1F00ED215A /* Release */ = { + 2A3CEEA42486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -396,7 +402,7 @@ }; name = Release; }; - 2A6F2AE824817B1F00ED215A /* Debug */ = { + 2A3CEEA62486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -417,7 +423,7 @@ }; name = Debug; }; - 2A6F2AE924817B1F00ED215A /* Release */ = { + 2A3CEEA72486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -441,34 +447,34 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 2A6F2AC024817B1E00ED215A /* Build configuration list for PBXProject "CombineSchedulers" */ = { + 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE224817B1F00ED215A /* Debug */, - 2A6F2AE324817B1F00ED215A /* Release */, + 2A3CEEA02486B4EC00C69C3C /* Debug */, + 2A3CEEA12486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A6F2AE424817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulers" */ = { + 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE524817B1F00ED215A /* Debug */, - 2A6F2AE624817B1F00ED215A /* Release */, + 2A3CEEA32486B4EC00C69C3C /* Debug */, + 2A3CEEA42486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A6F2AE724817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */ = { + 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE824817B1F00ED215A /* Debug */, - 2A6F2AE924817B1F00ED215A /* Release */, + 2A3CEEA62486B4EC00C69C3C /* Debug */, + 2A3CEEA72486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 2A6F2ABD24817B1E00ED215A /* Project object */; + rootObject = 2A3CEE7B2486B4EA00C69C3C /* Project object */; } diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme new file mode 100644 index 00000000..55e88ddc --- /dev/null +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/AppDelegate.swift b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/AppDelegate.swift index 72ab3aa0..bdf8201c 100644 --- a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/AppDelegate.swift +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/AppDelegate.swift @@ -2,7 +2,7 @@ // AppDelegate.swift // CombineSchedulers // -// Created by Point-Free on 5/29/20. +// Created by Point-Free on 6/2/20. // Copyright © 2020 Point-Free. All rights reserved. // diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/ContentView.swift b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/ContentView.swift index eae27d6e..42246f0f 100644 --- a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/ContentView.swift +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/ContentView.swift @@ -1,91 +1,149 @@ import Combine import SwiftUI -func validationMessage( - forPassword password: P -) -> AnyPublisher where P.Output == String, P.Failure == Never { - - password.map { - $0.count < 5 ? "Password is too short 👎" - : $0.count > 20 ? "Password is too long 👎" - : "Password is good 👍" - } - .eraseToAnyPublisher() - -} - - class RegisterViewModel: ObservableObject { + struct Alert: Identifiable { + var title: String + var id: String { self.title } + } + @Published var email = "" + @Published var errorAlert: Alert? + @Published var isRegistered = false + @Published var isRegisterRequestInFlight = false @Published var password = "" - @Published var isLoginSuccessful = false @Published var passwordValidationMessage = "" - var cancellables: Set = [] + let register: (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> - let login: (String, String) -> AnyPublisher + var cancellables: Set = [] init( - login: @escaping (String, String) -> AnyPublisher + register: @escaping (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError>, + validatePassword: @escaping (String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> ) { - self.login = login + self.register = register self.$password - .map { - $0.count < 5 ? "Password is too short 👎" - : $0.count > 20 ? "Password is too long 👎" - : "Password is good 👍" - } - .sink { self.passwordValidationMessage = $0 } + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) +// .debounce(for: .milliseconds(300), scheduler: ImmediateScheduler.shared) + .flatMap { password in + password.isEmpty + ? Just("").eraseToAnyPublisher() + : validatePassword(password) + .receive(on: DispatchQueue.main) +// .receive(on: ImmediateScheduler.shared) + .map { data, _ in + String(decoding: data, as: UTF8.self) + } + .replaceError(with: "Could not validate password.") + .eraseToAnyPublisher() + } + .sink { [weak self] in self?.passwordValidationMessage = $0 } .store(in: &self.cancellables) } - func loginButtonTapped() { - self.login(self.email, self.password) + func registerButtonTapped() { + self.isRegisterRequestInFlight = true + self.register(self.email, self.password) .receive(on: DispatchQueue.main) - .sink { self.isLoginSuccessful = $0 } +// .receive(on: ImmediateScheduler.shared) + .map { data, _ in + Bool(String(decoding: data, as: UTF8.self)) ?? false + } + .replaceError(with: false) + .sink { + self.isRegistered = $0 + self.isRegisterRequestInFlight = false + if !$0 { + self.errorAlert = Alert(title: "Failed to register. Please try again.") + } + } .store(in: &self.cancellables) } } +func registerRequest( + email: String, + password: String +) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + var components = URLComponents(string: "https://www.pointfree.co/register")! + components.queryItems = [ + URLQueryItem(name: "email", value: email), + URLQueryItem(name: "password", value: password) + ] + + return URLSession.shared + .dataTaskPublisher(for: components.url!) + .eraseToAnyPublisher() +} -//class PasswordViewModel: ObservableObject { -// @Published var password: String = "" -// -// //let passwordValidationMessage: AnyPublisher -// @Published var passwordValidationMessage = "" -// -// func validatePassword() { -// validationMessage(forPassword: Just(self.password)) -// .sink { self.passwordValidationMessage = $0 } -// } -//} - struct ContentView: View { -// @State var password: String = "" - @ObservedObject var viewModel = RegisterViewModel( - login: { _, _ in Just(true).eraseToAnyPublisher() } - ) + @ObservedObject var viewModel: RegisterViewModel var body: some View { - Form { - if self.viewModel.isLoginSuccessful { - Text("Logged in! Welcome!") + NavigationView { + if self.viewModel.isRegistered { + Text("Welcome!") } else { - TextField("Email", text: self.$viewModel.email) - TextField("Password", text: self.$viewModel.password) - - Text(self.viewModel.passwordValidationMessage) - - Button("Login") { self.viewModel.loginButtonTapped() } + Form { + Section(header: Text("Email")) { + TextField( + "blob@pointfree.co", + text: self.$viewModel.email + ) + } + + Section(header: Text("Password")) { + TextField( + "Password", + text: self.$viewModel.password + ) + if !self.viewModel.passwordValidationMessage.isEmpty { + Text(self.viewModel.passwordValidationMessage) + } + } + + if self.viewModel.isRegisterRequestInFlight { + Text("Registering...") + } else { + Button("Register") { self.viewModel.registerButtonTapped() } + } + } + .navigationBarTitle("Register") + .alert(item: self.$viewModel.errorAlert) { errorAlert in + Alert(title: Text(errorAlert.title)) + } } } } } +func mockValidate(password: String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + let message = password.count < 5 ? "Password is too short 👎" + : password.count > 20 ? "Password is too long 👎" + : "Password is good 👍" + return Just((Data(message.utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + ContentView( + viewModel: RegisterViewModel( + register: { _, _ in + Just((Data("false".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .delay(for: 1, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + }, + validatePassword: { + mockValidate(password: $0) + .delay(for: 0.5, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + }) + ) } } diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/SceneDelegate.swift b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/SceneDelegate.swift index f9d5b4e2..885a6b85 100644 --- a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/SceneDelegate.swift +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/SceneDelegate.swift @@ -1,64 +1,24 @@ -// -// SceneDelegate.swift -// CombineSchedulers -// -// Created by Point-Free on 5/29/20. -// Copyright © 2020 Point-Free. All rights reserved. -// - -import UIKit import SwiftUI +import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() + let contentView = ContentView( + viewModel: RegisterViewModel( + register: registerRequest(email:password:), + validatePassword: mockValidate(password:) + ) + ) - // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) - self.window = window - window.makeKeyAndVisible() + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + window.makeKeyAndVisible() } } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - } diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/TestScheduler.swift b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/TestScheduler.swift new file mode 100644 index 00000000..1771bdff --- /dev/null +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulers/TestScheduler.swift @@ -0,0 +1,113 @@ +import Combine +import Dispatch + +final class TestScheduler: Scheduler where SchedulerTimeType: Strideable, SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible { + + var minimumTolerance: SchedulerTimeType.Stride = 0 + var now: SchedulerTimeType + private var lastId = 0 + + private var scheduled: [(id: Int, action: () -> Void, date: SchedulerTimeType)] = [] + + init(now: SchedulerTimeType) { + self.now = now + } + + func advance(by stride: SchedulerTimeType.Stride = .zero) { + + self.scheduled.sort { lhs, rhs in + (lhs.date, lhs.id) < (rhs.date, rhs.id) + } + + guard + let nextDate = scheduled.first?.date, + self.now.advanced(by: stride) >= nextDate + else { + self.now = self.now.advanced(by: stride) + return + } + + let nextStride = stride - self.now.distance(to: nextDate) + self.now = nextDate + + while let (_, action, date) = self.scheduled.first, date == nextDate { + self.scheduled.removeFirst() + action() + } + + self.advance(by: nextStride) + + +// self.now = self.now.advanced(by: stride) +// +// var index = 0 +// while index < self.scheduled.count { +// let (id, action, date) = self.scheduled[index] +// if date <= self.now { +// action() +// self.scheduled.remove(at: index) +// } else { +// index += 1 +// } +// } +// for (id, action, date) in self.scheduled { +// if date <= self.now { +// action() +// } +// } + + self.scheduled.removeAll(where: { $0.date <= self.now }) + } + + func schedule( + options _: SchedulerOptions?, + _ action: @escaping () -> Void + ) { + self.scheduled.append((self.nextId(), action, self.now)) + } + + func schedule( + after date: SchedulerTimeType, + tolerance _: SchedulerTimeType.Stride, + options _: SchedulerOptions?, + _ action: @escaping () -> Void + ) { + self.scheduled.append((self.nextId(), action, date)) + } + + func schedule( + after date: SchedulerTimeType, + interval: SchedulerTimeType.Stride, + tolerance _: SchedulerTimeType.Stride, + options _: SchedulerOptions?, + _ action: @escaping () -> Void + ) -> Cancellable { + + let id = self.nextId() + + func scheduleAction(for date: SchedulerTimeType) -> () -> Void { + return { [weak self] in + let nextDate = date.advanced(by: interval) + self?.scheduled.append((id, scheduleAction(for: nextDate), nextDate)) + action() + } + } + + self.scheduled.append((id, scheduleAction(for: date), date)) + + return AnyCancellable { + self.scheduled.removeAll(where: { $0.id == id }) + } + } + + private func nextId() -> Int { + self.lastId += 1 + return self.lastId + } +} + +extension DispatchQueue { + static var testScheduler: TestScheduler { + TestScheduler(now: .init(.init(uptimeNanoseconds: 1))) + } +} diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift index 292f5719..8896c76f 100644 --- a/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift +++ b/0104-combine-schedulers-pt1/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift @@ -5,23 +5,264 @@ import XCTest class CombineSchedulersTests: XCTestCase { var cancellables: Set = [] - func testHappyPath() { + func testRegistrationSuccessful() { let viewModel = RegisterViewModel( - login: { _, _ in Just(true).eraseToAnyPublisher() } + register: { _, _ in + Just((Data("true".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() + }, + validatePassword: { _ in Empty(completeImmediately: true).eraseToAnyPublisher() } ) - var expectedOutput: [Bool] = [] - viewModel.$isLoginSuccessful - .sink { expectedOutput.append($0) } + var isRegistered: [Bool] = [] + viewModel.$isRegistered + .sink { isRegistered.append($0) } .store(in: &self.cancellables) - XCTAssertEqual(expectedOutput, [false, ]) +// XCTAssertEqual(viewModel.isRegistered, false) + XCTAssertEqual(isRegistered, [false]) + viewModel.email = "blob@pointfree.co" + XCTAssertEqual(isRegistered, [false]) + viewModel.password = "blob is awesome" - viewModel.loginButtonTapped() + XCTAssertEqual(isRegistered, [false]) + + viewModel.registerButtonTapped() + +// XCTAssertEqual(viewModel.isRegistered, true) + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.1) + XCTAssertEqual(isRegistered, [false, true]) + } + + func testRegistrationFailure() { + let viewModel = RegisterViewModel( + register: { _, _ in + Just((Data("false".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() + }, + validatePassword: { _ in Empty(completeImmediately: true).eraseToAnyPublisher() } + ) + + XCTAssertEqual(viewModel.isRegistered, false) + + viewModel.email = "blob@pointfree.co" + viewModel.password = "blob is awesome" + viewModel.registerButtonTapped() + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.01) - XCTAssertEqual(viewModel.isLoginSuccessful, true) - XCTAssertEqual(expectedOutput, [false, true]) + XCTAssertEqual(viewModel.isRegistered, false) + XCTAssertEqual(viewModel.errorAlert?.title, "Failed to register. Please try again.") + } + + func testValidatePassword() { + let viewModel = RegisterViewModel( + register: { _, _ in fatalError() }, + validatePassword: mockValidate(password:) + ) + + var passwordValidationMessage: [String] = [] + viewModel.$passwordValidationMessage + .sink { passwordValidationMessage.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(passwordValidationMessage, [""]) + + viewModel.password = "blob" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.31) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎"]) + + viewModel.password = "blob is awesome" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.21) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎"]) + + viewModel.password = "blob is awesome!!!!!!" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.31) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎", "Password is too long 👎"]) + } + + let scheduler = DispatchQueue.testScheduler + + func testImmediateScheduledAction() { + var isExecuted = false + scheduler.schedule { + isExecuted = true + } + + XCTAssertEqual(isExecuted, false) + scheduler.advance() + XCTAssertEqual(isExecuted, true) + } + + func testMultipleImmediateScheduledActions() { + var executionCount = 0 + + scheduler.schedule { + executionCount += 1 + } + scheduler.schedule { + executionCount += 1 + } + + XCTAssertEqual(executionCount, 0) + scheduler.advance() + XCTAssertEqual(executionCount, 2) + } + + func testImmedateScheduledActionWithPublisher() { + var output: [Int] = [] + + Just(1) + .receive(on: scheduler) + .sink { output.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(output, []) + scheduler.advance() + XCTAssertEqual(output, [1]) + } + + func testImmedateScheduledActionWithMultiplePublishers() { + var output: [Int] = [] + + Just(1) + .receive(on: scheduler) + .merge(with: Just(2).receive(on: scheduler)) + .sink { output.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(output, []) + scheduler.advance() + XCTAssertEqual(output, [1, 2]) } + func testScheduledAfterDelay() { + var isExecuted = false + scheduler.schedule(after: scheduler.now.advanced(by: 1)) { + isExecuted = true + } + + XCTAssertEqual(isExecuted, false) + scheduler.advance(by: .milliseconds(500)) + XCTAssertEqual(isExecuted, false) + scheduler.advance(by: .milliseconds(499)) + XCTAssertEqual(isExecuted, false) + scheduler.advance(by: .microseconds(999)) + XCTAssertEqual(isExecuted, false) + scheduler.advance(by: .microseconds(1)) + XCTAssertEqual(isExecuted, true) + } + + func testScheduledAfterALongDelay() { + var isExecuted = false + scheduler.schedule(after: scheduler.now.advanced(by: 1_000_000)) { + isExecuted = true + } + + XCTAssertEqual(isExecuted, false) + scheduler.advance(by: .seconds(1_000_000)) + XCTAssertEqual(isExecuted, true) + + } + + func testSchedulerInterval() { + var executionCount = 0 + + scheduler.schedule(after: scheduler.now, interval: 1) { + executionCount += 1 + } + .store(in: &self.cancellables) + + XCTAssertEqual(executionCount, 0) + scheduler.advance() + XCTAssertEqual(executionCount, 1) + scheduler.advance(by: .milliseconds(500)) + XCTAssertEqual(executionCount, 1) + scheduler.advance(by: .milliseconds(500)) + XCTAssertEqual(executionCount, 2) + scheduler.advance(by: .seconds(1)) + XCTAssertEqual(executionCount, 3) + + scheduler.advance(by: .seconds(5)) + XCTAssertEqual(executionCount, 8) + } + + func testScheduledTwoIntervals_Fail() { + var values: [String] = [] + scheduler.schedule(after: scheduler.now.advanced(by: 1), interval: 1) { + values.append("Hello") + } + .store(in: &self.cancellables) + scheduler.schedule(after: scheduler.now.advanced(by: 2), interval: 2) { + values.append("World") + } + .store(in: &self.cancellables) + + XCTAssertEqual(values, []) + scheduler.advance(by: 2) + XCTAssertEqual(values, ["Hello", "Hello", "World"]) + } + + func testSchedulerNow() { + var times: [UInt64] = [] + scheduler.schedule(after: scheduler.now, interval: 1) { + times.append(self.scheduler.now.dispatchTime.uptimeNanoseconds) + } + .store(in: &self.cancellables) + + XCTAssertEqual(times, []) + scheduler.advance(by: 3) + XCTAssertEqual(times, [1, 1_000_000_001, 2_000_000_001, 3_000_000_001]) + } + + func testScheduledIntervalCancellation() { + var executionCount = 0 + + scheduler.schedule(after: scheduler.now, interval: 1) { + executionCount += 1 + } + .store(in: &self.cancellables) + + XCTAssertEqual(executionCount, 0) + scheduler.advance() + XCTAssertEqual(executionCount, 1) + scheduler.advance(by: .milliseconds(500)) + XCTAssertEqual(executionCount, 1) + scheduler.advance(by: .milliseconds(500)) + XCTAssertEqual(executionCount, 2) + + self.cancellables.removeAll() + + scheduler.advance(by: .seconds(1)) + XCTAssertEqual(executionCount, 2) + } + + func testFun() { + var values: [Int] = [] + scheduler.schedule(after: scheduler.now, interval: 1) { + values.append(values.count) + } + .store(in: &self.cancellables) + + XCTAssertEqual(values, []) + scheduler.advance(by: 1000) + XCTAssertEqual(values, Array(0...1_000)) + } + + func testFail() { + let subject = PassthroughSubject() + + var count = 0 + subject + .debounce(for: 1, scheduler: scheduler) + .receive(on: scheduler) + .sink { count += 1 } + .store(in: &self.cancellables) + + subject.send() + scheduler.advance(by: 100) + XCTAssertEqual(count, 1) + } } diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/Contents.swift b/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/Contents.swift new file mode 100644 index 00000000..427b8493 --- /dev/null +++ b/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/Contents.swift @@ -0,0 +1,60 @@ +import Combine +import Dispatch +import Foundation + +var cancellables: Set = [] + +//DispatchQueue.main.schedule { +// print("DispatchQueue", "ASAP") +//} +//DispatchQueue.main.schedule(after: .init(.now() + 1)) { +// print("DispatchQueue", "delayed") +//} +//DispatchQueue.main.schedule(after: .init(.now()), interval: 1) { +// print("DispatchQueue", "timer") +//}.store(in: &cancellables) +// +//RunLoop.main.schedule { +// print("RunLoop", "ASAP") +//} +//RunLoop.main.schedule(after: .init(Date() + 1)) { +// print("RunLoop", "delayed") +//} +//RunLoop.main.schedule(after: .init(Date()), interval: 1) { +// print("RunLoop", "timer") +//}.store(in: &cancellables) +// +//OperationQueue.main.schedule { +// print("OperationQueue", "ASAP") +//} +//OperationQueue.main.schedule(after: .init(Date() + 1)) { +// print("OperationQueue", "delayed") +//} +//OperationQueue.main.schedule(after: .init(Date()), interval: 1) { +// print("OperationQueue", "timer") +//}.store(in: &cancellables) + + +//ImmediateScheduler.SchedulerTimeType + +//ImmediateScheduler.shared.now.advanced(by: 1) +// +//ImmediateScheduler.shared.schedule { +// print("ImmediateScheduler", "ASAP") +//} +//ImmediateScheduler.shared.schedule(after: ImmediateScheduler.shared.now.advanced(by: 1)) { +// print("ImmediateScheduler", "delayed") +//} +//ImmediateScheduler.shared.schedule(after: ImmediateScheduler.shared.now, interval: 1) { +// print("ImmediateScheduler", "timer") +//}.store(in: &cancellables) + + +Just(1) +.subscribe(on: <#T##Scheduler#>) +.receive(on: <#T##Scheduler#>) +.delay(for: <#T##SchedulerTimeIntervalConvertible & Comparable & SignedNumeric#>, scheduler: <#T##Scheduler#>) +.timeout(<#T##interval: SchedulerTimeIntervalConvertible & Comparable & SignedNumeric##SchedulerTimeIntervalConvertible & Comparable & SignedNumeric#>, scheduler: <#T##Scheduler#>) +.throttle(for: <#T##SchedulerTimeIntervalConvertible & Comparable & SignedNumeric#>, scheduler: <#T##Scheduler#>, latest: <#T##Bool#>) +.debounce(for: <#T##SchedulerTimeIntervalConvertible & Comparable & SignedNumeric#>, scheduler: <#T##Scheduler#>) + diff --git a/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/contents.xcplayground b/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/contents.xcplayground new file mode 100644 index 00000000..5da2641c --- /dev/null +++ b/0104-combine-schedulers-pt1/CombineSchedulers/Playground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj index 92eaac09..5909a800 100644 --- a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj +++ b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj @@ -7,48 +7,48 @@ objects = { /* Begin PBXBuildFile section */ - 2A6F2AC924817B1E00ED215A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */; }; - 2A6F2ACB24817B1E00ED215A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */; }; - 2A6F2ACD24817B1E00ED215A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACC24817B1E00ED215A /* ContentView.swift */; }; - 2A6F2ACF24817B1F00ED215A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */; }; - 2A6F2AD224817B1F00ED215A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */; }; - 2A6F2AD524817B1F00ED215A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */; }; - 2A6F2AE024817B1F00ED215A /* CombineSchedulersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */; }; + 2A3CEE872486B4EA00C69C3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */; }; + 2A3CEE892486B4EA00C69C3C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */; }; + 2A3CEE8B2486B4EA00C69C3C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */; }; + 2A3CEE8D2486B4EC00C69C3C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */; }; + 2A3CEE902486B4EC00C69C3C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */; }; + 2A3CEE932486B4EC00C69C3C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */; }; + 2A3CEE9E2486B4EC00C69C3C /* CombineSchedulersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 2A6F2ADC24817B1F00ED215A /* PBXContainerItemProxy */ = { + 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 2A6F2ABD24817B1E00ED215A /* Project object */; + containerPortal = 2A3CEE7B2486B4EA00C69C3C /* Project object */; proxyType = 1; - remoteGlobalIDString = 2A6F2AC424817B1E00ED215A; + remoteGlobalIDString = 2A3CEE822486B4EA00C69C3C; remoteInfo = CombineSchedulers; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CombineSchedulers.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 2A6F2ACC24817B1E00ED215A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2A6F2AD424817B1F00ED215A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 2A6F2AD624817B1F00ED215A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CombineSchedulersTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSchedulersTests.swift; sourceTree = ""; }; - 2A6F2AE124817B1F00ED215A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CombineSchedulers.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A3CEE922486B4EC00C69C3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2A3CEE942486B4EC00C69C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CombineSchedulersTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineSchedulersTests.swift; sourceTree = ""; }; + 2A3CEE9F2486B4EC00C69C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 2A6F2AC224817B1E00ED215A /* Frameworks */ = { + 2A3CEE802486B4EA00C69C3C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD824817B1F00ED215A /* Frameworks */ = { + 2A3CEE962486B4EC00C69C3C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -58,51 +58,51 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2A6F2ABC24817B1E00ED215A = { + 2A3CEE7A2486B4EA00C69C3C = { isa = PBXGroup; children = ( - 2A6F2AC724817B1E00ED215A /* CombineSchedulers */, - 2A6F2ADE24817B1F00ED215A /* CombineSchedulersTests */, - 2A6F2AC624817B1E00ED215A /* Products */, + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */, + 2A3CEE842486B4EA00C69C3C /* Products */, ); sourceTree = ""; }; - 2A6F2AC624817B1E00ED215A /* Products */ = { + 2A3CEE842486B4EA00C69C3C /* Products */ = { isa = PBXGroup; children = ( - 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */, - 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */, + 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */, + 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */, ); name = Products; sourceTree = ""; }; - 2A6F2AC724817B1E00ED215A /* CombineSchedulers */ = { + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */ = { isa = PBXGroup; children = ( - 2A6F2AC824817B1E00ED215A /* AppDelegate.swift */, - 2A6F2ACA24817B1E00ED215A /* SceneDelegate.swift */, - 2A6F2ACC24817B1E00ED215A /* ContentView.swift */, - 2A6F2ACE24817B1F00ED215A /* Assets.xcassets */, - 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */, - 2A6F2AD624817B1F00ED215A /* Info.plist */, - 2A6F2AD024817B1F00ED215A /* Preview Content */, + 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */, + 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */, + 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */, + 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */, + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */, + 2A3CEE942486B4EC00C69C3C /* Info.plist */, + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */, ); path = CombineSchedulers; sourceTree = ""; }; - 2A6F2AD024817B1F00ED215A /* Preview Content */ = { + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */ = { isa = PBXGroup; children = ( - 2A6F2AD124817B1F00ED215A /* Preview Assets.xcassets */, + 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; - 2A6F2ADE24817B1F00ED215A /* CombineSchedulersTests */ = { + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */ = { isa = PBXGroup; children = ( - 2A6F2ADF24817B1F00ED215A /* CombineSchedulersTests.swift */, - 2A6F2AE124817B1F00ED215A /* Info.plist */, + 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */, + 2A3CEE9F2486B4EC00C69C3C /* Info.plist */, ); path = CombineSchedulersTests; sourceTree = ""; @@ -110,13 +110,13 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2A6F2AC424817B1E00ED215A /* CombineSchedulers */ = { + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A6F2AE424817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulers" */; + buildConfigurationList = 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */; buildPhases = ( - 2A6F2AC124817B1E00ED215A /* Sources */, - 2A6F2AC224817B1E00ED215A /* Frameworks */, - 2A6F2AC324817B1E00ED215A /* Resources */, + 2A3CEE7F2486B4EA00C69C3C /* Sources */, + 2A3CEE802486B4EA00C69C3C /* Frameworks */, + 2A3CEE812486B4EA00C69C3C /* Resources */, ); buildRules = ( ); @@ -124,47 +124,47 @@ ); name = CombineSchedulers; productName = CombineSchedulers; - productReference = 2A6F2AC524817B1E00ED215A /* CombineSchedulers.app */; + productReference = 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */; productType = "com.apple.product-type.application"; }; - 2A6F2ADA24817B1F00ED215A /* CombineSchedulersTests */ = { + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A6F2AE724817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */; + buildConfigurationList = 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */; buildPhases = ( - 2A6F2AD724817B1F00ED215A /* Sources */, - 2A6F2AD824817B1F00ED215A /* Frameworks */, - 2A6F2AD924817B1F00ED215A /* Resources */, + 2A3CEE952486B4EC00C69C3C /* Sources */, + 2A3CEE962486B4EC00C69C3C /* Frameworks */, + 2A3CEE972486B4EC00C69C3C /* Resources */, ); buildRules = ( ); dependencies = ( - 2A6F2ADD24817B1F00ED215A /* PBXTargetDependency */, + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */, ); name = CombineSchedulersTests; productName = CombineSchedulersTests; - productReference = 2A6F2ADB24817B1F00ED215A /* CombineSchedulersTests.xctest */; + productReference = 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 2A6F2ABD24817B1E00ED215A /* Project object */ = { + 2A3CEE7B2486B4EA00C69C3C /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; LastUpgradeCheck = 1150; ORGANIZATIONNAME = "Point-Free"; TargetAttributes = { - 2A6F2AC424817B1E00ED215A = { + 2A3CEE822486B4EA00C69C3C = { CreatedOnToolsVersion = 11.5; }; - 2A6F2ADA24817B1F00ED215A = { + 2A3CEE982486B4EC00C69C3C = { CreatedOnToolsVersion = 11.5; - TestTargetID = 2A6F2AC424817B1E00ED215A; + TestTargetID = 2A3CEE822486B4EA00C69C3C; }; }; }; - buildConfigurationList = 2A6F2AC024817B1E00ED215A /* Build configuration list for PBXProject "CombineSchedulers" */; + buildConfigurationList = 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -172,29 +172,29 @@ en, Base, ); - mainGroup = 2A6F2ABC24817B1E00ED215A; - productRefGroup = 2A6F2AC624817B1E00ED215A /* Products */; + mainGroup = 2A3CEE7A2486B4EA00C69C3C; + productRefGroup = 2A3CEE842486B4EA00C69C3C /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 2A6F2AC424817B1E00ED215A /* CombineSchedulers */, - 2A6F2ADA24817B1F00ED215A /* CombineSchedulersTests */, + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 2A6F2AC324817B1E00ED215A /* Resources */ = { + 2A3CEE812486B4EA00C69C3C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AD524817B1F00ED215A /* LaunchScreen.storyboard in Resources */, - 2A6F2AD224817B1F00ED215A /* Preview Assets.xcassets in Resources */, - 2A6F2ACF24817B1F00ED215A /* Assets.xcassets in Resources */, + 2A3CEE932486B4EC00C69C3C /* LaunchScreen.storyboard in Resources */, + 2A3CEE902486B4EC00C69C3C /* Preview Assets.xcassets in Resources */, + 2A3CEE8D2486B4EC00C69C3C /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD924817B1F00ED215A /* Resources */ = { + 2A3CEE972486B4EC00C69C3C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -204,39 +204,39 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 2A6F2AC124817B1E00ED215A /* Sources */ = { + 2A3CEE7F2486B4EA00C69C3C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AC924817B1E00ED215A /* AppDelegate.swift in Sources */, - 2A6F2ACB24817B1E00ED215A /* SceneDelegate.swift in Sources */, - 2A6F2ACD24817B1E00ED215A /* ContentView.swift in Sources */, + 2A3CEE872486B4EA00C69C3C /* AppDelegate.swift in Sources */, + 2A3CEE892486B4EA00C69C3C /* SceneDelegate.swift in Sources */, + 2A3CEE8B2486B4EA00C69C3C /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A6F2AD724817B1F00ED215A /* Sources */ = { + 2A3CEE952486B4EC00C69C3C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A6F2AE024817B1F00ED215A /* CombineSchedulersTests.swift in Sources */, + 2A3CEE9E2486B4EC00C69C3C /* CombineSchedulersTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 2A6F2ADD24817B1F00ED215A /* PBXTargetDependency */ = { + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2A6F2AC424817B1E00ED215A /* CombineSchedulers */; - targetProxy = 2A6F2ADC24817B1F00ED215A /* PBXContainerItemProxy */; + target = 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */; + targetProxy = 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 2A6F2AD324817B1F00ED215A /* LaunchScreen.storyboard */ = { + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - 2A6F2AD424817B1F00ED215A /* Base */, + 2A3CEE922486B4EC00C69C3C /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -244,7 +244,7 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 2A6F2AE224817B1F00ED215A /* Debug */ = { + 2A3CEEA02486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -304,7 +304,7 @@ }; name = Debug; }; - 2A6F2AE324817B1F00ED215A /* Release */ = { + 2A3CEEA12486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -358,7 +358,7 @@ }; name = Release; }; - 2A6F2AE524817B1F00ED215A /* Debug */ = { + 2A3CEEA32486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -377,7 +377,7 @@ }; name = Debug; }; - 2A6F2AE624817B1F00ED215A /* Release */ = { + 2A3CEEA42486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -396,7 +396,7 @@ }; name = Release; }; - 2A6F2AE824817B1F00ED215A /* Debug */ = { + 2A3CEEA62486B4EC00C69C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -417,7 +417,7 @@ }; name = Debug; }; - 2A6F2AE924817B1F00ED215A /* Release */ = { + 2A3CEEA72486B4EC00C69C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; @@ -441,34 +441,34 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 2A6F2AC024817B1E00ED215A /* Build configuration list for PBXProject "CombineSchedulers" */ = { + 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE224817B1F00ED215A /* Debug */, - 2A6F2AE324817B1F00ED215A /* Release */, + 2A3CEEA02486B4EC00C69C3C /* Debug */, + 2A3CEEA12486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A6F2AE424817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulers" */ = { + 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE524817B1F00ED215A /* Debug */, - 2A6F2AE624817B1F00ED215A /* Release */, + 2A3CEEA32486B4EC00C69C3C /* Debug */, + 2A3CEEA42486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A6F2AE724817B1F00ED215A /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */ = { + 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A6F2AE824817B1F00ED215A /* Debug */, - 2A6F2AE924817B1F00ED215A /* Release */, + 2A3CEEA62486B4EC00C69C3C /* Debug */, + 2A3CEEA72486B4EC00C69C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 2A6F2ABD24817B1E00ED215A /* Project object */; + rootObject = 2A3CEE7B2486B4EA00C69C3C /* Project object */; } diff --git a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/AppDelegate.swift b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/AppDelegate.swift index 72ab3aa0..bdf8201c 100644 --- a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/AppDelegate.swift +++ b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/AppDelegate.swift @@ -2,7 +2,7 @@ // AppDelegate.swift // CombineSchedulers // -// Created by Point-Free on 5/29/20. +// Created by Point-Free on 6/2/20. // Copyright © 2020 Point-Free. All rights reserved. // diff --git a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/ContentView.swift b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/ContentView.swift index eae27d6e..d10dcbfd 100644 --- a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/ContentView.swift +++ b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/ContentView.swift @@ -1,91 +1,146 @@ import Combine import SwiftUI -func validationMessage( - forPassword password: P -) -> AnyPublisher where P.Output == String, P.Failure == Never { - - password.map { - $0.count < 5 ? "Password is too short 👎" - : $0.count > 20 ? "Password is too long 👎" - : "Password is good 👍" - } - .eraseToAnyPublisher() - -} - - class RegisterViewModel: ObservableObject { + struct Alert: Identifiable { + var title: String + var id: String { self.title } + } + @Published var email = "" + @Published var errorAlert: Alert? + @Published var isRegistered = false + @Published var isRegisterRequestInFlight = false @Published var password = "" - @Published var isLoginSuccessful = false @Published var passwordValidationMessage = "" - var cancellables: Set = [] + let register: (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> - let login: (String, String) -> AnyPublisher + var cancellables: Set = [] init( - login: @escaping (String, String) -> AnyPublisher + register: @escaping (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError>, + validatePassword: @escaping (String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> ) { - self.login = login + self.register = register self.$password - .map { - $0.count < 5 ? "Password is too short 👎" - : $0.count > 20 ? "Password is too long 👎" - : "Password is good 👍" - } - .sink { self.passwordValidationMessage = $0 } + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) + .flatMap { password in + password.isEmpty + ? Just("").eraseToAnyPublisher() + : validatePassword(password) + .receive(on: DispatchQueue.main) + .map { data, _ in + String(decoding: data, as: UTF8.self) + } + .replaceError(with: "Could not validate password.") + .eraseToAnyPublisher() + } + .sink { [weak self] in self?.passwordValidationMessage = $0 } .store(in: &self.cancellables) } - func loginButtonTapped() { - self.login(self.email, self.password) + func registerButtonTapped() { + self.isRegisterRequestInFlight = true + self.register(self.email, self.password) .receive(on: DispatchQueue.main) - .sink { self.isLoginSuccessful = $0 } + .map { data, _ in + Bool(String(decoding: data, as: UTF8.self)) ?? false + } + .replaceError(with: false) + .sink { + self.isRegistered = $0 + self.isRegisterRequestInFlight = false + if !$0 { + self.errorAlert = Alert(title: "Failed to register. Please try again.") + } + } .store(in: &self.cancellables) } } +func registerRequest( + email: String, + password: String +) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + var components = URLComponents(string: "https://www.pointfree.co/register")! + components.queryItems = [ + URLQueryItem(name: "email", value: email), + URLQueryItem(name: "password", value: password) + ] + + return URLSession.shared + .dataTaskPublisher(for: components.url!) + .eraseToAnyPublisher() +} -//class PasswordViewModel: ObservableObject { -// @Published var password: String = "" -// -// //let passwordValidationMessage: AnyPublisher -// @Published var passwordValidationMessage = "" -// -// func validatePassword() { -// validationMessage(forPassword: Just(self.password)) -// .sink { self.passwordValidationMessage = $0 } -// } -//} - struct ContentView: View { -// @State var password: String = "" - @ObservedObject var viewModel = RegisterViewModel( - login: { _, _ in Just(true).eraseToAnyPublisher() } - ) + @ObservedObject var viewModel: RegisterViewModel var body: some View { - Form { - if self.viewModel.isLoginSuccessful { - Text("Logged in! Welcome!") + NavigationView { + if self.viewModel.isRegistered { + Text("Welcome!") } else { - TextField("Email", text: self.$viewModel.email) - TextField("Password", text: self.$viewModel.password) - - Text(self.viewModel.passwordValidationMessage) - - Button("Login") { self.viewModel.loginButtonTapped() } + Form { + Section(header: Text("Email")) { + TextField( + "blob@pointfree.co", + text: self.$viewModel.email + ) + } + + Section(header: Text("Password")) { + TextField( + "Password", + text: self.$viewModel.password + ) + if !self.viewModel.passwordValidationMessage.isEmpty { + Text(self.viewModel.passwordValidationMessage) + } + } + + if self.viewModel.isRegisterRequestInFlight { + Text("Registering...") + } else { + Button("Register") { self.viewModel.registerButtonTapped() } + } + } + .navigationBarTitle("Register") + .alert(item: self.$viewModel.errorAlert) { errorAlert in + Alert(title: Text(errorAlert.title)) + } } } } } +func mockValidate(password: String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + let message = password.count < 5 ? "Password is too short 👎" + : password.count > 20 ? "Password is too long 👎" + : "Password is good 👍" + return Just((Data(message.utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + ContentView( + viewModel: RegisterViewModel( + register: { _, _ in + Just((Data("false".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .delay(for: 1, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + }, + validatePassword: { + mockValidate(password: $0) + .delay(for: 0.5, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + }) + ) } } diff --git a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/SceneDelegate.swift b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/SceneDelegate.swift index f9d5b4e2..885a6b85 100644 --- a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/SceneDelegate.swift +++ b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulers/SceneDelegate.swift @@ -1,64 +1,24 @@ -// -// SceneDelegate.swift -// CombineSchedulers -// -// Created by Point-Free on 5/29/20. -// Copyright © 2020 Point-Free. All rights reserved. -// - -import UIKit import SwiftUI +import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() + let contentView = ContentView( + viewModel: RegisterViewModel( + register: registerRequest(email:password:), + validatePassword: mockValidate(password:) + ) + ) - // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) - self.window = window - window.makeKeyAndVisible() + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + window.makeKeyAndVisible() } } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - } diff --git a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift index 292f5719..9d238dfe 100644 --- a/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift +++ b/0105-combine-schedulers-pt2/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift @@ -5,23 +5,81 @@ import XCTest class CombineSchedulersTests: XCTestCase { var cancellables: Set = [] - func testHappyPath() { + func testRegistrationSuccessful() { let viewModel = RegisterViewModel( - login: { _, _ in Just(true).eraseToAnyPublisher() } + register: { _, _ in + Just((Data("true".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() + }, + validatePassword: { _ in Empty(completeImmediately: true).eraseToAnyPublisher() } ) - var expectedOutput: [Bool] = [] - viewModel.$isLoginSuccessful - .sink { expectedOutput.append($0) } + var isRegistered: [Bool] = [] + viewModel.$isRegistered + .sink { isRegistered.append($0) } .store(in: &self.cancellables) - XCTAssertEqual(expectedOutput, [false, ]) +// XCTAssertEqual(viewModel.isRegistered, false) + XCTAssertEqual(isRegistered, [false]) + + viewModel.email = "blob@pointfree.co" + XCTAssertEqual(isRegistered, [false]) + + viewModel.password = "blob is awesome" + XCTAssertEqual(isRegistered, [false]) + + viewModel.registerButtonTapped() + +// XCTAssertEqual(viewModel.isRegistered, true) + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.1) + XCTAssertEqual(isRegistered, [false, true]) + } + + func testRegistrationFailure() { + let viewModel = RegisterViewModel( + register: { _, _ in + Just((Data("false".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() + }, + validatePassword: { _ in Empty(completeImmediately: true).eraseToAnyPublisher() } + ) + + XCTAssertEqual(viewModel.isRegistered, false) + viewModel.email = "blob@pointfree.co" viewModel.password = "blob is awesome" - viewModel.loginButtonTapped() + viewModel.registerButtonTapped() + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.01) - XCTAssertEqual(viewModel.isLoginSuccessful, true) - XCTAssertEqual(expectedOutput, [false, true]) + XCTAssertEqual(viewModel.isRegistered, false) + XCTAssertEqual(viewModel.errorAlert?.title, "Failed to register. Please try again.") } + + func testValidatePassword() { + let viewModel = RegisterViewModel( + register: { _, _ in fatalError() }, + validatePassword: mockValidate(password:) + ) + + var passwordValidationMessage: [String] = [] + viewModel.$passwordValidationMessage + .sink { passwordValidationMessage.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(passwordValidationMessage, [""]) + + viewModel.password = "blob" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.301) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎"]) + viewModel.password = "blob is awesome" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.21) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎"]) + + viewModel.password = "blob is awesome!!!!!!" + _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.31) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short 👎", "Password is too long 👎"]) + } }