From e43180d45c2cd3aaed8121bbcdf3ac5cfcf9479b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 4 Oct 2021 11:22:29 -0400 Subject: [PATCH] 162 --- 0158-safer-conciser-forms-pt1/README.md | 4 +- 0159-safer-conciser-forms-pt2/README.md | 4 +- 0160-navigation-pt1/README.md | 5 + 0161-navigation-pt2/README.md | 5 + 0162-navigation-pt3/README.md | 5 + .../project.pbxproj | 531 ++++++++++++++++++ .../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 | 180 ++++++ .../SwiftUINavigation/ItemView.swift | 74 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../SwiftUINavigation/SwiftUIHelpers.swift | 90 +++ .../SwiftUINavigationApp.swift | 27 + .../SwiftUINavigationTests.swift | 33 ++ README.md | 4 + 19 files changed, 1142 insertions(+), 4 deletions(-) create mode 100644 0160-navigation-pt1/README.md create mode 100644 0161-navigation-pt2/README.md create mode 100644 0162-navigation-pt3/README.md create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ContentView.swift create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Inventory.swift create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ItemView.swift create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift create mode 100644 0162-navigation-pt3/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift diff --git a/0158-safer-conciser-forms-pt1/README.md b/0158-safer-conciser-forms-pt1/README.md index ce40af27..17e57ada 100644 --- a/0158-safer-conciser-forms-pt1/README.md +++ b/0158-safer-conciser-forms-pt1/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Concise Forms: Bye Bye Boilerplate](https://www.pointfree.co/episodes/ep133-concise-forms-bye-bye-boilerplate) +> #### This directory contains code from Point-Free Episode: [Safer, Conciser Forms: Part 1](https://www.pointfree.co/episodes/ep158-safer-conciser-forms-part-1) > -> The Composable Architecture makes it easy to layer complexity onto a form, but it just can’t match the brevity of vanilla SwiftUI…or can it!? We will overcome a Swift language limitation using key paths and type erasure to finally say “bye!” to boilerplate. +> Previously we explored how SwiftUI makes building forms a snap, and we contrasted it with the boilerplate introduced by the Composable Architecture. We employed a number of advanced tools to close the gap, but we can do better! We’ll start by using a property wrapper to make things much safer than before. diff --git a/0159-safer-conciser-forms-pt2/README.md b/0159-safer-conciser-forms-pt2/README.md index ce40af27..3d98f083 100644 --- a/0159-safer-conciser-forms-pt2/README.md +++ b/0159-safer-conciser-forms-pt2/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Concise Forms: Bye Bye Boilerplate](https://www.pointfree.co/episodes/ep133-concise-forms-bye-bye-boilerplate) +> #### This directory contains code from Point-Free Episode: [Safer, Conciser Forms: Part 2](https://www.pointfree.co/episodes/ep159-safer-conciser-forms-part-2) > -> The Composable Architecture makes it easy to layer complexity onto a form, but it just can’t match the brevity of vanilla SwiftUI…or can it!? We will overcome a Swift language limitation using key paths and type erasure to finally say “bye!” to boilerplate. +> We just made the Composable Architecture’s concise binding helpers safer, but can we make them even more concise? We’ll start with a suggestion that came from the community and employ even more Swift tricks, like dynamic member lookup, to get things even conciser than vanilla SwiftUI. diff --git a/0160-navigation-pt1/README.md b/0160-navigation-pt1/README.md new file mode 100644 index 00000000..d01c8058 --- /dev/null +++ b/0160-navigation-pt1/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Navigation: Tabs & Alerts, Part 1](https://www.pointfree.co/episodes/ep160-swiftui-navigation-tabs-alerts-part-1) +> +> Navigation is a really, really complex topic, and it’s going to take us many episodes go deep into it. We will begin our journey by coming up with a precise definition of what “navigation” is, and by exploring a couple simpler forms of navigation. diff --git a/0161-navigation-pt2/README.md b/0161-navigation-pt2/README.md new file mode 100644 index 00000000..52242ccc --- /dev/null +++ b/0161-navigation-pt2/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Navigation: Tabs & Alerts, Part 2](https://www.pointfree.co/episodes/ep161-swiftui-navigation-tabs-alerts-part-2) +> +> We continue our journey exploring navigation with an examination of alerts and action sheets. We’ll compare their original APIs in SwiftUI to the ones that replace them in the SDK that just shipped, and do a domain modeling exercise to recover what was lost. diff --git a/0162-navigation-pt3/README.md b/0162-navigation-pt3/README.md new file mode 100644 index 00000000..e40b60f2 --- /dev/null +++ b/0162-navigation-pt3/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 1](https://www.pointfree.co/episodes/ep162-swiftui-navigation-sheets-popovers-part-1) +> +> It’s time to look at a more advanced kind of navigation: modals. We will implement a new feature that will be driven by a sheet and can be deep-linked into. Along the way we’ll introduce a helper to solve a domain modeling problem involving enum state. diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ffd59224 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj @@ -0,0 +1,531 @@ +// !$*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 */; }; + 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 = ""; }; + 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 */, + 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 */, + 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/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0162-navigation-pt3/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/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ContentView.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ContentView.swift new file mode 100644 index 00000000..ff419ec2 --- /dev/null +++ b/0162-navigation-pt3/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/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Inventory.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Inventory.swift new file mode 100644 index 00000000..17660a08 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Inventory.swift @@ -0,0 +1,180 @@ +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? + @Published var itemToDelete: Item? + + init( + inventory: IdentifiedArrayOf = [], + itemToAdd: Item? = nil, + itemToDelete: Item? = nil + ) { + self.itemToDelete = itemToDelete + self.itemToAdd = itemToAdd + self.inventory = inventory + } + + func delete(item: Item) { + withAnimation { + _ = self.inventory.remove(id: item.id) + } + } + + func deleteButtonTapped(item: Item) { + self.itemToDelete = item + } + + func add(item: Item) { + withAnimation { + self.inventory.append(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) { item in + HStack { + VStack(alignment: .leading) { + Text(item.name) + + switch 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 = item.color { + Rectangle() + .frame(width: 30, height: 30) + .foregroundColor(color.swiftUIColor) + .border(Color.black, width: 1) + } + + Button(action: { self.viewModel.deleteButtonTapped(item: item) }) { + Image(systemName: "trash.fill") + } + .padding(.leading) + } + .buttonStyle(.plain) + .foregroundColor(item.status.isInStock ? nil : Color.gray) + } + } + .alert( + title: { Text($0.name) }, + presenting: self.$viewModel.itemToDelete, + actions: { item in + Button("Delete", role: .destructive) { + self.viewModel.delete(item: item) + } + }, + message: { _ in + Text("Are you sure you want to delete this item?") + } + ) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Add") { self.viewModel.addButtonTapped() } + } + } + .navigationTitle("Inventory") +// .sheet(isPresented: self.$addItemIsPresented) { + .sheet(item: self.$viewModel.itemToAdd) { itemToAdd in + NavigationView { + ItemView( + item: itemToAdd, + onSave: { self.viewModel.add(item: $0) }, + onCancel: { self.viewModel.cancelButtonTapped() } + ) + } + } + } +} + +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: [ + keyboard, + Item(name: "Charger", color: .yellow, status: .inStock(quantity: 20)), + Item(name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true)), + Item(name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false)), + ], + itemToAdd: .init(name: "Mouse", color: .red, status: .inStock(quantity: 100)), + itemToDelete: nil + ) + ) + } + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ItemView.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ItemView.swift new file mode 100644 index 00000000..42818f4c --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/ItemView.swift @@ -0,0 +1,74 @@ +import CasePaths +import SwiftUI + +struct ItemView: View { + @State var item = Item(name: "", color: nil, status: .inStock(quantity: 1)) + + let onSave: (Item) -> Void + let onCancel: () -> Void + + init( + item: Item = Item(name: "", color: nil, status: .inStock(quantity: 1)), + onSave: @escaping (Item) -> Void, + onCancel: @escaping () -> Void + ) { + self._item = .init(wrappedValue: item) + self.onSave = onSave + self.onCancel = onCancel + print("ItemView.init", item.name) + } + + var body: some View { + let _ = print("ItemView.body", self.item.name) + 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) + } + } + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.onCancel() + } + } + ToolbarItem(placement: .primaryAction) { + Button("Save") { + self.onSave(self.item) + } + } + } + } +} + +struct ItemView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + ItemView(onSave: { _ in }, onCancel: { }) + } + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift new file mode 100644 index 00000000..5c00fd2a --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift @@ -0,0 +1,90 @@ +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 + } + } + ) + } +} + +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 { +// subscript( +// dynamicMember keyPath: WritableKeyPath +// ) -> Binding { +// Binding( +// get: { self.wrappedValue[keyPath: keyPath] }, +// set: { self.wrappedValue[keyPath: keyPath] = $0 } +// ) +// } +//} + diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift new file mode 100644 index 00000000..bd830bae --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigation/SwiftUINavigationApp.swift @@ -0,0 +1,27 @@ +import SwiftUI + +@main +struct SwiftUINavigationApp: App { + var body: some Scene { + let keyboard = Item(name: "Keyboard", color: .blue, status: .inStock(quantity: 100)) + + WindowGroup { + ContentView( + viewModel: .init( + inventoryViewModel: .init( + inventory: [ + keyboard, + Item(name: "Charger", color: .yellow, status: .inStock(quantity: 20)), + Item(name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true)), + Item(name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false)), + ], + itemToAdd: nil //.init(name: "", color: nil, status: .inStock(quantity: 1)) + //, +// itemToDelete: keyboard + ), + selectedTab: .inventory + ) + ) + } + } +} diff --git a/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift new file mode 100644 index 00000000..f933bc6c --- /dev/null +++ b/0162-navigation-pt3/SwiftUINavigation/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -0,0 +1,33 @@ +// +// SwiftUINavigationTests.swift +// SwiftUINavigationTests +// +// Created by Point-Free on 9/13/21. +// + +import XCTest +@testable import SwiftUINavigation + +class SwiftUINavigationTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/README.md b/README.md index 53b9d43d..dc14f403 100644 --- a/README.md +++ b/README.md @@ -159,3 +159,7 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [SwiftUI Focus State](0155-focus-state) 1. [SwiftUI Searchable: Part 1](0156-searchable-pt1) 1. [SwiftUI Searchable: Part 2](0157-searchable-pt2) +1. [Safer, Conciser Forms: Part 1](0158-safer-conciser-forms-pt1) +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)