From 9d205ac96b63fa063e065560b98582c26b7d95c0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 25 Oct 2021 14:11:08 -0400 Subject: [PATCH] 165 --- 0163-navigation-pt4/README.md | 5 + 0164-navigation-pt5/README.md | 5 + 0165-navigation-pt6/README.md | 5 + .../project.pbxproj | 535 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../SwiftUINavigation/ContentView.swift | 48 ++ .../SwiftUINavigation/Inventory.swift | 166 ++++++ .../SwiftUINavigation/ItemRow.swift | 196 +++++++ .../SwiftUINavigation/ItemView.swift | 55 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../SwiftUINavigation/SwiftUIHelpers.swift | 192 +++++++ .../SwiftUINavigationApp.swift | 29 + .../SwiftUINavigationTests.swift | 59 ++ README.md | 3 + 18 files changed, 1434 insertions(+) create mode 100644 0163-navigation-pt4/README.md create mode 100644 0164-navigation-pt5/README.md create mode 100644 0165-navigation-pt6/README.md create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ContentView.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Inventory.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemRow.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemView.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift create mode 100644 0165-navigation-pt6/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift diff --git a/0163-navigation-pt4/README.md b/0163-navigation-pt4/README.md new file mode 100644 index 00000000..067f0287 --- /dev/null +++ b/0163-navigation-pt4/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Navigation: Sheets and Popovers, Part 2](https://www.pointfree.co/episodes/ep163-swiftui-navigation-sheets-popovers-part-2) +> +> This week we'll explore how to drive a sheet with optional state and how to facilitate communication between the sheet and the view presenting it. In the process we will discover a wonderful binding transformation for working with optionals. diff --git a/0164-navigation-pt5/README.md b/0164-navigation-pt5/README.md new file mode 100644 index 00000000..366acc85 --- /dev/null +++ b/0164-navigation-pt5/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Navigation: Sheets and Popovers, Part 3](https://www.pointfree.co/episodes/ep164-swiftui-navigation-sheets-popovers-part-3) +> +> Now that we've built up the tools needed to bind application state to navigation, let's exercise them. We'll quickly add two more features to our application, beef up our navigation tools, and even write unit tests that assert against navigation and deep-linking. diff --git a/0165-navigation-pt6/README.md b/0165-navigation-pt6/README.md new file mode 100644 index 00000000..95c024a8 --- /dev/null +++ b/0165-navigation-pt6/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Navigation: Links, Part 1](https://www.pointfree.co/episodes/ep165-swiftui-navigation-links-part-1) +> +> It's time to explore the most complex form of navigation in SwiftUI: links! We’ll start with some simpler flavors of `NavigationLink` to see how they work, how they compare with other navigation APIs, and how they interact with the tools we've built in this series. diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7180eaf1 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj @@ -0,0 +1,535 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 2A3A7B8B26EF940C00A37A4D /* SwiftUINavigationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3A7B8A26EF940C00A37A4D /* SwiftUINavigationApp.swift */; }; + 2A3A7B8D26EF940C00A37A4D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3A7B8C26EF940C00A37A4D /* ContentView.swift */; }; + 2A3A7B8F26EF940D00A37A4D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3A7B8E26EF940D00A37A4D /* Assets.xcassets */; }; + 2A3A7B9226EF940D00A37A4D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A3A7B9126EF940D00A37A4D /* Preview Assets.xcassets */; }; + 2A3A7B9C26EF940D00A37A4D /* SwiftUINavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3A7B9B26EF940D00A37A4D /* SwiftUINavigationTests.swift */; }; + 2A3A7BB626EFA73100A37A4D /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3A7BB526EFA73100A37A4D /* IdentifiedCollections */; }; + 2AF10F1C26F3B56B00796207 /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF10F1B26F3B56B00796207 /* ItemView.swift */; }; + 2AF10F1E26F3D4A000796207 /* ItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF10F1D26F3D4A000796207 /* ItemRow.swift */; }; + 4B3057AD26F3BD7900A5C737 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = 4B3057AC26F3BD7900A5C737 /* CasePaths */; }; + 4BDDD1F426EF9CB00032CB71 /* Inventory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDDD1F326EF9CB00032CB71 /* Inventory.swift */; }; + 4BDDD1F626EFB0D00032CB71 /* SwiftUIHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDDD1F526EFB0D00032CB71 /* SwiftUIHelpers.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A3A7B9826EF940D00A37A4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A3A7B7F26EF940C00A37A4D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A3A7B8626EF940C00A37A4D; + remoteInfo = SwiftUINavigation; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A3A7B8726EF940C00A37A4D /* SwiftUINavigation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUINavigation.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3A7B8A26EF940C00A37A4D /* SwiftUINavigationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINavigationApp.swift; sourceTree = ""; }; + 2A3A7B8C26EF940C00A37A4D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2A3A7B8E26EF940D00A37A4D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A3A7B9126EF940D00A37A4D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A3A7B9726EF940D00A37A4D /* SwiftUINavigationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftUINavigationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A3A7B9B26EF940D00A37A4D /* SwiftUINavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINavigationTests.swift; sourceTree = ""; }; + 2AF10F1B26F3B56B00796207 /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; + 2AF10F1D26F3D4A000796207 /* ItemRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemRow.swift; sourceTree = ""; }; + 4BDDD1F326EF9CB00032CB71 /* Inventory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inventory.swift; sourceTree = ""; }; + 4BDDD1F526EFB0D00032CB71 /* SwiftUIHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelpers.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A3A7B8426EF940C00A37A4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3A7BB626EFA73100A37A4D /* IdentifiedCollections in Frameworks */, + 4B3057AD26F3BD7900A5C737 /* CasePaths in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3A7B9426EF940D00A37A4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A3A7B7E26EF940C00A37A4D = { + isa = PBXGroup; + children = ( + 2A3A7B8926EF940C00A37A4D /* SwiftUINavigation */, + 2A3A7B9A26EF940D00A37A4D /* SwiftUINavigationTests */, + 2A3A7B8826EF940C00A37A4D /* Products */, + ); + sourceTree = ""; + }; + 2A3A7B8826EF940C00A37A4D /* Products */ = { + isa = PBXGroup; + children = ( + 2A3A7B8726EF940C00A37A4D /* SwiftUINavigation.app */, + 2A3A7B9726EF940D00A37A4D /* SwiftUINavigationTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2A3A7B8926EF940C00A37A4D /* SwiftUINavigation */ = { + isa = PBXGroup; + children = ( + 2A3A7B8A26EF940C00A37A4D /* SwiftUINavigationApp.swift */, + 2A3A7B8C26EF940C00A37A4D /* ContentView.swift */, + 4BDDD1F326EF9CB00032CB71 /* Inventory.swift */, + 2AF10F1D26F3D4A000796207 /* ItemRow.swift */, + 2AF10F1B26F3B56B00796207 /* ItemView.swift */, + 4BDDD1F526EFB0D00032CB71 /* SwiftUIHelpers.swift */, + 2A3A7B8E26EF940D00A37A4D /* Assets.xcassets */, + 2A3A7B9026EF940D00A37A4D /* Preview Content */, + ); + path = SwiftUINavigation; + sourceTree = ""; + }; + 2A3A7B9026EF940D00A37A4D /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2A3A7B9126EF940D00A37A4D /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2A3A7B9A26EF940D00A37A4D /* SwiftUINavigationTests */ = { + isa = PBXGroup; + children = ( + 2A3A7B9B26EF940D00A37A4D /* SwiftUINavigationTests.swift */, + ); + path = SwiftUINavigationTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A3A7B8626EF940C00A37A4D /* SwiftUINavigation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A3A7BAB26EF940E00A37A4D /* Build configuration list for PBXNativeTarget "SwiftUINavigation" */; + buildPhases = ( + 2A3A7B8326EF940C00A37A4D /* Sources */, + 2A3A7B8426EF940C00A37A4D /* Frameworks */, + 2A3A7B8526EF940C00A37A4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftUINavigation; + packageProductDependencies = ( + 2A3A7BB526EFA73100A37A4D /* IdentifiedCollections */, + 4B3057AC26F3BD7900A5C737 /* CasePaths */, + ); + productName = SwiftUINavigation; + productReference = 2A3A7B8726EF940C00A37A4D /* SwiftUINavigation.app */; + productType = "com.apple.product-type.application"; + }; + 2A3A7B9626EF940D00A37A4D /* SwiftUINavigationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A3A7BAE26EF940E00A37A4D /* Build configuration list for PBXNativeTarget "SwiftUINavigationTests" */; + buildPhases = ( + 2A3A7B9326EF940D00A37A4D /* Sources */, + 2A3A7B9426EF940D00A37A4D /* Frameworks */, + 2A3A7B9526EF940D00A37A4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A3A7B9926EF940D00A37A4D /* PBXTargetDependency */, + ); + name = SwiftUINavigationTests; + productName = SwiftUINavigationTests; + productReference = 2A3A7B9726EF940D00A37A4D /* SwiftUINavigationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A3A7B7F26EF940C00A37A4D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 2A3A7B8626EF940C00A37A4D = { + CreatedOnToolsVersion = 13.0; + }; + 2A3A7B9626EF940D00A37A4D = { + CreatedOnToolsVersion = 13.0; + TestTargetID = 2A3A7B8626EF940C00A37A4D; + }; + }; + }; + buildConfigurationList = 2A3A7B8226EF940C00A37A4D /* Build configuration list for PBXProject "SwiftUINavigation" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A3A7B7E26EF940C00A37A4D; + packageReferences = ( + 2A3A7BB426EFA73100A37A4D /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + 4B3057AB26F3BD7900A5C737 /* XCRemoteSwiftPackageReference "swift-case-paths" */, + ); + productRefGroup = 2A3A7B8826EF940C00A37A4D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A3A7B8626EF940C00A37A4D /* SwiftUINavigation */, + 2A3A7B9626EF940D00A37A4D /* SwiftUINavigationTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A3A7B8526EF940C00A37A4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3A7B9226EF940D00A37A4D /* Preview Assets.xcassets in Resources */, + 2A3A7B8F26EF940D00A37A4D /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3A7B9526EF940D00A37A4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A3A7B8326EF940C00A37A4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4BDDD1F426EF9CB00032CB71 /* Inventory.swift in Sources */, + 4BDDD1F626EFB0D00032CB71 /* SwiftUIHelpers.swift in Sources */, + 2A3A7B8D26EF940C00A37A4D /* ContentView.swift in Sources */, + 2AF10F1E26F3D4A000796207 /* ItemRow.swift in Sources */, + 2AF10F1C26F3B56B00796207 /* ItemView.swift in Sources */, + 2A3A7B8B26EF940C00A37A4D /* SwiftUINavigationApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A3A7B9326EF940D00A37A4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A3A7B9C26EF940D00A37A4D /* SwiftUINavigationTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A3A7B9926EF940D00A37A4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A3A7B8626EF940C00A37A4D /* SwiftUINavigation */; + targetProxy = 2A3A7B9826EF940D00A37A4D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A3A7BA926EF940E00A37A4D /* 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++17"; + 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 = 15.0; + 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; + }; + 2A3A7BAA26EF940E00A37A4D /* 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++17"; + 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 = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A3A7BAC26EF940E00A37A4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUINavigation/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUINavigation; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A3A7BAD26EF940E00A37A4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUINavigation/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUINavigation; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A3A7BAF26EF940E00A37A4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUINavigationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUINavigation.app/SwiftUINavigation"; + }; + name = Debug; + }; + 2A3A7BB026EF940E00A37A4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUINavigationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUINavigation.app/SwiftUINavigation"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A3A7B8226EF940C00A37A4D /* Build configuration list for PBXProject "SwiftUINavigation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3A7BA926EF940E00A37A4D /* Debug */, + 2A3A7BAA26EF940E00A37A4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A3A7BAB26EF940E00A37A4D /* Build configuration list for PBXNativeTarget "SwiftUINavigation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3A7BAC26EF940E00A37A4D /* Debug */, + 2A3A7BAD26EF940E00A37A4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A3A7BAE26EF940E00A37A4D /* Build configuration list for PBXNativeTarget "SwiftUINavigationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A3A7BAF26EF940E00A37A4D /* Debug */, + 2A3A7BB026EF940E00A37A4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A3A7BB426EFA73100A37A4D /* XCRemoteSwiftPackageReference "swift-identified-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-identified-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.0; + }; + }; + 4B3057AB26F3BD7900A5C737 /* XCRemoteSwiftPackageReference "swift-case-paths" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-case-paths.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.7.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A3A7BB526EFA73100A37A4D /* IdentifiedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = 2A3A7BB426EFA73100A37A4D /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = IdentifiedCollections; + }; + 4B3057AC26F3BD7900A5C737 /* CasePaths */ = { + isa = XCSwiftPackageProductDependency; + package = 4B3057AB26F3BD7900A5C737 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePaths; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A3A7B7F26EF940C00A37A4D /* Project object */; +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/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/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ContentView.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ContentView.swift new file mode 100644 index 00000000..ff419ec2 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ContentView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +enum Tab { + case one, inventory, three +} + +class AppViewModel: ObservableObject { + @Published var inventoryViewModel: InventoryViewModel + @Published var selectedTab: Tab + + init( + inventoryViewModel: InventoryViewModel = .init(), + selectedTab: Tab = .one + ) { + self.inventoryViewModel = inventoryViewModel + self.selectedTab = selectedTab + } +} + +struct ContentView: View { + @ObservedObject var viewModel: AppViewModel + + var body: some View { + TabView(selection: self.$viewModel.selectedTab) { + Button("Go to 2nd tab") { + self.viewModel.selectedTab = .inventory + } + .tabItem { Text("One") } + .tag(Tab.one) + + NavigationView { + InventoryView(viewModel: self.viewModel.inventoryViewModel) + } + .tabItem { Text("Inventory") } + .tag(Tab.inventory) + + Text("Three") + .tabItem { Text("Three") } + .tag(Tab.three) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView(viewModel: .init(selectedTab: .inventory)) + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Inventory.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Inventory.swift new file mode 100644 index 00000000..93b388aa --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Inventory.swift @@ -0,0 +1,166 @@ +import IdentifiedCollections +import SwiftUI + +struct Item: Equatable, Identifiable { + let id = UUID() + var name: String + var color: Color? + var status: Status + + enum Status: Equatable { + case inStock(quantity: Int) + case outOfStock(isOnBackOrder: Bool) + + var isInStock: Bool { + guard case .inStock = self else { return false } + return true + } + } + + struct Color: Equatable, Hashable { + var name: String + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + static var defaults: [Self] = [ + .red, + .green, + .blue, + .black, + .yellow, + .white, + ] + + static let red = Self(name: "Red", red: 1) + static let green = Self(name: "Green", green: 1) + static let blue = Self(name: "Blue", blue: 1) + static let black = Self(name: "Black") + static let yellow = Self(name: "Yellow", red: 1, green: 1) + static let white = Self(name: "White", red: 1, green: 1, blue: 1) + + var swiftUIColor: SwiftUI.Color { + .init(red: self.red, green: self.green, blue: self.blue) + } + } +} + +class InventoryViewModel: ObservableObject { + @Published var inventory: IdentifiedArrayOf + @Published var itemToAdd: Item? + + init( + inventory: IdentifiedArrayOf = [], + itemToAdd: Item? = nil + ) { + self.itemToAdd = itemToAdd + self.inventory = [] + + for itemRowViewModel in inventory { + self.bind(itemRowViewModel: itemRowViewModel) + } + } + + private func bind(itemRowViewModel: ItemRowViewModel) { + itemRowViewModel.onDelete = { [weak self, item = itemRowViewModel.item] in + withAnimation { + self?.delete(item: item) + } + } + itemRowViewModel.onDuplicate = { [weak self] item in + withAnimation { + self?.add(item: item) + } + } + self.inventory.append(itemRowViewModel) + } + + func delete(item: Item) { + withAnimation { + _ = self.inventory.remove(id: item.id) + } + } + + func add(item: Item) { + withAnimation { + self.bind(itemRowViewModel: .init(item: item)) + self.itemToAdd = nil + } + } + + func addButtonTapped() { + self.itemToAdd = .init(name: "", color: nil, status: .inStock(quantity: 1)) + + Task { @MainActor in + try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) + self.itemToAdd?.name = "Bluetooth Keyboard" + } + } + + func cancelButtonTapped() { + self.itemToAdd = nil + } +} + +struct InventoryView: View { + @ObservedObject var viewModel: InventoryViewModel + + var body: some View { + List { + ForEach( + self.viewModel.inventory, + content: ItemRowView.init(viewModel:) + ) + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Add") { self.viewModel.addButtonTapped() } + } + } + .navigationTitle("Inventory") + .sheet(unwrap: self.$viewModel.itemToAdd) { $itemToAdd in + NavigationView { + ItemView(item: $itemToAdd) + .navigationTitle("Add") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { self.viewModel.cancelButtonTapped() } + } + ToolbarItem(placement: .primaryAction) { + Button("Save") { self.viewModel.add(item: itemToAdd) } + } + } + } + } + } +} + +struct TestView: View { + @State var collection = [1, 2, 3] + + var body: some View { + ForEach(self.$collection, id: \.self) { $element in + + } + } +} + +struct InventoryView_Previews: PreviewProvider { + static var previews: some View { + let keyboard = Item(name: "Keyboard", color: .blue, status: .inStock(quantity: 100)) + + NavigationView { + InventoryView( + viewModel: .init( + inventory: [ + .init(item: keyboard), + .init(item: Item(name: "Charger", color: .yellow, status: .inStock(quantity: 20))), + .init(item: Item(name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true))), + .init(item: Item(name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false))), + ], + itemToAdd: nil + ) + ) + } + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemRow.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemRow.swift new file mode 100644 index 00000000..c91dfe69 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemRow.swift @@ -0,0 +1,196 @@ +import CasePaths +import SwiftUI + +class ItemRowViewModel: Identifiable, ObservableObject { + @Published var item: Item + @Published var route: Route? + @Published var isSaving = false + + enum Route: Equatable { + case deleteAlert + case duplicate(Item) + case edit(Item) + } + + var onDelete: () -> Void = {} + var onDuplicate: (Item) -> Void = { _ in } + + var id: Item.ID { self.item.id } + + init( + item: Item, + route: Route? = nil + ) { + self.item = item + self.route = route + } + + func deleteButtonTapped() { + self.route = .deleteAlert + } + + func deleteConfirmationButtonTapped() { + self.onDelete() + } + +// func editButtonTapped() { +// self.route = .edit(self.item) +// } + + func setEditNavigation(isActive: Bool) { + self.route = isActive ? .edit(self.item) : nil + } + + func edit(item: Item) { + self.isSaving = true + + Task { @MainActor in + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + + self.isSaving = false + self.item = item + self.route = nil + } + } + + func cancelButtonTapped() { + self.route = nil + } + + func duplicateButtonTapped() { + self.route = .duplicate(self.item.duplicate()) + } + + func duplicate(item: Item) { + self.onDuplicate(item) + self.route = nil + } +} + +extension Item { + func duplicate() -> Self { + .init(name: self.name, color: self.color, status: self.status) + } +} + +struct ItemRowView: View { + @ObservedObject var viewModel: ItemRowViewModel + + var body: some View { + NavigationLink( + unwrap: self.$viewModel.route.case(/ItemRowViewModel.Route.edit), + onNavigate: self.viewModel.setEditNavigation(isActive:), + destination: { $item in + ItemView(item: $item) + .navigationBarTitle("Edit") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.viewModel.cancelButtonTapped() + } + } + ToolbarItem(placement: .primaryAction) { + HStack { + if self.viewModel.isSaving { + ProgressView() + } + Button("Save") { + self.viewModel.edit(item: $item.wrappedValue) + } + } + .disabled(self.viewModel.isSaving) + } + } + } + ) { + HStack { + VStack(alignment: .leading) { + Text(self.viewModel.item.name) + + switch self.viewModel.item.status { + case let .inStock(quantity): + Text("In stock: \(quantity)") + case let .outOfStock(isOnBackOrder): + Text("Out of stock" + (isOnBackOrder ? ": on back order" : "")) + } + } + + Spacer() + + if let color = self.viewModel.item.color { + Rectangle() + .frame(width: 30, height: 30) + .foregroundColor(color.swiftUIColor) + .border(Color.black, width: 1) + } + + Button(action: { self.viewModel.duplicateButtonTapped() }) { + Image(systemName: "square.fill.on.square.fill") + } + .padding(.leading) + + // Button(action: { self.viewModel.editButtonTapped() }) { + // Image(systemName: "pencil") + // } + // .padding(.leading) + + Button(action: { self.viewModel.deleteButtonTapped() }) { + Image(systemName: "trash.fill") + } + .padding(.leading) + } + .buttonStyle(.plain) + .foregroundColor(self.viewModel.item.status.isInStock ? nil : Color.gray) + .alert( + self.viewModel.item.name, + isPresented: self.$viewModel.route.isPresent(/ItemRowViewModel.Route.deleteAlert), + actions: { + Button("Delete", role: .destructive) { + self.viewModel.deleteConfirmationButtonTapped() + } + }, + message: { + Text("Are you sure you want to delete this item?") + } + ) + // .sheet(unwrap: self.$viewModel.route.case(/ItemRowViewModel.Route.edit)) { $item in + // NavigationView { + // ItemView(item: $item) + // .navigationBarTitle("Edit") + // .toolbar { + // ToolbarItem(placement: .cancellationAction) { + // Button("Cancel") { + // self.viewModel.cancelButtonTapped() + // } + // } + // ToolbarItem(placement: .primaryAction) { + // Button("Save") { + // self.viewModel.edit(item: item) + // } + // } + // } + // } + // } + .popover(unwrap: self.$viewModel.route.case(/ItemRowViewModel.Route.duplicate)) { $item in + NavigationView { + ItemView(item: $item) + .navigationBarTitle("Duplicate") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.viewModel.cancelButtonTapped() + } + } + ToolbarItem(placement: .primaryAction) { + Button("Add") { + self.viewModel.duplicate(item: item) + } + } + } + } + .frame(minWidth: 300, minHeight: 500) + } + } + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemView.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemView.swift new file mode 100644 index 00000000..77353845 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/ItemView.swift @@ -0,0 +1,55 @@ +import CasePaths +import SwiftUI + +struct ItemView: View { + @Binding var item: Item + + var body: some View { + Form { + TextField("Name", text: self.$item.name) + + Picker(selection: self.$item.color, label: Text("Color")) { + Text("None") + .tag(Item.Color?.none) + + ForEach(Item.Color.defaults, id: \.name) { color in + Text(color.name) + .tag(Optional(color)) + } + } + + IfCaseLet(self.$item.status, pattern: /Item.Status.inStock) { $quantity in + Section(header: Text("In stock")) { + Stepper("Quantity: \(quantity)", value: $quantity) + Button("Mark as sold out") { + self.item.status = .outOfStock(isOnBackOrder: false) + } + } + } + IfCaseLet(self.$item.status, pattern: /Item.Status.outOfStock) { $isOnBackOrder in + Section(header: Text("Out of stock")) { + Toggle("Is on back order?", isOn: $isOnBackOrder) + Button("Is back in stock!") { + self.item.status = .inStock(quantity: 1) + } + } + } + } + } +} + +struct ItemView_Previews: PreviewProvider { + struct WrapperView: View { + @State var item = Item(name: "", color: nil, status: .inStock(quantity: 1)) + + var body: some View { + ItemView(item: self.$item) + } + } + + static var previews: some View { + NavigationView { + WrapperView() + } + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift new file mode 100644 index 00000000..cb35c2f6 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift @@ -0,0 +1,192 @@ +import CasePaths +import SwiftUI + +extension Binding { + func isPresent() -> Binding + where Value == Wrapped? { + .init( + get: { self.wrappedValue != nil }, + set: { isPresented in + if !isPresented { + self.wrappedValue = nil + } + } + ) + } + + func isPresent(_ casePath: CasePath) -> Binding + where Value == Enum? { + Binding( + get: { + if let wrappedValue = self.wrappedValue, casePath.extract(from: wrappedValue) != nil { + return true + } else { + return false + } + }, + set: { isPresented in + if !isPresented { + self.wrappedValue = nil + } + } + ) + } + + func `case`(_ casePath: CasePath) -> Binding + where Value == Enum? { + Binding( + get: { + guard + let wrappedValue = self.wrappedValue, + let `case` = casePath.extract(from: wrappedValue) + else { return nil } + return `case` + }, + set: { `case` in + if let `case` = `case` { + self.wrappedValue = casePath.embed(`case`) + } else { + self.wrappedValue = nil + } + } + ) + } +} + +extension View { + func alert( + title: (T) -> Text, + presenting data: Binding, + @ViewBuilder actions: @escaping (T) -> A, + @ViewBuilder message: @escaping (T) -> M + ) -> some View { + self.alert( + data.wrappedValue.map(title) ?? Text(""), + isPresented: data.isPresent(), + presenting: data.wrappedValue, + actions: actions, + message: message + ) + } + + func confirmationDialog( + title: (T) -> Text, + titleVisibility: Visibility = .automatic, + presenting data: Binding, + @ViewBuilder actions: @escaping (T) -> A, + @ViewBuilder message: @escaping (T) -> M + ) -> some View { + self.confirmationDialog( + data.wrappedValue.map(title) ?? Text(""), + isPresented: data.isPresent(), + titleVisibility: titleVisibility, + presenting: data.wrappedValue, + actions: actions, + message: message + ) + } +} + +struct IfCaseLet: View where Content: View { + let binding: Binding + let casePath: CasePath + let content: (Binding) -> Content + + init( + _ binding: Binding, + pattern casePath: CasePath, + @ViewBuilder content: @escaping (Binding) -> Content + ) { + self.binding = binding + self.casePath = casePath + self.content = content + } + + var body: some View { + if let `case` = self.casePath.extract(from: self.binding.wrappedValue) { + self.content( + Binding( + get: { `case` }, + set: { binding.wrappedValue = self.casePath.embed($0) } + ) + ) + } + } +} + +extension Binding { + init?(unwrap binding: Binding) { + guard let wrappedValue = binding.wrappedValue + else { return nil } + + self.init( + get: { wrappedValue }, + set: { binding.wrappedValue = $0 } + ) + } +} + +extension View { + func sheet( + unwrap optionalValue: Binding, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View where Value: Identifiable, Content: View { + self.sheet( + item: optionalValue + ) { _ in + if let value = Binding(unwrap: optionalValue) { + content(value) + } + } + } + + func popover( + unwrap optionalValue: Binding, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View where Value: Identifiable, Content: View { + self.popover( + item: optionalValue + ) { _ in + if let value = Binding(unwrap: optionalValue) { + content(value) + } + } + } +} + +extension NavigationLink { + init( + unwrap optionalValue: Binding, + onNavigate: @escaping (Bool) -> Void, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: @escaping () -> Label + ) + where Destination == WrappedDestination? + { + self.init( + isActive: optionalValue.isPresent().didSet(onNavigate), + destination: { + if let value = Binding(unwrap: optionalValue) { + destination(value) + } + }, + label: label + ) + } +} + +extension Binding { + func didSet(_ callback: @escaping (Value) -> Void) -> Self { + .init( + get: { self.wrappedValue }, + set: { + self.wrappedValue = $0 + callback($0) + } + ) + } +} + +// ForEach.init: (Binding, (Binding) -> some View) -> ForEach +// .sheet: (Binding, (Binding) -> some View) -> some View +// NavLink.init: (Binding, (Binding) -> some View) -> NavLink diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift new file mode 100644 index 00000000..3e75bd0c --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift @@ -0,0 +1,29 @@ +import SwiftUI + +@main +struct SwiftUINavigationApp: App { + var body: some Scene { + let keyboard = Item(name: "Keyboard", color: .blue, status: .inStock(quantity: 100)) + + var editedKeyboard = keyboard + editedKeyboard.name = "Bluetooth Keyboard" + editedKeyboard.status = .inStock(quantity: 1000) + + return WindowGroup { + ContentView( + viewModel: .init( + inventoryViewModel: .init( + inventory: [ + .init(item: keyboard), + .init(item: Item(name: "Charger", color: .yellow, status: .inStock(quantity: 20))), + .init(item: Item(name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true))), + .init(item: Item(name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false))), + ], + itemToAdd: nil + ), + selectedTab: .inventory + ) + ) + } + } +} diff --git a/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift new file mode 100644 index 00000000..db10efc0 --- /dev/null +++ b/0165-navigation-pt6/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -0,0 +1,59 @@ +import CasePaths +import XCTest +@testable import SwiftUINavigation + +class SwiftUINavigationTests: XCTestCase { + func testAddItem() throws { + let viewModel = InventoryViewModel() + viewModel.addButtonTapped() + + let itemToAdd = try XCTUnwrap(viewModel.itemToAdd) + + viewModel.add(item: itemToAdd) + + XCTAssertNil(viewModel.itemToAdd) + XCTAssertEqual(viewModel.inventory.count, 1) + XCTAssertEqual(viewModel.inventory[0].item, itemToAdd) + } + + func testDeleteItem() { + let viewModel = InventoryViewModel( + inventory: [ + .init(item: .init(name: "Keyboard", color: .red, status: .inStock(quantity: 1))) + ] + ) + + viewModel.inventory[0].deleteButtonTapped() + + XCTAssertEqual(viewModel.inventory[0].route, .deleteAlert) + + viewModel.inventory[0].deleteConfirmationButtonTapped() + + XCTAssertEqual(viewModel.inventory.count, 0) + } + + func testDuplicateItem() throws { + let item = Item(name: "Keyboard", color: .red, status: .inStock(quantity: 1)) + let viewModel = InventoryViewModel( + inventory: [ + .init(item: item) + ] + ) + + viewModel.inventory[0].duplicateButtonTapped() + +// XCTAssertEqual(viewModel.inventory[0].route, .duplicate(item)) + XCTAssertNotNil( + (/ItemRowViewModel.Route.duplicate) + .extract(from: try XCTUnwrap(viewModel.inventory[0].route)) + ) + + let dupe = item.duplicate() + viewModel.inventory[0].duplicate(item: dupe) + + XCTAssertEqual(viewModel.inventory.count, 2) + XCTAssertEqual(viewModel.inventory[0].item, item) + XCTAssertEqual(viewModel.inventory[1].item, dupe) + XCTAssertNil(viewModel.inventory[0].route) + } +} diff --git a/README.md b/README.md index dc14f403..67259855 100644 --- a/README.md +++ b/README.md @@ -163,3 +163,6 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Safer, Conciser Forms: Part 2](0159-safer-conciser-forms-pt2) 1. [SwiftUI Navigation: Tabs & Alerts, Part 2](0161-navigation-pt2) 1. [SwiftUI Navigation: Sheets & Popovers, Part 1](0162-navigation-pt3) +1. [SwiftUI Navigation: Sheets & Popovers, Part 2](0163-navigation-pt4) +1. [SwiftUI Navigation: Sheets & Popovers, Part 3](0164-navigation-pt5) +1. [SwiftUI Navigation: Links, Part 1](0165-navigation-pt6)