From 628463ead8ac5bfcd42b197d535a03897e122dd5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Jul 2024 10:32:53 -0700 Subject: [PATCH] wip --- .../ModernUIKit.xcodeproj/project.pbxproj | 413 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../ModernUIKit/Assets.xcassets/Contents.json | 6 + .../ModernUIKit/CounterFeature.swift | 220 +++++++++ .../ModernUIKit/ModernUIKit/Info.plist | 11 + .../ModernUIKit/ModernUIKitApp.swift | 24 + .../ModernUIKit/ModernUIKit/Navigation.swift | 133 ++++++ .../ModernUIKit/ModernUIKit/Observation.swift | 52 +++ .../Preview Assets.xcassets/Contents.json | 6 + 0285-modern-uikit-pt5/README.md | 5 + .../ModernUIKit.xcodeproj/project.pbxproj | 417 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../ModernUIKit/Assets.xcassets/Contents.json | 6 + .../ModernUIKit/CounterFeature.swift | 220 +++++++++ .../ModernUIKit/ModernUIKit/Info.plist | 11 + .../ModernUIKit/ModernUIKitApp.swift | 17 + .../ModernUIKit/ModernUIKit/Navigation.swift | 131 ++++++ .../ModernUIKit/ModernUIKit/Observation.swift | 52 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../ModernUIKit/ModernUIKit/UIBinding.swift | 53 +++ 0286-modern-uikit-pt6/README.md | 5 + .../ModernUIKit.xcodeproj/project.pbxproj | 429 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ModernUIKit/ModernUIKit/AppFeature.swift | 96 ++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../ModernUIKit/Assets.xcassets/Contents.json | 6 + .../ModernUIKit/CounterFeature.swift | 237 ++++++++++ .../ModernUIKit/ModernUIKit/Info.plist | 11 + .../ModernUIKit/ModernUIKitApp.swift | 32 ++ .../ModernUIKit/ModernUIKit/Navigation.swift | 133 ++++++ .../NavigationStackController.swift | 63 +++ .../ModernUIKit/ModernUIKit/Observation.swift | 52 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../ModernUIKit/SettingsFeature.swift | 71 +++ .../ModernUIKit/ModernUIKit/UIBinding.swift | 79 ++++ 0287-modern-uikit-pt7/README.md | 5 + README.md | 5 +- 45 files changed, 3129 insertions(+), 1 deletion(-) create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/CounterFeature.swift create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Info.plist create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/ModernUIKitApp.swift create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Navigation.swift create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Observation.swift create mode 100644 0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0285-modern-uikit-pt5/README.md create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/CounterFeature.swift create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Info.plist create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/ModernUIKitApp.swift create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Navigation.swift create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Observation.swift create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/UIBinding.swift create mode 100644 0286-modern-uikit-pt6/README.md create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/AppFeature.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/CounterFeature.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Info.plist create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/ModernUIKitApp.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Navigation.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/NavigationStackController.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Observation.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/SettingsFeature.swift create mode 100644 0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/UIBinding.swift create mode 100644 0287-modern-uikit-pt7/README.md diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..54eb472c --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj @@ -0,0 +1,413 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = 2A1CEB262BFD271600753A66 /* Perception */; }; + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */; }; + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */; }; + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */; }; + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */; }; + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F6372BFD151E009B28BB /* Observation.swift */; }; + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */; }; + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */; }; + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F63E2BFD46DB009B28BB /* Navigation.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2A1CEB242BFD1A3800753A66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModernUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernUIKitApp.swift; sourceTree = ""; }; + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeature.swift; sourceTree = ""; }; + 4B69F6372BFD151E009B28BB /* Observation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observation.swift; sourceTree = ""; }; + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B54C21E2BFD0D1900E95174 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */, + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */, + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4B54C2182BFD0D1900E95174 = { + isa = PBXGroup; + children = ( + 4B54C2232BFD0D1900E95174 /* ModernUIKit */, + 4B54C2222BFD0D1900E95174 /* Products */, + ); + sourceTree = ""; + }; + 4B54C2222BFD0D1900E95174 /* Products */ = { + isa = PBXGroup; + children = ( + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */, + ); + name = Products; + sourceTree = ""; + }; + 4B54C2232BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXGroup; + children = ( + 2A1CEB242BFD1A3800753A66 /* Info.plist */, + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */, + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */, + 4B69F6372BFD151E009B28BB /* Observation.swift */, + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */, + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */, + 4B54C22A2BFD0D1A00E95174 /* Preview Content */, + ); + path = ModernUIKit; + sourceTree = ""; + }; + 4B54C22A2BFD0D1A00E95174 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B54C2202BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */; + buildPhases = ( + 4B54C21D2BFD0D1900E95174 /* Sources */, + 4B54C21E2BFD0D1900E95174 /* Frameworks */, + 4B54C21F2BFD0D1900E95174 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ModernUIKit; + packageProductDependencies = ( + 2A1CEB262BFD271600753A66 /* Perception */, + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */, + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */, + ); + productName = ModernUIKit; + productReference = 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B54C2192BFD0D1900E95174 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 4B54C2202BFD0D1900E95174 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4B54C2182BFD0D1900E95174; + packageReferences = ( + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */, + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + ); + productRefGroup = 4B54C2222BFD0D1900E95174 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B54C2202BFD0D1900E95174 /* ModernUIKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B54C21F2BFD0D1900E95174 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */, + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B54C21D2BFD0D1900E95174 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */, + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */, + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */, + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4B54C2432BFD0D1A00E95174 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + 4B54C2442BFD0D1A00E95174 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B54C2462BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4B54C2472BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2432BFD0D1A00E95174 /* Debug */, + 4B54C2442BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2462BFD0D1A00E95174 /* Debug */, + 4B54C2472BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-perception"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.7; + }; + }; + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A1CEB262BFD271600753A66 /* Perception */ = { + isa = XCSwiftPackageProductDependency; + package = 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */; + productName = Perception; + }; + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigation; + }; + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigationCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B54C2192BFD0D1900E95174 /* Project object */; +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/CounterFeature.swift b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/CounterFeature.swift new file mode 100644 index 00000000..3c539bb7 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/CounterFeature.swift @@ -0,0 +1,220 @@ +import Perception +@preconcurrency import SwiftUI +import SwiftUINavigation + +@MainActor +@Perceptible +class CounterModel { + var count = 0 + var fact: Fact? + var secondaryFact: Fact? + var factIsLoading = false + struct Fact: Identifiable { + var value: String + var id: String { value } + } + func incrementButtonTapped() { + count += 1 + fact = nil + } + func decrementButtonTapped() { + count -= 1 + fact = nil + } + func factButtonTapped() async { + withUIAnimation { + self.fact = nil + } + self.factIsLoading = true + defer { self.factIsLoading = false } + + do { + try await Task.sleep(for: .seconds(1)) + let loadedFact = try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(count)")! + ).0, + as: UTF8.self + ) + self.fact = Fact(value: loadedFact) + try? await Task.sleep(for: .seconds(2)) + self.secondaryFact = Fact( + value: try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(count)")! + ).0, + as: UTF8.self + ) + ) + } catch { + // TODO: error handling + } +// try? await Task.sleep(for: .seconds(1)) +// count += 1 +// try? await Task.sleep(for: .seconds(2)) +// fact = nil + } +} + +struct CounterView: View { + @Perception.Bindable var model: CounterModel + var body: some View { + WithPerceptionTracking { + Form { + Text("\(model.count)") + Button("Decrement") { model.decrementButtonTapped() } + Button("Increment") { model.incrementButtonTapped() } + + if model.factIsLoading { + ProgressView().id(UUID()) + } + + Button("Get fact") { + Task { + await model.factButtonTapped() + } + } + } + .disabled(model.factIsLoading) + .sheet(item: $model.fact) { fact in + Text(fact.value) + } +// .alert(item: $model.fact) { _ in +// Text(model.count.description) +// } actions: { _ in +// } message: { fact in +// Text(fact) +// } + } + } +} + +#Preview("SwiftUI") { + CounterView(model: CounterModel()) +} + +final class CounterViewController: UIViewController { + @Perception.Bindable var model: CounterModel + + init(model: CounterModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let countLabel = UILabel() + countLabel.textAlignment = .center + let decrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.decrementButtonTapped() + }) + decrementButton.setTitle("Decrement", for: .normal) + let incrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.incrementButtonTapped() + }) + incrementButton.setTitle("Increment", for: .normal) + + let factLabel = UILabel() + factLabel.numberOfLines = 0 + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + let factButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + guard let self else { return } + Task { await self.model.factButtonTapped() } + }) + factButton.setTitle("Get fact", for: .normal) + + let counterStack = UIStackView(arrangedSubviews: [ + countLabel, + decrementButton, + incrementButton, + factLabel, + activityIndicator, + factButton, + ]) + counterStack.axis = .vertical + counterStack.spacing = 12 + counterStack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(counterStack) + NSLayoutConstraint.activate([ + counterStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + counterStack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + counterStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + counterStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + countLabel.text = "\(model.count)" + + activityIndicator.isHidden = !model.factIsLoading + decrementButton.isEnabled = !model.factIsLoading + incrementButton.isEnabled = !model.factIsLoading + factButton.isEnabled = !model.factIsLoading + } + + navigationController?.pushViewController(item: self.$model.fact) { fact in + FactViewController(fact: fact.value) + } + + navigationController?.pushViewController(item: self.$model.secondaryFact) { fact in + FactViewController(fact: fact.value) + } + } +} + +class FactViewController: UIViewController { + let fact: String + init(fact: String) { + self.fact = fact + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + let factLabel = UILabel() + factLabel.text = fact + factLabel.numberOfLines = 0 + factLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(factLabel) + NSLayoutConstraint.activate([ + factLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + factLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + factLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + factLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } +} + +#Preview("UIKit") { + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController(model: CounterModel()) + ) + } +} + +struct UIViewControllerRepresenting: UIViewControllerRepresentable { + let base: UIViewController + init(base: () -> UIViewController) { + self.base = base() + } + + func makeUIViewController(context: Context) -> some UIViewController { + return base + } + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Info.plist b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/ModernUIKitApp.swift b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/ModernUIKitApp.swift new file mode 100644 index 00000000..48edcae6 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/ModernUIKitApp.swift @@ -0,0 +1,24 @@ +import SwiftUI + +@main +struct ModernUIKitApp: App { + init() { + @State var model = CounterModel() + model.fact = CounterModel.Fact(value: "good number!") + dump(Binding($model.fact)!.value) + } + + + var body: some Scene { + WindowGroup { +// CounterView(model: CounterModel()) + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController( + model: CounterModel() + ) + ) + } + } + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Navigation.swift b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Navigation.swift new file mode 100644 index 00000000..c253cbda --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Navigation.swift @@ -0,0 +1,133 @@ +import ObjectiveC +import UIKit +import SwiftUI + +extension UIViewController { + func present( + item: @autoclosure @escaping () -> Binding, + content: @escaping (Item) -> UIViewController + ) { +// observe { [weak self] in +// guard let self else { return } +// let item = item() +// if let unwrappedItem = item.wrappedValue { +// @MainActor +// func presentNewController() { +// let controller = content(unwrappedItem) +// controller.onDeinit = OnDeinit { [weak self] in +// if AnyHashable(unwrappedItem.id) == self?.presented?.id { +// item.wrappedValue = nil +// } +// } +// presented = Presented(controller: controller, id: unwrappedItem.id) +// present(controller, animated: true) +// } +// if let presented { +// guard AnyHashable(unwrappedItem.id) != presented.id +// else { return } +// presented.controller?.dismiss(animated: true, completion: { +// presentNewController() +// }) +// } else { +// presentNewController() +// } +// } else if item.wrappedValue == nil, let controller = presented?.controller { +// controller.dismiss(animated: true) +// presented = nil +// } +// } + } + + fileprivate var presented: [AnyHashable: Presented] { + get { + objc_getAssociatedObject( + self, + presentedKey + ) as? [AnyHashable: Presented] + ?? [:] + } + set { + objc_setAssociatedObject( + self, + presentedKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + + fileprivate var onDeinit: OnDeinit? { + get { + objc_getAssociatedObject( + self, + onDeinitKey + ) as? OnDeinit + } + set { + objc_setAssociatedObject( + self, + onDeinitKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} + +extension UINavigationController { + func pushViewController( + item: @autoclosure @escaping () -> Binding, + content: @escaping (Item) -> UIViewController + ) { +// observe { [weak self] in +// guard let self else { return } +// let item = item() +// if let unwrappedItem = item.wrappedValue, presented == nil { +// let controller = content(unwrappedItem) +// controller.onDeinit = OnDeinit { +// item.wrappedValue = nil +// } +// presented = Presented(controller: controller) +// pushViewController(controller, animated: true) +// } else if item.wrappedValue == nil, let controller = presented?.controller { +// popFromViewController(controller, animated: true) +// presented = nil +// } +// } + } + + private func popFromViewController( + _ controller: UIViewController, + animated: Bool + ) { + guard + let index = viewControllers.firstIndex(of: controller), + index != 0 + else { + return + } + popToViewController(viewControllers[index - 1], animated: true) + } +} + +private let presentedKey = malloc(1)! +private let onDeinitKey = malloc(1)! + +final fileprivate class OnDeinit { + let onDismiss: () -> Void + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + deinit { + onDismiss() + } +} + +fileprivate final class Presented { + weak var controller: UIViewController? + let id: AnyHashable? + init(controller: UIViewController? = nil, id: AnyHashable? = nil) { + self.controller = controller + self.id = id + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Observation.swift b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Observation.swift new file mode 100644 index 00000000..917afe08 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Observation.swift @@ -0,0 +1,52 @@ +import Foundation +import Perception +import UIKit + +@MainActor +func observe( + apply: @escaping @MainActor @Sendable () -> Void +) { + onChange(apply: apply) +} +@MainActor +func onChange( + apply: @escaping @MainActor @Sendable () -> Void +) { + withPerceptionTracking { + apply() + } onChange: { + Task { @MainActor in + if let animation = UIAnimation.current { + UIView.animate(withDuration: animation.duration) { + onChange(apply: apply) + } + } else { + onChange(apply: apply) + } + } + } +} + +extension NSObject { + @MainActor + func observe( + apply: @escaping @MainActor @Sendable () -> Void + ) { + ModernUIKit.observe(apply: apply) + } +} + +struct UIAnimation { + @TaskLocal fileprivate static var current: Self? + var duration: TimeInterval +} + +@MainActor +func withUIAnimation( + _ animation: UIAnimation? = UIAnimation(duration: 0.3), + body: @escaping () -> Void +) { + UIAnimation.$current.withValue(animation) { + body() + } +} diff --git a/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0285-modern-uikit-pt5/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0285-modern-uikit-pt5/README.md b/0285-modern-uikit-pt5/README.md new file mode 100644 index 00000000..6a701ebe --- /dev/null +++ b/0285-modern-uikit-pt5/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Modern UIKit: Unified Navigation](https://www.pointfree.co/episodes/ep285-modern-uikit-unified-navigation) +> +> We have built the foundation of powerful new UIKit navigation tools, but they’re not quite finished. Let’s improve these APIs to handle dismissal by leveraging another SwiftUI tool: bindings. We will see how SwiftUI bindings are (almost) the perfect tool for UIKit navigation, and we will see where they fall short. diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..26484117 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj @@ -0,0 +1,417 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = 2A1CEB262BFD271600753A66 /* Perception */; }; + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */; }; + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */; }; + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */; }; + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */; }; + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F6372BFD151E009B28BB /* Observation.swift */; }; + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */; }; + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */; }; + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F63E2BFD46DB009B28BB /* Navigation.swift */; }; + 4BB60BE22BFE77E4002516B4 /* UIBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2A1CEB242BFD1A3800753A66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModernUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernUIKitApp.swift; sourceTree = ""; }; + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeature.swift; sourceTree = ""; }; + 4B69F6372BFD151E009B28BB /* Observation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observation.swift; sourceTree = ""; }; + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; + 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBinding.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B54C21E2BFD0D1900E95174 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */, + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */, + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4B54C2182BFD0D1900E95174 = { + isa = PBXGroup; + children = ( + 4B54C2232BFD0D1900E95174 /* ModernUIKit */, + 4B54C2222BFD0D1900E95174 /* Products */, + ); + sourceTree = ""; + }; + 4B54C2222BFD0D1900E95174 /* Products */ = { + isa = PBXGroup; + children = ( + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */, + ); + name = Products; + sourceTree = ""; + }; + 4B54C2232BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXGroup; + children = ( + 2A1CEB242BFD1A3800753A66 /* Info.plist */, + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */, + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */, + 4B69F6372BFD151E009B28BB /* Observation.swift */, + 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */, + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */, + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */, + 4B54C22A2BFD0D1A00E95174 /* Preview Content */, + ); + path = ModernUIKit; + sourceTree = ""; + }; + 4B54C22A2BFD0D1A00E95174 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B54C2202BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */; + buildPhases = ( + 4B54C21D2BFD0D1900E95174 /* Sources */, + 4B54C21E2BFD0D1900E95174 /* Frameworks */, + 4B54C21F2BFD0D1900E95174 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ModernUIKit; + packageProductDependencies = ( + 2A1CEB262BFD271600753A66 /* Perception */, + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */, + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */, + ); + productName = ModernUIKit; + productReference = 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B54C2192BFD0D1900E95174 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 4B54C2202BFD0D1900E95174 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4B54C2182BFD0D1900E95174; + packageReferences = ( + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */, + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + ); + productRefGroup = 4B54C2222BFD0D1900E95174 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B54C2202BFD0D1900E95174 /* ModernUIKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B54C21F2BFD0D1900E95174 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */, + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B54C21D2BFD0D1900E95174 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */, + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */, + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */, + 4BB60BE22BFE77E4002516B4 /* UIBinding.swift in Sources */, + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4B54C2432BFD0D1A00E95174 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + 4B54C2442BFD0D1A00E95174 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B54C2462BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4B54C2472BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2432BFD0D1A00E95174 /* Debug */, + 4B54C2442BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2462BFD0D1A00E95174 /* Debug */, + 4B54C2472BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-perception"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.7; + }; + }; + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A1CEB262BFD271600753A66 /* Perception */ = { + isa = XCSwiftPackageProductDependency; + package = 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */; + productName = Perception; + }; + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigation; + }; + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigationCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B54C2192BFD0D1900E95174 /* Project object */; +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/CounterFeature.swift b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/CounterFeature.swift new file mode 100644 index 00000000..31f5e3a8 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/CounterFeature.swift @@ -0,0 +1,220 @@ +import Perception +@preconcurrency import SwiftUI +import SwiftUINavigation + +@MainActor +@Perceptible +class CounterModel { + var count = 0 + var fact: Fact? + var secondaryFact: Fact? + var factIsLoading = false + struct Fact: Identifiable { + var value: String + var id: String { value } + } + func incrementButtonTapped() { + count += 1 + fact = nil + } + func decrementButtonTapped() { + count -= 1 + fact = nil + } + func factButtonTapped() async { + withUIAnimation { + self.fact = nil + } + self.factIsLoading = true + defer { self.factIsLoading = false } + + do { + try await Task.sleep(for: .seconds(1)) + let loadedFact = try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(count)")! + ).0, + as: UTF8.self + ) + self.fact = Fact(value: loadedFact) + try? await Task.sleep(for: .seconds(2)) + self.secondaryFact = Fact( + value: try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(count)")! + ).0, + as: UTF8.self + ) + ) + } catch { + // TODO: error handling + } +// try? await Task.sleep(for: .seconds(1)) +// count += 1 +// try? await Task.sleep(for: .seconds(2)) +// fact = nil + } +} + +struct CounterView: View { + @Perception.Bindable var model: CounterModel + var body: some View { + WithPerceptionTracking { + Form { + Text("\(model.count)") + Button("Decrement") { model.decrementButtonTapped() } + Button("Increment") { model.incrementButtonTapped() } + + if model.factIsLoading { + ProgressView().id(UUID()) + } + + Button("Get fact") { + Task { + await model.factButtonTapped() + } + } + } + .disabled(model.factIsLoading) + .sheet(item: $model.fact) { fact in + Text(fact.value) + } +// .alert(item: $model.fact) { _ in +// Text(model.count.description) +// } actions: { _ in +// } message: { fact in +// Text(fact) +// } + } + } +} + +#Preview("SwiftUI") { + CounterView(model: CounterModel()) +} + +final class CounterViewController: UIViewController { + @UIBindable var model: CounterModel + + init(model: CounterModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let countLabel = UILabel() + countLabel.textAlignment = .center + let decrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.decrementButtonTapped() + }) + decrementButton.setTitle("Decrement", for: .normal) + let incrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.incrementButtonTapped() + }) + incrementButton.setTitle("Increment", for: .normal) + + let factLabel = UILabel() + factLabel.numberOfLines = 0 + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + let factButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + guard let self else { return } + Task { await self.model.factButtonTapped() } + }) + factButton.setTitle("Get fact", for: .normal) + + let counterStack = UIStackView(arrangedSubviews: [ + countLabel, + decrementButton, + incrementButton, + factLabel, + activityIndicator, + factButton, + ]) + counterStack.axis = .vertical + counterStack.spacing = 12 + counterStack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(counterStack) + NSLayoutConstraint.activate([ + counterStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + counterStack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + counterStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + counterStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + countLabel.text = "\(model.count)" + + activityIndicator.isHidden = !model.factIsLoading + decrementButton.isEnabled = !model.factIsLoading + incrementButton.isEnabled = !model.factIsLoading + factButton.isEnabled = !model.factIsLoading + } + + navigationController?.pushViewController(item: self.$model.fact) { fact in + FactViewController(fact: fact.value) + } + + navigationController?.pushViewController(item: self.$model.secondaryFact) { fact in + FactViewController(fact: fact.value) + } + } +} + +class FactViewController: UIViewController { + let fact: String + init(fact: String) { + self.fact = fact + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + let factLabel = UILabel() + factLabel.text = fact + factLabel.numberOfLines = 0 + factLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(factLabel) + NSLayoutConstraint.activate([ + factLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + factLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + factLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + factLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } +} + +#Preview("UIKit") { + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController(model: CounterModel()) + ) + } +} + +struct UIViewControllerRepresenting: UIViewControllerRepresentable { + let base: UIViewController + init(base: () -> UIViewController) { + self.base = base() + } + + func makeUIViewController(context: Context) -> some UIViewController { + return base + } + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Info.plist b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/ModernUIKitApp.swift b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/ModernUIKitApp.swift new file mode 100644 index 00000000..911ca9a5 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/ModernUIKitApp.swift @@ -0,0 +1,17 @@ +import SwiftUI + +@main +struct ModernUIKitApp: App { + var body: some Scene { + WindowGroup { +// CounterView(model: CounterModel()) + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController( + model: CounterModel() + ) + ) + } + } + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Navigation.swift b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Navigation.swift new file mode 100644 index 00000000..5d24854f --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Navigation.swift @@ -0,0 +1,131 @@ +import ObjectiveC +import UIKit +import SwiftUI + +extension UIViewController { + func present( + item: UIBinding, + content: @escaping (Item) -> UIViewController + ) { + observe { [weak self] in + guard let self else { return } + if let unwrappedItem = item.wrappedValue { + @MainActor + func presentNewController() { + let controller = content(unwrappedItem) + controller.onDeinit = OnDeinit { [weak self] in + if AnyHashable(unwrappedItem.id) == self?.presented[item]?.id { + item.wrappedValue = nil + } + } + presented[item] = Presented(controller: controller, id: unwrappedItem.id) + present(controller, animated: true) + } + if let presented = presented[item] { + guard AnyHashable(unwrappedItem.id) != presented.id + else { return } + presented.controller?.dismiss(animated: true, completion: { + presentNewController() + }) + } else { + presentNewController() + } + } else if item.wrappedValue == nil, let controller = presented[item]?.controller { + controller.dismiss(animated: true) + presented[item] = nil + } + } + } + + fileprivate var presented: [AnyHashable: Presented] { + get { + objc_getAssociatedObject( + self, + presentedKey + ) as? [AnyHashable: Presented] + ?? [:] + } + set { + objc_setAssociatedObject( + self, + presentedKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + + fileprivate var onDeinit: OnDeinit? { + get { + objc_getAssociatedObject( + self, + onDeinitKey + ) as? OnDeinit + } + set { + objc_setAssociatedObject( + self, + onDeinitKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} + +extension UINavigationController { + func pushViewController( + item: UIBinding, + content: @escaping (Item) -> UIViewController + ) { + observe { [weak self] in + guard let self else { return } + if let unwrappedItem = item.wrappedValue, presented[item] == nil { + let controller = content(unwrappedItem) + controller.onDeinit = OnDeinit { + item.wrappedValue = nil + } + presented[item] = Presented(controller: controller) + pushViewController(controller, animated: true) + } else if item.wrappedValue == nil, let controller = presented[item]?.controller { + popFromViewController(controller, animated: true) + presented[item] = nil + } + } + } + + private func popFromViewController( + _ controller: UIViewController, + animated: Bool + ) { + guard + let index = viewControllers.firstIndex(of: controller), + index != 0 + else { + return + } + popToViewController(viewControllers[index - 1], animated: true) + } +} + +private let presentedKey = malloc(1)! +private let onDeinitKey = malloc(1)! + +final fileprivate class OnDeinit { + let onDismiss: () -> Void + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + deinit { + onDismiss() + } +} + +fileprivate final class Presented { + weak var controller: UIViewController? + let id: AnyHashable? + init(controller: UIViewController? = nil, id: AnyHashable? = nil) { + self.controller = controller + self.id = id + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Observation.swift b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Observation.swift new file mode 100644 index 00000000..917afe08 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Observation.swift @@ -0,0 +1,52 @@ +import Foundation +import Perception +import UIKit + +@MainActor +func observe( + apply: @escaping @MainActor @Sendable () -> Void +) { + onChange(apply: apply) +} +@MainActor +func onChange( + apply: @escaping @MainActor @Sendable () -> Void +) { + withPerceptionTracking { + apply() + } onChange: { + Task { @MainActor in + if let animation = UIAnimation.current { + UIView.animate(withDuration: animation.duration) { + onChange(apply: apply) + } + } else { + onChange(apply: apply) + } + } + } +} + +extension NSObject { + @MainActor + func observe( + apply: @escaping @MainActor @Sendable () -> Void + ) { + ModernUIKit.observe(apply: apply) + } +} + +struct UIAnimation { + @TaskLocal fileprivate static var current: Self? + var duration: TimeInterval +} + +@MainActor +func withUIAnimation( + _ animation: UIAnimation? = UIAnimation(duration: 0.3), + body: @escaping () -> Void +) { + UIAnimation.$current.withValue(animation) { + body() + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/UIBinding.swift b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/UIBinding.swift new file mode 100644 index 00000000..1ee6c8c9 --- /dev/null +++ b/0286-modern-uikit-pt6/ModernUIKit/ModernUIKit/UIBinding.swift @@ -0,0 +1,53 @@ +import Perception +import SwiftUI + +@dynamicMemberLookup +@propertyWrapper +struct UIBinding: Hashable { + fileprivate let base: AnyObject + fileprivate let keyPath: AnyKeyPath + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(base)) + hasher.combine(keyPath) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.base === rhs.base && lhs.keyPath == rhs.keyPath + } + + var wrappedValue: Value { + get { + (base as Any)[keyPath: keyPath] as! Value + } + nonmutating set { + func open(_ root: Root) { + root[keyPath: keyPath as! ReferenceWritableKeyPath] = newValue + } + _openExistential(base, do: open) + } + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> UIBinding { + UIBinding(base: base, keyPath: self.keyPath.appending(path: keyPath)!) + } +} + +@dynamicMemberLookup +@propertyWrapper +struct UIBindable { + var wrappedValue: Value + var projectedValue: Self { + get { self } + set { self = newValue } + } + + init(wrappedValue: Value) where Value: Perceptible, Value: AnyObject { + self.wrappedValue = wrappedValue + } + + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> UIBinding + where Value: AnyObject { + UIBinding(base: wrappedValue, keyPath: keyPath) + } +} diff --git a/0286-modern-uikit-pt6/README.md b/0286-modern-uikit-pt6/README.md new file mode 100644 index 00000000..3523e977 --- /dev/null +++ b/0286-modern-uikit-pt6/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Modern UIKit: Tree-based Navigation](https://www.pointfree.co/episodes/ep286-modern-uikit-tree-based-navigation) +> +> While SwiftUI bindings were almost the perfect tool for UIKit navigation, they unfortunately hide some crucial information that we need to build out our tools. But never fear, we can rebuild them from scratch! Let’s build `@Binding` and `@Bindable` from scratch to see how they work, and we will use them to drive concise, tree-based navigation using enums. diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..91f1ddbd --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.pbxproj @@ -0,0 +1,429 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = 2A1CEB262BFD271600753A66 /* Perception */; }; + 2A6230292BFEA5C600930179 /* AppFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6230282BFEA5C600930179 /* AppFeature.swift */; }; + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */; }; + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */; }; + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */; }; + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */; }; + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F6372BFD151E009B28BB /* Observation.swift */; }; + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */; }; + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */; }; + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F63E2BFD46DB009B28BB /* Navigation.swift */; }; + 4BAE7F8F2BFEAEEE00B9FEAB /* NavigationStackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAE7F8E2BFEAEEE00B9FEAB /* NavigationStackController.swift */; }; + 4BB60BE22BFE77E4002516B4 /* UIBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */; }; + 4BB60BE42BFE8098002516B4 /* SettingsFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2A1CEB242BFD1A3800753A66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 2A6230282BFEA5C600930179 /* AppFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFeature.swift; sourceTree = ""; }; + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModernUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernUIKitApp.swift; sourceTree = ""; }; + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeature.swift; sourceTree = ""; }; + 4B69F6372BFD151E009B28BB /* Observation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observation.swift; sourceTree = ""; }; + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; + 4BAE7F8E2BFEAEEE00B9FEAB /* NavigationStackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackController.swift; sourceTree = ""; }; + 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBinding.swift; sourceTree = ""; }; + 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeature.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B54C21E2BFD0D1900E95174 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B69F63B2BFD365B009B28BB /* SwiftUINavigation in Frameworks */, + 4B69F63D2BFD365B009B28BB /* SwiftUINavigationCore in Frameworks */, + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4B54C2182BFD0D1900E95174 = { + isa = PBXGroup; + children = ( + 4B54C2232BFD0D1900E95174 /* ModernUIKit */, + 4B54C2222BFD0D1900E95174 /* Products */, + ); + sourceTree = ""; + }; + 4B54C2222BFD0D1900E95174 /* Products */ = { + isa = PBXGroup; + children = ( + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */, + ); + name = Products; + sourceTree = ""; + }; + 4B54C2232BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXGroup; + children = ( + 2A1CEB242BFD1A3800753A66 /* Info.plist */, + 2A6230282BFEA5C600930179 /* AppFeature.swift */, + 4B54C24E2BFD0D9B00E95174 /* CounterFeature.swift */, + 4B54C2242BFD0D1900E95174 /* ModernUIKitApp.swift */, + 4B69F63E2BFD46DB009B28BB /* Navigation.swift */, + 4BAE7F8E2BFEAEEE00B9FEAB /* NavigationStackController.swift */, + 4B69F6372BFD151E009B28BB /* Observation.swift */, + 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */, + 4BB60BE12BFE77E4002516B4 /* UIBinding.swift */, + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */, + 4B54C22A2BFD0D1A00E95174 /* Preview Content */, + ); + path = ModernUIKit; + sourceTree = ""; + }; + 4B54C22A2BFD0D1A00E95174 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B54C2202BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */; + buildPhases = ( + 4B54C21D2BFD0D1900E95174 /* Sources */, + 4B54C21E2BFD0D1900E95174 /* Frameworks */, + 4B54C21F2BFD0D1900E95174 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ModernUIKit; + packageProductDependencies = ( + 2A1CEB262BFD271600753A66 /* Perception */, + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */, + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */, + ); + productName = ModernUIKit; + productReference = 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B54C2192BFD0D1900E95174 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 4B54C2202BFD0D1900E95174 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4B54C2182BFD0D1900E95174; + packageReferences = ( + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */, + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + ); + productRefGroup = 4B54C2222BFD0D1900E95174 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B54C2202BFD0D1900E95174 /* ModernUIKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B54C21F2BFD0D1900E95174 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */, + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B54C21D2BFD0D1900E95174 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B54C2252BFD0D1900E95174 /* ModernUIKitApp.swift in Sources */, + 4B69F6382BFD151E009B28BB /* Observation.swift in Sources */, + 4B54C24F2BFD0D9B00E95174 /* CounterFeature.swift in Sources */, + 4BB60BE42BFE8098002516B4 /* SettingsFeature.swift in Sources */, + 4BAE7F8F2BFEAEEE00B9FEAB /* NavigationStackController.swift in Sources */, + 4BB60BE22BFE77E4002516B4 /* UIBinding.swift in Sources */, + 2A6230292BFEA5C600930179 /* AppFeature.swift in Sources */, + 4B69F63F2BFD46DB009B28BB /* Navigation.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4B54C2432BFD0D1A00E95174 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + 4B54C2442BFD0D1A00E95174 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B54C2462BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4B54C2472BFD0D1A00E95174 /* 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 = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + 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.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2432BFD0D1A00E95174 /* Debug */, + 4B54C2442BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2462BFD0D1A00E95174 /* Debug */, + 4B54C2472BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-perception"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.7; + }; + }; + 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A1CEB262BFD271600753A66 /* Perception */ = { + isa = XCSwiftPackageProductDependency; + package = 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */; + productName = Perception; + }; + 4B69F63A2BFD365B009B28BB /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigation; + }; + 4B69F63C2BFD365B009B28BB /* SwiftUINavigationCore */ = { + isa = XCSwiftPackageProductDependency; + package = 4B69F6392BFD365B009B28BB /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigationCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B54C2192BFD0D1900E95174 /* Project object */; +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/AppFeature.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/AppFeature.swift new file mode 100644 index 00000000..bdc98019 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/AppFeature.swift @@ -0,0 +1,96 @@ +import Perception +import SwiftUI + +@Perceptible +class AppModel { + var path: [Path] + init(path: [Path] = []) { + self.path = path + } + + enum Path: Hashable { + case counter(CounterModel) + case settings(SettingsModel) + } +} + +struct AppView: View { + @Perception.Bindable var model: AppModel + + var body: some View { + WithPerceptionTracking { + NavigationStack(path: $model.path) { + Form { + Button("Counter") { + model.path.append(.counter(CounterModel())) + } + Button("Settings") { + model.path.append(.settings(SettingsModel())) + } + } + .navigationDestination(for: AppModel.Path.self) { path in + switch path { + case let .counter(model): + CounterView(model: model) + case let .settings(model): + SettingsView(model: model) + } + } + } + } + } +} + +extension NavigationStackController where Data == [AppModel.Path] { + convenience init(model: AppModel) { + @UIBindable var model = model + self.init(path: $model.path) { + RootViewController(path: $model.path) + } destination: { path in + switch path { + case let .counter(model): + CounterViewController(model: model) + case let .settings(model): + SettingsViewController(model: model) + } + } + } +} + +final class RootViewController: UIViewController { + @UIBinding var path: [AppModel.Path] + + init(path: UIBinding<[AppModel.Path]>) { + self._path = path + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let counterButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.path.append(.counter(CounterModel())) + }) + counterButton.setTitle("Counter", for: .normal) + let settingsButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.path.append(.settings(SettingsModel())) + }) + settingsButton.setTitle("Settings", for: .normal) + let stack = UIStackView(arrangedSubviews: [ + counterButton, + settingsButton, + ]) + stack.axis = .vertical + stack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/CounterFeature.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/CounterFeature.swift new file mode 100644 index 00000000..5700f819 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/CounterFeature.swift @@ -0,0 +1,237 @@ +import Perception +@preconcurrency import SwiftUI +import SwiftUINavigation + +@MainActor +@Perceptible +class CounterModel: HashableObject { + @CasePathable + enum Destination { + case fact(Fact) + case settings(SettingsModel) + } + + var count = 0 + var destination: Destination? + var factIsLoading = false + struct Fact: Identifiable { + var value: String + var id: String { value } + } + func incrementButtonTapped() { + count += 1 + destination = nil + } + func decrementButtonTapped() { + count -= 1 + destination = nil + } + func factButtonTapped() async { + withUIAnimation { + self.destination = nil + } + self.factIsLoading = true + defer { self.factIsLoading = false } + + do { + try await Task.sleep(for: .seconds(1)) + let loadedFact = try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(count)")! + ).0, + as: UTF8.self + ) + destination = nil + try await Task.sleep(for: .seconds(0.1)) + self.destination = .fact(Fact(value: loadedFact)) + } catch { + // TODO: error handling + } +// try? await Task.sleep(for: .seconds(1)) +// count += 1 +// try? await Task.sleep(for: .seconds(2)) +// fact = nil + } + func settingsButtonTapped() { + destination = .settings(SettingsModel()) + } +} + +struct CounterView: View { + @Perception.Bindable var model: CounterModel + var body: some View { + WithPerceptionTracking { + Form { + Text("\(model.count)") + Button("Decrement") { model.decrementButtonTapped() } + Button("Increment") { model.incrementButtonTapped() } + + if model.factIsLoading { + ProgressView().id(UUID()) + } + + Button("Get fact") { + Task { + await model.factButtonTapped() + } + } + } + .disabled(model.factIsLoading) + .sheet(item: ($model.destination as Binding).fact) { fact in + Text(fact.value) + } + .navigationDestination(item: $model.destination.settings) { model in + SettingsView(model: model) + } + .toolbar { + ToolbarItem { + Button("Settings") { + model.settingsButtonTapped() + } + } + } +// .alert(item: $model.fact) { _ in +// Text(model.count.description) +// } actions: { _ in +// } message: { fact in +// Text(fact) +// } + } + } +} + +#Preview("SwiftUI") { + NavigationStack { + CounterView(model: CounterModel()) + } +} + +final class CounterViewController: UIViewController { + @UIBindable var model: CounterModel + + init(model: CounterModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + let countLabel = UILabel() + countLabel.textAlignment = .center + let decrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.decrementButtonTapped() + }) + decrementButton.setTitle("Decrement", for: .normal) + let incrementButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.model.incrementButtonTapped() + }) + incrementButton.setTitle("Increment", for: .normal) + + let factLabel = UILabel() + factLabel.numberOfLines = 0 + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + let factButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + guard let self else { return } + Task { await self.model.factButtonTapped() } + }) + factButton.setTitle("Get fact", for: .normal) + + let counterStack = UIStackView(arrangedSubviews: [ + countLabel, + decrementButton, + incrementButton, + factLabel, + activityIndicator, + factButton, + ]) + counterStack.axis = .vertical + counterStack.spacing = 12 + counterStack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(counterStack) + NSLayoutConstraint.activate([ + counterStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + counterStack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + counterStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + counterStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Settings", primaryAction: UIAction { [weak self] _ in + self?.model.settingsButtonTapped() + }) + + observe { [weak self] in + guard let self else { return } + countLabel.text = "\(model.count)" + + activityIndicator.isHidden = !model.factIsLoading + decrementButton.isEnabled = !model.factIsLoading + incrementButton.isEnabled = !model.factIsLoading + factButton.isEnabled = !model.factIsLoading + } + + present(item: $model.destination.fact) { fact in + FactViewController(fact: fact.value) + } + + navigationController?.pushViewController(item: $model.destination.settings) { model in + SettingsViewController(model: model) + } + } +} + +class FactViewController: UIViewController { + let fact: String + init(fact: String) { + self.fact = fact + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + let factLabel = UILabel() + factLabel.text = fact + factLabel.numberOfLines = 0 + factLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(factLabel) + NSLayoutConstraint.activate([ + factLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + factLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + factLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + factLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } +} + +#Preview("UIKit") { + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController(model: CounterModel()) + ) + } +} + +struct UIViewControllerRepresenting: UIViewControllerRepresentable { + let base: UIViewController + init(base: () -> UIViewController) { + self.base = base() + } + + func makeUIViewController(context: Context) -> some UIViewController { + return base + } + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Info.plist b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/ModernUIKitApp.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/ModernUIKitApp.swift new file mode 100644 index 00000000..258fc3b1 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/ModernUIKitApp.swift @@ -0,0 +1,32 @@ +import SwiftUI + +@main +struct ModernUIKitApp: App { + var body: some Scene { + WindowGroup { + + UIViewControllerRepresenting { + NavigationStackController( + model: AppModel( + path: [ + .counter(CounterModel()), + .settings(SettingsModel()), + .counter(CounterModel()), + ] + ) + ) + } + + //AppView(model: AppModel()) + +// CounterView(model: CounterModel()) +// UIViewControllerRepresenting { +// UINavigationController( +// rootViewController: CounterViewController( +// model: CounterModel() +// ) +// ) +// } + } + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Navigation.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Navigation.swift new file mode 100644 index 00000000..a2aa3d24 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Navigation.swift @@ -0,0 +1,133 @@ +import ObjectiveC +import UIKit +import SwiftUI + +extension UIViewController { + func present( + item: UIBinding, + content: @escaping (Item) -> UIViewController + ) { + observe { [weak self] in + guard let self else { return } + if let unwrappedItem = item.wrappedValue { + @MainActor + func presentNewController() { + let controller = content(unwrappedItem) + controller.onDeinit = OnDeinit { [weak self] in + if AnyHashable(unwrappedItem.id) == self?.presented[item]?.id { + item.wrappedValue = nil + } + } + presented[item] = Presented(controller: controller, id: unwrappedItem.id) + present(controller, animated: true) + } + if let presented = presented[item] { + guard AnyHashable(unwrappedItem.id) != presented.id + else { return } + presented.controller?.dismiss(animated: true, completion: { + presentNewController() + }) + } else { + presentNewController() + } + } else if item.wrappedValue == nil, let presented = presented[item] { + presented.controller?.dismiss(animated: true) + self.presented[item] = nil + } + } + } + + fileprivate var presented: [AnyHashable: Presented] { + get { + objc_getAssociatedObject( + self, + presentedKey + ) as? [AnyHashable: Presented] + ?? [:] + } + set { + objc_setAssociatedObject( + self, + presentedKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + + fileprivate var onDeinit: OnDeinit? { + get { + objc_getAssociatedObject( + self, + onDeinitKey + ) as? OnDeinit + } + set { + objc_setAssociatedObject( + self, + onDeinitKey, + newValue, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} + +extension UINavigationController { + func pushViewController( + item: UIBinding, + content: @escaping (Item) -> UIViewController + ) { + observe { [weak self] in + guard let self else { return } + if let unwrappedItem = item.wrappedValue, presented[item] == nil { + let controller = content(unwrappedItem) + controller.onDeinit = OnDeinit { + item.wrappedValue = nil + } + presented[item] = Presented(controller: controller) + pushViewController(controller, animated: true) + } else if item.wrappedValue == nil, let presented = presented[item] { + if let controller = presented.controller { + popFromViewController(controller, animated: true) + } + self.presented[item] = nil + } + } + } + + private func popFromViewController( + _ controller: UIViewController, + animated: Bool + ) { + guard + let index = viewControllers.firstIndex(of: controller), + index != 0 + else { + return + } + popToViewController(viewControllers[index - 1], animated: true) + } +} + +private let presentedKey = malloc(1)! +private let onDeinitKey = malloc(1)! + +final fileprivate class OnDeinit { + let onDismiss: () -> Void + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + deinit { + onDismiss() + } +} + +fileprivate final class Presented { + weak var controller: UIViewController? + let id: AnyHashable? + init(controller: UIViewController? = nil, id: AnyHashable? = nil) { + self.controller = controller + self.id = id + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/NavigationStackController.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/NavigationStackController.swift new file mode 100644 index 00000000..d147e779 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/NavigationStackController.swift @@ -0,0 +1,63 @@ +import UIKit + +open class NavigationStackController: UINavigationController +where Data.Element: Hashable { + @UIBinding var path: Data + let root: UIViewController + let destination: (Data.Element) -> UIViewController + + init( + path: UIBinding, + root: () -> UIViewController, + destination: @escaping (Data.Element) -> UIViewController + ) { + self._path = path + self.root = root() + self.destination = destination + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewDidLoad() { + super.viewDidLoad() + + observe { [weak self] in + guard let self else { return } + + setViewControllers( + [root] + path.map { element in + guard let existingController = self.viewControllers.first( + where: { $0.navigationID == AnyHashable(element) } + ) + else { + let controller = self.destination(element) + controller.navigationID = element + return controller + } + return existingController + }, + animated: true + ) + } + } +} + +extension UIViewController { + fileprivate var navigationID: AnyHashable? { + get { + return objc_getAssociatedObject(self, navigationIDKey) as? AnyHashable + } + set { + objc_setAssociatedObject( + self, + navigationIDKey, + newValue, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} +private let navigationIDKey = malloc(1)! diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Observation.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Observation.swift new file mode 100644 index 00000000..917afe08 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Observation.swift @@ -0,0 +1,52 @@ +import Foundation +import Perception +import UIKit + +@MainActor +func observe( + apply: @escaping @MainActor @Sendable () -> Void +) { + onChange(apply: apply) +} +@MainActor +func onChange( + apply: @escaping @MainActor @Sendable () -> Void +) { + withPerceptionTracking { + apply() + } onChange: { + Task { @MainActor in + if let animation = UIAnimation.current { + UIView.animate(withDuration: animation.duration) { + onChange(apply: apply) + } + } else { + onChange(apply: apply) + } + } + } +} + +extension NSObject { + @MainActor + func observe( + apply: @escaping @MainActor @Sendable () -> Void + ) { + ModernUIKit.observe(apply: apply) + } +} + +struct UIAnimation { + @TaskLocal fileprivate static var current: Self? + var duration: TimeInterval +} + +@MainActor +func withUIAnimation( + _ animation: UIAnimation? = UIAnimation(duration: 0.3), + body: @escaping () -> Void +) { + UIAnimation.$current.withValue(animation) { + body() + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/SettingsFeature.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/SettingsFeature.swift new file mode 100644 index 00000000..05898d4d --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/SettingsFeature.swift @@ -0,0 +1,71 @@ +import Perception +import SwiftUI +import SwiftUINavigation + +@MainActor +@Perceptible +class SettingsModel: HashableObject { + var isOn = false +} + +struct SettingsView: View { + @Perception.Bindable var model: SettingsModel + + var body: some View { + WithPerceptionTracking { + Form { + Toggle(isOn: $model.isOn) { + Text("Is on?") + } + } + } + } +} + +class SettingsViewController: UIViewController { + let model: SettingsModel + + init(model: SettingsModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + let isOnSwitch = UISwitch() + isOnSwitch.addAction( + UIAction { [weak model = self.model, weak isOnSwitch] _ in + guard let model, let isOnSwitch + else { return } + model.isOn = isOnSwitch.isOn + }, + for: .valueChanged + ) + isOnSwitch.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(isOnSwitch) + + NSLayoutConstraint.activate([ + isOnSwitch.centerXAnchor.constraint(equalTo: view.centerXAnchor), + isOnSwitch.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + isOnSwitch.setOn(model.isOn, animated: true) + } + } +} + +#Preview("SwiftUI") { + SettingsView(model: SettingsModel()) +} +#Preview("UIKit") { + UIViewControllerRepresenting { + SettingsViewController(model: SettingsModel()) + } +} diff --git a/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/UIBinding.swift b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/UIBinding.swift new file mode 100644 index 00000000..2bb5ccd3 --- /dev/null +++ b/0287-modern-uikit-pt7/ModernUIKit/ModernUIKit/UIBinding.swift @@ -0,0 +1,79 @@ +import CasePaths +import Perception +import SwiftUI + +@dynamicMemberLookup +@propertyWrapper +struct UIBinding: Hashable { + fileprivate let base: AnyObject + fileprivate let keyPath: AnyKeyPath + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(base)) + hasher.combine(keyPath) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.base === rhs.base && lhs.keyPath == rhs.keyPath + } + + var wrappedValue: Value { + get { + (base as Any)[keyPath: keyPath] as! Value + } + nonmutating set { + func open(_ root: Root) { + root[keyPath: keyPath as! ReferenceWritableKeyPath] = newValue + } + _openExistential(base, do: open) + } + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> UIBinding { + UIBinding(base: base, keyPath: self.keyPath.appending(path: keyPath)!) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> UIBinding + where Value == Enum? { + self[keyPath] + } +} + +@dynamicMemberLookup +@propertyWrapper +struct UIBindable { + var wrappedValue: Value + var projectedValue: Self { + get { self } + set { self = newValue } + } + + init(wrappedValue: Value) where Value: Perceptible, Value: AnyObject { + self.wrappedValue = wrappedValue + } + + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> UIBinding + where Value: AnyObject { + UIBinding(base: wrappedValue, keyPath: keyPath) + } +} + +extension Optional where Wrapped: CasePathable { + fileprivate subscript( + keyPath: KeyPath> + ) -> Member? { + get { + guard let wrapped = self else { return nil } + return Wrapped.allCasePaths[keyPath: keyPath].extract(from: wrapped) + } + set { + guard let newValue else { + self = nil + return + } + self = Wrapped.allCasePaths[keyPath: keyPath].embed(newValue) + } + } +} diff --git a/0287-modern-uikit-pt7/README.md b/0287-modern-uikit-pt7/README.md new file mode 100644 index 00000000..bbefa15d --- /dev/null +++ b/0287-modern-uikit-pt7/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Modern UIKit: Stack Navigation, Part 1](https://www.pointfree.co/episodes/ep287-modern-uikit-stack-navigation-part-1) +> +> We have now implemented tree-based navigation in UIKit, driven by the Observation framework, but there is another form of navigation to think about: stack-based navigation, where you drive your navigation from a flat collection of states rather than a heavily-nested type. Let’s leverage Observation to build a really nice tool for stack-based navigation. diff --git a/README.md b/README.md index 7a2d16fe..e8b01d1b 100644 --- a/README.md +++ b/README.md @@ -285,4 +285,7 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Modern UIKit: Sneak Peek, Part 1](0281-modern-uikit-pt1) 1. [Modern UIKit: Sneak Peek, Part 2](0282-modern-uikit-pt2) 1. [Modern UIKit: Observation](0283-modern-uikit-pt3) -1. [Modern UIKit: Basics of Observation](0284-modern-uikit-pt4) +1. [Modern UIKit: Basics of Navigation](0284-modern-uikit-pt4) +1. [Modern UIKit: Unified Navigation](0285-modern-uikit-pt5) +1. [Modern UIKit: Tree-based Navigation](0286-modern-uikit-pt6) +1. [Modern UIKit: Stack Navigation, Part 1](0287-modern-uikit-pt7)