From 649f0a47a29cec49f369dc7858ab9929482a2d5a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Jun 2020 09:37:27 -0400 Subject: [PATCH] 106 --- 0105-combine-schedulers-pt2/README.md | 4 +- .../project.pbxproj | 484 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/CombineSchedulers.xcscheme | 88 ++++ .../CombineSchedulers/AnyScheduler.swift | 47 ++ .../CombineSchedulers/AppDelegate.swift | 37 ++ .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../CombineSchedulers/ContentView.swift | 165 ++++++ .../CombineSchedulers/Info.plist | 60 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../CombineSchedulers/SceneDelegate.swift | 25 + .../CombineSchedulers/TestScheduler.swift | 113 ++++ .../CombineSchedulersTests.swift | 342 +++++++++++++ .../CombineSchedulersTests/Info.plist | 22 + .../Playground.playground/Contents.swift | 60 +++ .../contents.xcplayground | 4 + 0106-combine-schedulers-pt3/README.md | 5 + README.md | 1 + 21 files changed, 1605 insertions(+), 2 deletions(-) create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AnyScheduler.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AppDelegate.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/Contents.json create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Base.lproj/LaunchScreen.storyboard create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/ContentView.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Info.plist create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/SceneDelegate.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/TestScheduler.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/Info.plist create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/Contents.swift create mode 100644 0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/contents.xcplayground create mode 100644 0106-combine-schedulers-pt3/README.md diff --git a/0105-combine-schedulers-pt2/README.md b/0105-combine-schedulers-pt2/README.md index 73841c27..5950d28f 100644 --- a/0105-combine-schedulers-pt2/README.md +++ b/0105-combine-schedulers-pt2/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Combine Schedulers: Controlling Time](https://www.pointfree.co/episodes/ep105-combine-schedulers-controlling-time) +> #### This directory contains code from Point-Free Episode: [Combine Schedulers: Erasing Time](https://www.pointfree.co/episodes/ep106-combine-schedulers-erasing-time) > -> The `Scheduler` protocol of Combine is a powerful abstraction that unifies many ways of executing asynchronous work, and it can even control the flow of time through our code. Unfortunately Combine doesn't give us this ability out of the box, so let's build it from scratch. +> We refactor our application’s code so that we can run it in production with a live dispatch queue for the scheduler, while allowing us to run it in tests with a test scheduler. If we do this naively we will find that generics infect many parts of our code, but luckily we can employ the technique of type erasure to make things much nicer. diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f7a35546 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 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 */; }; + 2ADC7AFF24883F260034FD5A /* AnyScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ADC7AFE24883F260034FD5A /* AnyScheduler.swift */; }; + 4B648A612486E87B009448FB /* TestScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B648A602486E87B009448FB /* TestScheduler.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A3CEE7B2486B4EA00C69C3C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A3CEE822486B4EA00C69C3C; + remoteInfo = CombineSchedulers; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 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; }; + 2ADC7AFE24883F260034FD5A /* AnyScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyScheduler.swift; sourceTree = ""; }; + 4B648A602486E87B009448FB /* TestScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScheduler.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A3CEE802486B4EA00C69C3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3CEE962486B4EC00C69C3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A3CEE7A2486B4EA00C69C3C = { + isa = PBXGroup; + children = ( + 2A3CEEA82486E3C200C69C3C /* Playground.playground */, + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */, + 2A3CEE842486B4EA00C69C3C /* Products */, + ); + sourceTree = ""; + }; + 2A3CEE842486B4EA00C69C3C /* Products */ = { + isa = PBXGroup; + children = ( + 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */, + 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2A3CEE852486B4EA00C69C3C /* CombineSchedulers */ = { + isa = PBXGroup; + children = ( + 2A3CEE862486B4EA00C69C3C /* AppDelegate.swift */, + 2A3CEE882486B4EA00C69C3C /* SceneDelegate.swift */, + 2A3CEE8A2486B4EA00C69C3C /* ContentView.swift */, + 2ADC7AFE24883F260034FD5A /* AnyScheduler.swift */, + 4B648A602486E87B009448FB /* TestScheduler.swift */, + 2A3CEE8C2486B4EC00C69C3C /* Assets.xcassets */, + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */, + 2A3CEE942486B4EC00C69C3C /* Info.plist */, + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */, + ); + path = CombineSchedulers; + sourceTree = ""; + }; + 2A3CEE8E2486B4EC00C69C3C /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2A3CEE8F2486B4EC00C69C3C /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2A3CEE9C2486B4EC00C69C3C /* CombineSchedulersTests */ = { + isa = PBXGroup; + children = ( + 2A3CEE9D2486B4EC00C69C3C /* CombineSchedulersTests.swift */, + 2A3CEE9F2486B4EC00C69C3C /* Info.plist */, + ); + path = CombineSchedulersTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */; + buildPhases = ( + 2A3CEE7F2486B4EA00C69C3C /* Sources */, + 2A3CEE802486B4EA00C69C3C /* Frameworks */, + 2A3CEE812486B4EA00C69C3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CombineSchedulers; + productName = CombineSchedulers; + productReference = 2A3CEE832486B4EA00C69C3C /* CombineSchedulers.app */; + productType = "com.apple.product-type.application"; + }; + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */; + buildPhases = ( + 2A3CEE952486B4EC00C69C3C /* Sources */, + 2A3CEE962486B4EC00C69C3C /* Frameworks */, + 2A3CEE972486B4EC00C69C3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */, + ); + name = CombineSchedulersTests; + productName = CombineSchedulersTests; + productReference = 2A3CEE992486B4EC00C69C3C /* CombineSchedulersTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A3CEE7B2486B4EA00C69C3C /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1150; + LastUpgradeCheck = 1150; + ORGANIZATIONNAME = "Point-Free"; + TargetAttributes = { + 2A3CEE822486B4EA00C69C3C = { + CreatedOnToolsVersion = 11.5; + }; + 2A3CEE982486B4EC00C69C3C = { + CreatedOnToolsVersion = 11.5; + TestTargetID = 2A3CEE822486B4EA00C69C3C; + }; + }; + }; + buildConfigurationList = 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A3CEE7A2486B4EA00C69C3C; + productRefGroup = 2A3CEE842486B4EA00C69C3C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */, + 2A3CEE982486B4EC00C69C3C /* CombineSchedulersTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A3CEE812486B4EA00C69C3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3CEE932486B4EC00C69C3C /* LaunchScreen.storyboard in Resources */, + 2A3CEE902486B4EC00C69C3C /* Preview Assets.xcassets in Resources */, + 2A3CEE8D2486B4EC00C69C3C /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3CEE972486B4EC00C69C3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A3CEE7F2486B4EA00C69C3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3CEE872486B4EA00C69C3C /* AppDelegate.swift in Sources */, + 4B648A612486E87B009448FB /* TestScheduler.swift in Sources */, + 2A3CEE892486B4EA00C69C3C /* SceneDelegate.swift in Sources */, + 2A3CEE8B2486B4EA00C69C3C /* ContentView.swift in Sources */, + 2ADC7AFF24883F260034FD5A /* AnyScheduler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3CEE952486B4EC00C69C3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3CEE9E2486B4EC00C69C3C /* CombineSchedulersTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A3CEE9B2486B4EC00C69C3C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A3CEE822486B4EA00C69C3C /* CombineSchedulers */; + targetProxy = 2A3CEE9A2486B4EC00C69C3C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 2A3CEE912486B4EC00C69C3C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2A3CEE922486B4EC00C69C3C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 2A3CEEA02486B4EC00C69C3C /* 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.5; + 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; + }; + 2A3CEEA12486B4EC00C69C3C /* 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.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A3CEEA32486B4EC00C69C3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"CombineSchedulers/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = CombineSchedulers/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.CombineSchedulers; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A3CEEA42486B4EC00C69C3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"CombineSchedulers/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = CombineSchedulers/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.CombineSchedulers; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A3CEEA62486B4EC00C69C3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = CombineSchedulersTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.CombineSchedulersTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CombineSchedulers.app/CombineSchedulers"; + }; + name = Debug; + }; + 2A3CEEA72486B4EC00C69C3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = CombineSchedulersTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.CombineSchedulersTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CombineSchedulers.app/CombineSchedulers"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A3CEE7E2486B4EA00C69C3C /* Build configuration list for PBXProject "CombineSchedulers" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3CEEA02486B4EC00C69C3C /* Debug */, + 2A3CEEA12486B4EC00C69C3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A3CEEA22486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulers" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3CEEA32486B4EC00C69C3C /* Debug */, + 2A3CEEA42486B4EC00C69C3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A3CEEA52486B4EC00C69C3C /* Build configuration list for PBXNativeTarget "CombineSchedulersTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3CEEA62486B4EC00C69C3C /* Debug */, + 2A3CEEA72486B4EC00C69C3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2A3CEE7B2486B4EA00C69C3C /* Project object */; +} diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8ff61140 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme new file mode 100644 index 00000000..55e88ddc --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers.xcodeproj/xcshareddata/xcschemes/CombineSchedulers.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AnyScheduler.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AnyScheduler.swift new file mode 100644 index 00000000..23f2b3ef --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AnyScheduler.swift @@ -0,0 +1,47 @@ +import Combine + +struct AnyScheduler: Scheduler + where SchedulerTimeType: Strideable, SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible +{ + func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable { + self._schedulerWithInterval(date, interval, tolerance, options, action) + } + + private let _schedulerWithInterval: (SchedulerTimeType, SchedulerTimeType.Stride, SchedulerTimeType.Stride, SchedulerOptions?, @escaping () -> Void) -> Cancellable + + func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) { + self._scheduleAfterDelay(date, tolerance, options, action) + } + + private let _scheduleAfterDelay: (SchedulerTimeType, SchedulerTimeType.Stride, SchedulerOptions?, @escaping () -> Void) -> Void + + func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { + self._schedule(options, action) + } + + private let _schedule: (SchedulerOptions?, @escaping () -> Void) -> Void + + var now: SchedulerTimeType { + self._now() + } + var minimumTolerance: SchedulerTimeType.Stride { + self._minimumTolerance() + } + + private let _now: () -> SchedulerTimeType + private let _minimumTolerance: () -> SchedulerTimeType.Stride + + + init( + _ scheduler: S + ) where S.SchedulerTimeType == SchedulerTimeType, S.SchedulerOptions == SchedulerOptions { + + self._now = { scheduler.now } + self._minimumTolerance = { scheduler.minimumTolerance } + self._schedule = { scheduler.schedule(options: $0, $1) } + self._scheduleAfterDelay = { scheduler.schedule(after: $0, tolerance: $1, options: $2, $3) } + self._schedulerWithInterval = { scheduler.schedule(after: $0, interval: $1, tolerance: $2, options: $3, $4) } + } +} + +typealias AnySchedulerOf = AnyScheduler diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AppDelegate.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AppDelegate.swift new file mode 100644 index 00000000..bdf8201c --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// CombineSchedulers +// +// Created by Point-Free on 6/2/20. +// Copyright Β© 2020 Point-Free. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/AppIcon.appiconset/Contents.json b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/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/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/Contents.json b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Base.lproj/LaunchScreen.storyboard b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/ContentView.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/ContentView.swift new file mode 100644 index 00000000..71a20c09 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/ContentView.swift @@ -0,0 +1,165 @@ +import Combine +import SwiftUI + +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 passwordValidationMessage = "" + + let register: (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError> + + let scheduler: AnySchedulerOf +// let scheduler: AnyScheduler +// let scheduler: any Scheduler where .SchedulerTimeType == DispatchQueue.SchedulerTimeType, .SchedulerOptions == DispatchQueue.SchedulerOptions + + var cancellables: Set = [] + + init( + register: @escaping (String, String) -> AnyPublisher<(data: Data, response: URLResponse), URLError>, + validatePassword: @escaping (String) -> AnyPublisher<(data: Data, response: URLResponse), URLError>, + scheduler: AnySchedulerOf + ) { + self.register = register + self.scheduler = scheduler + + self.$password + .debounce(for: .milliseconds(300), scheduler: scheduler) +// .debounce(for: .milliseconds(300), scheduler: ImmediateScheduler.shared) + .flatMap { password in + password.isEmpty + ? Just("").eraseToAnyPublisher() + : validatePassword(password) + .receive(on: scheduler) +// .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 registerButtonTapped() { +// scheduler + self.isRegisterRequestInFlight = true + self.register(self.email, self.password) + .receive(on: scheduler) +// .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() +} + + +struct ContentView: View { + @ObservedObject var viewModel: RegisterViewModel + + var body: some View { + NavigationView { + if self.viewModel.isRegistered { + Text("Welcome!") + } else { + 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( + 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() + }, +// scheduler: AnyScheduler(DispatchQueue.main) + scheduler: DispatchQueue.main.eraseToAnyScheduler() + ) + ) + } +} + +extension Scheduler { + func eraseToAnyScheduler() -> AnySchedulerOf { + AnyScheduler(self) + } +} diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Info.plist b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Info.plist new file mode 100644 index 00000000..9742bf0f --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Info.plist @@ -0,0 +1,60 @@ + + + + + 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 + + + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Preview Content/Preview Assets.xcassets/Contents.json b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/SceneDelegate.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/SceneDelegate.swift new file mode 100644 index 00000000..f5207b18 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/SceneDelegate.swift @@ -0,0 +1,25 @@ +import SwiftUI +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + let contentView = ContentView( + viewModel: RegisterViewModel( + register: registerRequest(email:password:), + validatePassword: mockValidate(password:), + scheduler: DispatchQueue.main.eraseToAnyScheduler() + ) + ) + + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + window.makeKeyAndVisible() + } + } +} + diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/TestScheduler.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulers/TestScheduler.swift new file mode 100644 index 00000000..1771bdff --- /dev/null +++ b/0106-combine-schedulers-pt3/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/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift new file mode 100644 index 00000000..c509188c --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/CombineSchedulersTests.swift @@ -0,0 +1,342 @@ +import Combine +import XCTest +@testable import CombineSchedulers + +class CombineSchedulersTests: XCTestCase { + var cancellables: Set = [] + let scheduler = DispatchQueue.testScheduler + + func testRegistrationSuccessful() { + let viewModel = RegisterViewModel( + register: { _, _ in + Just((Data("true".utf8), URLResponse())) + .setFailureType(to: URLError.self) + .eraseToAnyPublisher() + }, + validatePassword: { _ in Empty(completeImmediately: true).eraseToAnyPublisher() }, + scheduler: scheduler.eraseToAnyScheduler() + ) + + var isRegistered: [Bool] = [] + viewModel.$isRegistered + .sink { isRegistered.append($0) } + .store(in: &self.cancellables) + +// 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) + scheduler.advance() + 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() }, + scheduler: scheduler.eraseToAnyScheduler() + ) + + XCTAssertEqual(viewModel.isRegistered, false) + + viewModel.email = "blob@pointfree.co" + viewModel.password = "blob is awesome" + viewModel.registerButtonTapped() + +// _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.01) + scheduler.advance() + 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:), + scheduler: scheduler.eraseToAnyScheduler() + ) + + 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) + scheduler.advance(by: .milliseconds(300)) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short πŸ‘Ž"]) + + viewModel.password = "blob is awesome" +// _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.21) + scheduler.advance(by: .milliseconds(200)) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short πŸ‘Ž"]) + + viewModel.password = "blob is awesome!!!!!!" +// _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.31) + scheduler.advance(by: .milliseconds(300)) + XCTAssertEqual(passwordValidationMessage, ["", "Password is too short πŸ‘Ž", "Password is too long πŸ‘Ž"]) + } + + 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) + } + + func testRace_CacheEmitsFirst() { + var output: [Int] = [] + + race( + cached: Future { callback in + self.scheduler.schedule(after: self.scheduler.now.advanced(by: 1)) { + callback(.success(2)) + } + }, + fresh: Future { callback in + self.scheduler.schedule(after: self.scheduler.now.advanced(by: 2)) { + callback(.success(42)) + } + } + ) + .sink { output.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(output, []) + scheduler.advance(by: 2) + XCTAssertEqual(output, [2, 42]) + } + + func testRace_FreshEmitsFirst() { + var output: [Int] = [] + + race( + cached: Future { callback in + self.scheduler.schedule(after: self.scheduler.now.advanced(by: 2)) { + callback(.success(2)) + } + }, + fresh: Future { callback in + self.scheduler.schedule(after: self.scheduler.now.advanced(by: 1)) { + callback(.success(42)) + } + } + ) + .sink { output.append($0) } + .store(in: &self.cancellables) + + XCTAssertEqual(output, []) + scheduler.advance(by: 2) + XCTAssertEqual(output, [42]) + } +} + +import Combine + +func race( + cached: Future, + fresh: Future + ) -> AnyPublisher { + + Publishers.Merge( + cached.map { (model: $0, isCached: true) }, + fresh.map { (model: $0, isCached: false) } + ) + .scan((nil, nil)) { accum, output in + (accum.1, output) + } + .prefix(while: { lhs, rhs in + !(rhs?.isCached ?? true) || (lhs?.isCached ?? true) + }) + .compactMap(\.1?.model) + .eraseToAnyPublisher() +} diff --git a/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/Info.plist b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/CombineSchedulersTests/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/0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/Contents.swift b/0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/Contents.swift new file mode 100644 index 00000000..427b8493 --- /dev/null +++ b/0106-combine-schedulers-pt3/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/0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/contents.xcplayground b/0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/contents.xcplayground new file mode 100644 index 00000000..5da2641c --- /dev/null +++ b/0106-combine-schedulers-pt3/CombineSchedulers/Playground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0106-combine-schedulers-pt3/README.md b/0106-combine-schedulers-pt3/README.md new file mode 100644 index 00000000..73841c27 --- /dev/null +++ b/0106-combine-schedulers-pt3/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Combine Schedulers: Controlling Time](https://www.pointfree.co/episodes/ep105-combine-schedulers-controlling-time) +> +> The `Scheduler` protocol of Combine is a powerful abstraction that unifies many ways of executing asynchronous work, and it can even control the flow of time through our code. Unfortunately Combine doesn't give us this ability out of the box, so let's build it from scratch. diff --git a/README.md b/README.md index cac8b001..baab695f 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,4 @@ This repository is the home of code written on episodes of 1. [A Tour of the Composable Architecture: Part 4](0103-swift-composable-architecture-tour-pt4) 1. [Combine Schedulers: Testing Time](0104-combine-schedulers-pt1) 1. [Combine Schedulers: Controlling Time](0105-combine-schedulers-pt2) +1. [Combine Schedulers: Erasing Time](0106-combine-schedulers-pt3)