diff --git a/0149-derived-behavior-pt4/README.md b/0149-derived-behavior-pt4/README.md index bf69530e..912ff61a 100644 --- a/0149-derived-behavior-pt4/README.md +++ b/0149-derived-behavior-pt4/README.md @@ -1,6 +1,6 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Derived Behavior: Collections](https://www.pointfree.co/episodes/ep149-derived-behavior-optionals-and-enums) +> #### This directory contains code from Point-Free Episode: [Derived Behavior: Optionals and Enums](https://www.pointfree.co/episodes/ep149-derived-behavior-optionals-and-enums) > > We will explore two more domain transformations in the Composable Architecture. One comes with the library: the ability to embed a smaller domain, optionally, in a larger domain. Another we will build from scratch: the ability to embed smaller domains in the cases of an enum! diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.pbxproj b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.pbxproj new file mode 100644 index 00000000..50184396 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.pbxproj @@ -0,0 +1,495 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 2A27EE31266806FF00CEB5B0 /* VanillaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A27EE30266806FF00CEB5B0 /* VanillaView.swift */; }; + 2A660519265FEE2600FC3837 /* DerivedBehaviorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A660518265FEE2600FC3837 /* DerivedBehaviorApp.swift */; }; + 2A66051B265FEE2600FC3837 /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A66051A265FEE2600FC3837 /* CounterView.swift */; }; + 2A66051D265FEE2600FC3837 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A66051C265FEE2600FC3837 /* Assets.xcassets */; }; + 2A660520265FEE2600FC3837 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A66051F265FEE2600FC3837 /* Preview Assets.xcassets */; }; + 2A66052B265FEE2700FC3837 /* DerivedBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A66052A265FEE2700FC3837 /* DerivedBehaviorTests.swift */; }; + 2A660545265FEEBA00FC3837 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 2A660544265FEEBA00FC3837 /* ComposableArchitecture */; }; + 2A660547265FEEEC00FC3837 /* FactClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A660546265FEEEC00FC3837 /* FactClient.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A660527265FEE2700FC3837 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A66050D265FEE2600FC3837 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A660514265FEE2600FC3837; + remoteInfo = DerivedBehavior; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A27EE30266806FF00CEB5B0 /* VanillaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VanillaView.swift; sourceTree = ""; }; + 2A660515265FEE2600FC3837 /* DerivedBehavior.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DerivedBehavior.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A660518265FEE2600FC3837 /* DerivedBehaviorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivedBehaviorApp.swift; sourceTree = ""; }; + 2A66051A265FEE2600FC3837 /* CounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterView.swift; sourceTree = ""; }; + 2A66051C265FEE2600FC3837 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A66051F265FEE2600FC3837 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A660521265FEE2600FC3837 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A660526265FEE2700FC3837 /* DerivedBehaviorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DerivedBehaviorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A66052A265FEE2700FC3837 /* DerivedBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivedBehaviorTests.swift; sourceTree = ""; }; + 2A66052C265FEE2700FC3837 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A660546265FEEEC00FC3837 /* FactClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactClient.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A660512265FEE2600FC3837 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A660545265FEEBA00FC3837 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A660523265FEE2700FC3837 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A66050C265FEE2600FC3837 = { + isa = PBXGroup; + children = ( + 2A660517265FEE2600FC3837 /* DerivedBehavior */, + 2A660529265FEE2700FC3837 /* DerivedBehaviorTests */, + 2A660516265FEE2600FC3837 /* Products */, + ); + sourceTree = ""; + }; + 2A660516265FEE2600FC3837 /* Products */ = { + isa = PBXGroup; + children = ( + 2A660515265FEE2600FC3837 /* DerivedBehavior.app */, + 2A660526265FEE2700FC3837 /* DerivedBehaviorTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2A660517265FEE2600FC3837 /* DerivedBehavior */ = { + isa = PBXGroup; + children = ( + 2A660518265FEE2600FC3837 /* DerivedBehaviorApp.swift */, + 2A66051A265FEE2600FC3837 /* CounterView.swift */, + 2A27EE30266806FF00CEB5B0 /* VanillaView.swift */, + 2A660546265FEEEC00FC3837 /* FactClient.swift */, + 2A66051C265FEE2600FC3837 /* Assets.xcassets */, + 2A660521265FEE2600FC3837 /* Info.plist */, + 2A66051E265FEE2600FC3837 /* Preview Content */, + ); + path = DerivedBehavior; + sourceTree = ""; + }; + 2A66051E265FEE2600FC3837 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2A66051F265FEE2600FC3837 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2A660529265FEE2700FC3837 /* DerivedBehaviorTests */ = { + isa = PBXGroup; + children = ( + 2A66052A265FEE2700FC3837 /* DerivedBehaviorTests.swift */, + 2A66052C265FEE2700FC3837 /* Info.plist */, + ); + path = DerivedBehaviorTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A660514265FEE2600FC3837 /* DerivedBehavior */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A66053A265FEE2700FC3837 /* Build configuration list for PBXNativeTarget "DerivedBehavior" */; + buildPhases = ( + 2A660511265FEE2600FC3837 /* Sources */, + 2A660512265FEE2600FC3837 /* Frameworks */, + 2A660513265FEE2600FC3837 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DerivedBehavior; + packageProductDependencies = ( + 2A660544265FEEBA00FC3837 /* ComposableArchitecture */, + ); + productName = DerivedBehavior; + productReference = 2A660515265FEE2600FC3837 /* DerivedBehavior.app */; + productType = "com.apple.product-type.application"; + }; + 2A660525265FEE2700FC3837 /* DerivedBehaviorTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A66053D265FEE2700FC3837 /* Build configuration list for PBXNativeTarget "DerivedBehaviorTests" */; + buildPhases = ( + 2A660522265FEE2700FC3837 /* Sources */, + 2A660523265FEE2700FC3837 /* Frameworks */, + 2A660524265FEE2700FC3837 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A660528265FEE2700FC3837 /* PBXTargetDependency */, + ); + name = DerivedBehaviorTests; + productName = DerivedBehaviorTests; + productReference = 2A660526265FEE2700FC3837 /* DerivedBehaviorTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A66050D265FEE2600FC3837 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + TargetAttributes = { + 2A660514265FEE2600FC3837 = { + CreatedOnToolsVersion = 12.5; + }; + 2A660525265FEE2700FC3837 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 2A660514265FEE2600FC3837; + }; + }; + }; + buildConfigurationList = 2A660510265FEE2600FC3837 /* Build configuration list for PBXProject "DerivedBehavior" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A66050C265FEE2600FC3837; + packageReferences = ( + 2A660543265FEEBA00FC3837 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + ); + productRefGroup = 2A660516265FEE2600FC3837 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A660514265FEE2600FC3837 /* DerivedBehavior */, + 2A660525265FEE2700FC3837 /* DerivedBehaviorTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A660513265FEE2600FC3837 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A660520265FEE2600FC3837 /* Preview Assets.xcassets in Resources */, + 2A66051D265FEE2600FC3837 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A660524265FEE2700FC3837 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A660511265FEE2600FC3837 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A66051B265FEE2600FC3837 /* CounterView.swift in Sources */, + 2A660547265FEEEC00FC3837 /* FactClient.swift in Sources */, + 2A27EE31266806FF00CEB5B0 /* VanillaView.swift in Sources */, + 2A660519265FEE2600FC3837 /* DerivedBehaviorApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A660522265FEE2700FC3837 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A66052B265FEE2700FC3837 /* DerivedBehaviorTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A660528265FEE2700FC3837 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A660514265FEE2600FC3837 /* DerivedBehavior */; + targetProxy = 2A660527265FEE2700FC3837 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A660538265FEE2700FC3837 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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 = 14.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; + }; + 2A660539265FEE2700FC3837 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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 = 14.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; + }; + 2A66053B265FEE2700FC3837 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"DerivedBehavior/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = DerivedBehavior/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DerivedBehavior; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A66053C265FEE2700FC3837 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"DerivedBehavior/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = DerivedBehavior/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DerivedBehavior; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A66053E265FEE2700FC3837 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DerivedBehaviorTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DerivedBehaviorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DerivedBehavior.app/DerivedBehavior"; + }; + name = Debug; + }; + 2A66053F265FEE2700FC3837 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DerivedBehaviorTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.DerivedBehaviorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DerivedBehavior.app/DerivedBehavior"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A660510265FEE2600FC3837 /* Build configuration list for PBXProject "DerivedBehavior" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A660538265FEE2700FC3837 /* Debug */, + 2A660539265FEE2700FC3837 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A66053A265FEE2700FC3837 /* Build configuration list for PBXNativeTarget "DerivedBehavior" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A66053B265FEE2700FC3837 /* Debug */, + 2A66053C265FEE2700FC3837 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A66053D265FEE2700FC3837 /* Build configuration list for PBXNativeTarget "DerivedBehaviorTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A66053E265FEE2700FC3837 /* Debug */, + 2A66053F265FEE2700FC3837 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A660543265FEEBA00FC3837 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.18.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A660544265FEEBA00FC3837 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = 2A660543265FEEBA00FC3837 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A66050D265FEE2600FC3837 /* Project object */; +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/xcshareddata/xcschemes/DerivedBehavior.xcscheme b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/xcshareddata/xcschemes/DerivedBehavior.xcscheme new file mode 100644 index 00000000..b9fcdfe4 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior.xcodeproj/xcshareddata/xcschemes/DerivedBehavior.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/AccentColor.colorset/Contents.json b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/AppIcon.appiconset/Contents.json b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/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/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/Contents.json b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/CounterView.swift b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/CounterView.swift new file mode 100644 index 00000000..c30de7ee --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/CounterView.swift @@ -0,0 +1,343 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +struct CounterState: Equatable { + var alert: Alert? + var count = 0 + + struct Alert: Equatable, Identifiable { + var message: String + var title: String + + var id: String { + self.title + self.message + } + } +} +enum CounterAction: Equatable { + case decrementButtonTapped + case dismissAlert + case incrementButtonTapped + case factButtonTapped + case factResponse(Result) +} + +struct CounterEnvironment { + let fact: FactClient + let mainQueue: AnySchedulerOf +} + +let counterReducer = Reducer { + state, action, environment in + + switch action { + case .decrementButtonTapped: + state.count -= 1 + return .none + + case .dismissAlert: + state.alert = nil + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + + case .factButtonTapped: + return environment.fact.fetch(state.count) + .receive(on: environment.mainQueue.animation()) + .catchToEffect() + .map(CounterAction.factResponse) + + case let .factResponse(.success(fact)): + return .none + + case .factResponse(.failure): + state.alert = .init(message: "Couldn't load fact.", title: "Error") + return .none + } +} + +struct CounterView: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + VStack { + HStack { + Button("-") { viewStore.send(.decrementButtonTapped) } + Text("\(viewStore.count)") + Button("+") { viewStore.send(.incrementButtonTapped) } + } + + Button("Fact") { viewStore.send(.factButtonTapped) } + } + .alert(item: viewStore.binding(get: \.alert, send: .dismissAlert)) { alert in + Alert( + title: Text(alert.title), + message: Text(alert.message) + ) + } + } + } +} + +struct CounterRowState: Equatable, Identifiable { + var counter: CounterState + let id: UUID +} + +enum CounterRowAction: Equatable { + case counter(CounterAction) + case removeButtonTapped +} + +struct CounterRowView: View { + let store: Store + + var body: some View { + HStack { + CounterView( + store: self.store.scope( + state: \.counter, + action: CounterRowAction.counter + ) + ) + + Spacer() + + WithViewStore(self.store.stateless) { viewStore in + Button("Remove") { + viewStore.send(.removeButtonTapped, animation: .default) + } + } + } + .buttonStyle(PlainButtonStyle()) + } +} + + +extension Reducer { + func optional() -> Reducer { + .init { state, action, environment in + guard var wrappedState = state + else { return .none } + defer { state = wrappedState } + return self.run(&wrappedState, action, environment) + } + } +} + +struct AppState: Equatable { + var counters: IdentifiedArrayOf + var factPrompt: FactPromptState? + + var sum: Int { + self.counters.reduce(0) { $0 + $1.counter.count } + } +} +enum AppAction: Equatable { + case addButtonTapped + case counterRow(id: UUID, action: CounterRowAction) + case factPrompt(FactPromptAction) +} +struct AppEnvironment { + var fact: FactClient + var mainQueue: AnySchedulerOf + var uuid: () -> UUID +} + +let appReducer: Reducer = .combine( + + counterReducer + .pullback( + state: \CounterRowState.counter, + action: /CounterRowAction.counter, + environment: { $0 } + ) + .forEach( + state: \AppState.counters, + action: /AppAction.counterRow(id:action:), + environment: { + CounterEnvironment( + fact: $0.fact, + mainQueue: $0.mainQueue + ) + } + ), + + factPromptReducer + .optional() + .pullback( + state: \AppState.factPrompt, + action: /AppAction.factPrompt, + environment: { + .init( + fact: $0.fact, + mainQueue: $0.mainQueue + ) + } + ), + + .init { state, action, environment in + switch action { + case .addButtonTapped: + state.counters.append( + .init(counter: .init(), id: environment.uuid()) + ) + return .none + + case let .counterRow(id: id, action: .removeButtonTapped): + state.counters.remove(id: id) + return .none + + case let .counterRow(id: id, action: .counter(.factResponse(.success(fact)))): + guard let count = state.counters[id: id]?.counter.count + else { return .none } + state.factPrompt = .init(count: count, fact: fact) + return .none + + case .counterRow: + return .none + + case .factPrompt(.dismissButtonTapped): + state.factPrompt = nil + return .none + + case .factPrompt: + return .none + } + } +) + +struct AppView: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + ZStack(alignment: .bottom) { + List { + Text("Sum: \(viewStore.sum)") + + ForEachStore( + self.store.scope( + state: \.counters, + action: AppAction.counterRow(id:action:) + ), + content: CounterRowView.init(store:) + ) + } + .navigationTitle("Counters") + .navigationBarItems( + trailing: Button("Add") { + viewStore.send(.addButtonTapped, animation: .default) + } + ) + + IfLetStore( + self.store.scope( + state: \.factPrompt, + action: AppAction.factPrompt + ), + then: FactPrompt.init(store:) + ) + } + } + } +} + +struct FactPromptState: Equatable { + let count: Int + var fact: String + var isLoading = false +} +enum FactPromptAction: Equatable { + case dismissButtonTapped + case getAnotherFactButtonTapped + case factResponse(Result) +} +struct FactPromptEnvironment { + var fact: FactClient + var mainQueue: AnySchedulerOf +} + +let factPromptReducer = Reducer { state, action, environment in + switch action { + case .dismissButtonTapped: + return .none + + case .getAnotherFactButtonTapped: + return environment.fact.fetch(state.count) + .receive(on: environment.mainQueue) + .catchToEffect() + .map(FactPromptAction.factResponse) + + case let .factResponse(.success(fact)): + state.isLoading = false + state.fact = fact + return .none + + case .factResponse(.failure): + state.isLoading = false + return .none + } +} + +struct FactPrompt: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "info.circle.fill") + Text("Fact") + } + .font(.title3.bold()) + + if viewStore.isLoading { + ProgressView() + } else { + Text(viewStore.fact) + } + } + + HStack(spacing: 12) { + Button("Get another fact") { + viewStore.send(.getAnotherFactButtonTapped) + } + + Button("Dismiss") { + viewStore.send(.dismissButtonTapped) + } + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .cornerRadius(8) + .shadow(color: .black.opacity(0.1), radius: 20) + .padding() + } + } +} + +struct CounterView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AppView( + store: .init( + initialState: .init(counters: []), + reducer: appReducer, + environment: AppEnvironment( + fact: .live, + mainQueue: .main, + uuid: UUID.init + ) + ) + ) + } + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/DerivedBehaviorApp.swift b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/DerivedBehaviorApp.swift new file mode 100644 index 00000000..85763dc0 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/DerivedBehaviorApp.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@main +struct DerivedBehaviorApp: App { + var body: some Scene { + WindowGroup { + NavigationView { + AppView( + store: .init(initialState: AppState.init(counters: []), reducer: appReducer, environment: AppEnvironment(fact: .live, mainQueue: .main, uuid: UUID.init)) + ) + } + } + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/FactClient.swift b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/FactClient.swift new file mode 100644 index 00000000..01ed46d9 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/FactClient.swift @@ -0,0 +1,17 @@ +import ComposableArchitecture + +struct FactClient { + var fetch: (Int) -> Effect + + struct Error: Swift.Error, Equatable {} +} +extension FactClient { + static let live = Self( + fetch: { number in + URLSession.shared.dataTaskPublisher(for: URL(string: "http://numbersapi.com/\(number)")!) + .map { data, _ in String(decoding: data, as: UTF8.self) } + .mapError { _ in Error() } + .eraseToEffect() + } + ) +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Info.plist b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Info.plist new file mode 100644 index 00000000..61d79be6 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Info.plist @@ -0,0 +1,55 @@ + + + + + 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 + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Preview Content/Preview Assets.xcassets/Contents.json b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/VanillaView.swift b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/VanillaView.swift new file mode 100644 index 00000000..5155fbd7 --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehavior/VanillaView.swift @@ -0,0 +1,312 @@ +import Combine +import CombineSchedulers +import SwiftUI + +class CounterViewModel: ObservableObject { + @Published var alert: Alert? + @Published var count = 0 + let onFact: (Int, String) -> Void + + let fact: FactClient + let mainQueue: AnySchedulerOf + + private var cancellables: Set = [] + + init( + fact: FactClient, + mainQueue: AnySchedulerOf, + onFact: @escaping (Int, String) -> Void + ) { + self.fact = fact + self.mainQueue = mainQueue + self.onFact = onFact + } + + struct Alert: Equatable, Identifiable { + var message: String + var title: String + + var id: String { + self.title + self.message + } + } + + func decrementButtonTapped() { + self.count -= 1 + } + func incrementButtonTapped() { + self.count += 1 + } + func factButtonTapped() { + self.fact.fetch(self.count) + .receive(on: self.mainQueue.animation()) + .sink( + receiveCompletion: { [weak self] completion in + if case .failure = completion { + self?.alert = .init(message: "Couldn't load fact", title: "Error") + } + }, + receiveValue: { [weak self] fact in + guard let self = self else { return } + self.onFact(self.count, fact) + } + ) + .store(in: &self.cancellables) + } +} + +struct VanillaCounterView: View { + @ObservedObject var viewModel: CounterViewModel + + var body: some View { + VStack { + HStack { + Button("-") { self.viewModel.decrementButtonTapped() } + Text("\(self.viewModel.count)") + Button("+") { self.viewModel.incrementButtonTapped() } + } + + Button("Fact") { self.viewModel.factButtonTapped() } + } + .alert(item: self.$viewModel.alert) { alert in + Alert( + title: Text(alert.title), + message: Text(alert.message) + ) + } + } +} + +class CounterRowViewModel: ObservableObject, Identifiable { + @Published var counter: CounterViewModel + let id: UUID + + init(counter: CounterViewModel, id: UUID) { + self.counter = counter + self.id = id + } + + func removeButtonTapped() { + // TODO: track analytics + } +} + +struct VanillaCounterRowView: View { + let viewModel: CounterRowViewModel + let onRemoveTapped: () -> Void + + var body: some View { + HStack { + VanillaCounterView( + viewModel: self.viewModel.counter + ) + + Spacer() + + Button("Remove") { + withAnimation { + self.onRemoveTapped() + self.viewModel.removeButtonTapped() + } + } + } + .buttonStyle(PlainButtonStyle()) + } +} + + +class FactPromptViewModel: ObservableObject { + let count: Int + @Published var fact: String + @Published var isLoading = false + + let factClient: FactClient + let mainQueue: AnySchedulerOf + + private var cancellables: Set = [] + + init( + count: Int, + fact: String, + factClient: FactClient, + mainQueue: AnySchedulerOf + ) { + self.count = count + self.fact = fact + self.factClient = factClient + self.mainQueue = mainQueue + } + + func dismissButtonTapped() { + + } + func getAnotherFactButtonTapped() { + self.isLoading = true + + self.factClient.fetch(self.count) + .receive(on: self.mainQueue.animation()) + .sink( + receiveCompletion: { [weak self] _ in + self?.isLoading = false + }, + receiveValue: { [weak self] fact in + self?.fact = fact + } + ) + .store(in: &self.cancellables) + } +} + +struct VanillaFactPrompt: View { + @ObservedObject var viewModel: FactPromptViewModel + let onDismissTapped: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "info.circle.fill") + Text("Fact") + } + .font(.title3.bold()) + + if self.viewModel.isLoading { + ProgressView() + } else { + Text(self.viewModel.fact) + } + } + + HStack(spacing: 12) { + Button("Get another fact") { + self.viewModel.getAnotherFactButtonTapped() + } + + Button("Dismiss") { + self.onDismissTapped() + //self.viewModel.dismissButtonTapped() + } + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .cornerRadius(8) + .shadow(color: .black.opacity(0.1), radius: 20) + .padding() + } +} + +class AppViewModel: ObservableObject { + @Published var counters: [CounterRowViewModel] = [] + @Published var factPrompt: FactPromptViewModel? + + let fact: FactClient + let mainQueue: AnySchedulerOf + let uuid: () -> UUID + + private var cancellables: Set = [] + + init( + fact: FactClient, + mainQueue: AnySchedulerOf, + uuid: @escaping () -> UUID + ) { + self.fact = fact + self.mainQueue = mainQueue + self.uuid = uuid + } + + var sum: Int { + self.counters.reduce(0) { $0 + $1.counter.count } + } + + func addButtonTapped() { + let counterViewModel = CounterViewModel( + fact: self.fact, + mainQueue: self.mainQueue, + onFact: { [weak self] count, fact in + guard let self = self else { return } + + self.factPrompt = .init( + count: count, + fact: fact, + factClient: self.fact, + mainQueue: self.mainQueue + ) + } + ) + + self.counters.append( + .init( + counter: counterViewModel, + id: self.uuid() + ) + ) + + counterViewModel.$count + .sink { [weak self] _ in self?.objectWillChange.send() } + .store(in: &self.cancellables) + } + + func removeButtonTapped(id: UUID) { + self.counters.removeAll(where: { $0.id == id }) + } + + func dismissFactPrompt() { + self.factPrompt = nil + } +} + +struct VanillaAppView: View { + @ObservedObject var viewModel: AppViewModel + + var body: some View { + ZStack(alignment: .bottom) { + List { + Text("Sum: \(self.viewModel.sum)") + + ForEach(self.viewModel.counters) { counterRow in + VanillaCounterRowView( + viewModel: counterRow, + onRemoveTapped: { + self.viewModel.removeButtonTapped(id: counterRow.id) + } + ) + } + } + .navigationTitle("Counters") + .navigationBarItems( + trailing: Button("Add") { + withAnimation { + self.viewModel.addButtonTapped() + } + } + ) + + if let factPrompt = self.viewModel.factPrompt { + VanillaFactPrompt( + viewModel: factPrompt, + onDismissTapped: { + self.viewModel.dismissFactPrompt() + } + ) + } + } + } +} + + +struct Vanilla_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + VanillaAppView( + viewModel: .init( + fact: .live, + mainQueue: .main, + uuid: UUID.init + ) + ) + } + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/DerivedBehaviorTests.swift b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/DerivedBehaviorTests.swift new file mode 100644 index 00000000..413b4b0b --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/DerivedBehaviorTests.swift @@ -0,0 +1,72 @@ +import ComposableArchitecture +import XCTest +@testable import DerivedBehavior + +class DerivedBehaviorTests: XCTestCase { + func testBasics() { + let id = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let store = TestStore( + initialState: AppState(counters: []), + reducer: appReducer, + environment: AppEnvironment( + fact: FactClient(fetch: { + .init(value: "\($0) is a good number.") + }), + mainQueue: .immediate, + uuid: { id } + ) + ) + store.send(.addButtonTapped) { + $0.counters = [ + .init(counter: .init(), id: id) + ] + } + store.send(.counterRow(id: id, action: .counter(.incrementButtonTapped))) { + $0.counters[id: id]?.counter.count = 1 + } + store.send(.counterRow(id: id, action: .counter(.factButtonTapped))) + store.receive(.counterRow(id: id, action: .counter(.factResponse(.success("1 is a good number."))))) { + $0.factPrompt = .init(count: 1, fact: "1 is a good number.") + } + store.send(.factPrompt(.dismissButtonTapped)) { + $0.factPrompt = nil + } + store.send(.counterRow(id: id, action: .removeButtonTapped)) { + $0.counters = [] + } + } + + func testViewModel() { + let id = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let viewModel = AppViewModel( + fact: FactClient(fetch: { + .init(value: "\($0) is a good number.") + }), + mainQueue: .immediate, + uuid: { id } + ) + + viewModel.addButtonTapped() + XCTAssertEqual( + viewModel.counters.count, + 1 + ) + XCTAssertEqual(viewModel.counters[0].counter.count, 0) + + viewModel.counters[0].counter.incrementButtonTapped() + XCTAssertEqual(viewModel.counters[0].counter.count, 1) + + viewModel.counters[0].counter.factButtonTapped() + XCTAssertNotNil(viewModel.factPrompt) + XCTAssertEqual(viewModel.factPrompt?.count, 1) + XCTAssertEqual(viewModel.factPrompt?.fact, "1 is a good number.") + + viewModel.dismissFactPrompt() + XCTAssertNil(viewModel.factPrompt) + + viewModel.counters[0].removeButtonTapped() + // TODO: assert analytics tracked + viewModel.removeButtonTapped(id: id) + XCTAssertEqual(viewModel.counters.count, 0) + } +} diff --git a/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/Info.plist b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/Info.plist new file mode 100644 index 00000000..e537dd8b --- /dev/null +++ b/0150-derived-behavior-pt5/DerivedBehavior/DerivedBehaviorTests/Info.plist @@ -0,0 +1,27 @@ + + + + + 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 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0150-derived-behavior-pt5/README.md b/0150-derived-behavior-pt5/README.md new file mode 100644 index 00000000..337eb261 --- /dev/null +++ b/0150-derived-behavior-pt5/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Derived Behavior: The Point](https://www.pointfree.co/episodes/ep150-derived-behavior-the-point) +> +> We typically rewrite vanilla SwiftUI applications into Composable Architecture applications, but this week we do the opposite! We will explore “deriving behavior” by taking an existing TCA app and rewriting it using only the SwiftUI tools Apple gives us. diff --git a/README.md b/README.md index 9f446aab..f9068c17 100644 --- a/README.md +++ b/README.md @@ -151,3 +151,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Derived Behavior: Composable-Architecture](0147-derived-behavior-pt2) 1. [Derived Behavior: Collections](0148-derived-behavior-pt3) 1. [Derived Behavior: Optionals and Enums](0149-derived-behavior-pt4) +1. [Derived Behavior: The Point](0150-derived-behavior-pt5)