From 14ac25170bcdc617dc9d3f47b781a5860207fba7 Mon Sep 17 00:00:00 2001 From: Andrew Monshizadeh <1282845+amonshiz@users.noreply.github.com> Date: Fri, 7 Apr 2023 11:10:53 -0400 Subject: [PATCH 001/124] Import Foundation to make Xcode 14.3 happy (#97) --- Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift index f01906c022..76bbdb89f2 100644 --- a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift +++ b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift @@ -37,6 +37,7 @@ public func runtimeWarn( #if canImport(os) import os + import Foundation // NB: Xcode runtime warnings offer a much better experience than traditional assertions and // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. From 075672729295d956b919c82bfc1341bb64811c51 Mon Sep 17 00:00:00 2001 From: Zev Eisenberg Date: Tue, 30 May 2023 10:57:16 -0400 Subject: [PATCH 002/124] Import Foundation to suppress warning. (#105) --- Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift index 76bbdb89f2..64716e828d 100644 --- a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift +++ b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift @@ -1,3 +1,5 @@ +import Foundation + @_spi(RuntimeWarn) @_transparent @inline(__always) From db81007362f998654239021ca9308a264e59d3e2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 May 2023 11:26:07 -0400 Subject: [PATCH 003/124] Clean up import --- Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift index 64716e828d..76bbdb89f2 100644 --- a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift +++ b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift @@ -1,5 +1,3 @@ -import Foundation - @_spi(RuntimeWarn) @_transparent @inline(__always) From 78a4e30098bca2774838db639ddc4b562bdbb585 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 28 Jun 2023 09:29:52 -0700 Subject: [PATCH 004/124] Move `View.bind` modifier to `_SwiftUINavigationState` (#110) It's a more general helper that is immediately useful in most TCA apps, which export this internal module automatically. --- Sources/{SwiftUINavigation => _SwiftUINavigationState}/Bind.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/{SwiftUINavigation => _SwiftUINavigationState}/Bind.swift (100%) diff --git a/Sources/SwiftUINavigation/Bind.swift b/Sources/_SwiftUINavigationState/Bind.swift similarity index 100% rename from Sources/SwiftUINavigation/Bind.swift rename to Sources/_SwiftUINavigationState/Bind.swift From 2aa885e719087ee19df251c08a5980ad3e787f12 Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:13:36 -0400 Subject: [PATCH 005/124] Rename _SwiftUINavigationState to SwiftUINavigationCore (#112) * wip * wip --- Package.swift | 20 +++++++++---------- .../SwiftUINavigation/Internal/Exports.swift | 2 +- Sources/SwiftUINavigation/Switch.swift | 2 +- .../AlertState.swift | 0 .../Bind.swift | 0 .../ButtonState.swift | 0 .../ButtonStateBuilder.swift | 0 .../ConfirmationDialogState.swift | 0 .../Internal/Deprecations.swift | 0 .../Internal/RuntimeWarnings.swift | 0 .../TextState.swift | 0 11 files changed, 12 insertions(+), 12 deletions(-) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/AlertState.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/Bind.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/ButtonState.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/ButtonStateBuilder.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/ConfirmationDialogState.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/Internal/Deprecations.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/Internal/RuntimeWarnings.swift (100%) rename Sources/{_SwiftUINavigationState => SwiftUINavigationCore}/TextState.swift (100%) diff --git a/Package.swift b/Package.swift index ec43ec4d35..d19c54e637 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,8 @@ let package = Package( targets: ["SwiftUINavigation"] ), .library( - name: "_SwiftUINavigationState", - targets: ["_SwiftUINavigationState"] + name: "SwiftUINavigationCore", + targets: ["SwiftUINavigationCore"] ), ], dependencies: [ @@ -27,17 +27,10 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.8.0"), ], targets: [ - .target( - name: "_SwiftUINavigationState", - dependencies: [ - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ] - ), .target( name: "SwiftUINavigation", dependencies: [ - "_SwiftUINavigationState", + "SwiftUINavigationCore", .product(name: "CasePaths", package: "swift-case-paths"), ] ), @@ -47,5 +40,12 @@ let package = Package( "SwiftUINavigation" ] ), + .target( + name: "SwiftUINavigationCore", + dependencies: [ + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ] + ), ] ) diff --git a/Sources/SwiftUINavigation/Internal/Exports.swift b/Sources/SwiftUINavigation/Internal/Exports.swift index 944ac1756c..786cddc462 100644 --- a/Sources/SwiftUINavigation/Internal/Exports.swift +++ b/Sources/SwiftUINavigation/Internal/Exports.swift @@ -1,2 +1,2 @@ @_exported import CasePaths -@_exported import _SwiftUINavigationState +@_exported import SwiftUINavigationCore diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index 4e600dfebc..a013569fca 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -1,5 +1,5 @@ import SwiftUI -@_spi(RuntimeWarn) import _SwiftUINavigationState +@_spi(RuntimeWarn) import SwiftUINavigationCore /// A view that can switch over a binding of enum state and exhaustively handle each case. /// diff --git a/Sources/_SwiftUINavigationState/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift similarity index 100% rename from Sources/_SwiftUINavigationState/AlertState.swift rename to Sources/SwiftUINavigationCore/AlertState.swift diff --git a/Sources/_SwiftUINavigationState/Bind.swift b/Sources/SwiftUINavigationCore/Bind.swift similarity index 100% rename from Sources/_SwiftUINavigationState/Bind.swift rename to Sources/SwiftUINavigationCore/Bind.swift diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift similarity index 100% rename from Sources/_SwiftUINavigationState/ButtonState.swift rename to Sources/SwiftUINavigationCore/ButtonState.swift diff --git a/Sources/_SwiftUINavigationState/ButtonStateBuilder.swift b/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift similarity index 100% rename from Sources/_SwiftUINavigationState/ButtonStateBuilder.swift rename to Sources/SwiftUINavigationCore/ButtonStateBuilder.swift diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift similarity index 100% rename from Sources/_SwiftUINavigationState/ConfirmationDialogState.swift rename to Sources/SwiftUINavigationCore/ConfirmationDialogState.swift diff --git a/Sources/_SwiftUINavigationState/Internal/Deprecations.swift b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift similarity index 100% rename from Sources/_SwiftUINavigationState/Internal/Deprecations.swift rename to Sources/SwiftUINavigationCore/Internal/Deprecations.swift diff --git a/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift similarity index 100% rename from Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift rename to Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift diff --git a/Sources/_SwiftUINavigationState/TextState.swift b/Sources/SwiftUINavigationCore/TextState.swift similarity index 100% rename from Sources/_SwiftUINavigationState/TextState.swift rename to Sources/SwiftUINavigationCore/TextState.swift From f5bcdac5b6bb3f826916b14705f37a3937c2fd34 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 30 Jul 2023 11:33:01 -0700 Subject: [PATCH 006/124] Bump dependencies --- Package.resolved | 12 ++++++------ Package.swift | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Package.resolved b/Package.resolved index 7b816a8b17..5ea3070364 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", - "version": "0.11.0" + "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version": "1.0.0" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", "state": { "branch": null, - "revision": "819d9d370cd721c9d87671e29d947279292e4541", - "version": "0.6.0" + "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version": "1.0.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", - "version": "0.8.0" + "revision": "302891700c7fa3b92ebde9fe7b42933f8349f3c7", + "version": "1.0.0" } } ] diff --git a/Package.swift b/Package.swift index d19c54e637..2a3e9803ca 100644 --- a/Package.swift +++ b/Package.swift @@ -22,9 +22,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.11.0"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.6.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.8.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], targets: [ .target( From 4829335b11e61bd61eb4376346e2f6b69df80b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Tue, 1 Aug 2023 00:30:13 +0900 Subject: [PATCH 007/124] Bump up dependency ver 1.0.0 (#113) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51b7438645..d3daa833d1 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ it's as simple as adding it to a `dependencies` clause in your `Package.swift`: ``` swift dependencies: [ - .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.7.1") + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "1.0.0") ] ``` From bb00f4414497f5136b273d7532e1587f61475aed Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 31 Jul 2023 11:47:28 -0400 Subject: [PATCH 008/124] Update versions for Examples project. --- Examples/Examples.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index f39b997361..36e74fd1ee 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -1056,7 +1056,7 @@ repositoryURL = "http://github.com/pointfreeco/swift-dependencies"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.1.0; + minimumVersion = 1.0.0; }; }; DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */ = { @@ -1072,7 +1072,7 @@ repositoryURL = "https://github.com/pointfreeco/swift-identified-collections.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.5.0; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6c3cf7fefd..8f4b1c9b88 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, - "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", - "version": "0.9.1" + "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version": "1.0.0" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", - "version": "0.11.0" + "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version": "1.0.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-clocks", "state": { "branch": null, - "revision": "20b25ca0dd88ebfb9111ec937814ddc5a8880172", - "version": "0.2.0" + "revision": "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version": "1.0.0" } }, { @@ -37,13 +37,22 @@ "version": "1.0.4" } }, + { + "package": "swift-concurrency-extras", + "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", + "state": { + "branch": null, + "revision": "ea631ce892687f5432a833312292b80db238186a", + "version": "1.0.0" + } + }, { "package": "swift-custom-dump", "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", "state": { "branch": null, - "revision": "ead7d30cc224c3642c150b546f4f1080d1c411a8", - "version": "0.6.1" + "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version": "1.0.0" } }, { @@ -51,8 +60,8 @@ "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", "state": { "branch": null, - "revision": "e49dfe4d9e4c5c06f3334361360b801aef41631c", - "version": "0.1.1" + "revision": "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version": "1.0.0" } }, { @@ -69,8 +78,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", "state": { "branch": null, - "revision": "fd34c544ad27f3ba6b19142b348005bfa85b6005", - "version": "0.6.0" + "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version": "1.0.0" } }, { @@ -87,8 +96,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", - "version": "0.8.0" + "revision": "302891700c7fa3b92ebde9fe7b42933f8349f3c7", + "version": "1.0.0" } } ] From 4138b1c8dcd42ffdd1079e4fff856cf6b30e4d64 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 8 Aug 2023 16:54:42 -0700 Subject: [PATCH 009/124] Bump Packages --- .../xcshareddata/swiftpm/Package.resolved | 42 +++++++++++++------ Package.resolved | 15 +++++-- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index adb1c6c530..f33c147a90 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, - "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", - "version": "0.9.1" + "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version": "1.0.0" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", - "version": "0.11.0" + "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", + "version": "1.0.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-clocks", "state": { "branch": null, - "revision": "20b25ca0dd88ebfb9111ec937814ddc5a8880172", - "version": "0.2.0" + "revision": "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version": "1.0.0" } }, { @@ -37,13 +37,31 @@ "version": "1.0.4" } }, + { + "package": "swift-concurrency-extras", + "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", + "state": { + "branch": null, + "revision": "ea631ce892687f5432a833312292b80db238186a", + "version": "1.0.0" + } + }, { "package": "swift-custom-dump", "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", "state": { "branch": null, - "revision": "819d9d370cd721c9d87671e29d947279292e4541", - "version": "0.6.0" + "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version": "1.0.0" + } + }, + { + "package": "swift-dependencies", + "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", + "state": { + "branch": null, + "revision": "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version": "1.0.0" } }, { @@ -60,8 +78,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", "state": { "branch": null, - "revision": "a08887de589e3829d488e0b4b707b2ca804b1060", - "version": "0.5.0" + "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version": "1.0.0" } }, { @@ -78,8 +96,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", - "version": "0.8.0" + "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version": "1.0.2" } } ] diff --git a/Package.resolved b/Package.resolved index 5ea3070364..924fe3c0b9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,7 +24,16 @@ "repositoryURL": "https://github.com/apple/swift-docc-plugin", "state": { "branch": null, - "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", + "version": "1.3.0" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", "version": "1.0.0" } }, @@ -33,8 +42,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version": "1.0.0" + "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version": "1.0.2" } } ] From 22b660a3874c40ca529ff73bfc8f62bb03af43ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 8 Aug 2023 16:58:01 -0700 Subject: [PATCH 010/124] Bump Workspace Packages --- .../xcshareddata/swiftpm/Package.resolved | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8f4b1c9b88..b4100c0bea 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,7 +69,16 @@ "repositoryURL": "https://github.com/apple/swift-docc-plugin", "state": { "branch": null, - "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", + "version": "1.3.0" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", "version": "1.0.0" } }, @@ -87,8 +96,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", "state": { "branch": null, - "revision": "af06825aaa6adffd636c10a2570b2010c7c07e6a", - "version": "0.9.0" + "revision": "3907a9438f5b57d317001dc99f3f11b46882272b", + "version": "0.10.0" } }, { @@ -96,8 +105,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version": "1.0.0" + "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version": "1.0.2" } } ] From db922a33bcfa26bcb4a560dd0186ced1fa893e77 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 8 Aug 2023 17:09:27 -0700 Subject: [PATCH 011/124] Fix tests for latest packages --- Tests/SwiftUINavigationTests/AlertTests.swift | 22 +++++++++---------- .../ButtonStateTests.swift | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 34140816da..257236b63f 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -8,7 +8,7 @@ final class AlertTests: XCTestCase { let alert = AlertState( title: .init("Alert!"), message: .init("Something went wrong..."), - primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), secondaryButton: .cancel(.init("Cancel"), action: .send(false)) ) XCTAssertNoDifference( @@ -16,7 +16,7 @@ final class AlertTests: XCTestCase { AlertState( title: .init("Alert!"), message: .init("Something went wrong..."), - primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), secondaryButton: .cancel(.init("Cancel"), action: .send(false)) ) ) @@ -30,16 +30,16 @@ final class AlertTests: XCTestCase { title: "Alert!", actions: [ [0]: ButtonState( - role: ButtonStateRole.destructive, - action: ButtonStateAction.send( + role: .destructive, + action: .send( true, animation: Animation.easeInOut ), label: "Destroy" ), [1]: ButtonState( - role: ButtonStateRole.cancel, - action: ButtonStateAction.send( + role: .cancel, + action: .send( false ), label: "Cancel" @@ -57,7 +57,7 @@ final class AlertTests: XCTestCase { title: .init("Alert!"), message: .init("Something went wrong..."), buttons: [ - .destructive(.init("Destroy"), action: .send(true, animation: .default)), + .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), .cancel(.init("Cancel"), action: .send(false)), ] ), @@ -70,16 +70,16 @@ final class AlertTests: XCTestCase { title: "Alert!", actions: [ [0]: ButtonState( - role: ButtonStateRole.destructive, - action: ButtonStateAction.send( + role: .destructive, + action: .send( true, animation: Animation.easeInOut ), label: "Destroy" ), [1]: ButtonState( - role: ButtonStateRole.cancel, - action: ButtonStateAction.send( + role: .cancel, + action: .send( false ), label: "Cancel" diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index 904694d56b..a2a63a386c 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -21,7 +21,7 @@ final class ButtonStateTests: XCTestCase { """ } - let button = ButtonState(action: .send((), animation: .default)) { + let button = ButtonState(action: .send((), animation: .easeInOut)) { TextState("Animate!") } From 780edccf1b5d4373a6513c20d994e5d5a0945c30 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Wed, 6 Sep 2023 15:12:25 -0400 Subject: [PATCH 012/124] Add `Sendable` conformances (#120) * Add `Sendable` conformances SwiftUI only annotated some of the types that are used in this package with Xcode 15 / Swift 5.9, so we need to guard conformances only when compiling with Swift 5.9 or higher. Thanks @rowjo for starting this work in https://github.com/pointfreeco/swiftui-navigation/pull/116. * Small fixes. --------- Co-authored-by: Brandon Williams --- Sources/SwiftUINavigationCore/ButtonState.swift | 8 +++++++- .../ConfirmationDialogState.swift | 10 +++++++++- Sources/SwiftUINavigationCore/TextState.swift | 16 +++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index f8bb7799ea..b2c7a64be7 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -147,7 +147,7 @@ public struct ButtonStateAction { /// A value that describes the purpose of a button. /// /// See `SwiftUI.ButtonRole` for more information. -public enum ButtonStateRole { +public enum ButtonStateRole: Sendable { /// A role that indicates a cancel button. /// /// See `SwiftUI.ButtonRole.cancel` for more information. @@ -227,6 +227,12 @@ extension ButtonState: Hashable where Action: Hashable { } } +#if swift(>=5.7) +extension ButtonStateAction: Sendable where Action: Sendable {} +extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} +extension ButtonState: Sendable where Action: Sendable {} +#endif + // MARK: - SwiftUI bridging extension Alert.Button { diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 07421173e5..5af5b78b04 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -199,7 +199,7 @@ public struct ConfirmationDialogState: Identifiable { /// platform, current context, and other factors. /// /// See `SwiftUI.Visibility` for more information. -public enum ConfirmationDialogStateTitleVisibility { +public enum ConfirmationDialogStateTitleVisibility: Sendable { /// The element may be visible or hidden depending on the policies of the component accepting the /// visibility configuration. /// @@ -265,6 +265,14 @@ extension ConfirmationDialogState: Hashable where Action: Hashable { } } +#if swift(>=5.7) +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState: Sendable where Action: Sendable {} +#endif + // MARK: - SwiftUI bridging @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) diff --git a/Sources/SwiftUINavigationCore/TextState.swift b/Sources/SwiftUINavigationCore/TextState.swift index d3bc899a3b..69c17f1a3f 100644 --- a/Sources/SwiftUINavigationCore/TextState.swift +++ b/Sources/SwiftUINavigationCore/TextState.swift @@ -43,11 +43,11 @@ import SwiftUI /// - Note: ``TextState`` does not support _all_ `LocalizedStringKey` permutations at this time /// (interpolated `SwiftUI.Image`s, for example). ``TextState`` also uses reflection to determine /// `LocalizedStringKey` equatability, so be mindful of edge cases. -public struct TextState: Equatable, Hashable { +public struct TextState: Equatable, Hashable, Sendable { fileprivate var modifiers: [Modifier] = [] fileprivate let storage: Storage - fileprivate enum Modifier: Equatable, Hashable { + fileprivate enum Modifier: Equatable, Hashable, Sendable { case accessibilityHeading(AccessibilityHeadingLevel) case accessibilityLabel(TextState) case accessibilityTextContentType(AccessibilityTextContentType) @@ -70,7 +70,7 @@ public struct TextState: Equatable, Hashable { case underline(isActive: Bool, pattern: LineStylePattern?, color: Color?) } - public enum FontWidth: String, Equatable, Hashable { + public enum FontWidth: String, Equatable, Hashable, Sendable { case compressed case condensed case expanded @@ -89,7 +89,7 @@ public struct TextState: Equatable, Hashable { #endif } - public enum LineStylePattern: String, Equatable, Hashable { + public enum LineStylePattern: String, Equatable, Hashable, Sendable { case dash case dashDot case dashDotDot @@ -108,7 +108,9 @@ public struct TextState: Equatable, Hashable { } } - fileprivate enum Storage: Equatable, Hashable { + // NB: LocalizedStringKey is documented as being Sendable, but its conformance appears to be + // unavailable. + fileprivate enum Storage: Equatable, Hashable, @unchecked Sendable { indirect case concatenated(TextState, TextState) case localized(LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?) case verbatim(String) @@ -305,7 +307,7 @@ extension TextState { // MARK: Accessibility extension TextState { - public enum AccessibilityTextContentType: String, Equatable, Hashable { + public enum AccessibilityTextContentType: String, Equatable, Hashable, Sendable { case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing #if compiler(>=5.5.1) @@ -325,7 +327,7 @@ extension TextState { #endif } - public enum AccessibilityHeadingLevel: String, Equatable, Hashable { + public enum AccessibilityHeadingLevel: String, Equatable, Hashable, Sendable { case h1, h2, h3, h4, h5, h6, unspecified #if compiler(>=5.5.1) From 905274b2bd98be556d2cfcf31b94d5979b924755 Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Wed, 6 Sep 2023 19:19:28 +0000 Subject: [PATCH 013/124] Run swift-format --- Sources/SwiftUINavigationCore/ButtonState.swift | 6 +++--- .../ConfirmationDialogState.swift | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index b2c7a64be7..8d702cd4f7 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -228,9 +228,9 @@ extension ButtonState: Hashable where Action: Hashable { } #if swift(>=5.7) -extension ButtonStateAction: Sendable where Action: Sendable {} -extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} -extension ButtonState: Sendable where Action: Sendable {} + extension ButtonStateAction: Sendable where Action: Sendable {} + extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} + extension ButtonState: Sendable where Action: Sendable {} #endif // MARK: - SwiftUI bridging diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 5af5b78b04..361a4c3c23 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -266,11 +266,11 @@ extension ConfirmationDialogState: Hashable where Action: Hashable { } #if swift(>=5.7) -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ConfirmationDialogState: Sendable where Action: Sendable {} + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState: Sendable where Action: Sendable {} #endif // MARK: - SwiftUI bridging From 6eb293c49505d86e9e24232cb6af6be7fff93bd5 Mon Sep 17 00:00:00 2001 From: Vid Tadel Date: Mon, 11 Sep 2023 20:29:26 +0200 Subject: [PATCH 014/124] Require BindWorkaround for iOS17 in NavigationDestination (#122) * require BindWorkaround for iOS17 * Update Sources/SwiftUINavigation/NavigationDestination.swift --------- Co-authored-by: Stephen Celis --- Sources/SwiftUINavigation/NavigationDestination.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index 6f9e611b4a..e72d1f13a0 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -100,6 +100,9 @@ } private let requiresBindWorkaround = { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + return true + } guard #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) else { return true } return false From 9299135df94a286a4468da0a47768dfc8f79b57b Mon Sep 17 00:00:00 2001 From: Hitesh Date: Tue, 3 Oct 2023 15:34:50 +0100 Subject: [PATCH 015/124] Updating documentation to demonstrate accurate use of AlertState handling (#125) Co-authored-by: Hitesh Savaliya --- Sources/SwiftUINavigationCore/AlertState.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index 929de41967..0a70dbc505 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -36,12 +36,14 @@ import SwiftUI /// ```swift /// class HomeScreenModel: ObservableObject { /// // ... -/// func alertButtonTapped(_ action: AlertAction) { +/// func alertButtonTapped(_ action: AlertAction?) { /// switch action { /// case .delete: /// // ... /// case .removeFromHomeScreen: /// // ... +/// case .none: +/// // ... /// } /// } /// } From 3d2bc6e100adf8a2cbb81a5e6b7d26e7196282b7 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Tue, 3 Oct 2023 16:42:00 -0400 Subject: [PATCH 016/124] Allow For Compilation on Windows & Add CI (#123) * Ensure files can import SwiftUI * Add Windows CI Definition --- .github/workflows/ci.yml | 24 +++++++++++++++++++ Sources/SwiftUINavigation/Alert.swift | 2 ++ Sources/SwiftUINavigation/Binding.swift | 2 ++ .../ConfirmationDialog.swift | 2 ++ .../SwiftUINavigation/FullScreenCover.swift | 2 ++ Sources/SwiftUINavigation/IfCaseLet.swift | 2 ++ Sources/SwiftUINavigation/IfLet.swift | 2 ++ .../Internal/Binding+Internal.swift | 2 ++ .../Internal/Deprecations.swift | 2 ++ .../SwiftUINavigation/Internal/Exports.swift | 2 ++ .../NavigationDestination.swift | 4 ++-- .../SwiftUINavigation/NavigationLink.swift | 2 ++ Sources/SwiftUINavigation/Popover.swift | 2 ++ Sources/SwiftUINavigation/Sheet.swift | 2 ++ Sources/SwiftUINavigation/Switch.swift | 2 ++ Sources/SwiftUINavigation/WithState.swift | 2 ++ .../SwiftUINavigationCore/AlertState.swift | 2 ++ Sources/SwiftUINavigationCore/Bind.swift | 2 ++ .../SwiftUINavigationCore/ButtonState.swift | 2 ++ .../ButtonStateBuilder.swift | 2 ++ .../ConfirmationDialogState.swift | 2 ++ .../Internal/Deprecations.swift | 3 +++ .../Internal/RuntimeWarnings.swift | 2 ++ Sources/SwiftUINavigationCore/TextState.swift | 2 ++ Tests/SwiftUINavigationTests/AlertTests.swift | 2 ++ .../ButtonStateTests.swift | 2 ++ .../SwiftUINavigationTests.swift | 2 ++ .../TextStateTests.swift | 2 ++ 28 files changed, 79 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfdab77877..954ae0690f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,27 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run tests run: make test + + windows: + name: Windows + strategy: + matrix: + os: [windows-latest] + config: ['debug', 'release'] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: compnerd/gha-setup-swift@main + with: + branch: swift-5.8.1-release + tag: 5.8.1-RELEASE + - uses: actions/checkout@v3 + - name: Build + run: swift build -c ${{ matrix.config }} + - name: Run tests (debug only) + # There is an issue that exists in the 5.8.1 toolchain + # which fails on release configuration testing, but + # this issue is fixed 5.9 so we can remove the if once + # that is generally available. + if: ${{ matrix.config == 'debug' }} + run: swift test \ No newline at end of file diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index b3cb1521a2..8a342ac272 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension View { @@ -303,3 +304,4 @@ extension View { // TODO: support iOS <15? } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index 58adb782f3..fd0455ee13 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension Binding { @@ -187,3 +188,4 @@ extension Binding { ) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 598f646935..42c7c623dd 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension View { @@ -313,3 +314,4 @@ extension View { // TODO: support iOS <15? } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index bcb5ed2784..49d91b54e5 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension View { @@ -87,3 +88,4 @@ extension View { self.fullScreenCover(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } } +#endif // canImport(SwiftUI) \ No newline at end of file diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index ba0e69e3ba..575e44788f 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI /// A view that computes content by extracting a case from a binding to an enum and passing a @@ -90,3 +91,4 @@ extension IfCaseLet where ElseContent == EmptyView { self.ifContent = ifContent } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift index 429f624d98..ec020ec9d4 100644 --- a/Sources/SwiftUINavigation/IfLet.swift +++ b/Sources/SwiftUINavigation/IfLet.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI /// A view that computes content by unwrapping a binding to an optional and passing a non-optional @@ -84,3 +85,4 @@ extension IfLet where ElseContent == EmptyView { self.init(value, then: ifContent, else: { EmptyView() }) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift index 7716ed670d..431753049e 100644 --- a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift +++ b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension Binding { @@ -11,3 +12,4 @@ extension Binding { ) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 7c1261a577..ee823ba2c8 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI // NB: Deprecated after 0.5.0 @@ -218,3 +219,4 @@ extension NavigationLink { ) } } +#endif // canImport(SwiftUI) \ No newline at end of file diff --git a/Sources/SwiftUINavigation/Internal/Exports.swift b/Sources/SwiftUINavigation/Internal/Exports.swift index 786cddc462..8364a5a4de 100644 --- a/Sources/SwiftUINavigation/Internal/Exports.swift +++ b/Sources/SwiftUINavigation/Internal/Exports.swift @@ -1,2 +1,4 @@ +#if canImport(SwiftUI) @_exported import CasePaths @_exported import SwiftUINavigationCore +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index e72d1f13a0..0e3f34bcc2 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -1,4 +1,4 @@ -#if swift(>=5.7) +#if swift(>=5.7) && canImport(SwiftUI) import SwiftUI @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) @@ -107,4 +107,4 @@ else { return true } return false }() -#endif +#endif // swift(>=5.7) && canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index 69b2094608..3bc44dbe2f 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension NavigationLink { @@ -133,3 +134,4 @@ extension NavigationLink { ) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 86bd1096ac..9bea08483a 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension View { @@ -96,3 +97,4 @@ extension View { ) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index c1c4acdbad..5f92fdae1c 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI #if canImport(UIKit) @@ -87,3 +88,4 @@ extension View { self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index a013569fca..9a54e29b8a 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI @_spi(RuntimeWarn) import SwiftUINavigationCore @@ -1114,3 +1115,4 @@ private func describeCase(_ enum: Enum) -> String { } return "\(type).\(`case`)" } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/WithState.swift b/Sources/SwiftUINavigation/WithState.swift index b4e40e330b..97de5e1490 100644 --- a/Sources/SwiftUINavigation/WithState.swift +++ b/Sources/SwiftUINavigation/WithState.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI /// A container view that provides a binding to another view. @@ -44,3 +45,4 @@ public struct WithState: View { self.content(self.$value) } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index 0a70dbc505..7e4208ecde 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI @@ -260,3 +261,4 @@ extension Alert { } } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Bind.swift b/Sources/SwiftUINavigationCore/Bind.swift index 3c1236b463..49c61439ec 100644 --- a/Sources/SwiftUINavigationCore/Bind.swift +++ b/Sources/SwiftUINavigationCore/Bind.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI extension View { @@ -82,3 +83,4 @@ extension FocusState.Binding: _Bindable {} extension SceneStorage: _Bindable {} extension State: _Bindable {} +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index 8d702cd4f7..3d538cbba0 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI @@ -368,3 +369,4 @@ func typeName(_ type: Any.Type) -> String { ) return sanitizedName } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift b/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift index 70957312fb..53ff0d809e 100644 --- a/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift +++ b/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) @resultBuilder public enum ButtonStateBuilder { public static func buildArray(_ components: [[ButtonState]]) -> [ButtonState] { @@ -30,3 +31,4 @@ public enum ButtonStateBuilder { component ?? [] } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 361a4c3c23..9e7bae4afd 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI @@ -288,3 +289,4 @@ extension Visibility { } } } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift index a706de75ae..a2c6d61c98 100644 --- a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI // NB: Deprecated after 0.5.0 @@ -307,3 +308,5 @@ extension ActionSheet { ) } } + +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index 76bbdb89f2..e0ae009c7b 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) @_spi(RuntimeWarn) @_transparent @inline(__always) @@ -70,3 +71,4 @@ public func runtimeWarn( }() #endif #endif +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/TextState.swift b/Sources/SwiftUINavigationCore/TextState.swift index 69c17f1a3f..bc1f5bf665 100644 --- a/Sources/SwiftUINavigationCore/TextState.swift +++ b/Sources/SwiftUINavigationCore/TextState.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI @@ -741,3 +742,4 @@ extension TextState: CustomDumpRepresentable { return dumpHelp(self) } } +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 257236b63f..3828ad9d93 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI import SwiftUINavigation @@ -117,3 +118,4 @@ private struct TestView: View { } } } +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index a2a63a386c..c06cd76a3d 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUI import SwiftUINavigation @@ -30,3 +31,4 @@ final class ButtonStateTests: XCTestCase { } } } +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift index 92d6107259..71d8825d50 100644 --- a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift +++ b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import SwiftUI import XCTest @@ -60,3 +61,4 @@ final class SwiftUINavigationTests: XCTestCase { XCTAssertEqual(failure.wrappedValue, nil) } } +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/TextStateTests.swift b/Tests/SwiftUINavigationTests/TextStateTests.swift index 8a01843aa7..42fa2bd4ff 100644 --- a/Tests/SwiftUINavigationTests/TextStateTests.swift +++ b/Tests/SwiftUINavigationTests/TextStateTests.swift @@ -1,3 +1,4 @@ +#if canImport(SwiftUI) import CustomDump import SwiftUINavigation import XCTest @@ -72,3 +73,4 @@ final class TextStateTests: XCTestCase { ) } } +#endif // canImport(SwiftUI) From 640d92c9e0f95c3934ba24bbbdcdfc58765dd110 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Tue, 3 Oct 2023 20:49:12 +0000 Subject: [PATCH 017/124] Run swift-format --- Sources/SwiftUINavigation/Alert.swift | 540 ++--- Sources/SwiftUINavigation/Binding.swift | 354 ++-- .../ConfirmationDialog.swift | 560 ++--- .../SwiftUINavigation/FullScreenCover.swift | 173 +- Sources/SwiftUINavigation/IfCaseLet.swift | 172 +- Sources/SwiftUINavigation/IfLet.swift | 161 +- .../Internal/Binding+Internal.swift | 24 +- .../Internal/Deprecations.swift | 348 ++-- .../SwiftUINavigation/Internal/Exports.swift | 6 +- .../NavigationDestination.swift | 2 +- .../SwiftUINavigation/NavigationLink.swift | 266 +-- Sources/SwiftUINavigation/Popover.swift | 190 +- Sources/SwiftUINavigation/Sheet.swift | 170 +- Sources/SwiftUINavigation/Switch.swift | 1842 ++++++++--------- Sources/SwiftUINavigation/WithState.swift | 86 +- .../SwiftUINavigationCore/AlertState.swift | 490 ++--- Sources/SwiftUINavigationCore/Bind.swift | 138 +- .../SwiftUINavigationCore/ButtonState.swift | 636 +++--- .../ButtonStateBuilder.swift | 54 +- .../ConfirmationDialogState.swift | 534 ++--- .../Internal/Deprecations.swift | 504 ++--- .../Internal/RuntimeWarnings.swift | 126 +- Sources/SwiftUINavigationCore/TextState.swift | 1301 ++++++------ Tests/SwiftUINavigationTests/AlertTests.swift | 158 +- .../ButtonStateTests.swift | 52 +- .../SwiftUINavigationTests.swift | 98 +- .../TextStateTests.swift | 142 +- 27 files changed, 4569 insertions(+), 4558 deletions(-) diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 8a342ac272..b771ddce30 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -1,307 +1,307 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension View { - /// Presents an alert from a binding to an optional value. - /// - /// SwiftUI's `alert` view modifiers are driven by two disconnected pieces of state: an - /// `isPresented` binding to a boolean that determines if the alert should be presented, and - /// optional alert `data` that is used to customize its actions and message. - /// - /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: - /// - /// * `isPresented` can be `true`, but `data` can be `nil`. - /// * `isPresented` can be `false`, but `data` can be non-`nil`. - /// - /// On top of that, SwiftUI's `alert` modifiers take static titles, which means the title cannot - /// be dynamically computed from the alert data. - /// - /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the - /// invalid runtime states at compile time by driving the alert's presentation from a single, - /// optional binding. When this binding is non-`nil`, the alert will be presented. Further, the - /// title can be customized from the alert data. - /// - /// ```swift - /// struct AlertDemo: View { - /// @State var randomMovie: Movie? - /// - /// var body: some View { - /// Button("Pick a random movie", action: self.getRandomMovie) - /// .alert( - /// title: { Text($0.title) }, - /// unwrapping: self.$randomMovie, - /// actions: { _ in - /// Button("Pick another", action: self.getRandomMovie) - /// }, - /// message: { Text($0.summary) } - /// ) - /// } - /// - /// func getRandomMovie() { - /// self.randomMovie = Movie.allCases.randomElement() - /// } - /// } - /// ``` - /// - /// - Parameters: - /// - title: A closure returning the alert's title given the current alert state. - /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed - /// to the modifier's closures. You can use this data to populate the fields of an alert - /// that the system displays to the user. When the user presses or taps one of the alert's - /// actions, the system sets this value to `nil` and dismisses the alert. - /// - actions: A view builder returning the alert's actions given the current alert state. - /// - message: A view builder returning the message for the alert given the current alert - /// state. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - title: (Value) -> Text, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M - ) -> some View { - self.alert( - value.wrappedValue.map(title) ?? Text(""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: actions, - message: message - ) - } - - /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case. - /// - /// A version of `alert(unwrapping:)` that works with enum state. - /// - /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths - /// - /// - Parameters: - /// - title: A closure returning the alert's title given the current alert state. - /// - enum: A binding to an optional enum that holds alert state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and then pass it to the modifier's closures. You can use it to populate the fields - /// of an alert that the system displays to the user. When the user presses or taps one of the - /// alert's actions, the system sets this value to `nil` and dismisses the alert. - /// - casePath: A case path that identifies a particular case that holds alert state. - /// - actions: A view builder returning the alert's actions given the current alert state. - /// - message: A view builder returning the message for the alert given the current alert - /// state. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - title: (Case) -> Text, - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder actions: (Case) -> A, - @ViewBuilder message: (Case) -> M - ) -> some View { - self.alert( - title: title, - unwrapping: `enum`.case(casePath), - actions: actions, - message: message - ) - } - - #if swift(>=5.7) - /// Presents an alert from a binding to optional ``AlertState``. + extension View { + /// Presents an alert from a binding to an optional value. /// - /// See for more information on how to use this API. + /// SwiftUI's `alert` view modifiers are driven by two disconnected pieces of state: an + /// `isPresented` binding to a boolean that determines if the alert should be presented, and + /// optional alert `data` that is used to customize its actions and message. /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to - /// populate the fields of an alert that the system displays to the user. When the user - /// presses or taps one of the alert's actions, the system sets this value to `nil` and - /// dismisses the alert, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents an alert from a binding to optional ``AlertState``. + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: + /// + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `alert` modifiers take static titles, which means the title cannot + /// be dynamically computed from the alert data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the alert's presentation from a single, + /// optional binding. When this binding is non-`nil`, the alert will be presented. Further, the + /// title can be customized from the alert data. /// - /// See for more information on how to use this API. + /// ```swift + /// struct AlertDemo: View { + /// @State var randomMovie: Movie? /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .alert( + /// title: { Text($0.title) }, + /// unwrapping: self.$randomMovie, + /// actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// }, + /// message: { Text($0.summary) } + /// ) + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// } + /// ``` /// /// - Parameters: + /// - title: A closure returning the alert's title given the current alert state. /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to - /// populate the fields of an alert that the system displays to the user. When the user - /// presses or taps one of the alert's actions, the system sets this value to `nil` and - /// dismisses the alert, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of an alert + /// that the system displays to the user. When the user presses or taps one of the alert's + /// actions, the system sets this value to `nil` and dismisses the alert. + /// - actions: A view builder returning the alert's actions given the current alert state. + /// - message: A view builder returning the message for the alert given the current alert + /// state. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + public func alert( + title: (Value) -> Text, + unwrapping value: Binding, + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + value.wrappedValue.map(title) ?? Text(""), isPresented: value.isPresent(), presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } + actions: actions, + message: message ) } /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case of ``AlertState``. - /// - /// A version of `alert(unwrapping:)` that works with enum state. See for - /// more information on how to use this API. - /// - /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds alert state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an alert that the system displays to the user. - /// When the user presses or taps one of the alert's actions, the system sets this value to - /// `nil` and dismisses the alert, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds alert state. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - - /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case of ``AlertState``. + /// specific case. /// - /// A version of `alert(unwrapping:)` that works with enum state. See for - /// more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. + /// A version of `alert(unwrapping:)` that works with enum state. /// /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths /// /// - Parameters: + /// - title: A closure returning the alert's title given the current alert state. /// - enum: A binding to an optional enum that holds alert state at a particular case. When /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an alert that the system displays to the user. - /// When the user presses or taps one of the alert's actions, the system sets this value to - /// `nil` and dismisses the alert, and the action is fed to the `action` closure. + /// state and then pass it to the modifier's closures. You can use it to populate the fields + /// of an alert that the system displays to the user. When the user presses or taps one of the + /// alert's actions, the system sets this value to `nil` and dismisses the alert. /// - casePath: A case path that identifies a particular case that holds alert state. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. + /// - actions: A view builder returning the alert's actions given the current alert state. + /// - message: A view builder returning the message for the alert given the current alert + /// state. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - #else - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void + public func alert( + title: (Case) -> Text, + unwrapping enum: Binding, + case casePath: CasePath, + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } + title: title, + unwrapping: `enum`.case(casePath), + actions: actions, + message: message ) } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } + #if swift(>=5.7) + /// Presents an alert from a binding to optional ``AlertState``. + /// + /// See for more information on how to use this API. + /// + /// - Parameters: + /// - value: A binding to an optional value that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to + /// populate the fields of an alert that the system displays to the user. When the user + /// presses or taps one of the alert's actions, the system sets this value to `nil` and + /// dismisses the alert, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?> - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0) { _ in } - } - }, - message: { $0.message.map { Text($0) } } - ) - } + /// Presents an alert from a binding to optional ``AlertState``. + /// + /// See for more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - value: A binding to an optional value that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to + /// populate the fields of an alert that the system displays to the user. When the user + /// presses or taps one of the alert's actions, the system sets this value to `nil` and + /// dismisses the alert, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } + /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a + /// specific case of ``AlertState``. + /// + /// A version of `alert(unwrapping:)` that works with enum state. See for + /// more information on how to use this API. + /// + /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds alert state at a particular case. When + /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this + /// state and use it to populate the fields of an alert that the system displays to the user. + /// When the user presses or taps one of the alert's actions, the system sets this value to + /// `nil` and dismisses the alert, and the action is fed to the `action` closure. + /// - casePath: A case path that identifies a particular case that holds alert state. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: handler) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } + /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a + /// specific case of ``AlertState``. + /// + /// A version of `alert(unwrapping:)` that works with enum state. See for + /// more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds alert state at a particular case. When + /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this + /// state and use it to populate the fields of an alert that the system displays to the user. + /// When the user presses or taps one of the alert's actions, the system sets this value to + /// `nil` and dismisses the alert, and the action is fed to the `action` closure. + /// - casePath: A case path that identifies a particular case that holds alert state. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: handler) + } + #else + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath> - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath)) { (_: Never?) in } - } - #endif + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?> + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0) { _ in } + } + }, + message: { $0.message.map { Text($0) } } + ) + } - // TODO: support iOS <15? -} -#endif // canImport(SwiftUI) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) -> Void + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: handler) + } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) async -> Void + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: handler) + } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath> + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath)) { (_: Never?) in } + } + #endif + + // TODO: support iOS <15? + } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index fd0455ee13..a0074920f6 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -1,191 +1,191 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension Binding { - /// Creates a binding by projecting the base value to an unwrapped value. - /// - /// Useful for producing non-optional bindings from optional ones. - /// - /// See ``IfLet`` for a view builder-friendly version of this initializer. - /// - /// > Note: SwiftUI comes with an equivalent failable initializer, `Binding.init(_:)`, but using - /// > it can lead to crashes at runtime. [Feedback][FB8367784] has been filed, but in the meantime - /// > this initializer exists as a workaround. - /// - /// [FB8367784]: https://gist.github.com/stephencelis/3a232a1b718bab0ae1127ebd5fcf6f97 - /// - /// - Parameter base: A value to project to an unwrapped value. - /// - Returns: A new binding or `nil` when `base` is `nil`. - public init?(unwrapping base: Binding) { - self.init(unwrapping: base, case: /Optional.some) - } + extension Binding { + /// Creates a binding by projecting the base value to an unwrapped value. + /// + /// Useful for producing non-optional bindings from optional ones. + /// + /// See ``IfLet`` for a view builder-friendly version of this initializer. + /// + /// > Note: SwiftUI comes with an equivalent failable initializer, `Binding.init(_:)`, but using + /// > it can lead to crashes at runtime. [Feedback][FB8367784] has been filed, but in the meantime + /// > this initializer exists as a workaround. + /// + /// [FB8367784]: https://gist.github.com/stephencelis/3a232a1b718bab0ae1127ebd5fcf6f97 + /// + /// - Parameter base: A value to project to an unwrapped value. + /// - Returns: A new binding or `nil` when `base` is `nil`. + public init?(unwrapping base: Binding) { + self.init(unwrapping: base, case: /Optional.some) + } - /// Creates a binding by projecting the base enum value to an unwrapped case. - /// - /// Useful for extracting bindings of non-optional state from the case of an enum. - /// - /// See ``IfCaseLet`` for a view builder-friendly version of this initializer. - /// - /// - Parameters: - /// - enum: An enum to project to a particular case. - /// - casePath: A case path that identifies a particular case to unwrap. - /// - Returns: A new binding or `nil` when `base` is `nil`. - public init?(unwrapping enum: Binding, case casePath: CasePath) { - guard var `case` = casePath.extract(from: `enum`.wrappedValue) - else { return nil } + /// Creates a binding by projecting the base enum value to an unwrapped case. + /// + /// Useful for extracting bindings of non-optional state from the case of an enum. + /// + /// See ``IfCaseLet`` for a view builder-friendly version of this initializer. + /// + /// - Parameters: + /// - enum: An enum to project to a particular case. + /// - casePath: A case path that identifies a particular case to unwrap. + /// - Returns: A new binding or `nil` when `base` is `nil`. + public init?(unwrapping enum: Binding, case casePath: CasePath) { + guard var `case` = casePath.extract(from: `enum`.wrappedValue) + else { return nil } - self.init( - get: { - `case` = casePath.extract(from: `enum`.wrappedValue) ?? `case` - return `case` - }, - set: { - guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } - `case` = $0 - `enum`.transaction($1).wrappedValue = casePath.embed($0) - } - ) - } + self.init( + get: { + `case` = casePath.extract(from: `enum`.wrappedValue) ?? `case` + return `case` + }, + set: { + guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } + `case` = $0 + `enum`.transaction($1).wrappedValue = casePath.embed($0) + } + ) + } - /// Creates a binding by projecting the current optional enum value to the value at a particular - /// case. - /// - /// > Note: This method is constrained to optionals so that the projected value can write `nil` - /// > back to the parent, which is useful for navigation, particularly dismissal. - /// - /// - Parameter casePath: A case path that identifies a particular case to unwrap. - /// - Returns: A binding to an enum case. - public func `case`(_ casePath: CasePath) -> Binding - where Value == Enum? { - .init( - get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, - set: { newValue, transaction in - self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) - } - ) - } + /// Creates a binding by projecting the current optional enum value to the value at a particular + /// case. + /// + /// > Note: This method is constrained to optionals so that the projected value can write `nil` + /// > back to the parent, which is useful for navigation, particularly dismissal. + /// + /// - Parameter casePath: A case path that identifies a particular case to unwrap. + /// - Returns: A binding to an enum case. + public func `case`(_ casePath: CasePath) -> Binding + where Value == Enum? { + .init( + get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, + set: { newValue, transaction in + self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) + } + ) + } - /// Creates a binding by projecting the current optional value to a boolean describing if it's - /// non-`nil`. - /// - /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing. - /// - /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. - public func isPresent() -> Binding - where Value == Wrapped? { - .init( - get: { self.wrappedValue != nil }, - set: { isPresent, transaction in - if !isPresent { - self.transaction(transaction).wrappedValue = nil + /// Creates a binding by projecting the current optional value to a boolean describing if it's + /// non-`nil`. + /// + /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing. + /// + /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. + public func isPresent() -> Binding + where Value == Wrapped? { + .init( + get: { self.wrappedValue != nil }, + set: { isPresent, transaction in + if !isPresent { + self.transaction(transaction).wrappedValue = nil + } } - } - ) - } + ) + } - /// Creates a binding by projecting the current optional enum value to a boolean describing - /// whether or not it matches the given case path. - /// - /// Writing `false` to the binding will `nil` out the base enum value. Writing `true` does - /// nothing. - /// - /// Useful for interacting with APIs that take a binding of a boolean that you want to drive with - /// with an enum case that has no associated data. - /// - /// For example, a view may model all of its presentations in a single destination enum to prevent - /// the invalid states that can be introduced by holding onto many booleans and optionals, - /// instead. Even the simple case of two booleans driving two alerts introduces a potential - /// runtime state where both alerts are presented at the same time. By modeling these alerts - /// using a two-case enum instead of two booleans, we can eliminate this invalid state at compile - /// time. Then we can transform a binding to the destination enum into a boolean binding using - /// `isPresent`, so that it can be passed to various presentation APIs. - /// - /// ```swift - /// enum Destination { - /// case deleteAlert - /// ... - /// } - /// - /// struct ProductView: View { - /// @State var destination: Destination? - /// @State var product: Product - /// - /// var body: some View { - /// Button("Delete") { - /// self.model.destination = .deleteAlert - /// } - /// // SwiftUI's vanilla alert modifier - /// .alert( - /// self.product.name - /// isPresented: self.$model.destination.isPresent(/Destination.deleteAlert), - /// actions: { - /// Button("Delete", role: .destructive) { - /// self.model.deleteConfirmationButtonTapped() - /// } - /// }, - /// message: { - /// Text("Are you sure you want to delete this product?") - /// } - /// ) - /// } - /// } - /// ``` - /// - /// - Parameter casePath: A case path that identifies a particular case to match. - /// - Returns: A binding to a boolean. - public func isPresent(_ casePath: CasePath) -> Binding - where Value == Enum? { - self.case(casePath).isPresent() - } + /// Creates a binding by projecting the current optional enum value to a boolean describing + /// whether or not it matches the given case path. + /// + /// Writing `false` to the binding will `nil` out the base enum value. Writing `true` does + /// nothing. + /// + /// Useful for interacting with APIs that take a binding of a boolean that you want to drive with + /// with an enum case that has no associated data. + /// + /// For example, a view may model all of its presentations in a single destination enum to prevent + /// the invalid states that can be introduced by holding onto many booleans and optionals, + /// instead. Even the simple case of two booleans driving two alerts introduces a potential + /// runtime state where both alerts are presented at the same time. By modeling these alerts + /// using a two-case enum instead of two booleans, we can eliminate this invalid state at compile + /// time. Then we can transform a binding to the destination enum into a boolean binding using + /// `isPresent`, so that it can be passed to various presentation APIs. + /// + /// ```swift + /// enum Destination { + /// case deleteAlert + /// ... + /// } + /// + /// struct ProductView: View { + /// @State var destination: Destination? + /// @State var product: Product + /// + /// var body: some View { + /// Button("Delete") { + /// self.model.destination = .deleteAlert + /// } + /// // SwiftUI's vanilla alert modifier + /// .alert( + /// self.product.name + /// isPresented: self.$model.destination.isPresent(/Destination.deleteAlert), + /// actions: { + /// Button("Delete", role: .destructive) { + /// self.model.deleteConfirmationButtonTapped() + /// } + /// }, + /// message: { + /// Text("Are you sure you want to delete this product?") + /// } + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter casePath: A case path that identifies a particular case to match. + /// - Returns: A binding to a boolean. + public func isPresent(_ casePath: CasePath) -> Binding + where Value == Enum? { + self.case(casePath).isPresent() + } - /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. - /// - /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` - /// may write `nil` twice][FB9404926] when dismissing its destination via the navigation bar's - /// back button. Logic attached to this dismissal will execute twice, which may not be desirable. - /// - /// [FB9404926]: https://gist.github.com/mbrandonw/70df235e42d505b3b1b9b7d0d006b049 - /// - /// - Parameter isDuplicate: A closure to evaluate whether two elements are equivalent, for - /// purposes of filtering writes. Return `true` from this closure to indicate that the second - /// element is a duplicate of the first. - public func removeDuplicates(by isDuplicate: @escaping (Value, Value) -> Bool) -> Self { - .init( - get: { self.wrappedValue }, - set: { newValue, transaction in - guard !isDuplicate(self.wrappedValue, newValue) else { return } - self.transaction(transaction).wrappedValue = newValue - } - ) + /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. + /// + /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` + /// may write `nil` twice][FB9404926] when dismissing its destination via the navigation bar's + /// back button. Logic attached to this dismissal will execute twice, which may not be desirable. + /// + /// [FB9404926]: https://gist.github.com/mbrandonw/70df235e42d505b3b1b9b7d0d006b049 + /// + /// - Parameter isDuplicate: A closure to evaluate whether two elements are equivalent, for + /// purposes of filtering writes. Return `true` from this closure to indicate that the second + /// element is a duplicate of the first. + public func removeDuplicates(by isDuplicate: @escaping (Value, Value) -> Bool) -> Self { + .init( + get: { self.wrappedValue }, + set: { newValue, transaction in + guard !isDuplicate(self.wrappedValue, newValue) else { return } + self.transaction(transaction).wrappedValue = newValue + } + ) + } } -} -extension Binding where Value: Equatable { - /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. - /// - /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` - /// may write `nil` twice][FB9404926] when dismissing its destination via the navigation bar's - /// back button. Logic attached to this dismissal will execute twice, which may not be desirable. - /// - /// [FB9404926]: https://gist.github.com/mbrandonw/70df235e42d505b3b1b9b7d0d006b049 - public func removeDuplicates() -> Self { - self.removeDuplicates(by: ==) + extension Binding where Value: Equatable { + /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. + /// + /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` + /// may write `nil` twice][FB9404926] when dismissing its destination via the navigation bar's + /// back button. Logic attached to this dismissal will execute twice, which may not be desirable. + /// + /// [FB9404926]: https://gist.github.com/mbrandonw/70df235e42d505b3b1b9b7d0d006b049 + public func removeDuplicates() -> Self { + self.removeDuplicates(by: ==) + } } -} -extension Binding { - public func _printChanges(_ prefix: String = "") -> Self { - Self( - get: { self.wrappedValue }, - set: { newValue, transaction in - var oldDescription = "" - debugPrint(self.wrappedValue, terminator: "", to: &oldDescription) - var newDescription = "" - debugPrint(newValue, terminator: "", to: &newDescription) - print("\(prefix.isEmpty ? "\(Self.self)" : prefix):", oldDescription, "=", newDescription) - self.transaction(transaction).wrappedValue = newValue - } - ) + extension Binding { + public func _printChanges(_ prefix: String = "") -> Self { + Self( + get: { self.wrappedValue }, + set: { newValue, transaction in + var oldDescription = "" + debugPrint(self.wrappedValue, terminator: "", to: &oldDescription) + var newDescription = "" + debugPrint(newValue, terminator: "", to: &newDescription) + print("\(prefix.isEmpty ? "\(Self.self)" : prefix):", oldDescription, "=", newDescription) + self.transaction(transaction).wrappedValue = newValue + } + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 42c7c623dd..cd5a776c35 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -1,317 +1,317 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension View { - /// Presents a confirmation dialog from a binding to an optional value. - /// - /// SwiftUI's `confirmationDialog` view modifiers are driven by two disconnected pieces of - /// state: an `isPresented` binding to a boolean that determines if the dialog should be - /// presented, and optional dialog `data` that is used to customize its actions and message. - /// - /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: - /// - /// * `isPresented` can be `true`, but `data` can be `nil`. - /// * `isPresented` can be `false`, but `data` can be non-`nil`. - /// - /// On top of that, SwiftUI's `confirmationDialog` modifiers take static titles, which means the - /// title cannot be dynamically computed from the dialog data. - /// - /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the - /// invalid runtime states at compile time by driving the dialog's presentation from a single, - /// optional binding. When this binding is non-`nil`, the dialog will be presented. Further, the - /// title can be customized from the dialog data. - /// - /// ```swift - /// struct DialogDemo: View { - /// @State var randomMovie: Movie? - /// - /// var body: some View { - /// Button("Pick a random movie", action: self.getRandomMovie) - /// .confirmationDialog( - /// title: { Text($0.title) }, - /// titleVisibility: .always, - /// unwrapping: self.$randomMovie, - /// actions: { _ in - /// Button("Pick another", action: self.getRandomMovie) - /// }, - /// message: { Text($0.summary) } - /// ) - /// } - /// - /// func getRandomMovie() { - /// self.randomMovie = Movie.allCases.randomElement() - /// } - /// } - /// ``` - /// - /// See for more information on how to use this API. - /// - /// - Parameters: - /// - title: A closure returning the dialog's title given the current dialog state. - /// - titleVisibility: The visibility of the dialog's title. - /// - value: A binding to an optional value that determines whether a dialog should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed - /// to the modifier's closures. You can use this data to populate the fields of a dialog - /// that the system displays to the user. When the user presses or taps one of the dialog's - /// actions, the system sets this value to `nil` and dismisses the dialog. - /// - actions: A view builder returning the dialog's actions given the current dialog state. - /// - message: A view builder returning the message for the dialog given the current dialog - /// state. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - title: (Value) -> Text, - titleVisibility: Visibility = .automatic, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M - ) -> some View { - self.confirmationDialog( - value.wrappedValue.map(title) ?? Text(""), - isPresented: value.isPresent(), - titleVisibility: titleVisibility, - presenting: value.wrappedValue, - actions: actions, - message: message - ) - } - - /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case. - /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See - /// for more information on how to use this API. - /// - /// - Parameters: - /// - title: A closure returning the dialog's title given the current dialog case. - /// - titleVisibility: The visibility of the dialog's title. - /// - enum: A binding to an optional enum that holds dialog state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and then pass it to the modifier's closures. You can use it to populate the fields - /// of a dialog that the system displays to the user. When the user presses or taps one of - /// the dialog's actions, the system sets this value to `nil` and dismisses the dialog. - /// - casePath: A case path that identifies a particular dialog case to handle. - /// - actions: A view builder returning the dialog's actions given the current dialog case. - /// - message: A view builder returning the message for the dialog given the current dialog - /// case. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - title: (Case) -> Text, - titleVisibility: Visibility = .automatic, - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder actions: (Case) -> A, - @ViewBuilder message: (Case) -> M - ) -> some View { - self.confirmationDialog( - title: title, - titleVisibility: titleVisibility, - unwrapping: `enum`.case(casePath), - actions: actions, - message: message - ) - } - - #if swift(>=5.7) - /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. + extension View { + /// Presents a confirmation dialog from a binding to an optional value. /// - /// See for more information on how to use this API. + /// SwiftUI's `confirmationDialog` view modifiers are driven by two disconnected pieces of + /// state: an `isPresented` binding to a boolean that determines if the dialog should be + /// presented, and optional dialog `data` that is used to customize its actions and message. /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether a confirmation dialog should - /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used - /// to populate the fields of a dialog that the system displays to the user. When the user - /// presses or taps one of the dialog's actions, the system sets this value to `nil` and - /// dismisses the dialog, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: /// - /// See for more information on how to use this API. + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `confirmationDialog` modifiers take static titles, which means the + /// title cannot be dynamically computed from the dialog data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the dialog's presentation from a single, + /// optional binding. When this binding is non-`nil`, the dialog will be presented. Further, the + /// title can be customized from the dialog data. + /// + /// ```swift + /// struct DialogDemo: View { + /// @State var randomMovie: Movie? /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .confirmationDialog( + /// title: { Text($0.title) }, + /// titleVisibility: .always, + /// unwrapping: self.$randomMovie, + /// actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// }, + /// message: { Text($0.summary) } + /// ) + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// } + /// ``` + /// + /// See for more information on how to use this API. /// /// - Parameters: - /// - value: A binding to an optional value that determines whether a confirmation dialog should - /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used - /// to populate the fields of a dialog that the system displays to the user. When the user - /// presses or taps one of the dialog's actions, the system sets this value to `nil` and - /// dismisses the dialog, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. + /// - title: A closure returning the dialog's title given the current dialog state. + /// - titleVisibility: The visibility of the dialog's title. + /// - value: A binding to an optional value that determines whether a dialog should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of a dialog + /// that the system displays to the user. When the user presses or taps one of the dialog's + /// actions, the system sets this value to `nil` and dismisses the dialog. + /// - actions: A view builder returning the dialog's actions given the current dialog state. + /// - message: A view builder returning the message for the dialog given the current dialog + /// state. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + public func confirmationDialog( + title: (Value) -> Text, + titleVisibility: Visibility = .automatic, + unwrapping value: Binding, + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M ) -> some View { self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + value.wrappedValue.map(title) ?? Text(""), isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + titleVisibility: titleVisibility, presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } + actions: actions, + message: message ) } /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case of ``ConfirmationDialogState``. + /// specific case. /// /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See /// for more information on how to use this API. /// /// - Parameters: + /// - title: A closure returning the dialog's title given the current dialog case. + /// - titleVisibility: The visibility of the dialog's title. /// - enum: A binding to an optional enum that holds dialog state at a particular case. When /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an dialog that the system displays to the user. - /// When the user presses or taps one of the dialog's actions, the system sets this value to - /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds dialog state. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. + /// state and then pass it to the modifier's closures. You can use it to populate the fields + /// of a dialog that the system displays to the user. When the user presses or taps one of + /// the dialog's actions, the system sets this value to `nil` and dismisses the dialog. + /// - casePath: A case path that identifies a particular dialog case to handle. + /// - actions: A view builder returning the dialog's actions given the current dialog case. + /// - message: A view builder returning the message for the dialog given the current dialog + /// case. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } + public func confirmationDialog( + title: (Case) -> Text, + titleVisibility: Visibility = .automatic, + unwrapping enum: Binding, + case casePath: CasePath, + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M ) -> some View { self.confirmationDialog( + title: title, + titleVisibility: titleVisibility, unwrapping: `enum`.case(casePath), - action: handler + actions: actions, + message: message ) } - /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case of ``ConfirmationDialogState``. - /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See - /// for more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds dialog state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an dialog that the system displays to the user. - /// When the user presses or taps one of the dialog's actions, the system sets this value to - /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds dialog state. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } - #else - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } + #if swift(>=5.7) + /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. + /// + /// See for more information on how to use this API. + /// + /// - Parameters: + /// - value: A binding to an optional value that determines whether a confirmation dialog should + /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used + /// to populate the fields of a dialog that the system displays to the user. When the user + /// presses or taps one of the dialog's actions, the system sets this value to `nil` and + /// dismisses the dialog, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } + /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. + /// + /// See for more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - value: A binding to an optional value that determines whether a confirmation dialog should + /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used + /// to populate the fields of a dialog that the system displays to the user. When the user + /// presses or taps one of the dialog's actions, the system sets this value to `nil` and + /// dismisses the dialog, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?> - ) -> some View { - self.confirmationDialog(unwrapping: value) { _ in } - } + /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a + /// specific case of ``ConfirmationDialogState``. + /// + /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See + /// for more information on how to use this API. + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds dialog state at a particular case. When + /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this + /// state and use it to populate the fields of an dialog that the system displays to the user. + /// When the user presses or taps one of the dialog's actions, the system sets this value to + /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. + /// - casePath: A case path that identifies a particular case that holds dialog state. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.confirmationDialog( + unwrapping: `enum`.case(casePath), + action: handler + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } + /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a + /// specific case of ``ConfirmationDialogState``. + /// + /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See + /// for more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds dialog state at a particular case. When + /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this + /// state and use it to populate the fields of an dialog that the system displays to the user. + /// When the user presses or taps one of the dialog's actions, the system sets this value to + /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. + /// - casePath: A case path that identifies a particular case that holds dialog state. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.confirmationDialog( + unwrapping: `enum`.case(casePath), + action: handler + ) + } + #else + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath> - ) -> some View { - self.confirmationDialog(unwrapping: `enum`.case(casePath)) { _ in } - } - #endif + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?> + ) -> some View { + self.confirmationDialog(unwrapping: value) { _ in } + } - // TODO: support iOS <15? -} -#endif // canImport(SwiftUI) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) -> Void + ) -> some View { + self.confirmationDialog( + unwrapping: `enum`.case(casePath), + action: handler + ) + } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value?) async -> Void + ) -> some View { + self.confirmationDialog( + unwrapping: `enum`.case(casePath), + action: handler + ) + } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath> + ) -> some View { + self.confirmationDialog(unwrapping: `enum`.case(casePath)) { _ in } + } + #endif + + // TODO: support iOS <15? + } +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index 49d91b54e5..88049a377d 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -1,91 +1,92 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension View { - /// Presents a full-screen cover using a binding as a data source for the sheet's content. - /// - /// SwiftUI comes with a `fullScreenCover(item:)` view modifier that is powered by a binding to - /// some hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the - /// content closure. This value, however, is completely static, which prevents the sheet from - /// modifying it. - /// - /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This - /// gives the sheet the ability to write changes back to its source of truth. - /// - /// Also unlike `fullScreenCover(item:)`, the binding's value does _not_ need to be hashable. - /// - /// ```swift - /// struct TimelineView: View { - /// @State var draft: Post? - /// - /// var body: Body { - /// Button("Compose") { - /// self.draft = Post() - /// } - /// .fullScreenCover(unwrapping: self.$draft) { $draft in - /// ComposeView(post: $draft, onSubmit: { ... }) - /// } - /// } - /// } - /// - /// struct ComposeView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// - Parameters: - /// - value: A binding to a source of truth for the sheet. When `value` is non-`nil`, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to - /// `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is - /// dismissed. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @available(iOS 14, tvOS 14, watchOS 7, *) - @available(macOS, unavailable) - public func fullScreenCover( - unwrapping value: Binding, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.fullScreenCover( - isPresented: value.isPresent(), - onDismiss: onDismiss - ) { - Binding(unwrapping: value).map(content) + extension View { + /// Presents a full-screen cover using a binding as a data source for the sheet's content. + /// + /// SwiftUI comes with a `fullScreenCover(item:)` view modifier that is powered by a binding to + /// some hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the + /// content closure. This value, however, is completely static, which prevents the sheet from + /// modifying it. + /// + /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This + /// gives the sheet the ability to write changes back to its source of truth. + /// + /// Also unlike `fullScreenCover(item:)`, the binding's value does _not_ need to be hashable. + /// + /// ```swift + /// struct TimelineView: View { + /// @State var draft: Post? + /// + /// var body: Body { + /// Button("Compose") { + /// self.draft = Post() + /// } + /// .fullScreenCover(unwrapping: self.$draft) { $draft in + /// ComposeView(post: $draft, onSubmit: { ... }) + /// } + /// } + /// } + /// + /// struct ComposeView: View { + /// @Binding var post: Post + /// var body: some View { ... } + /// } + /// ``` + /// + /// - Parameters: + /// - value: A binding to a source of truth for the sheet. When `value` is non-`nil`, a + /// non-optional binding to the value is passed to the `content` closure. You use this binding + /// to produce content that the system presents to the user in a sheet. Changes made to the + /// sheet's binding will be reflected back in the source of truth. Likewise, changes to + /// `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is + /// dismissed. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @available(iOS 14, tvOS 14, watchOS 7, *) + @available(macOS, unavailable) + public func fullScreenCover( + unwrapping value: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.fullScreenCover( + isPresented: value.isPresent(), + onDismiss: onDismiss + ) { + Binding(unwrapping: value).map(content) + } } - } - /// Presents a full-screen cover using a binding and case path as a data source for the sheet's - /// content. - /// - /// A version of `fullScreenCover(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` - /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or - /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the sheet. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @available(iOS 14, tvOS 14, watchOS 7, *) - @available(macOS, unavailable) - public func fullScreenCover( - unwrapping enum: Binding, - case casePath: CasePath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.fullScreenCover(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + /// Presents a full-screen cover using a binding and case path as a data source for the sheet's + /// content. + /// + /// A version of `fullScreenCover(unwrapping:)` that works with enum state. + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a + /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a + /// non-optional binding to the value is passed to the `content` closure. You use this binding + /// to produce content that the system presents to the user in a sheet. Changes made to the + /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` + /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or + /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. + /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for + /// the sheet. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @available(iOS 14, tvOS 14, watchOS 7, *) + @available(macOS, unavailable) + public func fullScreenCover( + unwrapping enum: Binding, + case casePath: CasePath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.fullScreenCover( + unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + } } -} -#endif // canImport(SwiftUI) \ No newline at end of file +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index 575e44788f..e2af61ba3b 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -1,94 +1,94 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -/// A view that computes content by extracting a case from a binding to an enum and passing a -/// non-optional binding to the case's associated value to its content closure. -/// -/// Useful when working with enum state and building views that require the associated value at a -/// particular case. -/// -/// For example, a warehousing application may model the status of an inventory item using an enum. -/// ``IfCaseLet`` can be used to produce bindings to the associated values of each case. -/// -/// ```swift -/// enum ItemStatus { -/// case inStock(quantity: Int) -/// case outOfStock(isOnBackOrder: Bool) -/// } -/// -/// struct InventoryItemView: View { -/// @State var status: ItemStatus -/// -/// var body: some View { -/// IfCaseLet(self.$status, pattern: /ItemStatus.inStock) { $quantity in -/// HStack { -/// Text("Quantity: \(quantity)") -/// Stepper("Quantity", value: $quantity) -/// } -/// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } -/// } -/// IfCaseLet(self.$status, pattern: /ItemStatus.outOfStock) { $isOnBackOrder in -/// Toggle("Is on back order?", isOn: $isOnBackOrder) -/// Button("In stock") { self.status = .inStock(quantity: 1) } -/// } -/// } -/// } -/// ``` -/// -/// To exhaustively handle every case of a binding to an enum, see ``Switch``. Or, to unwrap a -/// binding to an optional, see ``IfLet``. -public struct IfCaseLet: View -where IfContent: View, ElseContent: View { - public let `enum`: Binding - public let casePath: CasePath - public let ifContent: (Binding) -> IfContent - public let elseContent: ElseContent - - /// Computes content by extracting a case from a binding to an enum and passing a non-optional - /// binding to the case's associated value to its content closure. + /// A view that computes content by extracting a case from a binding to an enum and passing a + /// non-optional binding to the case's associated value to its content closure. /// - /// - Parameters: - /// - enum: A binding to an enum that holds the source of truth for the content at a particular - /// case. When `casePath` successfully extracts a value from `enum`, a non-optional binding to - /// the value is passed to the `content` closure. The closure can use this binding to produce - /// its content and write changes back to the source of truth. Upstream changes to the case's - /// value will also be instantly reflected in the presented content. If `enum` becomes a - /// different case, nothing is computed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the content. - /// - ifContent: A closure for computing content when `enum` matches a particular case. - /// - elseContent: A closure for computing content when `enum` does not match the case. - public init( - _ `enum`: Binding, - pattern casePath: CasePath, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder else elseContent: () -> ElseContent - ) { - self.casePath = casePath - self.elseContent = elseContent() - self.enum = `enum` - self.ifContent = ifContent - } + /// Useful when working with enum state and building views that require the associated value at a + /// particular case. + /// + /// For example, a warehousing application may model the status of an inventory item using an enum. + /// ``IfCaseLet`` can be used to produce bindings to the associated values of each case. + /// + /// ```swift + /// enum ItemStatus { + /// case inStock(quantity: Int) + /// case outOfStock(isOnBackOrder: Bool) + /// } + /// + /// struct InventoryItemView: View { + /// @State var status: ItemStatus + /// + /// var body: some View { + /// IfCaseLet(self.$status, pattern: /ItemStatus.inStock) { $quantity in + /// HStack { + /// Text("Quantity: \(quantity)") + /// Stepper("Quantity", value: $quantity) + /// } + /// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } + /// } + /// IfCaseLet(self.$status, pattern: /ItemStatus.outOfStock) { $isOnBackOrder in + /// Toggle("Is on back order?", isOn: $isOnBackOrder) + /// Button("In stock") { self.status = .inStock(quantity: 1) } + /// } + /// } + /// } + /// ``` + /// + /// To exhaustively handle every case of a binding to an enum, see ``Switch``. Or, to unwrap a + /// binding to an optional, see ``IfLet``. + public struct IfCaseLet: View + where IfContent: View, ElseContent: View { + public let `enum`: Binding + public let casePath: CasePath + public let ifContent: (Binding) -> IfContent + public let elseContent: ElseContent - public var body: some View { - if let $case = Binding(unwrapping: self.enum, case: self.casePath) { - self.ifContent($case) - } else { - self.elseContent + /// Computes content by extracting a case from a binding to an enum and passing a non-optional + /// binding to the case's associated value to its content closure. + /// + /// - Parameters: + /// - enum: A binding to an enum that holds the source of truth for the content at a particular + /// case. When `casePath` successfully extracts a value from `enum`, a non-optional binding to + /// the value is passed to the `content` closure. The closure can use this binding to produce + /// its content and write changes back to the source of truth. Upstream changes to the case's + /// value will also be instantly reflected in the presented content. If `enum` becomes a + /// different case, nothing is computed. + /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for + /// the content. + /// - ifContent: A closure for computing content when `enum` matches a particular case. + /// - elseContent: A closure for computing content when `enum` does not match the case. + public init( + _ `enum`: Binding, + pattern casePath: CasePath, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent + ) { + self.casePath = casePath + self.elseContent = elseContent() + self.enum = `enum` + self.ifContent = ifContent + } + + public var body: some View { + if let $case = Binding(unwrapping: self.enum, case: self.casePath) { + self.ifContent($case) + } else { + self.elseContent + } } } -} -extension IfCaseLet where ElseContent == EmptyView { - public init( - _ `enum`: Binding, - pattern casePath: CasePath, - @ViewBuilder ifContent: @escaping (Binding) -> IfContent - ) { - self.casePath = casePath - self.elseContent = EmptyView() - self.enum = `enum` - self.ifContent = ifContent + extension IfCaseLet where ElseContent == EmptyView { + public init( + _ `enum`: Binding, + pattern casePath: CasePath, + @ViewBuilder ifContent: @escaping (Binding) -> IfContent + ) { + self.casePath = casePath + self.elseContent = EmptyView() + self.enum = `enum` + self.ifContent = ifContent + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift index ec020ec9d4..e4576f0c10 100644 --- a/Sources/SwiftUINavigation/IfLet.swift +++ b/Sources/SwiftUINavigation/IfLet.swift @@ -1,88 +1,89 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -/// A view that computes content by unwrapping a binding to an optional and passing a non-optional -/// binding to its content closure. -/// -/// Useful when working with optional state and building views that require non-optional state. -/// -/// For example, a warehousing application may model the quantity of an inventory item using an -/// optional integer, where a `nil` value denotes an item that is out-of-stock. In order to produce -/// a binding to a non-optional integer for a stepper, ``IfLet`` can be used to safely unwrap the -/// optional binding. -/// -/// ```swift -/// struct InventoryItemView: View { -/// @State var quantity: Int? -/// -/// var body: some View { -/// IfLet(self.$quantity) { $quantity in -/// HStack { -/// Text("Quantity: \(quantity)") -/// Stepper("Quantity", value: $quantity) -/// } -/// Button("Out of stock") { self.quantity = nil } -/// } else: { -/// Button("In stock") { self.quantity = 1 } -/// } -/// } -/// } -/// ``` -/// -/// To unwrap a particular case of a binding to an enum, see ``IfCaseLet``, or, to exhaustively -/// handle every case, see ``Switch``. -public struct IfLet: View where IfContent: View, ElseContent: View { - public let value: Binding - public let ifContent: (Binding) -> IfContent - public let elseContent: ElseContent - - /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to - /// its content closure. + /// A view that computes content by unwrapping a binding to an optional and passing a non-optional + /// binding to its content closure. /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the content. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The - /// closure can use this binding to produce its content and write changes back to the source - /// of truth. Upstream changes to `value` will also be instantly reflected in the presented - /// content. If `value` becomes `nil`, the `elseContent` closure is used to produce content - /// instead. - /// - ifContent: A closure for computing content when `value` is non-`nil`. - /// - elseContent: A closure for computing content when `value` is `nil`. - public init( - _ value: Binding, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder else elseContent: () -> ElseContent - ) { - self.value = value - self.ifContent = ifContent - self.elseContent = elseContent() - } + /// Useful when working with optional state and building views that require non-optional state. + /// + /// For example, a warehousing application may model the quantity of an inventory item using an + /// optional integer, where a `nil` value denotes an item that is out-of-stock. In order to produce + /// a binding to a non-optional integer for a stepper, ``IfLet`` can be used to safely unwrap the + /// optional binding. + /// + /// ```swift + /// struct InventoryItemView: View { + /// @State var quantity: Int? + /// + /// var body: some View { + /// IfLet(self.$quantity) { $quantity in + /// HStack { + /// Text("Quantity: \(quantity)") + /// Stepper("Quantity", value: $quantity) + /// } + /// Button("Out of stock") { self.quantity = nil } + /// } else: { + /// Button("In stock") { self.quantity = 1 } + /// } + /// } + /// } + /// ``` + /// + /// To unwrap a particular case of a binding to an enum, see ``IfCaseLet``, or, to exhaustively + /// handle every case, see ``Switch``. + public struct IfLet: View + where IfContent: View, ElseContent: View { + public let value: Binding + public let ifContent: (Binding) -> IfContent + public let elseContent: ElseContent + + /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to + /// its content closure. + /// + /// - Parameters: + /// - value: A binding to an optional source of truth for the content. When `value` is + /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The + /// closure can use this binding to produce its content and write changes back to the source + /// of truth. Upstream changes to `value` will also be instantly reflected in the presented + /// content. If `value` becomes `nil`, the `elseContent` closure is used to produce content + /// instead. + /// - ifContent: A closure for computing content when `value` is non-`nil`. + /// - elseContent: A closure for computing content when `value` is `nil`. + public init( + _ value: Binding, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent + ) { + self.value = value + self.ifContent = ifContent + self.elseContent = elseContent() + } - public var body: some View { - if let $value = Binding(unwrapping: self.value) { - self.ifContent($value) - } else { - self.elseContent + public var body: some View { + if let $value = Binding(unwrapping: self.value) { + self.ifContent($value) + } else { + self.elseContent + } } } -} -extension IfLet where ElseContent == EmptyView { - /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to - /// its content closure. - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the content. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The - /// closure can use this binding to produce its content and write changes back to the source - /// of truth. Upstream changes to `value` will also be instantly reflected in the presented - /// content. If `value` becomes `nil`, nothing is computed. - /// - ifContent: A closure for computing content when `value` is non-`nil`. - public init( - _ value: Binding, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent - ) { - self.init(value, then: ifContent, else: { EmptyView() }) + extension IfLet where ElseContent == EmptyView { + /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to + /// its content closure. + /// + /// - Parameters: + /// - value: A binding to an optional source of truth for the content. When `value` is + /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The + /// closure can use this binding to produce its content and write changes back to the source + /// of truth. Upstream changes to `value` will also be instantly reflected in the presented + /// content. If `value` becomes `nil`, nothing is computed. + /// - ifContent: A closure for computing content when `value` is non-`nil`. + public init( + _ value: Binding, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent + ) { + self.init(value, then: ifContent, else: { EmptyView() }) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift index 431753049e..670a5b1336 100644 --- a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift +++ b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift @@ -1,15 +1,15 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension Binding { - func didSet(_ perform: @escaping (Value) -> Void) -> Self { - .init( - get: { self.wrappedValue }, - set: { newValue, transaction in - self.transaction(transaction).wrappedValue = newValue - perform(newValue) - } - ) + extension Binding { + func didSet(_ perform: @escaping (Value) -> Void) -> Self { + .init( + get: { self.wrappedValue }, + set: { newValue, transaction in + self.transaction(transaction).wrappedValue = newValue + perform(newValue) + } + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index ee823ba2c8..33f11fe933 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,222 +1,222 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -// NB: Deprecated after 0.5.0 + // NB: Deprecated after 0.5.0 -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension View { - #if swift(>=5.7) - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension View { + #if swift(>=5.7) + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.alert(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) + ) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.alert(unwrapping: value) { (value: Value?) in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) + ) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.confirmationDialog(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) + ) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.confirmationDialog(unwrapping: value) { (value: Value?) in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) + ) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } } } - } - #else - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + #else + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.alert(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) + ) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void + ) -> some View { + self.alert(unwrapping: value) { (value: Value?) in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) + ) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void + ) -> some View { + self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.confirmationDialog(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) + ) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void + ) -> some View { + self.confirmationDialog(unwrapping: value) { (value: Value?) in + if let value = value { + await handler(value) + } } } - } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ + @_disfavoredOverload + @available( + *, + deprecated, + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ - ) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) + ) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void + ) -> some View { + self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } } } - } - #endif -} + #endif + } -// NB: Deprecated after 0.3.0 + // NB: Deprecated after 0.3.0 -@available(*, deprecated, renamed: "init(_:pattern:then:else:)") -extension IfCaseLet { - public init( - _ `enum`: Binding, - pattern casePath: CasePath, - @ViewBuilder ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder elseContent: () -> ElseContent - ) { - self.init(`enum`, pattern: casePath, then: ifContent, else: elseContent) + @available(*, deprecated, renamed: "init(_:pattern:then:else:)") + extension IfCaseLet { + public init( + _ `enum`: Binding, + pattern casePath: CasePath, + @ViewBuilder ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder elseContent: () -> ElseContent + ) { + self.init(`enum`, pattern: casePath, then: ifContent, else: elseContent) + } } -} -// NB: Deprecated after 0.2.0 + // NB: Deprecated after 0.2.0 -extension NavigationLink { - @available(*, deprecated, renamed: "init(unwrapping:onNavigate:destination:label:)") - public init( - unwrapping value: Binding, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - onNavigate: @escaping (_ isActive: Bool) -> Void, - @ViewBuilder label: () -> Label - ) where Destination == WrappedDestination? { - self.init( - destination: Binding(unwrapping: value).map(destination), - isActive: value.isPresent().didSet(onNavigate), - label: label - ) - } + extension NavigationLink { + @available(*, deprecated, renamed: "init(unwrapping:onNavigate:destination:label:)") + public init( + unwrapping value: Binding, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + onNavigate: @escaping (_ isActive: Bool) -> Void, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + destination: Binding(unwrapping: value).map(destination), + isActive: value.isPresent().didSet(onNavigate), + label: label + ) + } - @available(*, deprecated, renamed: "init(unwrapping:case:onNavigate:destination:label:)") - public init( - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - onNavigate: @escaping (Bool) -> Void, - @ViewBuilder label: () -> Label - ) where Destination == WrappedDestination? { - self.init( - unwrapping: `enum`.case(casePath), - onNavigate: onNavigate, - destination: destination, - label: label - ) + @available(*, deprecated, renamed: "init(unwrapping:case:onNavigate:destination:label:)") + public init( + unwrapping enum: Binding, + case casePath: CasePath, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + onNavigate: @escaping (Bool) -> Void, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + unwrapping: `enum`.case(casePath), + onNavigate: onNavigate, + destination: destination, + label: label + ) + } } -} -#endif // canImport(SwiftUI) \ No newline at end of file +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Exports.swift b/Sources/SwiftUINavigation/Internal/Exports.swift index 8364a5a4de..ea425f3f83 100644 --- a/Sources/SwiftUINavigation/Internal/Exports.swift +++ b/Sources/SwiftUINavigation/Internal/Exports.swift @@ -1,4 +1,4 @@ #if canImport(SwiftUI) -@_exported import CasePaths -@_exported import SwiftUINavigationCore -#endif // canImport(SwiftUI) + @_exported import CasePaths + @_exported import SwiftUINavigationCore +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index 0e3f34bcc2..d131222bf7 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -107,4 +107,4 @@ else { return true } return false }() -#endif // swift(>=5.7) && canImport(SwiftUI) +#endif // swift(>=5.7) && canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index 3bc44dbe2f..3b387f9d51 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -1,137 +1,137 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension NavigationLink { - /// Creates a navigation link that presents the destination view when a bound value is non-`nil`. - /// - /// This allows you to drive navigation to a destination from an optional value. When the - /// optional value becomes non-`nil` a binding to an honest value is derived and passed to the - /// destination. Any edits made to the binding in the destination are automatically reflected - /// in the parent. - /// - /// ```swift - /// struct ContentView: View { - /// @State var postToEdit: Post? - /// @State var posts: [Post] - /// - /// var body: some View { - /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$postToEdit) { isActive in - /// self.postToEdit = isActive ? post : nil - /// } destination: { $draft in - /// EditPostView(post: $draft) - /// } label: { - /// Text(post.title) - /// } - /// } - /// } - /// } - /// - /// struct EditPostView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the destination. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. The - /// destination can use this binding to produce its content and write changes back to the - /// source of truth. Upstream changes to `value` will also be instantly reflected in the - /// destination. If `value` becomes `nil`, the destination is dismissed. - /// - onNavigate: A closure that executes when the link becomes active or inactive with a - /// boolean that describes if the link was activated or not. Use this closure to populate the - /// source of truth when it is passed a value of `true`. When passed `false`, the system will - /// automatically write `nil` to `value`. - /// - destination: A view for the navigation link to present. - /// - label: A view builder to produce a label describing the `destination` to present. - @available(iOS, introduced: 13, deprecated: 16) - @available(macOS, introduced: 10.15, deprecated: 13) - @available(tvOS, introduced: 13, deprecated: 16) - @available(watchOS, introduced: 6, deprecated: 9) - public init( - unwrapping value: Binding, - onNavigate: @escaping (_ isActive: Bool) -> Void, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - @ViewBuilder label: () -> Label - ) where Destination == WrappedDestination? { - self.init( - destination: Binding(unwrapping: value).map(destination), - isActive: value.isPresent().didSet(onNavigate), - label: label - ) - } + extension NavigationLink { + /// Creates a navigation link that presents the destination view when a bound value is non-`nil`. + /// + /// This allows you to drive navigation to a destination from an optional value. When the + /// optional value becomes non-`nil` a binding to an honest value is derived and passed to the + /// destination. Any edits made to the binding in the destination are automatically reflected + /// in the parent. + /// + /// ```swift + /// struct ContentView: View { + /// @State var postToEdit: Post? + /// @State var posts: [Post] + /// + /// var body: some View { + /// ForEach(self.posts) { post in + /// NavigationLink(unwrapping: self.$postToEdit) { isActive in + /// self.postToEdit = isActive ? post : nil + /// } destination: { $draft in + /// EditPostView(post: $draft) + /// } label: { + /// Text(post.title) + /// } + /// } + /// } + /// } + /// + /// struct EditPostView: View { + /// @Binding var post: Post + /// var body: some View { ... } + /// } + /// ``` + /// + /// - Parameters: + /// - value: A binding to an optional source of truth for the destination. When `value` is + /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. The + /// destination can use this binding to produce its content and write changes back to the + /// source of truth. Upstream changes to `value` will also be instantly reflected in the + /// destination. If `value` becomes `nil`, the destination is dismissed. + /// - onNavigate: A closure that executes when the link becomes active or inactive with a + /// boolean that describes if the link was activated or not. Use this closure to populate the + /// source of truth when it is passed a value of `true`. When passed `false`, the system will + /// automatically write `nil` to `value`. + /// - destination: A view for the navigation link to present. + /// - label: A view builder to produce a label describing the `destination` to present. + @available(iOS, introduced: 13, deprecated: 16) + @available(macOS, introduced: 10.15, deprecated: 13) + @available(tvOS, introduced: 13, deprecated: 16) + @available(watchOS, introduced: 6, deprecated: 9) + public init( + unwrapping value: Binding, + onNavigate: @escaping (_ isActive: Bool) -> Void, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + destination: Binding(unwrapping: value).map(destination), + isActive: value.isPresent().didSet(onNavigate), + label: label + ) + } - /// Creates a navigation link that presents the destination view when a bound enum is non-`nil` - /// and matches a particular case. - /// - /// This allows you to drive navigation to a destination from an enum of values. When the - /// optional value becomes non-`nil` _and_ matches a particular case of the enum, a binding to an - /// honest value is derived and passed to the destination. Any edits made to the binding in the - /// destination are automatically reflected in the parent. - /// - /// ```swift - /// struct ContentView: View { - /// @State var destination: Destination? - /// @State var posts: [Post] - /// - /// enum Destination { - /// case edit(Post) - /// /* other destinations */ - /// } - /// - /// var body: some View { - /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$destination, case: /Destination.edit) { isActive in - /// self.destination = isActive ? .edit(post) : nil - /// } destination: { $draft in - /// EditPostView(post: $draft) - /// } label: { - /// Text(post.title) - /// } - /// } - /// } - /// } - /// - /// struct EditPostView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// See `NavigationLink.init(unwrapping:destination:onNavigate:label)` for a version of this - /// initializer that works with optional state instead of enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional source of truth for the destination. When `enum` is - /// non-`nil`, and `casePath` successfully extracts a value, a non-optional binding to the - /// value is passed to the `destination` closure. The destination can use this binding to - /// produce its content and write changes back to the source of truth. Upstream changes to - /// `enum` will also be instantly reflected in the destination. If `enum` becomes `nil`, the - /// destination is dismissed. - /// - onNavigate: A closure that executes when the link becomes active or inactive with a - /// boolean that describes if the link was activated or not. Use this closure to populate the - /// source of truth when it is passed a value of `true`. When passed `false`, the system will - /// automatically write `nil` to `enum`. - /// - destination: A view for the navigation link to present. - /// - label: A view builder to produce a label describing the `destination` to present. - @available(iOS, introduced: 13, deprecated: 16) - @available(macOS, introduced: 10.15, deprecated: 13) - @available(tvOS, introduced: 13, deprecated: 16) - @available(watchOS, introduced: 6, deprecated: 9) - public init( - unwrapping enum: Binding, - case casePath: CasePath, - onNavigate: @escaping (Bool) -> Void, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - @ViewBuilder label: () -> Label - ) where Destination == WrappedDestination? { - self.init( - unwrapping: `enum`.case(casePath), - onNavigate: onNavigate, - destination: destination, - label: label - ) + /// Creates a navigation link that presents the destination view when a bound enum is non-`nil` + /// and matches a particular case. + /// + /// This allows you to drive navigation to a destination from an enum of values. When the + /// optional value becomes non-`nil` _and_ matches a particular case of the enum, a binding to an + /// honest value is derived and passed to the destination. Any edits made to the binding in the + /// destination are automatically reflected in the parent. + /// + /// ```swift + /// struct ContentView: View { + /// @State var destination: Destination? + /// @State var posts: [Post] + /// + /// enum Destination { + /// case edit(Post) + /// /* other destinations */ + /// } + /// + /// var body: some View { + /// ForEach(self.posts) { post in + /// NavigationLink(unwrapping: self.$destination, case: /Destination.edit) { isActive in + /// self.destination = isActive ? .edit(post) : nil + /// } destination: { $draft in + /// EditPostView(post: $draft) + /// } label: { + /// Text(post.title) + /// } + /// } + /// } + /// } + /// + /// struct EditPostView: View { + /// @Binding var post: Post + /// var body: some View { ... } + /// } + /// ``` + /// + /// See `NavigationLink.init(unwrapping:destination:onNavigate:label)` for a version of this + /// initializer that works with optional state instead of enum state. + /// + /// - Parameters: + /// - enum: A binding to an optional source of truth for the destination. When `enum` is + /// non-`nil`, and `casePath` successfully extracts a value, a non-optional binding to the + /// value is passed to the `destination` closure. The destination can use this binding to + /// produce its content and write changes back to the source of truth. Upstream changes to + /// `enum` will also be instantly reflected in the destination. If `enum` becomes `nil`, the + /// destination is dismissed. + /// - onNavigate: A closure that executes when the link becomes active or inactive with a + /// boolean that describes if the link was activated or not. Use this closure to populate the + /// source of truth when it is passed a value of `true`. When passed `false`, the system will + /// automatically write `nil` to `enum`. + /// - destination: A view for the navigation link to present. + /// - label: A view builder to produce a label describing the `destination` to present. + @available(iOS, introduced: 13, deprecated: 16) + @available(macOS, introduced: 10.15, deprecated: 13) + @available(tvOS, introduced: 13, deprecated: 16) + @available(watchOS, introduced: 6, deprecated: 9) + public init( + unwrapping enum: Binding, + case casePath: CasePath, + onNavigate: @escaping (Bool) -> Void, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + unwrapping: `enum`.case(casePath), + onNavigate: onNavigate, + destination: destination, + label: label + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 9bea08483a..7c5fde4e0d 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -1,100 +1,100 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension View { - /// Presents a popover using a binding as a data source for the popover's content. - /// - /// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content - /// closure. This value, however, is completely static, which prevents the popover from modifying - /// it. - /// - /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This - /// gives the popover the ability to write changes back to its source of truth. - /// - /// Also unlike `popover(item:)`, the binding's value does _not_ need to be hashable. - /// - /// ```swift - /// struct TimelineView: View { - /// @State var draft: Post? - /// - /// var body: Body { - /// Button("Compose") { - /// self.draft = Post() - /// } - /// .popover(unwrapping: self.$draft) { $draft in - /// ComposeView(post: $draft, onSubmit: { ... }) - /// } - /// } - /// } - /// - /// struct ComposeView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the popover. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You use - /// this binding to produce content that the system presents to the user in a popover. Changes - /// made to the popover's binding will be reflected back in the source of truth. Likewise, - /// changes to `value` are instantly reflected in the popover. If `value` becomes `nil`, the - /// popover is dismissed. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow. - /// - content: A closure returning the content of the popover. - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public func popover( - unwrapping value: Binding, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View where Content: View { - self.popover( - isPresented: value.isPresent(), - attachmentAnchor: attachmentAnchor, - arrowEdge: arrowEdge - ) { - Binding(unwrapping: value).map(content) + extension View { + /// Presents a popover using a binding as a data source for the popover's content. + /// + /// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some + /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content + /// closure. This value, however, is completely static, which prevents the popover from modifying + /// it. + /// + /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This + /// gives the popover the ability to write changes back to its source of truth. + /// + /// Also unlike `popover(item:)`, the binding's value does _not_ need to be hashable. + /// + /// ```swift + /// struct TimelineView: View { + /// @State var draft: Post? + /// + /// var body: Body { + /// Button("Compose") { + /// self.draft = Post() + /// } + /// .popover(unwrapping: self.$draft) { $draft in + /// ComposeView(post: $draft, onSubmit: { ... }) + /// } + /// } + /// } + /// + /// struct ComposeView: View { + /// @Binding var post: Post + /// var body: some View { ... } + /// } + /// ``` + /// + /// - Parameters: + /// - value: A binding to an optional source of truth for the popover. When `value` is + /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You use + /// this binding to produce content that the system presents to the user in a popover. Changes + /// made to the popover's binding will be reflected back in the source of truth. Likewise, + /// changes to `value` are instantly reflected in the popover. If `value` becomes `nil`, the + /// popover is dismissed. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow. + /// - content: A closure returning the content of the popover. + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + unwrapping value: Binding, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View where Content: View { + self.popover( + isPresented: value.isPresent(), + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge + ) { + Binding(unwrapping: value).map(content) + } } - } - /// Presents a popover using a binding and case path as the data source for the popover's content. - /// - /// A version of `popover(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the popover at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a popover. Changes made to the - /// popover's binding will be reflected back in the source of truth. Likewise, changes to - /// `enum` at the given case are instantly reflected in the popover. If `enum` becomes `nil`, - /// or becomes a case other than the one identified by `casePath`, the popover is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the popover. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow. - /// - content: A closure returning the content of the popover. - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public func popover( - unwrapping enum: Binding, - case casePath: CasePath, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View where Content: View { - self.popover( - unwrapping: `enum`.case(casePath), - attachmentAnchor: attachmentAnchor, - arrowEdge: arrowEdge, - content: content - ) + /// Presents a popover using a binding and case path as the data source for the popover's content. + /// + /// A version of `popover(unwrapping:)` that works with enum state. + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds the source of truth for the popover at a + /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a + /// non-optional binding to the value is passed to the `content` closure. You use this binding + /// to produce content that the system presents to the user in a popover. Changes made to the + /// popover's binding will be reflected back in the source of truth. Likewise, changes to + /// `enum` at the given case are instantly reflected in the popover. If `enum` becomes `nil`, + /// or becomes a case other than the one identified by `casePath`, the popover is dismissed. + /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for + /// the popover. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow. + /// - content: A closure returning the content of the popover. + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + unwrapping enum: Binding, + case casePath: CasePath, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View where Content: View { + self.popover( + unwrapping: `enum`.case(casePath), + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge, + content: content + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index 5f92fdae1c..bf871d9a22 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -1,91 +1,91 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -#if canImport(UIKit) - import UIKit -#elseif canImport(AppKit) - import AppKit -#endif + #if canImport(UIKit) + import UIKit + #elseif canImport(AppKit) + import AppKit + #endif -extension View { - /// Presents a sheet using a binding as a data source for the sheet's content. - /// - /// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content - /// closure. This value, however, is completely static, which prevents the sheet from modifying - /// it. - /// - /// This overload differs in that it passes a _binding_ to the content closure, instead. This - /// gives the sheet the ability to write changes back to its source of truth. - /// - /// Also unlike `sheet(item:)`, the binding's value does _not_ need to be hashable. - /// - /// ```swift - /// struct TimelineView: View { - /// @State var draft: Post? - /// - /// var body: Body { - /// Button("Compose") { - /// self.draft = Post() - /// } - /// .sheet(unwrapping: self.$draft) { $draft in - /// ComposeView(post: $draft, onSubmit: { ... }) - /// } - /// } - /// } - /// - /// struct ComposeView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the sheet. When `value` is non-`nil`, - /// a non-optional binding to the value is passed to the `content` closure. You use this - /// binding to produce content that the system presents to the user in a sheet. Changes made - /// to the sheet's binding will be reflected back in the source of truth. Likewise, changes - /// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is - /// dismissed. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @MainActor - public func sheet( - unwrapping value: Binding, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.sheet(isPresented: value.isPresent(), onDismiss: onDismiss) { - Binding(unwrapping: value).map(content) + extension View { + /// Presents a sheet using a binding as a data source for the sheet's content. + /// + /// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some + /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content + /// closure. This value, however, is completely static, which prevents the sheet from modifying + /// it. + /// + /// This overload differs in that it passes a _binding_ to the content closure, instead. This + /// gives the sheet the ability to write changes back to its source of truth. + /// + /// Also unlike `sheet(item:)`, the binding's value does _not_ need to be hashable. + /// + /// ```swift + /// struct TimelineView: View { + /// @State var draft: Post? + /// + /// var body: Body { + /// Button("Compose") { + /// self.draft = Post() + /// } + /// .sheet(unwrapping: self.$draft) { $draft in + /// ComposeView(post: $draft, onSubmit: { ... }) + /// } + /// } + /// } + /// + /// struct ComposeView: View { + /// @Binding var post: Post + /// var body: some View { ... } + /// } + /// ``` + /// + /// - Parameters: + /// - value: A binding to an optional source of truth for the sheet. When `value` is non-`nil`, + /// a non-optional binding to the value is passed to the `content` closure. You use this + /// binding to produce content that the system presents to the user in a sheet. Changes made + /// to the sheet's binding will be reflected back in the source of truth. Likewise, changes + /// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is + /// dismissed. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @MainActor + public func sheet( + unwrapping value: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.sheet(isPresented: value.isPresent(), onDismiss: onDismiss) { + Binding(unwrapping: value).map(content) + } } - } - /// Presents a sheet using a binding and case path as the data source for the sheet's content. - /// - /// A version of `View.sheet(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` - /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or - /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the sheet. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @MainActor - public func sheet( - unwrapping enum: Binding, - case casePath: CasePath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + /// Presents a sheet using a binding and case path as the data source for the sheet's content. + /// + /// A version of `View.sheet(unwrapping:)` that works with enum state. + /// + /// - Parameters: + /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a + /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a + /// non-optional binding to the value is passed to the `content` closure. You use this binding + /// to produce content that the system presents to the user in a sheet. Changes made to the + /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` + /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or + /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. + /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for + /// the sheet. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @MainActor + public func sheet( + unwrapping enum: Binding, + case casePath: CasePath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index 9a54e29b8a..2bcfbfbc8a 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -1,774 +1,518 @@ #if canImport(SwiftUI) -import SwiftUI -@_spi(RuntimeWarn) import SwiftUINavigationCore + import SwiftUI + @_spi(RuntimeWarn) import SwiftUINavigationCore -/// A view that can switch over a binding of enum state and exhaustively handle each case. -/// -/// Useful for computing a view from enum state where every case should be handled (using a -/// ``CaseLet`` view), or where there should be a default fallback view (using a ``Default`` view). -/// -/// For example, a warehousing application may model the status of an inventory item using an enum -/// with cases that distinguish in-stock and out-of-stock statuses. ``Switch`` (and ``CaseLet``) can -/// be used to produce bindings to the associated values of each case. -/// -/// ```swift -/// enum ItemStatus { -/// case inStock(quantity: Int) -/// case outOfStock(isOnBackOrder: Bool) -/// } -/// -/// struct InventoryItemView: View { -/// @State var status: ItemStatus -/// -/// var body: some View { -/// Switch(self.$status) { -/// CaseLet(/ItemStatus.inStock) { $quantity in -/// HStack { -/// Text("Quantity: \(quantity)") -/// Stepper("Quantity", value: $quantity) -/// } -/// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } -/// } -/// CaseLet(/ItemStatus.outOfStock) { $isOnBackOrder in -/// Toggle("Is on back order?", isOn: $isOnBackOrder) -/// Button("In stock") { self.status = .inStock(quantity: 1) } -/// } -/// } -/// } -/// } -/// ``` -/// -/// To unwrap an individual case of a binding to an enum (_i.e._, if exhaustivity is not needed), -/// use ``IfCaseLet``, instead. Or, to unwrap a binding to an optional, use ``IfLet``. -/// -/// > Note: In debug builds, exhaustivity is handled at runtime: if the `Switch` encounters an -/// > unhandled case, and no ``Default`` view is present, a runtime warning is issued and a warning -/// > view is presented. -public struct Switch: View { - public let `enum`: Binding - public let content: Content - - private init( - enum: Binding, - @ViewBuilder content: () -> Content - ) { - self.enum = `enum` - self.content = content() - } - - public var body: some View { - self.content - .environmentObject(BindingObject(binding: self.enum)) - } -} - -/// A view that handles a specific case of enum state in a ``Switch``. -public struct CaseLet: View -where Content: View { - @EnvironmentObject private var `enum`: BindingObject - public let casePath: CasePath - public let content: (Binding) -> Content - - /// Computes content for a particular case of an enum handled by a ``Switch``. + /// A view that can switch over a binding of enum state and exhaustively handle each case. /// - /// - Parameters: - /// - casePath: A case path that identifies a case of the ``Switch``'s enum that holds a source - /// of truth for the content. - /// - content: A closure returning the content to be computed from a binding to an enum case. - public init( - _ casePath: CasePath, - @ViewBuilder then content: @escaping (Binding) -> Content - ) { - self.casePath = casePath - self.content = content - } - - public var body: some View { - Binding(unwrapping: self.enum.wrappedValue, case: self.casePath).map(self.content) - } -} + /// Useful for computing a view from enum state where every case should be handled (using a + /// ``CaseLet`` view), or where there should be a default fallback view (using a ``Default`` view). + /// + /// For example, a warehousing application may model the status of an inventory item using an enum + /// with cases that distinguish in-stock and out-of-stock statuses. ``Switch`` (and ``CaseLet``) can + /// be used to produce bindings to the associated values of each case. + /// + /// ```swift + /// enum ItemStatus { + /// case inStock(quantity: Int) + /// case outOfStock(isOnBackOrder: Bool) + /// } + /// + /// struct InventoryItemView: View { + /// @State var status: ItemStatus + /// + /// var body: some View { + /// Switch(self.$status) { + /// CaseLet(/ItemStatus.inStock) { $quantity in + /// HStack { + /// Text("Quantity: \(quantity)") + /// Stepper("Quantity", value: $quantity) + /// } + /// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } + /// } + /// CaseLet(/ItemStatus.outOfStock) { $isOnBackOrder in + /// Toggle("Is on back order?", isOn: $isOnBackOrder) + /// Button("In stock") { self.status = .inStock(quantity: 1) } + /// } + /// } + /// } + /// } + /// ``` + /// + /// To unwrap an individual case of a binding to an enum (_i.e._, if exhaustivity is not needed), + /// use ``IfCaseLet``, instead. Or, to unwrap a binding to an optional, use ``IfLet``. + /// + /// > Note: In debug builds, exhaustivity is handled at runtime: if the `Switch` encounters an + /// > unhandled case, and no ``Default`` view is present, a runtime warning is issued and a warning + /// > view is presented. + public struct Switch: View { + public let `enum`: Binding + public let content: Content -/// A view that covers any cases that aren't explicitly addressed in a ``Switch``. -/// -/// If you wish to use ``Switch`` in a non-exhaustive manner (_i.e._, you do not want to provide a -/// ``CaseLet`` for every case of the enum), then you must insert a ``Default`` view at the end of -/// the ``Switch``'s body, or use ``IfCaseLet`` instead. -public struct Default: View { - private let content: Content + private init( + enum: Binding, + @ViewBuilder content: () -> Content + ) { + self.enum = `enum` + self.content = content() + } - /// Initializes a ``Default`` view that computes content depending on if a binding to enum state - /// does not match a particular case. - /// - /// - Parameter content: A function that returns a view that is visible only when the switch - /// view's state does not match a preceding ``CaseLet`` view. - public init(@ViewBuilder content: () -> Content) { - self.content = content() + public var body: some View { + self.content + .environmentObject(BindingObject(binding: self.enum)) + } } - public var body: some View { - self.content - } -} + /// A view that handles a specific case of enum state in a ``Switch``. + public struct CaseLet: View + where Content: View { + @EnvironmentObject private var `enum`: BindingObject + public let casePath: CasePath + public let content: (Binding) -> Content -extension Switch { - public init( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - CaseLet, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - if content.0.casePath ~= `enum`.wrappedValue { - content.0 - } else { - content.1 - } + /// Computes content for a particular case of an enum handled by a ``Switch``. + /// + /// - Parameters: + /// - casePath: A case path that identifies a case of the ``Switch``'s enum that holds a source + /// of truth for the content. + /// - content: A closure returning the content to be computed from a binding to an enum case. + public init( + _ casePath: CasePath, + @ViewBuilder then content: @escaping (Binding) -> Content + ) { + self.casePath = casePath + self.content = content } - } - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> CaseLet - ) - where - Content == _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - { - self.init(`enum`) { - content() - Default { _ExhaustivityCheckView(file: file, line: line) } + public var body: some View { + Binding(unwrapping: self.enum.wrappedValue, case: self.casePath).map(self.content) } } - public init( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - default: - content.2 - } + /// A view that covers any cases that aren't explicitly addressed in a ``Switch``. + /// + /// If you wish to use ``Switch`` in a non-exhaustive manner (_i.e._, you do not want to provide a + /// ``CaseLet`` for every case of the enum), then you must insert a ``Default`` view at the end of + /// the ``Switch``'s body, or use ``IfCaseLet`` instead. + public struct Default: View { + private let content: Content + + /// Initializes a ``Default`` view that computes content depending on if a binding to enum state + /// does not match a particular case. + /// + /// - Parameter content: A function that returns a view that is visible only when the switch + /// view's state does not match a preceding ``CaseLet`` view. + public init(@ViewBuilder content: () -> Content) { + self.content = content() } - } - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - Default { _ExhaustivityCheckView(file: file, line: line) } + public var body: some View { + self.content } } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + extension Switch { + public init( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, Default > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - default: - content.3 + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath ~= `enum`.wrappedValue { + content.0 + } else { + content.1 + } } } - } - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> CaseLet + ) + where + Content == _ConditionalContent< CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, Default<_ExhaustivityCheckView> > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - Default { _ExhaustivityCheckView(file: file, line: line) } + { + self.init(`enum`) { + content() + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - default: - content.4 + Default + > + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + default: + content.2 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default<_ExhaustivityCheckView> + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, - CaseLet + Default > - >, - _ConditionalContent< - CaseLet, - Default > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - default: - content.5 + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + default: + content.3 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, _ConditionalContent< CaseLet, - CaseLet + Default<_ExhaustivityCheckView> > - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - Default { _ExhaustivityCheckView(file: file, line: line) } + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( CaseLet, - CaseLet - >, - _ConditionalContent< + CaseLet, CaseLet, - CaseLet - > - >, - _ConditionalContent< + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< - CaseLet, - CaseLet + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > >, Default > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - default: - content.6 + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + default: + content.4 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( CaseLet, - CaseLet - >, - _ConditionalContent< + CaseLet, CaseLet, CaseLet - > - >, - _ConditionalContent< + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< - CaseLet, - CaseLet + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > >, Default<_ExhaustivityCheckView> > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - Default { _ExhaustivityCheckView(file: file, line: line) } + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( CaseLet, - CaseLet - >, - _ConditionalContent< + CaseLet, CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< + CaseLet, CaseLet, - CaseLet + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > >, _ConditionalContent< - CaseLet, + CaseLet, Default > > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - default: - content.7 + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + case content.4.casePath: + content.4 + default: + content.5 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( CaseLet, - CaseLet - >, - _ConditionalContent< + CaseLet, CaseLet, - CaseLet - > - >, - _ConditionalContent< + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< - CaseLet, - CaseLet + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > >, _ConditionalContent< - CaseLet, + CaseLet, Default<_ExhaustivityCheckView> > > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - Default { _ExhaustivityCheckView(file: file, line: line) } + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, @@ -784,69 +528,55 @@ extension Switch { CaseLet, CaseLet >, - _ConditionalContent< - CaseLet, - CaseLet - > + Default > - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - case content.7.casePath: - content.7 - default: - content.8 + > + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + case content.4.casePath: + content.4 + case content.5.casePath: + content.5 + default: + content.6 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, @@ -862,60 +592,48 @@ extension Switch { CaseLet, CaseLet >, - _ConditionalContent< - CaseLet, - CaseLet - > + Default<_ExhaustivityCheckView> > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - Default { _ExhaustivityCheckView(file: file, line: line) } + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, @@ -933,74 +651,60 @@ extension Switch { >, _ConditionalContent< CaseLet, - CaseLet + Default > > - >, - _ConditionalContent< - CaseLet, - Default > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - case content.7.casePath: - content.7 - case content.8.casePath: - content.8 - default: - content.9 + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + case content.4.casePath: + content.4 + case content.5.casePath: + content.5 + case content.6.casePath: + content.6 + default: + content.7 + } } } - } - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< CaseLet, @@ -1018,101 +722,397 @@ extension Switch { >, _ConditionalContent< CaseLet, - CaseLet + Default<_ExhaustivityCheckView> > > - >, - _ConditionalContent< - CaseLet, + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + Default + > + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + case content.4.casePath: + content.4 + case content.5.casePath: + content.5 + case content.6.casePath: + content.6 + case content.7.casePath: + content.7 + default: + content.8 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, Default<_ExhaustivityCheckView> > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - content.value.8 - Default { _ExhaustivityCheckView(file: file, line: line) } + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + Case9, Content9, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default + > + > + { + self.init(enum: `enum`) { + let content = content().value + switch `enum`.wrappedValue { + case content.0.casePath: + content.0 + case content.1.casePath: + content.1 + case content.2.casePath: + content.2 + case content.3.casePath: + content.3 + case content.4.casePath: + content.4 + case content.5.casePath: + content.5 + case content.6.casePath: + content.6 + case content.7.casePath: + content.7 + case content.8.casePath: + content.8 + default: + content.9 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + Case9, Content9 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + content.value.8 + Default { _ExhaustivityCheckView(file: file, line: line) } + } } } -} -public struct _ExhaustivityCheckView: View { - @EnvironmentObject private var `enum`: BindingObject - let file: StaticString - let line: UInt + public struct _ExhaustivityCheckView: View { + @EnvironmentObject private var `enum`: BindingObject + let file: StaticString + let line: UInt - public var body: some View { - #if DEBUG - let message = """ - Warning: Switch.body@\(self.file):\(self.line) + public var body: some View { + #if DEBUG + let message = """ + Warning: Switch.body@\(self.file):\(self.line) - "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" + "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ - provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. - """ - VStack(spacing: 17) { - self.exclamation() - .font(.largeTitle) + Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ + provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. + """ + VStack(spacing: 17) { + self.exclamation() + .font(.largeTitle) - Text(message) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .foregroundColor(.white) - .padding() - .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { runtimeWarn(message, file: self.file, line: self.line) } - #else - EmptyView() - #endif - } + Text(message) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.white) + .padding() + .background(Color.red.edgesIgnoringSafeArea(.all)) + .onAppear { runtimeWarn(message, file: self.file, line: self.line) } + #else + EmptyView() + #endif + } - func exclamation() -> some View { - #if os(macOS) - return Text("⚠️") - #else - return Image(systemName: "exclamationmark.triangle.fill") - #endif + func exclamation() -> some View { + #if os(macOS) + return Text("⚠️") + #else + return Image(systemName: "exclamationmark.triangle.fill") + #endif + } } -} -private class BindingObject: ObservableObject { - let wrappedValue: Binding + private class BindingObject: ObservableObject { + let wrappedValue: Binding - init(binding: Binding) { - self.wrappedValue = binding + init(binding: Binding) { + self.wrappedValue = binding + } } -} -private func describeCase(_ enum: Enum) -> String { - let mirror = Mirror(reflecting: `enum`) - let `case`: String - if mirror.displayStyle == .enum, let child = mirror.children.first, let label = child.label { - let childMirror = Mirror(reflecting: child.value) - let associatedValuesMirror = - childMirror.displayStyle == .tuple - ? childMirror - : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) - `case` = """ - \(label)(\ - \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ - ) - """ - } else { - `case` = "\(`enum`)" - } - var type = String(reflecting: Enum.self) - if let index = type.firstIndex(of: ".") { - type.removeSubrange(...index) + private func describeCase(_ enum: Enum) -> String { + let mirror = Mirror(reflecting: `enum`) + let `case`: String + if mirror.displayStyle == .enum, let child = mirror.children.first, let label = child.label { + let childMirror = Mirror(reflecting: child.value) + let associatedValuesMirror = + childMirror.displayStyle == .tuple + ? childMirror + : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) + `case` = """ + \(label)(\ + \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ + ) + """ + } else { + `case` = "\(`enum`)" + } + var type = String(reflecting: Enum.self) + if let index = type.firstIndex(of: ".") { + type.removeSubrange(...index) + } + return "\(type).\(`case`)" } - return "\(type).\(`case`)" -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/WithState.swift b/Sources/SwiftUINavigation/WithState.swift index 97de5e1490..13c2f9d0ec 100644 --- a/Sources/SwiftUINavigation/WithState.swift +++ b/Sources/SwiftUINavigation/WithState.swift @@ -1,48 +1,48 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -/// A container view that provides a binding to another view. -/// -/// This view is most helpful for creating Xcode previews of views that require bindings. -/// -/// For example, if you wanted to create a preview for a text field, you cannot simply introduce -/// some `@State` to the preview since `previews` is static: -/// -/// ```swift -/// struct TextField_Previews: PreviewProvider { -/// @State static var text = "" // ⚠️ @State static does not work. -/// -/// static var previews: some View { -/// TextField("Test", text: self.$text) -/// } -/// } -/// ``` -/// -/// So, instead you can use ``WithState``: -/// -/// ```swift -/// struct TextField_Previews: PreviewProvider { -/// static var previews: some View { -/// WithState(initialValue: "") { $text in -/// TextField("Test", text: $text) -/// } -/// } -/// } -/// ``` -public struct WithState: View { - @State var value: Value - @ViewBuilder let content: (Binding) -> Content + /// A container view that provides a binding to another view. + /// + /// This view is most helpful for creating Xcode previews of views that require bindings. + /// + /// For example, if you wanted to create a preview for a text field, you cannot simply introduce + /// some `@State` to the preview since `previews` is static: + /// + /// ```swift + /// struct TextField_Previews: PreviewProvider { + /// @State static var text = "" // ⚠️ @State static does not work. + /// + /// static var previews: some View { + /// TextField("Test", text: self.$text) + /// } + /// } + /// ``` + /// + /// So, instead you can use ``WithState``: + /// + /// ```swift + /// struct TextField_Previews: PreviewProvider { + /// static var previews: some View { + /// WithState(initialValue: "") { $text in + /// TextField("Test", text: $text) + /// } + /// } + /// } + /// ``` + public struct WithState: View { + @State var value: Value + @ViewBuilder let content: (Binding) -> Content - public init( - initialValue value: Value, - @ViewBuilder content: @escaping (Binding) -> Content - ) { - self._value = State(wrappedValue: value) - self.content = content - } + public init( + initialValue value: Value, + @ViewBuilder content: @escaping (Binding) -> Content + ) { + self._value = State(wrappedValue: value) + self.content = content + } - public var body: some View { - self.content(self.$value) + public var body: some View { + self.content(self.$value) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index 7e4208ecde..c23c4cda3c 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -1,264 +1,264 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUI + import CustomDump + import SwiftUI -/// A data type that describes the state of an alert that can be shown to the user. The `Action` -/// generic is the type of actions that can be sent from tapping on a button in the alert. -/// -/// This type can be used in your application's state in order to control the presentation and -/// actions of alerts. This API can be used to push the logic of alert presentation and actions into -/// your model, making it easier to test, and simplifying your view layer. -/// -/// To use this API, you first describe all of the actions that can take place in all of your -/// alerts as an enum: -/// -/// ```swift -/// class HomeScreenModel: ObservableObject { -/// enum AlertAction { -/// case delete -/// case removeFromHomeScreen -/// } -/// // ... -/// } -/// ``` -/// -/// Then you hold onto optional `AlertState` as a `@Published` field in your model, which can -/// start off as `nil`: -/// -/// ```swift -/// class HomeScreenModel: ObservableObject { -/// @Published var alert: AlertState? -/// // ... -/// } -/// ``` -/// -/// And you define an endpoint for handling each alert action: -/// -/// ```swift -/// class HomeScreenModel: ObservableObject { -/// // ... -/// func alertButtonTapped(_ action: AlertAction?) { -/// switch action { -/// case .delete: -/// // ... -/// case .removeFromHomeScreen: -/// // ... -/// case .none: -/// // ... -/// } -/// } -/// } -/// ``` -/// -/// Then, whenever you need to show an alert you can simply construct an ``AlertState`` value to -/// represent the alert: -/// -/// ```swift -/// class HomeScreenModel: ObservableObject { -/// // ... -/// func deleteAppButtonTapped() { -/// self.alert = AlertState { -/// TextState(#"Remove "Twitter"?"#) -/// } actions: { -/// ButtonState(role: .destructive, action: .send(.delete)) { -/// TextState("Delete App") -/// } -/// ButtonState(action: .send(.removeFromHomeScreen)) { -/// TextState("Remove from Home Screen") -/// } -/// } message: { -/// TextState( -/// "Removing from Home Screen will keep the app in your App Library." -/// ) -/// } -/// } -/// } -/// ``` -/// -/// And in your view you can use the `.alert(unwrapping:action:)` view modifier to present the -/// alert: -/// -/// ```swift -/// struct FeatureView: View { -/// @ObservedObject var model: HomeScreenModel -/// -/// var body: some View { -/// VStack { -/// Button("Delete") { -/// self.model.deleteAppButtonTapped() -/// } -/// } -/// .alert(unwrapping: self.$model.alert) { action in -/// self.model.alertButtonTapped(action) -/// } -/// } -/// } -/// ``` -/// -/// This makes your model in complete control of when the alert is shown or dismissed, and makes it -/// so that any choice made in the alert is automatically fed back into the model so that you can -/// handle its logic. -/// -/// Even better, because `AlertState` is equatable (when `Action` is equatable), you can instantly -/// write tests that your alert behavior works as expected: -/// -/// ```swift -/// let model = HomeScreenModel() -/// -/// model.deleteAppButtonTapped() -/// XCTAssertEqual( -/// model.alert, -/// AlertState { -/// TextState(#"Remove "Twitter"?"#) -/// } actions: { -/// ButtonState(role: .destructive, action: .deleteButtonTapped) { -/// TextState("Delete App"), -/// }, -/// ButtonState(action: .removeFromHomeScreenButtonTapped) { -/// TextState("Remove from Home Screen"), -/// } -/// } message: { -/// TextState( -/// "Removing from Home Screen will keep the app in your App Library." -/// ) -/// } -/// ) -/// -/// model.alertButtonTapped(.delete) { -/// // Also verify that delete logic executed correctly -/// } -/// model.alert = nil -/// ``` -public struct AlertState: Identifiable { - public let id: UUID - public var buttons: [ButtonState] - public var message: TextState? - public var title: TextState + /// A data type that describes the state of an alert that can be shown to the user. The `Action` + /// generic is the type of actions that can be sent from tapping on a button in the alert. + /// + /// This type can be used in your application's state in order to control the presentation and + /// actions of alerts. This API can be used to push the logic of alert presentation and actions into + /// your model, making it easier to test, and simplifying your view layer. + /// + /// To use this API, you first describe all of the actions that can take place in all of your + /// alerts as an enum: + /// + /// ```swift + /// class HomeScreenModel: ObservableObject { + /// enum AlertAction { + /// case delete + /// case removeFromHomeScreen + /// } + /// // ... + /// } + /// ``` + /// + /// Then you hold onto optional `AlertState` as a `@Published` field in your model, which can + /// start off as `nil`: + /// + /// ```swift + /// class HomeScreenModel: ObservableObject { + /// @Published var alert: AlertState? + /// // ... + /// } + /// ``` + /// + /// And you define an endpoint for handling each alert action: + /// + /// ```swift + /// class HomeScreenModel: ObservableObject { + /// // ... + /// func alertButtonTapped(_ action: AlertAction?) { + /// switch action { + /// case .delete: + /// // ... + /// case .removeFromHomeScreen: + /// // ... + /// case .none: + /// // ... + /// } + /// } + /// } + /// ``` + /// + /// Then, whenever you need to show an alert you can simply construct an ``AlertState`` value to + /// represent the alert: + /// + /// ```swift + /// class HomeScreenModel: ObservableObject { + /// // ... + /// func deleteAppButtonTapped() { + /// self.alert = AlertState { + /// TextState(#"Remove "Twitter"?"#) + /// } actions: { + /// ButtonState(role: .destructive, action: .send(.delete)) { + /// TextState("Delete App") + /// } + /// ButtonState(action: .send(.removeFromHomeScreen)) { + /// TextState("Remove from Home Screen") + /// } + /// } message: { + /// TextState( + /// "Removing from Home Screen will keep the app in your App Library." + /// ) + /// } + /// } + /// } + /// ``` + /// + /// And in your view you can use the `.alert(unwrapping:action:)` view modifier to present the + /// alert: + /// + /// ```swift + /// struct FeatureView: View { + /// @ObservedObject var model: HomeScreenModel + /// + /// var body: some View { + /// VStack { + /// Button("Delete") { + /// self.model.deleteAppButtonTapped() + /// } + /// } + /// .alert(unwrapping: self.$model.alert) { action in + /// self.model.alertButtonTapped(action) + /// } + /// } + /// } + /// ``` + /// + /// This makes your model in complete control of when the alert is shown or dismissed, and makes it + /// so that any choice made in the alert is automatically fed back into the model so that you can + /// handle its logic. + /// + /// Even better, because `AlertState` is equatable (when `Action` is equatable), you can instantly + /// write tests that your alert behavior works as expected: + /// + /// ```swift + /// let model = HomeScreenModel() + /// + /// model.deleteAppButtonTapped() + /// XCTAssertEqual( + /// model.alert, + /// AlertState { + /// TextState(#"Remove "Twitter"?"#) + /// } actions: { + /// ButtonState(role: .destructive, action: .deleteButtonTapped) { + /// TextState("Delete App"), + /// }, + /// ButtonState(action: .removeFromHomeScreenButtonTapped) { + /// TextState("Remove from Home Screen"), + /// } + /// } message: { + /// TextState( + /// "Removing from Home Screen will keep the app in your App Library." + /// ) + /// } + /// ) + /// + /// model.alertButtonTapped(.delete) { + /// // Also verify that delete logic executed correctly + /// } + /// model.alert = nil + /// ``` + public struct AlertState: Identifiable { + public let id: UUID + public var buttons: [ButtonState] + public var message: TextState? + public var title: TextState - init( - id: UUID, - buttons: [ButtonState], - message: TextState?, - title: TextState - ) { - self.id = id - self.buttons = buttons - self.message = message - self.title = title - } + init( + id: UUID, + buttons: [ButtonState], + message: TextState?, + title: TextState + ) { + self.id = id + self.buttons = buttons + self.message = message + self.title = title + } - /// Creates alert state. - /// - /// - Parameters: - /// - title: The title of the alert. - /// - actions: A ``ButtonStateBuilder`` returning the alert's actions. - /// - message: The message for the alert. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public init( - title: () -> TextState, - @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, - message: (() -> TextState)? = nil - ) { - self.init( - id: UUID(), - buttons: actions(), - message: message?(), - title: title() - ) - } + /// Creates alert state. + /// + /// - Parameters: + /// - title: The title of the alert. + /// - actions: A ``ButtonStateBuilder`` returning the alert's actions. + /// - message: The message for the alert. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public init( + title: () -> TextState, + @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, + message: (() -> TextState)? = nil + ) { + self.init( + id: UUID(), + buttons: actions(), + message: message?(), + title: title() + ) + } - public func map(_ transform: (Action?) -> NewAction?) -> AlertState { - AlertState( - id: self.id, - buttons: self.buttons.map { $0.map(transform) }, - message: self.message, - title: self.title - ) + public func map(_ transform: (Action?) -> NewAction?) -> AlertState { + AlertState( + id: self.id, + buttons: self.buttons.map { $0.map(transform) }, + message: self.message, + title: self.title + ) + } } -} -extension AlertState: CustomDumpReflectable { - public var customDumpMirror: Mirror { - var children: [(label: String?, value: Any)] = [ - ("title", self.title) - ] - if !self.buttons.isEmpty { - children.append(("actions", self.buttons)) - } - if let message = self.message { - children.append(("message", message)) + extension AlertState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + var children: [(label: String?, value: Any)] = [ + ("title", self.title) + ] + if !self.buttons.isEmpty { + children.append(("actions", self.buttons)) + } + if let message = self.message { + children.append(("message", message)) + } + return Mirror( + self, + children: children, + displayStyle: .struct + ) } - return Mirror( - self, - children: children, - displayStyle: .struct - ) } -} -extension AlertState: Equatable where Action: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.title == rhs.title - && lhs.message == rhs.message - && lhs.buttons == rhs.buttons + extension AlertState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons + } } -} -extension AlertState: Hashable where Action: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.title) - hasher.combine(self.message) - hasher.combine(self.buttons) + extension AlertState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.title) + hasher.combine(self.message) + hasher.combine(self.buttons) + } } -} -// MARK: - SwiftUI bridging + // MARK: - SwiftUI bridging -extension Alert { - /// Creates an alert from alert state. - /// - /// - Parameters: - /// - state: Alert state used to populate the alert. - /// - action: An action handler, called when a button with an action is tapped, by passing the - /// action to the closure. - public init(_ state: AlertState, action: @escaping (Action?) -> Void) { - if state.buttons.count == 2 { - self.init( - title: Text(state.title), - message: state.message.map { Text($0) }, - primaryButton: .init(state.buttons[0], action: action), - secondaryButton: .init(state.buttons[1], action: action) - ) - } else { - self.init( - title: Text(state.title), - message: state.message.map { Text($0) }, - dismissButton: state.buttons.first.map { .init($0, action: action) } - ) + extension Alert { + /// Creates an alert from alert state. + /// + /// - Parameters: + /// - state: Alert state used to populate the alert. + /// - action: An action handler, called when a button with an action is tapped, by passing the + /// action to the closure. + public init(_ state: AlertState, action: @escaping (Action?) -> Void) { + if state.buttons.count == 2 { + self.init( + title: Text(state.title), + message: state.message.map { Text($0) }, + primaryButton: .init(state.buttons[0], action: action), + secondaryButton: .init(state.buttons[1], action: action) + ) + } else { + self.init( + title: Text(state.title), + message: state.message.map { Text($0) }, + dismissButton: state.buttons.first.map { .init($0, action: action) } + ) + } } - } - /// Creates an alert from alert state. - /// - /// - Parameters: - /// - state: Alert state used to populate the alert. - /// - action: An action handler, called when a button with an action is tapped, by passing the - /// action to the closure. - public init(_ state: AlertState, action: @escaping (Action?) async -> Void) { - if state.buttons.count == 2 { - self.init( - title: Text(state.title), - message: state.message.map { Text($0) }, - primaryButton: .init(state.buttons[0], action: action), - secondaryButton: .init(state.buttons[1], action: action) - ) - } else { - self.init( - title: Text(state.title), - message: state.message.map { Text($0) }, - dismissButton: state.buttons.first.map { .init($0, action: action) } - ) + /// Creates an alert from alert state. + /// + /// - Parameters: + /// - state: Alert state used to populate the alert. + /// - action: An action handler, called when a button with an action is tapped, by passing the + /// action to the closure. + public init(_ state: AlertState, action: @escaping (Action?) async -> Void) { + if state.buttons.count == 2 { + self.init( + title: Text(state.title), + message: state.message.map { Text($0) }, + primaryButton: .init(state.buttons[0], action: action), + secondaryButton: .init(state.buttons[1], action: action) + ) + } else { + self.init( + title: Text(state.title), + message: state.message.map { Text($0) }, + dismissButton: state.buttons.first.map { .init($0, action: action) } + ) + } } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Bind.swift b/Sources/SwiftUINavigationCore/Bind.swift index 49c61439ec..c530c606a1 100644 --- a/Sources/SwiftUINavigationCore/Bind.swift +++ b/Sources/SwiftUINavigationCore/Bind.swift @@ -1,86 +1,86 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -extension View { - /// Synchronizes model state to view state via two-way bindings. - /// - /// SwiftUI comes with many property wrappers that can be used in views to drive view state, like - /// field focus. Unfortunately, these property wrappers _must_ be used in views. It's not possible - /// to extract this logic to an observable object and integrate it with the rest of the model's - /// business logic, and be in a better position to test this state. - /// - /// We can work around these limitations by introducing a published field to your observable - /// object and synchronizing it to view state with this view modifier. - /// - /// - Parameters: - /// - modelValue: A binding from model state. _E.g._, a binding derived from a published field - /// on an observable object. - /// - viewValue: A binding from view state. _E.g._, a focus binding. - @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) - public func bind( - _ modelValue: ModelValue, to viewValue: ViewValue - ) -> some View - where ModelValue.Value == ViewValue.Value, ModelValue.Value: Equatable { - self.modifier(_Bind(modelValue: modelValue, viewValue: viewValue)) + extension View { + /// Synchronizes model state to view state via two-way bindings. + /// + /// SwiftUI comes with many property wrappers that can be used in views to drive view state, like + /// field focus. Unfortunately, these property wrappers _must_ be used in views. It's not possible + /// to extract this logic to an observable object and integrate it with the rest of the model's + /// business logic, and be in a better position to test this state. + /// + /// We can work around these limitations by introducing a published field to your observable + /// object and synchronizing it to view state with this view modifier. + /// + /// - Parameters: + /// - modelValue: A binding from model state. _E.g._, a binding derived from a published field + /// on an observable object. + /// - viewValue: A binding from view state. _E.g._, a focus binding. + @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) + public func bind( + _ modelValue: ModelValue, to viewValue: ViewValue + ) -> some View + where ModelValue.Value == ViewValue.Value, ModelValue.Value: Equatable { + self.modifier(_Bind(modelValue: modelValue, viewValue: viewValue)) + } } -} -@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) -private struct _Bind: ViewModifier -where ModelValue.Value == ViewValue.Value, ModelValue.Value: Equatable { - let modelValue: ModelValue - let viewValue: ViewValue + @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) + private struct _Bind: ViewModifier + where ModelValue.Value == ViewValue.Value, ModelValue.Value: Equatable { + let modelValue: ModelValue + let viewValue: ViewValue - @State var hasAppeared = false + @State var hasAppeared = false - func body(content: Content) -> some View { - content - .onAppear { - guard !self.hasAppeared else { return } - self.hasAppeared = true - guard self.viewValue.wrappedValue != self.modelValue.wrappedValue else { return } - self.viewValue.wrappedValue = self.modelValue.wrappedValue - } - .onChange(of: self.modelValue.wrappedValue) { - guard self.viewValue.wrappedValue != $0 - else { return } - self.viewValue.wrappedValue = $0 - } - .onChange(of: self.viewValue.wrappedValue) { - guard self.modelValue.wrappedValue != $0 - else { return } - self.modelValue.wrappedValue = $0 - } + func body(content: Content) -> some View { + content + .onAppear { + guard !self.hasAppeared else { return } + self.hasAppeared = true + guard self.viewValue.wrappedValue != self.modelValue.wrappedValue else { return } + self.viewValue.wrappedValue = self.modelValue.wrappedValue + } + .onChange(of: self.modelValue.wrappedValue) { + guard self.viewValue.wrappedValue != $0 + else { return } + self.viewValue.wrappedValue = $0 + } + .onChange(of: self.viewValue.wrappedValue) { + guard self.modelValue.wrappedValue != $0 + else { return } + self.modelValue.wrappedValue = $0 + } + } } -} -public protocol _Bindable { - associatedtype Value - var wrappedValue: Value { get nonmutating set } -} + public protocol _Bindable { + associatedtype Value + var wrappedValue: Value { get nonmutating set } + } -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension AccessibilityFocusState: _Bindable {} + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension AccessibilityFocusState: _Bindable {} -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension AccessibilityFocusState.Binding: _Bindable {} + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension AccessibilityFocusState.Binding: _Bindable {} -@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) -extension AppStorage: _Bindable {} + @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) + extension AppStorage: _Bindable {} -extension Binding: _Bindable {} + extension Binding: _Bindable {} -@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) -extension FocusedBinding: _Bindable {} + @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) + extension FocusedBinding: _Bindable {} -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension FocusState: _Bindable {} + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension FocusState: _Bindable {} -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension FocusState.Binding: _Bindable {} + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension FocusState.Binding: _Bindable {} -@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) -extension SceneStorage: _Bindable {} + @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) + extension SceneStorage: _Bindable {} -extension State: _Bindable {} -#endif // canImport(SwiftUI) + extension State: _Bindable {} +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index 3d538cbba0..faac4cbc74 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -1,372 +1,372 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUI - -public struct ButtonState: Identifiable { - public let id: UUID - public let action: ButtonStateAction - public let label: TextState - public let role: ButtonStateRole? - - init( - id: UUID, - action: ButtonStateAction, - label: TextState, - role: ButtonStateRole? - ) { - self.id = id - self.action = action - self.label = label - self.role = role - } + import CustomDump + import SwiftUI + + public struct ButtonState: Identifiable { + public let id: UUID + public let action: ButtonStateAction + public let label: TextState + public let role: ButtonStateRole? + + init( + id: UUID, + action: ButtonStateAction, + label: TextState, + role: ButtonStateRole? + ) { + self.id = id + self.action = action + self.label = label + self.role = role + } - /// Creates button state. - /// - /// - Parameters: - /// - role: An optional semantic role that describes the button. A value of `nil` means that the - /// button doesn't have an assigned role. - /// - action: The action to send when the user interacts with the button. - /// - label: A view that describes the purpose of the button's `action`. - public init( - role: ButtonStateRole? = nil, - action: ButtonStateAction = .send(nil), - label: () -> TextState - ) { - self.init(id: UUID(), action: action, label: label(), role: role) - } + /// Creates button state. + /// + /// - Parameters: + /// - role: An optional semantic role that describes the button. A value of `nil` means that the + /// button doesn't have an assigned role. + /// - action: The action to send when the user interacts with the button. + /// - label: A view that describes the purpose of the button's `action`. + public init( + role: ButtonStateRole? = nil, + action: ButtonStateAction = .send(nil), + label: () -> TextState + ) { + self.init(id: UUID(), action: action, label: label(), role: role) + } - /// Creates button state. - /// - /// - Parameters: - /// - role: An optional semantic role that describes the button. A value of `nil` means that the - /// button doesn't have an assigned role. - /// - action: The action to send when the user interacts with the button. - /// - label: A view that describes the purpose of the button's `action`. - public init( - role: ButtonStateRole? = nil, - action: Action, - label: () -> TextState - ) { - self.init(id: UUID(), action: .send(action), label: label(), role: role) - } + /// Creates button state. + /// + /// - Parameters: + /// - role: An optional semantic role that describes the button. A value of `nil` means that the + /// button doesn't have an assigned role. + /// - action: The action to send when the user interacts with the button. + /// - label: A view that describes the purpose of the button's `action`. + public init( + role: ButtonStateRole? = nil, + action: Action, + label: () -> TextState + ) { + self.init(id: UUID(), action: .send(action), label: label(), role: role) + } - /// Handle the button's action in a closure. - /// - /// - Parameter perform: Unwraps and passes a button's action to a closure to be performed. If the - /// action has an associated animation, the context will be wrapped using SwiftUI's - /// `withAnimation`. - public func withAction(_ perform: (Action?) -> Void) { - switch self.action.type { - case let .send(action): - perform(action) - case let .animatedSend(action, animation): - withAnimation(animation) { + /// Handle the button's action in a closure. + /// + /// - Parameter perform: Unwraps and passes a button's action to a closure to be performed. If the + /// action has an associated animation, the context will be wrapped using SwiftUI's + /// `withAnimation`. + public func withAction(_ perform: (Action?) -> Void) { + switch self.action.type { + case let .send(action): perform(action) + case let .animatedSend(action, animation): + withAnimation(animation) { + perform(action) + } } } - } - /// Handle the button's action in an async closure. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameter perform: Unwraps and passes a button's action to a closure to be performed. - public func withAction(_ perform: (Action?) async -> Void) async { - switch self.action.type { - case let .send(action): - await perform(action) - case let .animatedSend(action, _): - var output = "" - customDump(self.action, to: &output, indent: 4) - runtimeWarn( - """ - An animated action was performed asynchronously: … - - Action: - \((output)) - - Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, or \ - use 'SwiftUI.withAnimation' explicitly. - """ - ) - await perform(action) + /// Handle the button's action in an async closure. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameter perform: Unwraps and passes a button's action to a closure to be performed. + public func withAction(_ perform: (Action?) async -> Void) async { + switch self.action.type { + case let .send(action): + await perform(action) + case let .animatedSend(action, _): + var output = "" + customDump(self.action, to: &output, indent: 4) + runtimeWarn( + """ + An animated action was performed asynchronously: … + + Action: + \((output)) + + Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, or \ + use 'SwiftUI.withAnimation' explicitly. + """ + ) + await perform(action) + } } - } - /// Transforms a button state's action into a new action. - /// - /// - Parameter transform: A closure that transforms an optional action into a new optional - /// action. - /// - Returns: Button state over a new action. - public func map(_ transform: (Action?) -> NewAction?) -> ButtonState { - ButtonState( - id: self.id, - action: self.action.map(transform), - label: self.label, - role: self.role - ) + /// Transforms a button state's action into a new action. + /// + /// - Parameter transform: A closure that transforms an optional action into a new optional + /// action. + /// - Returns: Button state over a new action. + public func map(_ transform: (Action?) -> NewAction?) -> ButtonState { + ButtonState( + id: self.id, + action: self.action.map(transform), + label: self.label, + role: self.role + ) + } } -} -/// A type that wraps an action with additional context, _e.g._ for animation. -public struct ButtonStateAction { - public let type: _ActionType + /// A type that wraps an action with additional context, _e.g._ for animation. + public struct ButtonStateAction { + public let type: _ActionType - public static func send(_ action: Action?) -> Self { - .init(type: .send(action)) - } + public static func send(_ action: Action?) -> Self { + .init(type: .send(action)) + } - public static func send(_ action: Action?, animation: Animation?) -> Self { - .init(type: .animatedSend(action, animation: animation)) - } + public static func send(_ action: Action?, animation: Animation?) -> Self { + .init(type: .animatedSend(action, animation: animation)) + } - public var action: Action? { - switch self.type { - case let .animatedSend(action, animation: _), let .send(action): - return action + public var action: Action? { + switch self.type { + case let .animatedSend(action, animation: _), let .send(action): + return action + } } - } - public func map( - _ transform: (Action?) -> NewAction? - ) -> ButtonStateAction { - switch self.type { - case let .animatedSend(action, animation: animation): - return .send(transform(action), animation: animation) - case let .send(action): - return .send(transform(action)) + public func map( + _ transform: (Action?) -> NewAction? + ) -> ButtonStateAction { + switch self.type { + case let .animatedSend(action, animation: animation): + return .send(transform(action), animation: animation) + case let .send(action): + return .send(transform(action)) + } } - } - public enum _ActionType { - case send(Action?) - case animatedSend(Action?, animation: Animation?) + public enum _ActionType { + case send(Action?) + case animatedSend(Action?, animation: Animation?) + } } -} -/// A value that describes the purpose of a button. -/// -/// See `SwiftUI.ButtonRole` for more information. -public enum ButtonStateRole: Sendable { - /// A role that indicates a cancel button. + /// A value that describes the purpose of a button. /// - /// See `SwiftUI.ButtonRole.cancel` for more information. - case cancel - - /// A role that indicates a destructive button. - /// - /// See `SwiftUI.ButtonRole.destructive` for more information. - case destructive -} - -extension ButtonState: CustomDumpReflectable { - public var customDumpMirror: Mirror { - var children: [(label: String?, value: Any)] = [] - if let role = self.role { - children.append(("role", role)) - } - children.append(("action", self.action)) - children.append(("label", self.label)) - return Mirror( - self, - children: children, - displayStyle: .struct - ) + /// See `SwiftUI.ButtonRole` for more information. + public enum ButtonStateRole: Sendable { + /// A role that indicates a cancel button. + /// + /// See `SwiftUI.ButtonRole.cancel` for more information. + case cancel + + /// A role that indicates a destructive button. + /// + /// See `SwiftUI.ButtonRole.destructive` for more information. + case destructive } -} -extension ButtonStateAction: CustomDumpReflectable { - public var customDumpMirror: Mirror { - switch self.type { - case let .send(action): - return Mirror( - self, - children: [ - "send": action as Any - ], - displayStyle: .enum - ) - case let .animatedSend(action, animation): + extension ButtonState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + var children: [(label: String?, value: Any)] = [] + if let role = self.role { + children.append(("role", role)) + } + children.append(("action", self.action)) + children.append(("label", self.label)) return Mirror( self, - children: [ - "send": (action, animation: animation) - ], - displayStyle: .enum + children: children, + displayStyle: .struct ) } } -} - -extension ButtonStateAction: Equatable where Action: Equatable {} -extension ButtonStateAction._ActionType: Equatable where Action: Equatable {} -extension ButtonStateRole: Equatable {} -extension ButtonState: Equatable where Action: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.action == rhs.action - && lhs.label == rhs.label - && lhs.role == rhs.role - } -} - -extension ButtonStateAction: Hashable where Action: Hashable {} -extension ButtonStateAction._ActionType: Hashable where Action: Hashable { - public func hash(into hasher: inout Hasher) { - switch self { - case let .send(action), let .animatedSend(action, animation: _): - hasher.combine(action) + + extension ButtonStateAction: CustomDumpReflectable { + public var customDumpMirror: Mirror { + switch self.type { + case let .send(action): + return Mirror( + self, + children: [ + "send": action as Any + ], + displayStyle: .enum + ) + case let .animatedSend(action, animation): + return Mirror( + self, + children: [ + "send": (action, animation: animation) + ], + displayStyle: .enum + ) + } } } -} -extension ButtonStateRole: Hashable {} -extension ButtonState: Hashable where Action: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.action) - hasher.combine(self.label) - hasher.combine(self.role) - } -} - -#if swift(>=5.7) - extension ButtonStateAction: Sendable where Action: Sendable {} - extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} - extension ButtonState: Sendable where Action: Sendable {} -#endif - -// MARK: - SwiftUI bridging -extension Alert.Button { - /// Initializes a `SwiftUI.Alert.Button` from `ButtonState` and an action handler. - /// - /// - Parameters: - /// - button: Button state. - /// - action: An action closure that is invoked when the button is tapped. - public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { - let action = { button.withAction(action) } - switch button.role { - case .cancel: - self = .cancel(Text(button.label), action: action) - case .destructive: - self = .destructive(Text(button.label), action: action) - case .none: - self = .default(Text(button.label), action: action) + extension ButtonStateAction: Equatable where Action: Equatable {} + extension ButtonStateAction._ActionType: Equatable where Action: Equatable {} + extension ButtonStateRole: Equatable {} + extension ButtonState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.action == rhs.action + && lhs.label == rhs.label + && lhs.role == rhs.role } } - /// Initializes a `SwiftUI.Alert.Button` from `ButtonState` and an async action handler. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - button: Button state. - /// - action: An action closure that is invoked when the button is tapped. - public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { - let action = { _ = Task { await button.withAction(action) } } - switch button.role { - case .cancel: - self = .cancel(Text(button.label), action: action) - case .destructive: - self = .destructive(Text(button.label), action: action) - case .none: - self = .default(Text(button.label), action: action) + extension ButtonStateAction: Hashable where Action: Hashable {} + extension ButtonStateAction._ActionType: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case let .send(action), let .animatedSend(action, animation: _): + hasher.combine(action) + } } } -} - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension ButtonRole { - public init(_ role: ButtonStateRole) { - switch role { - case .cancel: - self = .cancel - case .destructive: - self = .destructive + extension ButtonStateRole: Hashable {} + extension ButtonState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.action) + hasher.combine(self.label) + hasher.combine(self.role) } } -} -extension Button where Label == Text { - /// Initializes a `SwiftUI.Button` from `ButtonState` and an async action handler. - /// - /// - Parameters: - /// - button: Button state. - /// - action: An action closure that is invoked when the button is tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { - self.init( - role: button.role.map(ButtonRole.init), - action: { button.withAction(action) } - ) { - Text(button.label) + #if swift(>=5.7) + extension ButtonStateAction: Sendable where Action: Sendable {} + extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} + extension ButtonState: Sendable where Action: Sendable {} + #endif + + // MARK: - SwiftUI bridging + + extension Alert.Button { + /// Initializes a `SwiftUI.Alert.Button` from `ButtonState` and an action handler. + /// + /// - Parameters: + /// - button: Button state. + /// - action: An action closure that is invoked when the button is tapped. + public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { + let action = { button.withAction(action) } + switch button.role { + case .cancel: + self = .cancel(Text(button.label), action: action) + case .destructive: + self = .destructive(Text(button.label), action: action) + case .none: + self = .default(Text(button.label), action: action) + } + } + + /// Initializes a `SwiftUI.Alert.Button` from `ButtonState` and an async action handler. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - button: Button state. + /// - action: An action closure that is invoked when the button is tapped. + public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { + let action = { _ = Task { await button.withAction(action) } } + switch button.role { + case .cancel: + self = .cancel(Text(button.label), action: action) + case .destructive: + self = .destructive(Text(button.label), action: action) + case .none: + self = .default(Text(button.label), action: action) + } } } - /// Initializes a `SwiftUI.Button` from `ButtonState` and an action handler. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - button: Button state. - /// - action: An action closure that is invoked when the button is tapped. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { - self.init( - role: button.role.map(ButtonRole.init), - action: { Task { await button.withAction(action) } } - ) { - Text(button.label) + extension ButtonRole { + public init(_ role: ButtonStateRole) { + switch role { + case .cancel: + self = .cancel + case .destructive: + self = .destructive + } } } -} - -@usableFromInline -func debugCaseOutput(_ value: Any) -> String { - func debugCaseOutputHelp(_ value: Any) -> String { - let mirror = Mirror(reflecting: value) - switch mirror.displayStyle { - case .enum: - guard let child = mirror.children.first else { - let childOutput = "\(value)" - return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)" + + extension Button where Label == Text { + /// Initializes a `SwiftUI.Button` from `ButtonState` and an async action handler. + /// + /// - Parameters: + /// - button: Button state. + /// - action: An action closure that is invoked when the button is tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { + self.init( + role: button.role.map(ButtonRole.init), + action: { button.withAction(action) } + ) { + Text(button.label) } - let childOutput = debugCaseOutputHelp(child.value) - return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")" - case .tuple: - return mirror.children.map { label, value in - let childOutput = debugCaseOutputHelp(value) - return - "\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")" + } + + /// Initializes a `SwiftUI.Button` from `ButtonState` and an action handler. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - button: Button state. + /// - action: An action closure that is invoked when the button is tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { + self.init( + role: button.role.map(ButtonRole.init), + action: { Task { await button.withAction(action) } } + ) { + Text(button.label) } - .joined(separator: ", ") - default: - return "" } } - return (value as? CustomDebugStringConvertible)?.debugDescription - ?? "\(typeName(type(of: value)))\(debugCaseOutputHelp(value))" -} + @usableFromInline + func debugCaseOutput(_ value: Any) -> String { + func debugCaseOutputHelp(_ value: Any) -> String { + let mirror = Mirror(reflecting: value) + switch mirror.displayStyle { + case .enum: + guard let child = mirror.children.first else { + let childOutput = "\(value)" + return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)" + } + let childOutput = debugCaseOutputHelp(child.value) + return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")" + case .tuple: + return mirror.children.map { label, value in + let childOutput = debugCaseOutputHelp(value) + return + "\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")" + } + .joined(separator: ", ") + default: + return "" + } + } + + return (value as? CustomDebugStringConvertible)?.debugDescription + ?? "\(typeName(type(of: value)))\(debugCaseOutputHelp(value))" + } -private func isUnlabeledArgument(_ label: String) -> Bool { - label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil -} + private func isUnlabeledArgument(_ label: String) -> Bool { + label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil + } -@usableFromInline -func typeName(_ type: Any.Type) -> String { - var name = _typeName(type, qualified: true) - if let index = name.firstIndex(of: ".") { - name.removeSubrange(...index) + @usableFromInline + func typeName(_ type: Any.Type) -> String { + var name = _typeName(type, qualified: true) + if let index = name.firstIndex(of: ".") { + name.removeSubrange(...index) + } + let sanitizedName = + name + .replacingOccurrences( + of: #"<.+>|\(unknown context at \$[[:xdigit:]]+\)\."#, + with: "", + options: .regularExpression + ) + return sanitizedName } - let sanitizedName = - name - .replacingOccurrences( - of: #"<.+>|\(unknown context at \$[[:xdigit:]]+\)\."#, - with: "", - options: .regularExpression - ) - return sanitizedName -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift b/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift index 53ff0d809e..84222ee6ab 100644 --- a/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift +++ b/Sources/SwiftUINavigationCore/ButtonStateBuilder.swift @@ -1,34 +1,36 @@ #if canImport(SwiftUI) -@resultBuilder -public enum ButtonStateBuilder { - public static func buildArray(_ components: [[ButtonState]]) -> [ButtonState] { - components.flatMap { $0 } - } + @resultBuilder + public enum ButtonStateBuilder { + public static func buildArray(_ components: [[ButtonState]]) -> [ButtonState] { + components.flatMap { $0 } + } - public static func buildBlock(_ components: [ButtonState]...) -> [ButtonState] { - components.flatMap { $0 } - } + public static func buildBlock(_ components: [ButtonState]...) -> [ButtonState] { + components.flatMap { $0 } + } - public static func buildLimitedAvailability( - _ component: [ButtonState] - ) -> [ButtonState] { - component - } + public static func buildLimitedAvailability( + _ component: [ButtonState] + ) -> [ButtonState] { + component + } - public static func buildEither(first component: [ButtonState]) -> [ButtonState] { - component - } + public static func buildEither(first component: [ButtonState]) -> [ButtonState] + { + component + } - public static func buildEither(second component: [ButtonState]) -> [ButtonState] { - component - } + public static func buildEither(second component: [ButtonState]) -> [ButtonState] + { + component + } - public static func buildExpression(_ expression: ButtonState) -> [ButtonState] { - [expression] - } + public static func buildExpression(_ expression: ButtonState) -> [ButtonState] { + [expression] + } - public static func buildOptional(_ component: [ButtonState]?) -> [ButtonState] { - component ?? [] + public static func buildOptional(_ component: [ButtonState]?) -> [ButtonState] { + component ?? [] + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 9e7bae4afd..e5c13e3a6e 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -1,292 +1,292 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUI + import CustomDump + import SwiftUI -/// A data type that describes the state of a confirmation dialog that can be shown to the user. The -/// `Action` generic is the type of actions that can be sent from tapping on a button in the sheet. -/// -/// This type can be used in your application's state in order to control the presentation and -/// actions of dialogs. This API can be used to push the logic of alert presentation and action into -/// your model, making it easier to test, and simplifying your view layer. -/// -/// To use this API, you describe all of a dialog's actions as cases in an enum: -/// -/// ```swift -/// class FeatureModel: ObservableObject { -/// enum ConfirmationDialogAction { -/// case delete -/// case favorite -/// } -/// // ... -/// } -/// ``` -/// -/// You model the state for showing the alert in as a published field, which can start off `nil`: -/// -/// ```swift -/// class FeatureModel: ObservableObject { -/// // ... -/// @Published var dialog: ConfirmationDialogState? -/// // ... -/// } -/// ``` -/// -/// And you define an endpoint for handling each alert action: -/// -/// ```swift -/// class FeatureModel: ObservableObject { -/// // ... -/// func dialogButtonTapped(_ action: ConfirmationDialogAction) { -/// switch action { -/// case .delete: -/// // ... -/// case .favorite: -/// // ... -/// } -/// } -/// } -/// ``` -/// -/// Then, in an endpoint that should display an alert, you can construct a -/// ``ConfirmationDialogState`` value to represent it: -/// -/// ```swift -/// class FeatureModel: ObservableObject { -/// // ... -/// func infoButtonTapped() { -/// self.dialog = ConfirmationDialogState( -/// title: "What would you like to do?", -/// buttons: [ -/// .default(TextState("Favorite"), action: .send(.favorite)), -/// .destructive(TextState("Delete"), action: .send(.delete)), -/// .cancel(TextState("Cancel")), -/// ] -/// ) -/// } -/// } -/// ``` -/// -/// And in your view you can use the `.confirmationDialog(unwrapping:action:)` view modifier to -/// present the dialog: -/// -/// ```swift -/// struct ItemView: View { -/// @ObservedObject var model: FeatureModel -/// -/// var body: some View { -/// VStack { -/// Button("Info") { -/// self.model.infoButtonTapped() -/// } -/// } -/// .confirmationDialog(unwrapping: self.$model.dialog) { action in -/// self.model.dialogButtonTapped(action) -/// } -/// } -/// } -/// ``` -/// -/// This makes your model in complete control of when the alert is shown or dismissed, and makes it -/// so that any choice made in the alert is automatically fed back into the model so that you can -/// handle its logic. -/// -/// Even better, you can instantly write tests that your alert behavior works as expected: -/// -/// ```swift -/// let model = FeatureModel() -/// -/// model.infoButtonTapped() -/// XCTAssertEqual( -/// model.dialog, -/// ConfirmationDialogState( -/// title: "What would you like to do?", -/// buttons: [ -/// .default(TextState("Favorite"), action: .send(.favorite)), -/// .destructive(TextState("Delete"), action: .send(.delete)), -/// .cancel(TextState("Cancel")), -/// ] -/// ) -/// ) -/// -/// model.dialogButtonTapped(.favorite) -/// // Verify that favorite logic executed correctly -/// model.dialog = nil -/// ``` -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -public struct ConfirmationDialogState: Identifiable { - public let id: UUID - public var buttons: [ButtonState] - public var message: TextState? - public var title: TextState - public var titleVisibility: ConfirmationDialogStateTitleVisibility - - init( - id: UUID, - buttons: [ButtonState], - message: TextState?, - title: TextState, - titleVisibility: ConfirmationDialogStateTitleVisibility - ) { - self.id = id - self.buttons = buttons - self.message = message - self.title = title - self.titleVisibility = titleVisibility - } - - /// Creates confirmation dialog state. + /// A data type that describes the state of a confirmation dialog that can be shown to the user. The + /// `Action` generic is the type of actions that can be sent from tapping on a button in the sheet. /// - /// - Parameters: - /// - titleVisibility: The visibility of the dialog's title. - /// - title: The title of the dialog. - /// - actions: A ``ButtonStateBuilder`` returning the dialog's actions. - /// - message: The message for the dialog. - @available(iOS 15, *) - @available(macOS 12, *) - @available(tvOS 15, *) - @available(watchOS 8, *) - public init( - titleVisibility: ConfirmationDialogStateTitleVisibility, - title: () -> TextState, - @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, - message: (() -> TextState)? = nil - ) { - self.init( - id: UUID(), - buttons: actions(), - message: message?(), - title: title(), - titleVisibility: titleVisibility - ) - } - - /// Creates confirmation dialog state. + /// This type can be used in your application's state in order to control the presentation and + /// actions of dialogs. This API can be used to push the logic of alert presentation and action into + /// your model, making it easier to test, and simplifying your view layer. /// - /// - Parameters: - /// - title: The title of the dialog. - /// - actions: A ``ButtonStateBuilder`` returning the dialog's actions. - /// - message: The message for the dialog. - public init( - title: () -> TextState, - @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, - message: (() -> TextState)? = nil - ) { - self.init( - id: UUID(), - buttons: actions(), - message: message?(), - title: title(), - titleVisibility: .automatic - ) - } - - public func map( - _ transform: (Action?) -> NewAction? - ) -> ConfirmationDialogState { - ConfirmationDialogState( - id: self.id, - buttons: self.buttons.map { $0.map(transform) }, - message: self.message, - title: self.title, - titleVisibility: self.titleVisibility - ) - } -} - -/// The visibility of a confirmation dialog title element, chosen automatically based on the -/// platform, current context, and other factors. -/// -/// See `SwiftUI.Visibility` for more information. -public enum ConfirmationDialogStateTitleVisibility: Sendable { - /// The element may be visible or hidden depending on the policies of the component accepting the - /// visibility configuration. + /// To use this API, you describe all of a dialog's actions as cases in an enum: /// - /// See `SwiftUI.Visibility.automatic` for more information. - case automatic - - /// The element may be hidden. + /// ```swift + /// class FeatureModel: ObservableObject { + /// enum ConfirmationDialogAction { + /// case delete + /// case favorite + /// } + /// // ... + /// } + /// ``` + /// + /// You model the state for showing the alert in as a published field, which can start off `nil`: /// - /// See `SwiftUI.Visibility.hidden` for more information. - case hidden - /// The element may be visible. + /// ```swift + /// class FeatureModel: ObservableObject { + /// // ... + /// @Published var dialog: ConfirmationDialogState? + /// // ... + /// } + /// ``` /// - /// See `SwiftUI.Visibility.visible` for more information. - case visible -} + /// And you define an endpoint for handling each alert action: + /// + /// ```swift + /// class FeatureModel: ObservableObject { + /// // ... + /// func dialogButtonTapped(_ action: ConfirmationDialogAction) { + /// switch action { + /// case .delete: + /// // ... + /// case .favorite: + /// // ... + /// } + /// } + /// } + /// ``` + /// + /// Then, in an endpoint that should display an alert, you can construct a + /// ``ConfirmationDialogState`` value to represent it: + /// + /// ```swift + /// class FeatureModel: ObservableObject { + /// // ... + /// func infoButtonTapped() { + /// self.dialog = ConfirmationDialogState( + /// title: "What would you like to do?", + /// buttons: [ + /// .default(TextState("Favorite"), action: .send(.favorite)), + /// .destructive(TextState("Delete"), action: .send(.delete)), + /// .cancel(TextState("Cancel")), + /// ] + /// ) + /// } + /// } + /// ``` + /// + /// And in your view you can use the `.confirmationDialog(unwrapping:action:)` view modifier to + /// present the dialog: + /// + /// ```swift + /// struct ItemView: View { + /// @ObservedObject var model: FeatureModel + /// + /// var body: some View { + /// VStack { + /// Button("Info") { + /// self.model.infoButtonTapped() + /// } + /// } + /// .confirmationDialog(unwrapping: self.$model.dialog) { action in + /// self.model.dialogButtonTapped(action) + /// } + /// } + /// } + /// ``` + /// + /// This makes your model in complete control of when the alert is shown or dismissed, and makes it + /// so that any choice made in the alert is automatically fed back into the model so that you can + /// handle its logic. + /// + /// Even better, you can instantly write tests that your alert behavior works as expected: + /// + /// ```swift + /// let model = FeatureModel() + /// + /// model.infoButtonTapped() + /// XCTAssertEqual( + /// model.dialog, + /// ConfirmationDialogState( + /// title: "What would you like to do?", + /// buttons: [ + /// .default(TextState("Favorite"), action: .send(.favorite)), + /// .destructive(TextState("Delete"), action: .send(.delete)), + /// .cancel(TextState("Cancel")), + /// ] + /// ) + /// ) + /// + /// model.dialogButtonTapped(.favorite) + /// // Verify that favorite logic executed correctly + /// model.dialog = nil + /// ``` + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + public struct ConfirmationDialogState: Identifiable { + public let id: UUID + public var buttons: [ButtonState] + public var message: TextState? + public var title: TextState + public var titleVisibility: ConfirmationDialogStateTitleVisibility -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ConfirmationDialogState: CustomDumpReflectable { - public var customDumpMirror: Mirror { - var children: [(label: String?, value: Any)] = [] - if self.titleVisibility != .automatic { - children.append(("titleVisibility", self.titleVisibility)) + init( + id: UUID, + buttons: [ButtonState], + message: TextState?, + title: TextState, + titleVisibility: ConfirmationDialogStateTitleVisibility + ) { + self.id = id + self.buttons = buttons + self.message = message + self.title = title + self.titleVisibility = titleVisibility } - children.append(("title", self.title)) - if !self.buttons.isEmpty { - children.append(("actions", self.buttons)) + + /// Creates confirmation dialog state. + /// + /// - Parameters: + /// - titleVisibility: The visibility of the dialog's title. + /// - title: The title of the dialog. + /// - actions: A ``ButtonStateBuilder`` returning the dialog's actions. + /// - message: The message for the dialog. + @available(iOS 15, *) + @available(macOS 12, *) + @available(tvOS 15, *) + @available(watchOS 8, *) + public init( + titleVisibility: ConfirmationDialogStateTitleVisibility, + title: () -> TextState, + @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, + message: (() -> TextState)? = nil + ) { + self.init( + id: UUID(), + buttons: actions(), + message: message?(), + title: title(), + titleVisibility: titleVisibility + ) + } + + /// Creates confirmation dialog state. + /// + /// - Parameters: + /// - title: The title of the dialog. + /// - actions: A ``ButtonStateBuilder`` returning the dialog's actions. + /// - message: The message for the dialog. + public init( + title: () -> TextState, + @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, + message: (() -> TextState)? = nil + ) { + self.init( + id: UUID(), + buttons: actions(), + message: message?(), + title: title(), + titleVisibility: .automatic + ) } - if let message = self.message { - children.append(("message", message)) + + public func map( + _ transform: (Action?) -> NewAction? + ) -> ConfirmationDialogState { + ConfirmationDialogState( + id: self.id, + buttons: self.buttons.map { $0.map(transform) }, + message: self.message, + title: self.title, + titleVisibility: self.titleVisibility + ) } - return Mirror( - self, - children: children, - displayStyle: .struct - ) } -} -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ConfirmationDialogState: Equatable where Action: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.title == rhs.title - && lhs.message == rhs.message - && lhs.buttons == rhs.buttons + /// The visibility of a confirmation dialog title element, chosen automatically based on the + /// platform, current context, and other factors. + /// + /// See `SwiftUI.Visibility` for more information. + public enum ConfirmationDialogStateTitleVisibility: Sendable { + /// The element may be visible or hidden depending on the policies of the component accepting the + /// visibility configuration. + /// + /// See `SwiftUI.Visibility.automatic` for more information. + case automatic + + /// The element may be hidden. + /// + /// See `SwiftUI.Visibility.hidden` for more information. + case hidden + /// The element may be visible. + /// + /// See `SwiftUI.Visibility.visible` for more information. + case visible + } + + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + var children: [(label: String?, value: Any)] = [] + if self.titleVisibility != .automatic { + children.append(("titleVisibility", self.titleVisibility)) + } + children.append(("title", self.title)) + if !self.buttons.isEmpty { + children.append(("actions", self.buttons)) + } + if let message = self.message { + children.append(("message", message)) + } + return Mirror( + self, + children: children, + displayStyle: .struct + ) + } } -} -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ConfirmationDialogState: Hashable where Action: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.title) - hasher.combine(self.message) - hasher.combine(self.buttons) + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons + } } -} -#if swift(>=5.7) @available(iOS 13, *) @available(macOS 12, *) @available(tvOS 13, *) @available(watchOS 6, *) - extension ConfirmationDialogState: Sendable where Action: Sendable {} -#endif + extension ConfirmationDialogState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.title) + hasher.combine(self.message) + hasher.combine(self.buttons) + } + } + + #if swift(>=5.7) + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState: Sendable where Action: Sendable {} + #endif -// MARK: - SwiftUI bridging + // MARK: - SwiftUI bridging -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension Visibility { - public init(_ visibility: ConfirmationDialogStateTitleVisibility) { - switch visibility { - case .automatic: - self = .automatic - case .hidden: - self = .hidden - case .visible: - self = .visible + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension Visibility { + public init(_ visibility: ConfirmationDialogStateTitleVisibility) { + switch visibility { + case .automatic: + self = .automatic + case .hidden: + self = .hidden + case .visible: + self = .visible + } } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift index a2c6d61c98..b473de04b7 100644 --- a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift @@ -1,312 +1,318 @@ #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -// NB: Deprecated after 0.5.0 + // NB: Deprecated after 0.5.0 -extension ButtonState { - @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") - public typealias Handler = ButtonStateAction + extension ButtonState { + @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") + public typealias Handler = ButtonStateAction - @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") - public typealias ButtonAction = ButtonStateAction + @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") + public typealias ButtonAction = ButtonStateAction - @available(*, deprecated, message: "Use 'ButtonStateRole' instead.") - public typealias Role = ButtonStateRole -} + @available(*, deprecated, message: "Use 'ButtonStateRole' instead.") + public typealias Role = ButtonStateRole + } -extension ButtonStateAction { - @available(*, deprecated, message: "Use 'ButtonState.withAction' instead.") - public typealias ActionType = _ActionType -} + extension ButtonStateAction { + @available(*, deprecated, message: "Use 'ButtonState.withAction' instead.") + public typealias ActionType = _ActionType + } -// NB: Deprecated after 0.3.0 + // NB: Deprecated after 0.3.0 -extension AlertState { - @available(*, deprecated, message: "Use 'ButtonState' instead.") - public typealias Button = ButtonState + extension AlertState { + @available(*, deprecated, message: "Use 'ButtonState' instead.") + public typealias Button = ButtonState - @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") - public typealias ButtonAction = ButtonStateAction + @available(*, deprecated, message: "Use 'ButtonStateAction' instead.") + public typealias ButtonAction = ButtonStateAction - @available(*, deprecated, message: "Use 'ButtonStateRole' instead.") - public typealias ButtonRole = ButtonStateRole + @available(*, deprecated, message: "Use 'ButtonStateRole' instead.") + public typealias ButtonRole = ButtonStateRole - @available( - iOS, introduced: 15, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - macOS, - introduced: 12, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - @available( - tvOS, introduced: 15, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - watchOS, - introduced: 8, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - public init( - title: TextState, - message: TextState? = nil, - buttons: [ButtonState] - ) { - self.init( - id: UUID(), - buttons: buttons, - message: message, - title: title + @available( + iOS, introduced: 15, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." ) - } - - @available( - iOS, introduced: 13, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - macOS, - introduced: 10.15, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - @available( - tvOS, introduced: 13, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - public init( - title: TextState, - message: TextState? = nil, - dismissButton: ButtonState? = nil - ) { - self.init( - id: UUID(), - buttons: dismissButton.map { [$0] } ?? [], - message: message, - title: title + @available( + macOS, + introduced: 12, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." ) - } - - @available( - iOS, introduced: 13, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - macOS, - introduced: 10.15, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - @available( - tvOS, introduced: 13, deprecated: 100000, message: "Use 'init(title:actions:message:)' instead." - ) - @available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." - ) - public init( - title: TextState, - message: TextState? = nil, - primaryButton: ButtonState, - secondaryButton: ButtonState - ) { - self.init( - id: UUID(), - buttons: [primaryButton, secondaryButton], - message: message, - title: title + @available( + tvOS, introduced: 15, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." ) - } -} - -@available( - iOS, - introduced: 13, - deprecated: 100000, - message: "Use 'ButtonState.init(role:action:label:)' instead." -) -@available( - macOS, introduced: 10.15, - deprecated: 100000, - message: "Use 'ButtonState.init(role:action:label:)' instead." -) -@available( - tvOS, - introduced: 13, - deprecated: 100000, - message: "Use 'ButtonState.init(role:action:label:)' instead." -) -@available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "Use 'ButtonState.init(role:action:label:)' instead." -) -extension ButtonState { - public static func cancel( - _ label: TextState, action: ButtonStateAction = .send(nil) - ) -> Self { - Self(role: .cancel, action: action) { - label + @available( + watchOS, + introduced: 8, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + public init( + title: TextState, + message: TextState? = nil, + buttons: [ButtonState] + ) { + self.init( + id: UUID(), + buttons: buttons, + message: message, + title: title + ) } - } - public static func `default`( - _ label: TextState, action: ButtonStateAction = .send(nil) - ) -> Self { - Self(action: action) { - label + @available( + iOS, introduced: 13, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + macOS, + introduced: 10.15, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + tvOS, introduced: 13, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + public init( + title: TextState, + message: TextState? = nil, + dismissButton: ButtonState? = nil + ) { + self.init( + id: UUID(), + buttons: dismissButton.map { [$0] } ?? [], + message: message, + title: title + ) } - } - public static func destructive( - _ label: TextState, action: ButtonStateAction = .send(nil) - ) -> Self { - Self(role: .destructive, action: action) { - label + @available( + iOS, introduced: 13, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + macOS, + introduced: 10.15, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + tvOS, introduced: 13, deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + public init( + title: TextState, + message: TextState? = nil, + primaryButton: ButtonState, + secondaryButton: ButtonState + ) { + self.init( + id: UUID(), + buttons: [primaryButton, secondaryButton], + message: message, + title: title + ) } } -} - -@available(iOS 13, *) -@available(macOS 12, *) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ConfirmationDialogState { - @available(*, deprecated, message: "Use 'ButtonState' instead.") - public typealias Button = ButtonState - - @available(*, deprecated, renamed: "ConfirmationDialogStateTitleVisibility") - public typealias Visibility = ConfirmationDialogStateTitleVisibility @available( iOS, introduced: 13, deprecated: 100000, - message: "Use 'init(titleVisibility:title:actions:message:)' instead." + message: "Use 'ButtonState.init(role:action:label:)' instead." ) @available( - macOS, - introduced: 12, + macOS, introduced: 10.15, deprecated: 100000, - message: "Use 'init(titleVisibility:title:actions:message:)' instead." + message: "Use 'ButtonState.init(role:action:label:)' instead." ) @available( tvOS, introduced: 13, deprecated: 100000, - message: "Use 'init(titleVisibility:title:actions:message:)' instead." + message: "Use 'ButtonState.init(role:action:label:)' instead." ) @available( watchOS, introduced: 6, deprecated: 100000, - message: "Use 'init(titleVisibility:title:actions:message:)' instead." + message: "Use 'ButtonState.init(role:action:label:)' instead." ) - public init( - title: TextState, - titleVisibility: ConfirmationDialogStateTitleVisibility, - message: TextState? = nil, - buttons: [ButtonState] = [] - ) { - self.init( - id: UUID(), - buttons: buttons, - message: message, - title: title, - titleVisibility: titleVisibility + extension ButtonState { + public static func cancel( + _ label: TextState, action: ButtonStateAction = .send(nil) + ) -> Self { + Self(role: .cancel, action: action) { + label + } + } + + public static func `default`( + _ label: TextState, action: ButtonStateAction = .send(nil) + ) -> Self { + Self(action: action) { + label + } + } + + public static func destructive( + _ label: TextState, action: ButtonStateAction = .send(nil) + ) -> Self { + Self(role: .destructive, action: action) { + label + } + } + } + + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState { + @available(*, deprecated, message: "Use 'ButtonState' instead.") + public typealias Button = ButtonState + + @available(*, deprecated, renamed: "ConfirmationDialogStateTitleVisibility") + public typealias Visibility = ConfirmationDialogStateTitleVisibility + + @available( + iOS, + introduced: 13, + deprecated: 100000, + message: "Use 'init(titleVisibility:title:actions:message:)' instead." + ) + @available( + macOS, + introduced: 12, + deprecated: 100000, + message: "Use 'init(titleVisibility:title:actions:message:)' instead." + ) + @available( + tvOS, + introduced: 13, + deprecated: 100000, + message: "Use 'init(titleVisibility:title:actions:message:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "Use 'init(titleVisibility:title:actions:message:)' instead." + ) + public init( + title: TextState, + titleVisibility: ConfirmationDialogStateTitleVisibility, + message: TextState? = nil, + buttons: [ButtonState] = [] + ) { + self.init( + id: UUID(), + buttons: buttons, + message: message, + title: title, + titleVisibility: titleVisibility + ) + } + + @available( + iOS, + introduced: 13, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + macOS, + introduced: 12, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." ) + @available( + tvOS, + introduced: 13, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "Use 'init(title:actions:message:)' instead." + ) + public init( + title: TextState, + message: TextState? = nil, + buttons: [ButtonState] = [] + ) { + self.init( + id: UUID(), + buttons: buttons, + message: message, + title: title, + titleVisibility: .automatic + ) + } } + @available(iOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") + @available(macOS, introduced: 12, unavailable) + @available(tvOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") + @available(watchOS, introduced: 6, deprecated: 100000, renamed: "ConfirmationDialogState") + public typealias ActionSheetState = ConfirmationDialogState + @available( iOS, introduced: 13, deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." + message: + "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." ) @available( macOS, introduced: 12, - deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." + unavailable ) @available( tvOS, introduced: 13, deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." + message: + "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." ) @available( watchOS, introduced: 6, deprecated: 100000, - message: "Use 'init(title:actions:message:)' instead." + message: + "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." ) - public init( - title: TextState, - message: TextState? = nil, - buttons: [ButtonState] = [] - ) { - self.init( - id: UUID(), - buttons: buttons, - message: message, - title: title, - titleVisibility: .automatic - ) - } -} - -@available(iOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") -@available(macOS, introduced: 12, unavailable) -@available(tvOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") -@available(watchOS, introduced: 6, deprecated: 100000, renamed: "ConfirmationDialogState") -public typealias ActionSheetState = ConfirmationDialogState - -@available( - iOS, - introduced: 13, - deprecated: 100000, - message: - "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." -) -@available( - macOS, - introduced: 12, - unavailable -) -@available( - tvOS, - introduced: 13, - deprecated: 100000, - message: - "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." -) -@available( - watchOS, - introduced: 6, - deprecated: 100000, - message: - "use 'View.confirmationDialog(title:isPresented:titleVisibility:presenting::actions:)' instead." -) -extension ActionSheet { - public init( - _ state: ConfirmationDialogState, - action: @escaping (Action?) -> Void - ) { - self.init( - title: Text(state.title), - message: state.message.map { Text($0) }, - buttons: state.buttons.map { .init($0, action: action) } - ) + extension ActionSheet { + public init( + _ state: ConfirmationDialogState, + action: @escaping (Action?) -> Void + ) { + self.init( + title: Text(state.title), + message: state.message.map { Text($0) }, + buttons: state.buttons.map { .init($0, action: action) } + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index e0ae009c7b..5d188c1ebb 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -1,74 +1,74 @@ #if canImport(SwiftUI) -@_spi(RuntimeWarn) -@_transparent -@inline(__always) -public func runtimeWarn( - _ message: @autoclosure () -> String, - category: String? = "SwiftUINavigation", - file: StaticString? = nil, - line: UInt? = nil -) { - #if DEBUG - let message = message() - let category = category ?? "Runtime Warning" - if _XCTIsTesting { - if let file = file, let line = line { - XCTFail(message, file: file, line: line) + @_spi(RuntimeWarn) + @_transparent + @inline(__always) + public func runtimeWarn( + _ message: @autoclosure () -> String, + category: String? = "SwiftUINavigation", + file: StaticString? = nil, + line: UInt? = nil + ) { + #if DEBUG + let message = message() + let category = category ?? "Runtime Warning" + if _XCTIsTesting { + if let file = file, let line = line { + XCTFail(message, file: file, line: line) + } else { + XCTFail(message) + } } else { - XCTFail(message) + #if canImport(os) + os_log( + .fault, + dso: dso, + log: OSLog(subsystem: "com.apple.runtime-issues", category: category), + "%@", + message + ) + #else + fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr) + #endif } - } else { - #if canImport(os) - os_log( - .fault, - dso: dso, - log: OSLog(subsystem: "com.apple.runtime-issues", category: category), - "%@", - message - ) - #else - fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr) - #endif - } - #endif -} + #endif + } -#if DEBUG - import XCTestDynamicOverlay + #if DEBUG + import XCTestDynamicOverlay - #if canImport(os) - import os - import Foundation + #if canImport(os) + import os + import Foundation - // NB: Xcode runtime warnings offer a much better experience than traditional assertions and - // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. - // To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead. - // - // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc - @usableFromInline - let dso = { () -> UnsafeMutableRawPointer in - let count = _dyld_image_count() - for i in 0.. UnsafeMutableRawPointer in + let count = _dyld_image_count() + for i in 0..=5.7.1) - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - var toSwiftUI: SwiftUI.Font.Width { - switch self { - case .compressed: return .compressed - case .condensed: return .condensed - case .expanded: return .expanded - case .standard: return .standard + public enum FontWidth: String, Equatable, Hashable, Sendable { + case compressed + case condensed + case expanded + case standard + + #if swift(>=5.7.1) + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + var toSwiftUI: SwiftUI.Font.Width { + switch self { + case .compressed: return .compressed + case .condensed: return .condensed + case .expanded: return .expanded + case .standard: return .standard + } } - } - #endif - } - - public enum LineStylePattern: String, Equatable, Hashable, Sendable { - case dash - case dashDot - case dashDotDot - case dot - case solid - - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - var toSwiftUI: SwiftUI.Text.LineStyle.Pattern { - switch self { - case .dash: return .dash - case .dashDot: return .dashDot - case .dashDotDot: return .dashDotDot - case .dot: return .dot - case .solid: return .solid - } + #endif } - } - - // NB: LocalizedStringKey is documented as being Sendable, but its conformance appears to be - // unavailable. - fileprivate enum Storage: Equatable, Hashable, @unchecked Sendable { - indirect case concatenated(TextState, TextState) - case localized(LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?) - case verbatim(String) - - static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.concatenated(l1, l2), .concatenated(r1, r2)): - return l1 == r1 && l2 == r2 - case let (.localized(lk, lt, lb, lc), .localized(rk, rt, rb, rc)): - return lk.formatted(tableName: lt, bundle: lb, comment: lc) - == rk.formatted(tableName: rt, bundle: rb, comment: rc) + public enum LineStylePattern: String, Equatable, Hashable, Sendable { + case dash + case dashDot + case dashDotDot + case dot + case solid - case let (.verbatim(lhs), .verbatim(rhs)): - return lhs == rhs - - case let (.localized(key, tableName, bundle, comment), .verbatim(string)), - let (.verbatim(string), .localized(key, tableName, bundle, comment)): - return key.formatted(tableName: tableName, bundle: bundle, comment: comment) == string - - // NB: We do not attempt to equate concatenated cases. - default: - return false + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + var toSwiftUI: SwiftUI.Text.LineStyle.Pattern { + switch self { + case .dash: return .dash + case .dashDot: return .dashDot + case .dashDotDot: return .dashDotDot + case .dot: return .dot + case .solid: return .solid + } } } - func hash(into hasher: inout Hasher) { - enum Key { - case concatenated - case localized - case verbatim + // NB: LocalizedStringKey is documented as being Sendable, but its conformance appears to be + // unavailable. + fileprivate enum Storage: Equatable, Hashable, @unchecked Sendable { + indirect case concatenated(TextState, TextState) + case localized( + LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?) + case verbatim(String) + + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.concatenated(l1, l2), .concatenated(r1, r2)): + return l1 == r1 && l2 == r2 + + case let (.localized(lk, lt, lb, lc), .localized(rk, rt, rb, rc)): + return lk.formatted(tableName: lt, bundle: lb, comment: lc) + == rk.formatted(tableName: rt, bundle: rb, comment: rc) + + case let (.verbatim(lhs), .verbatim(rhs)): + return lhs == rhs + + case let (.localized(key, tableName, bundle, comment), .verbatim(string)), + let (.verbatim(string), .localized(key, tableName, bundle, comment)): + return key.formatted(tableName: tableName, bundle: bundle, comment: comment) == string + + // NB: We do not attempt to equate concatenated cases. + default: + return false + } } - switch self { - case let (.concatenated(first, second)): - hasher.combine(Key.concatenated) - hasher.combine(first) - hasher.combine(second) - - case let .localized(key, tableName, bundle, comment): - hasher.combine(Key.localized) - hasher.combine(key.formatted(tableName: tableName, bundle: bundle, comment: comment)) + func hash(into hasher: inout Hasher) { + enum Key { + case concatenated + case localized + case verbatim + } - case let .verbatim(string): - hasher.combine(Key.verbatim) - hasher.combine(string) + switch self { + case let (.concatenated(first, second)): + hasher.combine(Key.concatenated) + hasher.combine(first) + hasher.combine(second) + + case let .localized(key, tableName, bundle, comment): + hasher.combine(Key.localized) + hasher.combine(key.formatted(tableName: tableName, bundle: bundle, comment: comment)) + + case let .verbatim(string): + hasher.combine(Key.verbatim) + hasher.combine(string) + } } } } -} -// MARK: - API + // MARK: - API -extension TextState { - public init(verbatim content: String) { - self.storage = .verbatim(content) - } + extension TextState { + public init(verbatim content: String) { + self.storage = .verbatim(content) + } - @_disfavoredOverload - public init(_ content: S) { - self.init(verbatim: String(content)) - } + @_disfavoredOverload + public init(_ content: S) { + self.init(verbatim: String(content)) + } - public init( - _ key: LocalizedStringKey, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ) { - self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment) - } + public init( + _ key: LocalizedStringKey, + tableName: String? = nil, + bundle: Bundle? = nil, + comment: StaticString? = nil + ) { + self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment) + } - public static func + (lhs: Self, rhs: Self) -> Self { - .init(storage: .concatenated(lhs, rhs)) - } + public static func + (lhs: Self, rhs: Self) -> Self { + .init(storage: .concatenated(lhs, rhs)) + } - public func baselineOffset(_ baselineOffset: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.baselineOffset(baselineOffset)) - return `self` - } + public func baselineOffset(_ baselineOffset: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.baselineOffset(baselineOffset)) + return `self` + } - public func bold() -> Self { - var `self` = self - `self`.modifiers.append(.bold(isActive: true)) - return `self` - } + public func bold() -> Self { + var `self` = self + `self`.modifiers.append(.bold(isActive: true)) + return `self` + } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public func bold(isActive: Bool) -> Self { - var `self` = self - `self`.modifiers.append(.bold(isActive: isActive)) - return `self` - } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public func bold(isActive: Bool) -> Self { + var `self` = self + `self`.modifiers.append(.bold(isActive: isActive)) + return `self` + } - public func font(_ font: Font?) -> Self { - var `self` = self - `self`.modifiers.append(.font(font)) - return `self` - } + public func font(_ font: Font?) -> Self { + var `self` = self + `self`.modifiers.append(.font(font)) + return `self` + } - public func fontDesign(_ design: Font.Design?) -> Self { - var `self` = self - `self`.modifiers.append(.fontDesign(design)) - return `self` - } + public func fontDesign(_ design: Font.Design?) -> Self { + var `self` = self + `self`.modifiers.append(.fontDesign(design)) + return `self` + } - public func fontWeight(_ weight: Font.Weight?) -> Self { - var `self` = self - `self`.modifiers.append(.fontWeight(weight)) - return `self` - } + public func fontWeight(_ weight: Font.Weight?) -> Self { + var `self` = self + `self`.modifiers.append(.fontWeight(weight)) + return `self` + } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public func fontWidth(_ width: FontWidth?) -> Self { - var `self` = self - `self`.modifiers.append(.fontWidth(width)) - return `self` - } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public func fontWidth(_ width: FontWidth?) -> Self { + var `self` = self + `self`.modifiers.append(.fontWidth(width)) + return `self` + } - public func foregroundColor(_ color: Color?) -> Self { - var `self` = self - `self`.modifiers.append(.foregroundColor(color)) - return `self` - } + public func foregroundColor(_ color: Color?) -> Self { + var `self` = self + `self`.modifiers.append(.foregroundColor(color)) + return `self` + } - public func italic() -> Self { - var `self` = self - `self`.modifiers.append(.italic(isActive: true)) - return `self` - } + public func italic() -> Self { + var `self` = self + `self`.modifiers.append(.italic(isActive: true)) + return `self` + } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public func italic(isActive: Bool) -> Self { - var `self` = self - `self`.modifiers.append(.italic(isActive: isActive)) - return `self` - } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public func italic(isActive: Bool) -> Self { + var `self` = self + `self`.modifiers.append(.italic(isActive: isActive)) + return `self` + } - public func kerning(_ kerning: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.kerning(kerning)) - return `self` - } + public func kerning(_ kerning: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.kerning(kerning)) + return `self` + } - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func monospacedDigit() -> Self { - var `self` = self - `self`.modifiers.append(.monospacedDigit) - return `self` - } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func monospacedDigit() -> Self { + var `self` = self + `self`.modifiers.append(.monospacedDigit) + return `self` + } - public func strikethrough(_ isActive: Bool = true, color: Color? = nil) -> Self { - var `self` = self - `self`.modifiers.append(.strikethrough(isActive: isActive, pattern: .solid, color: color)) - return `self` - } + public func strikethrough(_ isActive: Bool = true, color: Color? = nil) -> Self { + var `self` = self + `self`.modifiers.append(.strikethrough(isActive: isActive, pattern: .solid, color: color)) + return `self` + } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public func strikethrough( - _ isActive: Bool = true, - pattern: LineStylePattern, - color: Color? = nil - ) -> Self { - var `self` = self - `self`.modifiers.append(.strikethrough(isActive: isActive, pattern: pattern, color: color)) - return `self` - } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public func strikethrough( + _ isActive: Bool = true, + pattern: LineStylePattern, + color: Color? = nil + ) -> Self { + var `self` = self + `self`.modifiers.append(.strikethrough(isActive: isActive, pattern: pattern, color: color)) + return `self` + } - public func tracking(_ tracking: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.tracking(tracking)) - return `self` - } + public func tracking(_ tracking: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.tracking(tracking)) + return `self` + } - public func underline(_ isActive: Bool = true, color: Color? = nil) -> Self { - var `self` = self - `self`.modifiers.append(.underline(isActive: isActive, pattern: .solid, color: color)) - return `self` - } + public func underline(_ isActive: Bool = true, color: Color? = nil) -> Self { + var `self` = self + `self`.modifiers.append(.underline(isActive: isActive, pattern: .solid, color: color)) + return `self` + } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public func underline( - _ isActive: Bool = true, - pattern: LineStylePattern, - color: Color? = nil - ) -> Self { - var `self` = self - `self`.modifiers.append(.underline(isActive: isActive, pattern: pattern, color: color)) - return `self` + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public func underline( + _ isActive: Bool = true, + pattern: LineStylePattern, + color: Color? = nil + ) -> Self { + var `self` = self + `self`.modifiers.append(.underline(isActive: isActive, pattern: pattern, color: color)) + return `self` + } } -} -// MARK: Accessibility + // MARK: Accessibility -extension TextState { - public enum AccessibilityTextContentType: String, Equatable, Hashable, Sendable { - case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing + extension TextState { + public enum AccessibilityTextContentType: String, Equatable, Hashable, Sendable { + case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing - #if compiler(>=5.5.1) - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - var toSwiftUI: SwiftUI.AccessibilityTextContentType { - switch self { - case .console: return .console - case .fileSystem: return .fileSystem - case .messaging: return .messaging - case .narrative: return .narrative - case .plain: return .plain - case .sourceCode: return .sourceCode - case .spreadsheet: return .spreadsheet - case .wordProcessing: return .wordProcessing + #if compiler(>=5.5.1) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.AccessibilityTextContentType { + switch self { + case .console: return .console + case .fileSystem: return .fileSystem + case .messaging: return .messaging + case .narrative: return .narrative + case .plain: return .plain + case .sourceCode: return .sourceCode + case .spreadsheet: return .spreadsheet + case .wordProcessing: return .wordProcessing + } } - } - #endif - } + #endif + } - public enum AccessibilityHeadingLevel: String, Equatable, Hashable, Sendable { - case h1, h2, h3, h4, h5, h6, unspecified + public enum AccessibilityHeadingLevel: String, Equatable, Hashable, Sendable { + case h1, h2, h3, h4, h5, h6, unspecified - #if compiler(>=5.5.1) - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - var toSwiftUI: SwiftUI.AccessibilityHeadingLevel { - switch self { - case .h1: return .h1 - case .h2: return .h2 - case .h3: return .h3 - case .h4: return .h4 - case .h5: return .h5 - case .h6: return .h6 - case .unspecified: return .unspecified + #if compiler(>=5.5.1) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.AccessibilityHeadingLevel { + switch self { + case .h1: return .h1 + case .h2: return .h2 + case .h3: return .h3 + case .h4: return .h4 + case .h5: return .h5 + case .h6: return .h6 + case .unspecified: return .unspecified + } } - } - #endif - } -} - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension TextState { - public func accessibilityHeading(_ headingLevel: AccessibilityHeadingLevel) -> Self { - var `self` = self - `self`.modifiers.append(.accessibilityHeading(headingLevel)) - return `self` + #endif + } } - public func accessibilityLabel(_ label: Self) -> Self { - var `self` = self - `self`.modifiers.append(.accessibilityLabel(label)) - return `self` - } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension TextState { + public func accessibilityHeading(_ headingLevel: AccessibilityHeadingLevel) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityHeading(headingLevel)) + return `self` + } - public func accessibilityLabel(_ string: String) -> Self { - var `self` = self - `self`.modifiers.append(.accessibilityLabel(.init(string))) - return `self` - } + public func accessibilityLabel(_ label: Self) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(label)) + return `self` + } - public func accessibilityLabel(_ string: S) -> Self { - var `self` = self - `self`.modifiers.append(.accessibilityLabel(.init(string))) - return `self` - } + public func accessibilityLabel(_ string: String) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(.init(string))) + return `self` + } - public func accessibilityLabel( - _ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, - comment: StaticString? = nil - ) -> Self { - var `self` = self - `self`.modifiers.append( - .accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment))) - return `self` - } + public func accessibilityLabel(_ string: S) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(.init(string))) + return `self` + } - public func accessibilityTextContentType(_ type: AccessibilityTextContentType) -> Self { - var `self` = self - `self`.modifiers.append(.accessibilityTextContentType(type)) - return `self` - } + public func accessibilityLabel( + _ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, + comment: StaticString? = nil + ) -> Self { + var `self` = self + `self`.modifiers.append( + .accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment))) + return `self` + } - public func speechAdjustedPitch(_ value: Double) -> Self { - var `self` = self - `self`.modifiers.append(.speechAdjustedPitch(value)) - return `self` - } + public func accessibilityTextContentType(_ type: AccessibilityTextContentType) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityTextContentType(type)) + return `self` + } - public func speechAlwaysIncludesPunctuation(_ value: Bool = true) -> Self { - var `self` = self - `self`.modifiers.append(.speechAlwaysIncludesPunctuation(value)) - return `self` - } + public func speechAdjustedPitch(_ value: Double) -> Self { + var `self` = self + `self`.modifiers.append(.speechAdjustedPitch(value)) + return `self` + } - public func speechAnnouncementsQueued(_ value: Bool = true) -> Self { - var `self` = self - `self`.modifiers.append(.speechAnnouncementsQueued(value)) - return `self` - } + public func speechAlwaysIncludesPunctuation(_ value: Bool = true) -> Self { + var `self` = self + `self`.modifiers.append(.speechAlwaysIncludesPunctuation(value)) + return `self` + } - public func speechSpellsOutCharacters(_ value: Bool = true) -> Self { - var `self` = self - `self`.modifiers.append(.speechSpellsOutCharacters(value)) - return `self` + public func speechAnnouncementsQueued(_ value: Bool = true) -> Self { + var `self` = self + `self`.modifiers.append(.speechAnnouncementsQueued(value)) + return `self` + } + + public func speechSpellsOutCharacters(_ value: Bool = true) -> Self { + var `self` = self + `self`.modifiers.append(.speechSpellsOutCharacters(value)) + return `self` + } } -} - -extension Text { - public init(_ state: TextState) { - let text: Text - switch state.storage { - case let .concatenated(first, second): - text = Text(first) + Text(second) - case let .localized(content, tableName, bundle, comment): - text = .init(content, tableName: tableName, bundle: bundle, comment: comment) - case let .verbatim(content): - text = .init(verbatim: content) - } - self = state.modifiers.reduce(text) { text, modifier in - switch modifier { - #if compiler(>=5.5.1) - case let .accessibilityHeading(level): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.accessibilityHeading(level.toSwiftUI) - } else { + + extension Text { + public init(_ state: TextState) { + let text: Text + switch state.storage { + case let .concatenated(first, second): + text = Text(first) + Text(second) + case let .localized(content, tableName, bundle, comment): + text = .init(content, tableName: tableName, bundle: bundle, comment: comment) + case let .verbatim(content): + text = .init(verbatim: content) + } + self = state.modifiers.reduce(text) { text, modifier in + switch modifier { + #if compiler(>=5.5.1) + case let .accessibilityHeading(level): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.accessibilityHeading(level.toSwiftUI) + } else { + return text + } + case let .accessibilityLabel(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + switch value.storage { + case let .verbatim(string): + return text.accessibilityLabel(string) + case let .localized(key, tableName, bundle, comment): + return text.accessibilityLabel( + Text(key, tableName: tableName, bundle: bundle, comment: comment)) + case .concatenated(_, _): + assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") + return text + } + } else { + return text + } + case let .accessibilityTextContentType(type): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.accessibilityTextContentType(type.toSwiftUI) + } else { + return text + } + #else + case .accessibilityHeading, + .accessibilityLabel, + .accessibilityTextContentType: return text - } - case let .accessibilityLabel(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - switch value.storage { - case let .verbatim(string): - return text.accessibilityLabel(string) - case let .localized(key, tableName, bundle, comment): - return text.accessibilityLabel( - Text(key, tableName: tableName, bundle: bundle, comment: comment)) - case .concatenated(_, _): - assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") + #endif + case let .baselineOffset(baselineOffset): + return text.baselineOffset(baselineOffset) + case let .bold(isActive): + #if swift(>=5.7.1) + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.bold(isActive) + } else { + return text.bold() + } + #else + _ = isActive + return text.bold() + #endif + case let .font(font): + return text.font(font) + case let .fontDesign(design): + #if swift(>=5.7.1) + if #available(iOS 16.1, macOS 13, tvOS 16.1, watchOS 9.1, *) { + return text.fontDesign(design) + } else { return text } - } else { + #else + _ = design return text - } - case let .accessibilityTextContentType(type): + #endif + case let .fontWeight(weight): + return text.fontWeight(weight) + case let .fontWidth(width): + #if swift(>=5.7.1) + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.fontWidth(width?.toSwiftUI) + } else { + return text + } + #else + _ = width + return text + #endif + case let .foregroundColor(color): + return text.foregroundColor(color) + case let .italic(isActive): + #if swift(>=5.7.1) + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.italic(isActive) + } else { + return text.italic() + } + #else + _ = isActive + return text.italic() + #endif + case let .kerning(kerning): + return text.kerning(kerning) + case .monospacedDigit: if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.accessibilityTextContentType(type.toSwiftUI) + return text.monospacedDigit() } else { return text } - #else - case .accessibilityHeading, - .accessibilityLabel, - .accessibilityTextContentType: - return text - #endif - case let .baselineOffset(baselineOffset): - return text.baselineOffset(baselineOffset) - case let .bold(isActive): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.bold(isActive) + case let .speechAdjustedPitch(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.speechAdjustedPitch(value) } else { - return text.bold() + return text } - #else - _ = isActive - return text.bold() - #endif - case let .font(font): - return text.font(font) - case let .fontDesign(design): - #if swift(>=5.7.1) - if #available(iOS 16.1, macOS 13, tvOS 16.1, watchOS 9.1, *) { - return text.fontDesign(design) + case let .speechAlwaysIncludesPunctuation(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.speechAlwaysIncludesPunctuation(value) } else { return text } - #else - _ = design - return text - #endif - case let .fontWeight(weight): - return text.fontWeight(weight) - case let .fontWidth(width): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.fontWidth(width?.toSwiftUI) + case let .speechAnnouncementsQueued(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.speechAnnouncementsQueued(value) } else { return text } - #else - _ = width - return text - #endif - case let .foregroundColor(color): - return text.foregroundColor(color) - case let .italic(isActive): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.italic(isActive) + case let .speechSpellsOutCharacters(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.speechSpellsOutCharacters(value) } else { - return text.italic() + return text } - #else - _ = isActive - return text.italic() - #endif - case let .kerning(kerning): - return text.kerning(kerning) - case .monospacedDigit: - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.monospacedDigit() - } else { - return text - } - case let .speechAdjustedPitch(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.speechAdjustedPitch(value) - } else { - return text - } - case let .speechAlwaysIncludesPunctuation(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.speechAlwaysIncludesPunctuation(value) - } else { - return text - } - case let .speechAnnouncementsQueued(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.speechAnnouncementsQueued(value) - } else { - return text - } - case let .speechSpellsOutCharacters(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.speechSpellsOutCharacters(value) - } else { - return text - } - case let .strikethrough(isActive, pattern, color): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { - return text.strikethrough(isActive, pattern: pattern.toSwiftUI, color: color) - } else { + case let .strikethrough(isActive, pattern, color): + #if swift(>=5.7.1) + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { + return text.strikethrough(isActive, pattern: pattern.toSwiftUI, color: color) + } else { + return text.strikethrough(isActive, color: color) + } + #else + _ = pattern return text.strikethrough(isActive, color: color) - } - #else - _ = pattern - return text.strikethrough(isActive, color: color) - #endif - case let .tracking(tracking): - return text.tracking(tracking) - case let .underline(isActive, pattern, color): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { - return text.underline(isActive, pattern: pattern.toSwiftUI, color: color) - } else { - return text.underline(isActive, color: color) - } - #else - _ = pattern - return text.strikethrough(isActive, color: color) - #endif + #endif + case let .tracking(tracking): + return text.tracking(tracking) + case let .underline(isActive, pattern, color): + #if swift(>=5.7.1) + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { + return text.underline(isActive, pattern: pattern.toSwiftUI, color: color) + } else { + return text.underline(isActive, color: color) + } + #else + _ = pattern + return text.strikethrough(isActive, color: color) + #endif + } } } } -} -extension String { - public init(state: TextState, locale: Locale? = nil) { - switch state.storage { - case let .concatenated(lhs, rhs): - self = String(state: lhs, locale: locale) + String(state: rhs, locale: locale) + extension String { + public init(state: TextState, locale: Locale? = nil) { + switch state.storage { + case let .concatenated(lhs, rhs): + self = String(state: lhs, locale: locale) + String(state: rhs, locale: locale) - case let .localized(key, tableName, bundle, comment): - self = key.formatted( - locale: locale, - tableName: tableName, - bundle: bundle, - comment: comment - ) + case let .localized(key, tableName, bundle, comment): + self = key.formatted( + locale: locale, + tableName: tableName, + bundle: bundle, + comment: comment + ) - case let .verbatim(string): - self = string + case let .verbatim(string): + self = string + } } } -} - -extension LocalizedStringKey { - // NB: `LocalizedStringKey` conforms to `Equatable` but returns false for equivalent format - // strings. To account for this we reflect on it to extract and string-format its storage. - fileprivate func formatted( - locale: Locale? = nil, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ) -> String { - let children = Array(Mirror(reflecting: self).children) - let key = children[0].value as! String - let arguments: [CVarArg] = Array(Mirror(reflecting: children[2].value).children) - .compactMap { - let children = Array(Mirror(reflecting: $0.value).children) - let value: Any - let formatter: Formatter? - // `LocalizedStringKey.FormatArgument` differs depending on OS/platform. - if children[0].label == "storage" { - (value, formatter) = - Array(Mirror(reflecting: children[0].value).children)[0].value as! (Any, Formatter?) - } else { - value = children[0].value - formatter = children[1].value as? Formatter + + extension LocalizedStringKey { + // NB: `LocalizedStringKey` conforms to `Equatable` but returns false for equivalent format + // strings. To account for this we reflect on it to extract and string-format its storage. + fileprivate func formatted( + locale: Locale? = nil, + tableName: String? = nil, + bundle: Bundle? = nil, + comment: StaticString? = nil + ) -> String { + let children = Array(Mirror(reflecting: self).children) + let key = children[0].value as! String + let arguments: [CVarArg] = Array(Mirror(reflecting: children[2].value).children) + .compactMap { + let children = Array(Mirror(reflecting: $0.value).children) + let value: Any + let formatter: Formatter? + // `LocalizedStringKey.FormatArgument` differs depending on OS/platform. + if children[0].label == "storage" { + (value, formatter) = + Array(Mirror(reflecting: children[0].value).children)[0].value as! (Any, Formatter?) + } else { + value = children[0].value + formatter = children[1].value as? Formatter + } + return formatter?.string(for: value) ?? value as! CVarArg } - return formatter?.string(for: value) ?? value as! CVarArg - } - let format = NSLocalizedString( - key, - tableName: tableName, - bundle: bundle ?? .main, - value: "", - comment: comment.map(String.init) ?? "" - ) - return String(format: format, locale: locale, arguments: arguments) + let format = NSLocalizedString( + key, + tableName: tableName, + bundle: bundle ?? .main, + value: "", + comment: comment.map(String.init) ?? "" + ) + return String(format: format, locale: locale, arguments: arguments) + } } -} -// MARK: - CustomDumpRepresentable + // MARK: - CustomDumpRepresentable -extension TextState: CustomDumpRepresentable { - public var customDumpValue: Any { - func dumpHelp(_ textState: Self) -> String { - var output: String - switch textState.storage { - case let .concatenated(lhs, rhs): - output = dumpHelp(lhs) + dumpHelp(rhs) - case let .localized(key, tableName, bundle, comment): - output = key.formatted(tableName: tableName, bundle: bundle, comment: comment) - case let .verbatim(string): - output = string - } - func tag(_ name: String, attribute: String? = nil, _ value: String? = nil) { - output = """ - <\(name)\(attribute.map { " \($0)" } ?? "")\(value.map { "=\($0)" } ?? "")>\ - \(output)\ - - """ - } - for modifier in textState.modifiers { - switch modifier { - case let .accessibilityHeading(headingLevel): - tag("accessibility-heading-level", headingLevel.rawValue) - case let .accessibilityLabel(value): - tag("accessibility-label", dumpHelp(value)) - case let .accessibilityTextContentType(type): - tag("accessibility-text-content-type", type.rawValue) - case let .baselineOffset(baselineOffset): - tag("baseline-offset", "\(baselineOffset)") - case .bold(isActive: true), .fontWeight(.some(.bold)): - output = "**\(output)**" - case .font(.some): - break // TODO: capture Font description using DSL similar to TextState and print here - case let .fontDesign(.some(design)): - func describe(design: Font.Design) -> String { - switch design { - case .default: return "default" - case .serif: return "serif" - case .rounded: return "rounded" - case .monospaced: return "monospaced" - @unknown default: return "\(design)" + extension TextState: CustomDumpRepresentable { + public var customDumpValue: Any { + func dumpHelp(_ textState: Self) -> String { + var output: String + switch textState.storage { + case let .concatenated(lhs, rhs): + output = dumpHelp(lhs) + dumpHelp(rhs) + case let .localized(key, tableName, bundle, comment): + output = key.formatted(tableName: tableName, bundle: bundle, comment: comment) + case let .verbatim(string): + output = string + } + func tag(_ name: String, attribute: String? = nil, _ value: String? = nil) { + output = """ + <\(name)\(attribute.map { " \($0)" } ?? "")\(value.map { "=\($0)" } ?? "")>\ + \(output)\ + + """ + } + for modifier in textState.modifiers { + switch modifier { + case let .accessibilityHeading(headingLevel): + tag("accessibility-heading-level", headingLevel.rawValue) + case let .accessibilityLabel(value): + tag("accessibility-label", dumpHelp(value)) + case let .accessibilityTextContentType(type): + tag("accessibility-text-content-type", type.rawValue) + case let .baselineOffset(baselineOffset): + tag("baseline-offset", "\(baselineOffset)") + case .bold(isActive: true), .fontWeight(.some(.bold)): + output = "**\(output)**" + case .font(.some): + break // TODO: capture Font description using DSL similar to TextState and print here + case let .fontDesign(.some(design)): + func describe(design: Font.Design) -> String { + switch design { + case .default: return "default" + case .serif: return "serif" + case .rounded: return "rounded" + case .monospaced: return "monospaced" + @unknown default: return "\(design)" + } } - } - tag("font-design", describe(design: design)) - case let .fontWeight(.some(weight)): - func describe(weight: Font.Weight) -> String { - switch weight { - case .black: return "black" - case .bold: return "bold" - case .heavy: return "heavy" - case .light: return "light" - case .medium: return "medium" - case .regular: return "regular" - case .semibold: return "semibold" - case .thin: return "thin" - default: return "\(weight)" + tag("font-design", describe(design: design)) + case let .fontWeight(.some(weight)): + func describe(weight: Font.Weight) -> String { + switch weight { + case .black: return "black" + case .bold: return "bold" + case .heavy: return "heavy" + case .light: return "light" + case .medium: return "medium" + case .regular: return "regular" + case .semibold: return "semibold" + case .thin: return "thin" + default: return "\(weight)" + } } + tag("font-weight", describe(weight: weight)) + case let .fontWidth(.some(width)): + tag("font-width", width.rawValue) + case let .foregroundColor(.some(color)): + tag("foreground-color", "\(color)") + case .italic(isActive: true): + output = "_\(output)_" + case let .kerning(kerning): + tag("kerning", "\(kerning)") + case let .speechAdjustedPitch(value): + tag("speech-adjusted-pitch", "\(value)") + case .speechAlwaysIncludesPunctuation(true): + tag("speech-always-includes-punctuation") + case .speechAnnouncementsQueued(true): + tag("speech-announcements-queued") + case .speechSpellsOutCharacters(true): + tag("speech-spells-out-characters") + case let .strikethrough(isActive: true, pattern: _, color: .some(color)): + tag("s", attribute: "color", "\(color)") + case .strikethrough(isActive: true, pattern: _, color: .none): + output = "~~\(output)~~" + case let .tracking(tracking): + tag("tracking", "\(tracking)") + case let .underline(isActive: true, pattern: _, .some(color)): + tag("u", attribute: "color", "\(color)") + case .underline(isActive: true, pattern: _, color: .none): + tag("u") + case .bold(isActive: false), + .font(.none), + .fontDesign(.none), + .fontWeight(.none), + .fontWidth(.none), + .foregroundColor(.none), + .italic(isActive: false), + .monospacedDigit, + .speechAlwaysIncludesPunctuation(false), + .speechAnnouncementsQueued(false), + .speechSpellsOutCharacters(false), + .strikethrough(isActive: false, pattern: _, color: _), + .underline(isActive: false, pattern: _, color: _): + break } - tag("font-weight", describe(weight: weight)) - case let .fontWidth(.some(width)): - tag("font-width", width.rawValue) - case let .foregroundColor(.some(color)): - tag("foreground-color", "\(color)") - case .italic(isActive: true): - output = "_\(output)_" - case let .kerning(kerning): - tag("kerning", "\(kerning)") - case let .speechAdjustedPitch(value): - tag("speech-adjusted-pitch", "\(value)") - case .speechAlwaysIncludesPunctuation(true): - tag("speech-always-includes-punctuation") - case .speechAnnouncementsQueued(true): - tag("speech-announcements-queued") - case .speechSpellsOutCharacters(true): - tag("speech-spells-out-characters") - case let .strikethrough(isActive: true, pattern: _, color: .some(color)): - tag("s", attribute: "color", "\(color)") - case .strikethrough(isActive: true, pattern: _, color: .none): - output = "~~\(output)~~" - case let .tracking(tracking): - tag("tracking", "\(tracking)") - case let .underline(isActive: true, pattern: _, .some(color)): - tag("u", attribute: "color", "\(color)") - case .underline(isActive: true, pattern: _, color: .none): - tag("u") - case .bold(isActive: false), - .font(.none), - .fontDesign(.none), - .fontWeight(.none), - .fontWidth(.none), - .foregroundColor(.none), - .italic(isActive: false), - .monospacedDigit, - .speechAlwaysIncludesPunctuation(false), - .speechAnnouncementsQueued(false), - .speechSpellsOutCharacters(false), - .strikethrough(isActive: false, pattern: _, color: _), - .underline(isActive: false, pattern: _, color: _): - break } + return output } - return output - } - return dumpHelp(self) + return dumpHelp(self) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 3828ad9d93..9690784768 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -1,73 +1,33 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUI -import SwiftUINavigation -import XCTest + import CustomDump + import SwiftUI + import SwiftUINavigation + import XCTest -final class AlertTests: XCTestCase { - func testAlertState() { - let alert = AlertState( - title: .init("Alert!"), - message: .init("Something went wrong..."), - primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), - secondaryButton: .cancel(.init("Cancel"), action: .send(false)) - ) - XCTAssertNoDifference( - alert, - AlertState( + final class AlertTests: XCTestCase { + func testAlertState() { + let alert = AlertState( title: .init("Alert!"), message: .init("Something went wrong..."), primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), secondaryButton: .cancel(.init("Cancel"), action: .send(false)) ) - ) - - var dump = "" - customDump(alert, to: &dump) - XCTAssertNoDifference( - dump, - """ - AlertState( - title: "Alert!", - actions: [ - [0]: ButtonState( - role: .destructive, - action: .send( - true, - animation: Animation.easeInOut - ), - label: "Destroy" - ), - [1]: ButtonState( - role: .cancel, - action: .send( - false - ), - label: "Cancel" - ) - ], - message: "Something went wrong..." - ) - """ - ) - - if #available(iOS 13, macOS 12, tvOS 13, watchOS 6, *) { - dump = "" - customDump( - ConfirmationDialogState( + XCTAssertNoDifference( + alert, + AlertState( title: .init("Alert!"), message: .init("Something went wrong..."), - buttons: [ - .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), - .cancel(.init("Cancel"), action: .send(false)), - ] - ), - to: &dump + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), + secondaryButton: .cancel(.init("Cancel"), action: .send(false)) + ) ) + + var dump = "" + customDump(alert, to: &dump) XCTAssertNoDifference( dump, """ - ConfirmationDialogState( + AlertState( title: "Alert!", actions: [ [0]: ButtonState( @@ -90,32 +50,72 @@ final class AlertTests: XCTestCase { ) """ ) + + if #available(iOS 13, macOS 12, tvOS 13, watchOS 6, *) { + dump = "" + customDump( + ConfirmationDialogState( + title: .init("Alert!"), + message: .init("Something went wrong..."), + buttons: [ + .destructive(.init("Destroy"), action: .send(true, animation: .easeInOut)), + .cancel(.init("Cancel"), action: .send(false)), + ] + ), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + ConfirmationDialogState( + title: "Alert!", + actions: [ + [0]: ButtonState( + role: .destructive, + action: .send( + true, + animation: Animation.easeInOut + ), + label: "Destroy" + ), + [1]: ButtonState( + role: .cancel, + action: .send( + false + ), + label: "Cancel" + ) + ], + message: "Something went wrong..." + ) + """ + ) + } } } -} -// NB: This is a compile time test to make sure that async action closures can be used in -// Swift <5.7. -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -private struct TestView: View { - @State var alert: AlertState? - enum AlertAction { - case confirm - case deny - } + // NB: This is a compile time test to make sure that async action closures can be used in + // Swift <5.7. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + private struct TestView: View { + @State var alert: AlertState? + enum AlertAction { + case confirm + case deny + } - var body: some View { - Text("") - .alert(unwrapping: self.$alert) { - await self.alertButtonTapped($0) - } - } + var body: some View { + Text("") + .alert(unwrapping: self.$alert) { + await self.alertButtonTapped($0) + } + } - private func alertButtonTapped(_ action: AlertAction?) async { - switch action { - case .some(.confirm), .some(.deny), .none: - break + private func alertButtonTapped(_ action: AlertAction?) async { + switch action { + case .some(.confirm), .some(.deny), .none: + break + } } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index c06cd76a3d..788a8faff8 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -1,34 +1,34 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUI -import SwiftUINavigation -import XCTest + import CustomDump + import SwiftUI + import SwiftUINavigation + import XCTest -@MainActor -final class ButtonStateTests: XCTestCase { - func testAsyncAnimationWarning() async { - XCTExpectFailure { - $0.compactDescription == """ - An animated action was performed asynchronously: … + @MainActor + final class ButtonStateTests: XCTestCase { + func testAsyncAnimationWarning() async { + XCTExpectFailure { + $0.compactDescription == """ + An animated action was performed asynchronously: … - Action: - ButtonStateAction.send( - (), - animation: Animation.easeInOut - ) + Action: + ButtonStateAction.send( + (), + animation: Animation.easeInOut + ) - Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, or \ - use 'SwiftUI.withAnimation' explicitly. - """ - } + Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, or \ + use 'SwiftUI.withAnimation' explicitly. + """ + } - let button = ButtonState(action: .send((), animation: .easeInOut)) { - TextState("Animate!") - } + let button = ButtonState(action: .send((), animation: .easeInOut)) { + TextState("Animate!") + } - await button.withAction { _ in - await Task.yield() + await button.withAction { _ in + await Task.yield() + } } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift index 71d8825d50..abd3bda8ea 100644 --- a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift +++ b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -1,64 +1,64 @@ #if canImport(SwiftUI) -import SwiftUI -import XCTest + import SwiftUI + import XCTest -@testable import SwiftUINavigation + @testable import SwiftUINavigation -final class SwiftUINavigationTests: XCTestCase { - func testBindingUnwrap() throws { - var value: Int? - let binding = Binding(get: { value }, set: { value = $0 }) + final class SwiftUINavigationTests: XCTestCase { + func testBindingUnwrap() throws { + var value: Int? + let binding = Binding(get: { value }, set: { value = $0 }) - XCTAssertNil(Binding(unwrapping: binding)) + XCTAssertNil(Binding(unwrapping: binding)) - binding.wrappedValue = 1 - let unwrapped = try XCTUnwrap(Binding(unwrapping: binding)) - XCTAssertEqual(binding.wrappedValue, 1) - XCTAssertEqual(unwrapped.wrappedValue, 1) + binding.wrappedValue = 1 + let unwrapped = try XCTUnwrap(Binding(unwrapping: binding)) + XCTAssertEqual(binding.wrappedValue, 1) + XCTAssertEqual(unwrapped.wrappedValue, 1) - unwrapped.wrappedValue = 42 - XCTAssertEqual(binding.wrappedValue, 42) - XCTAssertEqual(unwrapped.wrappedValue, 42) + unwrapped.wrappedValue = 42 + XCTAssertEqual(binding.wrappedValue, 42) + XCTAssertEqual(unwrapped.wrappedValue, 42) - binding.wrappedValue = 1729 - XCTAssertEqual(binding.wrappedValue, 1729) - XCTAssertEqual(unwrapped.wrappedValue, 1729) + binding.wrappedValue = 1729 + XCTAssertEqual(binding.wrappedValue, 1729) + XCTAssertEqual(unwrapped.wrappedValue, 1729) - binding.wrappedValue = nil - XCTAssertEqual(binding.wrappedValue, nil) - XCTAssertEqual(unwrapped.wrappedValue, 1729) - } + binding.wrappedValue = nil + XCTAssertEqual(binding.wrappedValue, nil) + XCTAssertEqual(unwrapped.wrappedValue, 1729) + } - func testBindingCase() throws { - struct MyError: Error, Equatable {} - var value: Result? = nil - let binding = Binding(get: { value }, set: { value = $0 }) + func testBindingCase() throws { + struct MyError: Error, Equatable {} + var value: Result? = nil + let binding = Binding(get: { value }, set: { value = $0 }) - let success = binding.case(/Result.success) - let failure = binding.case(/Result.failure) - XCTAssertEqual(binding.wrappedValue, nil) - XCTAssertEqual(success.wrappedValue, nil) - XCTAssertEqual(failure.wrappedValue, nil) + let success = binding.case(/Result.success) + let failure = binding.case(/Result.failure) + XCTAssertEqual(binding.wrappedValue, nil) + XCTAssertEqual(success.wrappedValue, nil) + XCTAssertEqual(failure.wrappedValue, nil) - binding.wrappedValue = .success(1) - XCTAssertEqual(binding.wrappedValue, .success(1)) - XCTAssertEqual(success.wrappedValue, 1) - XCTAssertEqual(failure.wrappedValue, nil) + binding.wrappedValue = .success(1) + XCTAssertEqual(binding.wrappedValue, .success(1)) + XCTAssertEqual(success.wrappedValue, 1) + XCTAssertEqual(failure.wrappedValue, nil) - success.wrappedValue = 42 - XCTAssertEqual(binding.wrappedValue, .success(42)) - XCTAssertEqual(success.wrappedValue, 42) - XCTAssertEqual(failure.wrappedValue, nil) + success.wrappedValue = 42 + XCTAssertEqual(binding.wrappedValue, .success(42)) + XCTAssertEqual(success.wrappedValue, 42) + XCTAssertEqual(failure.wrappedValue, nil) - failure.wrappedValue = MyError() - XCTAssertEqual(binding.wrappedValue, .failure(MyError())) - XCTAssertEqual(success.wrappedValue, nil) - XCTAssertEqual(failure.wrappedValue, MyError()) + failure.wrappedValue = MyError() + XCTAssertEqual(binding.wrappedValue, .failure(MyError())) + XCTAssertEqual(success.wrappedValue, nil) + XCTAssertEqual(failure.wrappedValue, MyError()) - success.wrappedValue = nil - XCTAssertEqual(binding.wrappedValue, nil) - XCTAssertEqual(success.wrappedValue, nil) - XCTAssertEqual(failure.wrappedValue, nil) + success.wrappedValue = nil + XCTAssertEqual(binding.wrappedValue, nil) + XCTAssertEqual(success.wrappedValue, nil) + XCTAssertEqual(failure.wrappedValue, nil) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Tests/SwiftUINavigationTests/TextStateTests.swift b/Tests/SwiftUINavigationTests/TextStateTests.swift index 42fa2bd4ff..8ef9d2712f 100644 --- a/Tests/SwiftUINavigationTests/TextStateTests.swift +++ b/Tests/SwiftUINavigationTests/TextStateTests.swift @@ -1,76 +1,76 @@ #if canImport(SwiftUI) -import CustomDump -import SwiftUINavigation -import XCTest + import CustomDump + import SwiftUINavigation + import XCTest -final class TextStateTests: XCTestCase { - func testTextState() { - var dump = "" - customDump(TextState("Hello, world!"), to: &dump) - XCTAssertEqual( - dump, - """ - "Hello, world!" - """ - ) + final class TextStateTests: XCTestCase { + func testTextState() { + var dump = "" + customDump(TextState("Hello, world!"), to: &dump) + XCTAssertEqual( + dump, + """ + "Hello, world!" + """ + ) - dump = "" - customDump( - TextState("Hello, ") - + TextState("world").bold().italic() - + TextState("!"), - to: &dump - ) - XCTAssertEqual( - dump, - """ - "Hello, _**world**_!" - """ - ) + dump = "" + customDump( + TextState("Hello, ") + + TextState("world").bold().italic() + + TextState("!"), + to: &dump + ) + XCTAssertEqual( + dump, + """ + "Hello, _**world**_!" + """ + ) - dump = "" - customDump( - TextState("Offset by 10.5").baselineOffset(10.5) - + TextState("\n") + TextState("Headline").font(.headline) - + TextState("\n") + TextState("No font").font(nil) - + TextState("\n") + TextState("Light font weight").fontWeight(.light) - + TextState("\n") + TextState("No font weight").fontWeight(nil) - + TextState("\n") + TextState("Red").foregroundColor(.red) - + TextState("\n") + TextState("No color").foregroundColor(nil) - + TextState("\n") + TextState("Italic").italic() - + TextState("\n") + TextState("Kerning of 2.5").kerning(2.5) - + TextState("\n") + TextState("Stricken").strikethrough() - + TextState("\n") + TextState("Stricken green").strikethrough(color: .green) - + TextState("\n") + TextState("Not stricken blue").strikethrough(false, color: .blue) - + TextState("\n") + TextState("Tracking of 5.5").tracking(5.5) - + TextState("\n") + TextState("Underlined").underline() - + TextState("\n") + TextState("Underlined pink").underline(color: .pink) - + TextState("\n") + TextState("Not underlined purple").underline(false, color: .pink), - to: &dump - ) - XCTAssertNoDifference( - dump, - #""" - """ - Offset by 10.5 - Headline - No font - Light font weight - No font weight - Red - No color - _Italic_ - Kerning of 2.5 - ~~Stricken~~ - Stricken green - Not stricken blue - Tracking of 5.5 - Underlined - Underlined pink - Not underlined purple - """ - """# - ) + dump = "" + customDump( + TextState("Offset by 10.5").baselineOffset(10.5) + + TextState("\n") + TextState("Headline").font(.headline) + + TextState("\n") + TextState("No font").font(nil) + + TextState("\n") + TextState("Light font weight").fontWeight(.light) + + TextState("\n") + TextState("No font weight").fontWeight(nil) + + TextState("\n") + TextState("Red").foregroundColor(.red) + + TextState("\n") + TextState("No color").foregroundColor(nil) + + TextState("\n") + TextState("Italic").italic() + + TextState("\n") + TextState("Kerning of 2.5").kerning(2.5) + + TextState("\n") + TextState("Stricken").strikethrough() + + TextState("\n") + TextState("Stricken green").strikethrough(color: .green) + + TextState("\n") + TextState("Not stricken blue").strikethrough(false, color: .blue) + + TextState("\n") + TextState("Tracking of 5.5").tracking(5.5) + + TextState("\n") + TextState("Underlined").underline() + + TextState("\n") + TextState("Underlined pink").underline(color: .pink) + + TextState("\n") + TextState("Not underlined purple").underline(false, color: .pink), + to: &dump + ) + XCTAssertNoDifference( + dump, + #""" + """ + Offset by 10.5 + Headline + No font + Light font weight + No font weight + Red + No color + _Italic_ + Kerning of 2.5 + ~~Stricken~~ + Stricken green + Not stricken blue + Tracking of 5.5 + Underlined + Underlined pink + Not underlined purple + """ + """# + ) + } } -} -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) From 391abd41fbb60486c5835511ae5d3c2abe39edf0 Mon Sep 17 00:00:00 2001 From: Marius Rackwitz Date: Wed, 11 Oct 2023 23:05:11 +0200 Subject: [PATCH 018/124] Add `Sendable` conformance to `AlertState` (#127) --- Sources/SwiftUINavigationCore/AlertState.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index c23c4cda3c..f0bfa52ff7 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -212,6 +212,8 @@ } } + extension AlertState: Sendable where Action: Sendable {} + // MARK: - SwiftUI bridging extension Alert { From 13d9b0ffc7063f107ee38fbf69d26cd39865ea2a Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:13:26 -0400 Subject: [PATCH 019/124] Modernize with Observation (#130) * Modernize for @Observable. * wipo * wip * wip * prune * wip * wip --------- Co-authored-by: Stephen Celis --- Examples/CaseStudies/01-Alerts.swift | 35 +- .../CaseStudies/02-ConfirmationDialogs.swift | 33 +- Examples/CaseStudies/03-Sheets.swift | 34 +- Examples/CaseStudies/04-Popovers.swift | 34 +- .../CaseStudies/05-FullScreenCovers.swift | 34 +- .../06-NavigationDestinations.swift | 51 +- Examples/CaseStudies/07-NavigationLinks.swift | 81 +-- .../CaseStudies/08-NavigationLinkList.swift | 146 ----- .../{09-Routing.swift => 08-Routing.swift} | 29 +- ...onents.swift => 09-CustomComponents.swift} | 6 +- ...gs.swift => 10-SynchronizedBindings.swift} | 17 +- .../{12-IfLet.swift => 11-IfLet.swift} | 6 +- ...{13-IfCaseLet.swift => 12-IfCaseLet.swift} | 6 +- Examples/CaseStudies/RootView.swift | 6 +- Examples/Examples.xcodeproj/project.pbxproj | 560 +----------------- .../xcshareddata/xcschemes/Inventory.xcscheme | 2 +- .../xcshareddata/xcschemes/Standups.xcscheme | 99 ---- Examples/Inventory/App.swift | 13 +- Examples/Inventory/Inventory.swift | 59 +- Examples/Inventory/Item.swift | 14 +- Examples/Inventory/ItemRow.swift | 22 +- Examples/Standups/Readme.md | 77 --- Examples/Standups/Resources/ding.wav | Bin 535904 -> 0 bytes .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../Standups/Assets.xcassets/Contents.json | 6 - .../Assets.xcassets/Themes/Contents.json | 6 - .../Themes/bubblegum.colorset/Contents.json | 38 -- .../Themes/buttercup.colorset/Contents.json | 38 -- .../Themes/indigo.colorset/Contents.json | 38 -- .../Themes/lavender.colorset/Contents.json | 38 -- .../Themes/magenta.colorset/Contents.json | 38 -- .../Themes/navy.colorset/Contents.json | 38 -- .../Themes/orange.colorset/Contents.json | 38 -- .../Themes/oxblood.colorset/Contents.json | 38 -- .../Themes/periwinkle.colorset/Contents.json | 38 -- .../Themes/poppy.colorset/Contents.json | 38 -- .../Themes/purple.colorset/Contents.json | 38 -- .../Themes/seafoam.colorset/Contents.json | 38 -- .../Themes/sky.colorset/Contents.json | 38 -- .../Themes/tan.colorset/Contents.json | 38 -- .../Themes/teal.colorset/Contents.json | 38 -- .../Themes/yellow.colorset/Contents.json | 38 -- .../Standups/Dependencies/DataManager.swift | 59 -- .../Standups/Dependencies/OpenSettings.swift | 19 - .../Dependencies/SoundEffectClient.swift | 45 -- .../Standups/Dependencies/SpeechClient.swift | 193 ------ Examples/Standups/Standups/Helpers.swift | 47 -- Examples/Standups/Standups/Models.swift | 117 ---- .../Preview Assets.xcassets/Contents.json | 6 - .../Standups/Standups/RecordMeeting.swift | 403 ------------- .../Standups/Standups/StandupDetail.swift | 412 ------------- Examples/Standups/Standups/StandupForm.swift | 136 ----- Examples/Standups/Standups/StandupsApp.swift | 29 - Examples/Standups/Standups/StandupsList.swift | 350 ----------- .../StandupsTests/EditStandupTests.swift | 138 ----- .../StandupsTests/RecordMeetingTests.swift | 345 ----------- .../StandupsTests/StandupDetailTests.swift | 166 ------ .../StandupsTests/StandupsListTests.swift | 208 ------- .../StandupsUITests/StandupsListUITests.swift | 49 -- README.md | 2 +- .../Articles/AlertsDialogs.md | 9 +- .../Documentation.docc/Articles/Bindings.md | 19 +- .../Articles/WhatIsNavigation.md | 26 +- .../SwiftUINavigationCore/AlertState.swift | 16 +- Sources/SwiftUINavigationCore/Bind.swift | 12 +- .../ConfirmationDialogState.swift | 14 +- Sources/SwiftUINavigationCore/TextState.swift | 5 +- .../xcschemes/SwiftUINavigation.xcscheme | 2 +- 69 files changed, 356 insertions(+), 4479 deletions(-) delete mode 100644 Examples/CaseStudies/08-NavigationLinkList.swift rename Examples/CaseStudies/{09-Routing.swift => 08-Routing.swift} (85%) rename Examples/CaseStudies/{10-CustomComponents.swift => 09-CustomComponents.swift} (96%) rename Examples/CaseStudies/{11-SynchronizedBindings.swift => 10-SynchronizedBindings.swift} (79%) rename Examples/CaseStudies/{12-IfLet.swift => 11-IfLet.swift} (89%) rename Examples/CaseStudies/{13-IfCaseLet.swift => 12-IfCaseLet.swift} (90%) delete mode 100644 Examples/Examples.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme delete mode 100644 Examples/Standups/Readme.md delete mode 100644 Examples/Standups/Resources/ding.wav delete mode 100644 Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json delete mode 100644 Examples/Standups/Standups/Dependencies/DataManager.swift delete mode 100644 Examples/Standups/Standups/Dependencies/OpenSettings.swift delete mode 100644 Examples/Standups/Standups/Dependencies/SoundEffectClient.swift delete mode 100644 Examples/Standups/Standups/Dependencies/SpeechClient.swift delete mode 100644 Examples/Standups/Standups/Helpers.swift delete mode 100644 Examples/Standups/Standups/Models.swift delete mode 100644 Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 Examples/Standups/Standups/RecordMeeting.swift delete mode 100644 Examples/Standups/Standups/StandupDetail.swift delete mode 100644 Examples/Standups/Standups/StandupForm.swift delete mode 100644 Examples/Standups/Standups/StandupsApp.swift delete mode 100644 Examples/Standups/Standups/StandupsList.swift delete mode 100644 Examples/Standups/StandupsTests/EditStandupTests.swift delete mode 100644 Examples/Standups/StandupsTests/RecordMeetingTests.swift delete mode 100644 Examples/Standups/StandupsTests/StandupDetailTests.swift delete mode 100644 Examples/Standups/StandupsTests/StandupsListTests.swift delete mode 100644 Examples/Standups/StandupsUITests/StandupsListUITests.swift diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index dc1baeafaf..d72b8f1149 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -3,12 +3,14 @@ import SwiftUINavigation @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) struct OptionalAlerts: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { Stepper("Number: \(self.model.count)", value: self.$model.count) - Button(action: { self.model.numberFactButtonTapped() }) { + Button { + Task { await self.model.numberFactButtonTapped() } + } label: { HStack { Text("Get number fact") if self.model.isLoading { @@ -24,9 +26,9 @@ struct OptionalAlerts: View { unwrapping: self.$model.fact, actions: { Button("Get another fact about \($0.number)") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } - Button("Cancel", role: .cancel) { + Button("Close", role: .cancel) { self.model.fact = nil } }, @@ -36,17 +38,20 @@ struct OptionalAlerts: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var isLoading = false - @Published var fact: Fact? +@Observable +private class FeatureModel { + var count = 0 + var isLoading = false + var fact: Fact? - func numberFactButtonTapped() { - Task { - self.isLoading = true - self.fact = await getNumberFact(self.count) - self.isLoading = false - } + @MainActor + func numberFactButtonTapped() async { + self.isLoading = true + self.fact = await getNumberFact(self.count) + self.isLoading = false } } + +#Preview { + OptionalAlerts() +} diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index 312b080ad0..ffa1b26065 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -3,12 +3,14 @@ import SwiftUINavigation @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) struct OptionalConfirmationDialogs: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { Stepper("Number: \(self.model.count)", value: self.$model.count) - Button(action: { self.model.numberFactButtonTapped() }) { + Button { + Task { await self.model.numberFactButtonTapped() } + } label: { HStack { Text("Get number fact") if self.model.isLoading { @@ -24,7 +26,7 @@ struct OptionalConfirmationDialogs: View { unwrapping: self.$model.fact, actions: { Button("Get another fact about \($0.number)") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } }, message: { Text($0.description) } @@ -34,17 +36,20 @@ struct OptionalConfirmationDialogs: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var isLoading = false - @Published var fact: Fact? +@Observable +private class FeatureModel { + var count = 0 + var isLoading = false + var fact: Fact? - func numberFactButtonTapped() { - Task { - self.isLoading = true - self.fact = await getNumberFact(self.count) - self.isLoading = false - } + @MainActor + func numberFactButtonTapped() async { + self.isLoading = true + self.fact = await getNumberFact(self.count) + self.isLoading = false } } + +#Preview { + OptionalConfirmationDialogs() +} diff --git a/Examples/CaseStudies/03-Sheets.swift b/Examples/CaseStudies/03-Sheets.swift index 1067768a00..80e2ab5577 100644 --- a/Examples/CaseStudies/03-Sheets.swift +++ b/Examples/CaseStudies/03-Sheets.swift @@ -2,7 +2,7 @@ import SwiftUI import SwiftUINavigation struct OptionalSheets: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { @@ -11,7 +11,7 @@ struct OptionalSheets: View { HStack { Button("Get number fact") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } if self.model.isLoading { @@ -33,7 +33,7 @@ struct OptionalSheets: View { } } .sheet(unwrapping: self.$model.fact) { $fact in - NavigationView { + NavigationStack { FactEditor(fact: $fact.description) .disabled(self.model.isLoading) .foregroundColor(self.model.isLoading ? .gray : nil) @@ -67,35 +67,40 @@ private struct FactEditor: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var fact: Fact? - @Published var isLoading = false - @Published var savedFacts: [Fact] = [] - private var task: Task? +@Observable +private class FeatureModel { + var count = 0 + var fact: Fact? + var isLoading = false + var savedFacts: [Fact] = [] + private var task: Task? deinit { self.task?.cancel() } - func numberFactButtonTapped() { + @MainActor + func numberFactButtonTapped() async { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false - try Task.checkCancellation() + guard !Task.isCancelled + else { return } self.fact = fact } + await self.task?.value } + @MainActor func cancelButtonTapped() { self.task?.cancel() self.task = nil self.fact = nil } + @MainActor func saveButtonTapped(fact: Fact) { self.task?.cancel() self.task = nil @@ -103,7 +108,12 @@ private class FeatureModel: ObservableObject { self.fact = nil } + @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { self.savedFacts.remove(atOffsets: offsets) } } + +#Preview { + OptionalSheets() +} diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift index 3582d89649..da6d85de9d 100644 --- a/Examples/CaseStudies/04-Popovers.swift +++ b/Examples/CaseStudies/04-Popovers.swift @@ -2,7 +2,7 @@ import SwiftUI import SwiftUINavigation struct OptionalPopovers: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { @@ -11,10 +11,10 @@ struct OptionalPopovers: View { HStack { Button("Get number fact") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } .popover(unwrapping: self.$model.fact, arrowEdge: .bottom) { $fact in - NavigationView { + NavigationStack { FactEditor(fact: $fact.description) .disabled(self.model.isLoading) .foregroundColor(self.model.isLoading ? .gray : nil) @@ -63,35 +63,40 @@ private struct FactEditor: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var fact: Fact? - @Published var isLoading = false - @Published var savedFacts: [Fact] = [] - private var task: Task? +@Observable +private class FeatureModel { + var count = 0 + var fact: Fact? + var isLoading = false + var savedFacts: [Fact] = [] + private var task: Task? deinit { self.task?.cancel() } - func numberFactButtonTapped() { + @MainActor + func numberFactButtonTapped() async { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false - try Task.checkCancellation() + guard !Task.isCancelled + else { return } self.fact = fact } + await self.task?.value } + @MainActor func cancelButtonTapped() { self.task?.cancel() self.task = nil self.fact = nil } + @MainActor func saveButtonTapped(fact: Fact) { self.task?.cancel() self.task = nil @@ -99,7 +104,12 @@ private class FeatureModel: ObservableObject { self.fact = nil } + @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { self.savedFacts.remove(atOffsets: offsets) } } + +#Preview { + OptionalPopovers() +} diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift index 588d1c7663..2fb26c50d7 100644 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ b/Examples/CaseStudies/05-FullScreenCovers.swift @@ -2,7 +2,7 @@ import SwiftUI import SwiftUINavigation struct OptionalFullScreenCovers: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { @@ -11,7 +11,7 @@ struct OptionalFullScreenCovers: View { HStack { Button("Get number fact") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } if self.model.isLoading { @@ -33,7 +33,7 @@ struct OptionalFullScreenCovers: View { } } .fullScreenCover(unwrapping: self.$model.fact) { $fact in - NavigationView { + NavigationStack { FactEditor(fact: $fact.description) .disabled(self.model.isLoading) .foregroundColor(self.model.isLoading ? .gray : nil) @@ -67,31 +67,36 @@ private struct FactEditor: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var fact: Fact? - @Published var isLoading = false - @Published var savedFacts: [Fact] = [] - private var task: Task? +@Observable +private class FeatureModel { + var count = 0 + var fact: Fact? + var isLoading = false + var savedFacts: [Fact] = [] + private var task: Task? - func numberFactButtonTapped() { + @MainActor + func numberFactButtonTapped() async { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false - try Task.checkCancellation() + guard !Task.isCancelled + else { return } self.fact = fact } + await self.task?.value } + @MainActor func cancelButtonTapped() { self.task?.cancel() self.task = nil self.fact = nil } + @MainActor func saveButtonTapped(fact: Fact) { self.task?.cancel() self.task = nil @@ -99,7 +104,12 @@ private class FeatureModel: ObservableObject { self.fact = nil } + @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { self.savedFacts.remove(atOffsets: offsets) } } + +#Preview { + OptionalFullScreenCovers() +} diff --git a/Examples/CaseStudies/06-NavigationDestinations.swift b/Examples/CaseStudies/06-NavigationDestinations.swift index 27b8c7aa9a..426ae5e065 100644 --- a/Examples/CaseStudies/06-NavigationDestinations.swift +++ b/Examples/CaseStudies/06-NavigationDestinations.swift @@ -3,7 +3,7 @@ import SwiftUINavigation @available(iOS 16, *) struct NavigationDestinations: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { @@ -12,7 +12,7 @@ struct NavigationDestinations: View { HStack { Button("Get number fact") { - self.model.numberFactButtonTapped() + Task { await self.model.numberFactButtonTapped() } } if self.model.isLoading { @@ -42,12 +42,12 @@ struct NavigationDestinations: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.model.cancelButtonTapped() + Task { await self.model.cancelButtonTapped() } } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.model.saveButtonTapped(fact: fact) + Task { await self.model.saveButtonTapped(fact: fact) } } } } @@ -71,28 +71,31 @@ private struct FactEditor: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var fact: Fact? - @Published var isLoading = false - @Published var savedFacts: [Fact] = [] - private var task: Task? +@Observable +private class FeatureModel { + var count = 0 + var fact: Fact? + var isLoading = false + var savedFacts: [Fact] = [] + private var task: Task? deinit { self.task?.cancel() } - func setFactNavigation(isActive: Bool) { + @MainActor + func setFactNavigation(isActive: Bool) async { if isActive { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false - try Task.checkCancellation() + guard !Task.isCancelled + else { return } self.fact = fact } + await self.task?.value } else { self.task?.cancel() self.task = nil @@ -100,20 +103,30 @@ private class FeatureModel: ObservableObject { } } - func numberFactButtonTapped() { - self.setFactNavigation(isActive: true) + @MainActor + func numberFactButtonTapped() async { + await self.setFactNavigation(isActive: true) } - func cancelButtonTapped() { - self.setFactNavigation(isActive: false) + @MainActor + func cancelButtonTapped() async { + await self.setFactNavigation(isActive: false) } - func saveButtonTapped(fact: Fact) { + @MainActor + func saveButtonTapped(fact: Fact) async { self.savedFacts.append(fact) - self.setFactNavigation(isActive: false) + await self.setFactNavigation(isActive: false) } + @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { self.savedFacts.remove(atOffsets: offsets) } } + +#Preview { + NavigationStack { + NavigationDestinations() + } +} diff --git a/Examples/CaseStudies/07-NavigationLinks.swift b/Examples/CaseStudies/07-NavigationLinks.swift index 51ce98e2c3..88aa512392 100644 --- a/Examples/CaseStudies/07-NavigationLinks.swift +++ b/Examples/CaseStudies/07-NavigationLinks.swift @@ -2,7 +2,7 @@ import SwiftUI import SwiftUINavigation struct OptionalNavigationLinks: View { - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { List { @@ -10,27 +10,8 @@ struct OptionalNavigationLinks: View { Stepper("Number: \(self.model.count)", value: self.$model.count) HStack { - NavigationLink(unwrapping: self.$model.fact) { - self.model.setFactNavigation(isActive: $0) - } destination: { $fact in - FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - self.model.cancelButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - self.model.saveButtonTapped(fact: fact) - } - } - } - } label: { - Text("Get number fact") + Button("Get number fact") { + Task { await self.model.setFactNavigation(isActive: true) } } if self.model.isLoading { @@ -51,6 +32,24 @@ struct OptionalNavigationLinks: View { Text("Saved Facts") } } + .navigationDestination(unwrapping: self.$model.fact) { $fact in + FactEditor(fact: $fact.description) + .disabled(self.model.isLoading) + .foregroundColor(self.model.isLoading ? .gray : nil) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + Task { await self.model.cancelButtonTapped() } + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + Task { await self.model.saveButtonTapped(fact: fact) } + } + } + } + } .navigationTitle("Links") } } @@ -67,28 +66,31 @@ private struct FactEditor: View { } } -@MainActor -private class FeatureModel: ObservableObject { - @Published var count = 0 - @Published var fact: Fact? - @Published var isLoading = false - @Published var savedFacts: [Fact] = [] - private var task: Task? +@Observable +private class FeatureModel { + var count = 0 + var fact: Fact? + var isLoading = false + var savedFacts: [Fact] = [] + private var task: Task? deinit { self.task?.cancel() } - func setFactNavigation(isActive: Bool) { + @MainActor + func setFactNavigation(isActive: Bool) async { if isActive { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false - try Task.checkCancellation() + guard !Task.isCancelled + else { return } self.fact = fact } + await self.task?.value } else { self.task?.cancel() self.task = nil @@ -96,16 +98,25 @@ private class FeatureModel: ObservableObject { } } - func cancelButtonTapped() { - self.setFactNavigation(isActive: false) + @MainActor + func cancelButtonTapped() async { + await self.setFactNavigation(isActive: false) } - func saveButtonTapped(fact: Fact) { + @MainActor + func saveButtonTapped(fact: Fact) async { self.savedFacts.append(fact) - self.setFactNavigation(isActive: false) + await self.setFactNavigation(isActive: false) } + @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { self.savedFacts.remove(atOffsets: offsets) } } + +#Preview { + NavigationStack { + OptionalNavigationLinks() + } +} diff --git a/Examples/CaseStudies/08-NavigationLinkList.swift b/Examples/CaseStudies/08-NavigationLinkList.swift deleted file mode 100644 index 170b1fe2b3..0000000000 --- a/Examples/CaseStudies/08-NavigationLinkList.swift +++ /dev/null @@ -1,146 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This case study demonstrates how to model a list of navigation links. Tap a row to drill down \ - and edit a counter. Edit screen allows cancelling or saving the edits. - - The domain for a row in the list has its own ObservableObject and Destination enum, and it uses \ - the library's NavigationLink initializer to drive navigation from the destination enum. - """ - -struct ListOfNavigationLinks: View { - @ObservedObject var model: ListOfNavigationLinksModel - - var body: some View { - Form { - Section { - Text(readMe) - } - - List { - ForEach(self.model.rows) { rowModel in - RowView(model: rowModel) - } - .onDelete(perform: self.model.deleteButtonTapped(indexSet:)) - } - } - .navigationTitle("List of links") - .toolbar { - ToolbarItem { - Button("Add") { - self.model.addButtonTapped() - } - } - } - } -} - -class ListOfNavigationLinksModel: ObservableObject { - @Published var rows: [ListOfNavigationLinksRowModel] - - init(rows: [ListOfNavigationLinksRowModel] = []) { - self.rows = rows - } - - func addButtonTapped() { - withAnimation { - self.rows.append(.init()) - } - } - - func deleteButtonTapped(indexSet: IndexSet) { - self.rows.remove(atOffsets: indexSet) - } -} - -private struct RowView: View { - @ObservedObject var model: ListOfNavigationLinksRowModel - - var body: some View { - NavigationLink( - unwrapping: self.$model.destination, - case: /ListOfNavigationLinksRowModel.Destination.edit - ) { isActive in - self.model.setEditNavigation(isActive: isActive) - } destination: { $counter in - EditView(counter: $counter) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Save") { self.model.saveButtonTapped(counter: counter) } - } - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { self.model.cancelButtonTapped() } - } - } - } label: { - Text("\(self.model.counter)") - } - } -} - -class ListOfNavigationLinksRowModel: Identifiable, ObservableObject { - let id = UUID() - @Published var counter: Int - @Published var destination: Destination? - - enum Destination { - case edit(Int) - } - - init( - counter: Int = 0, - destination: Destination? = nil - ) { - self.counter = counter - self.destination = destination - } - - func setEditNavigation(isActive: Bool) { - self.destination = isActive ? .edit(self.counter) : nil - } - - func saveButtonTapped(counter: Int) { - self.counter = counter - self.destination = nil - } - - func cancelButtonTapped() { - self.destination = nil - } -} - -private struct EditView: View { - @Binding var counter: Int - - var body: some View { - Form { - Text("Count: \(self.counter)") - Button("Increment") { - self.counter += 1 - } - Button("Decrement") { - self.counter -= 1 - } - } - } -} - -struct ListOfNavigationLinks_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - ListOfNavigationLinks( - model: .init( - rows: [ - .init(counter: 0), - .init(counter: 0), - .init(counter: 0), - .init(counter: 0), - .init(counter: 0), - ] - ) - ) - } - } -} diff --git a/Examples/CaseStudies/09-Routing.swift b/Examples/CaseStudies/08-Routing.swift similarity index 85% rename from Examples/CaseStudies/09-Routing.swift rename to Examples/CaseStudies/08-Routing.swift index 4d60633b10..d6546a4076 100644 --- a/Examples/CaseStudies/09-Routing.swift +++ b/Examples/CaseStudies/08-Routing.swift @@ -71,17 +71,8 @@ struct Routing: View { ) } - NavigationLink(unwrapping: self.$destination, case: /Destination.link) { isActive in - if isActive { - self.destination = .link(self.count) - } - } destination: { $count in - Form { - Stepper("Count: \(count)", value: $count) - } - .navigationTitle("Routing link") - } label: { - Text("Link") + Button("Link") { + self.destination = .link(self.count) } Button("Sheet") { @@ -112,8 +103,14 @@ struct Routing: View { break } } + .navigationDestination(unwrapping: self.$destination, case: /Destination.link) { $count in + Form { + Stepper("Count: \(count)", value: $count) + } + .navigationTitle("Routing link") + } .sheet(unwrapping: self.$destination, case: /Destination.sheet) { $count in - NavigationView { + NavigationStack { Form { Stepper("Count: \(count)", value: $count) } @@ -123,10 +120,8 @@ struct Routing: View { } } -struct Routing_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - Routing() - } +#Preview { + NavigationStack { + Routing() } } diff --git a/Examples/CaseStudies/10-CustomComponents.swift b/Examples/CaseStudies/09-CustomComponents.swift similarity index 96% rename from Examples/CaseStudies/10-CustomComponents.swift rename to Examples/CaseStudies/09-CustomComponents.swift index f537cf4f39..fb37764b0b 100644 --- a/Examples/CaseStudies/10-CustomComponents.swift +++ b/Examples/CaseStudies/09-CustomComponents.swift @@ -117,8 +117,6 @@ extension View { } } -struct CustomComponents_Previews: PreviewProvider { - static var previews: some View { - CustomComponents() - } +#Preview { + CustomComponents() } diff --git a/Examples/CaseStudies/11-SynchronizedBindings.swift b/Examples/CaseStudies/10-SynchronizedBindings.swift similarity index 79% rename from Examples/CaseStudies/11-SynchronizedBindings.swift rename to Examples/CaseStudies/10-SynchronizedBindings.swift index 80f718c5e5..c141de8330 100644 --- a/Examples/CaseStudies/11-SynchronizedBindings.swift +++ b/Examples/CaseStudies/10-SynchronizedBindings.swift @@ -10,7 +10,7 @@ private let readMe = """ struct SynchronizedBindings: View { @FocusState private var focusedField: FeatureModel.Field? - @ObservedObject private var model = FeatureModel() + @State private var model = FeatureModel() var body: some View { Form { @@ -37,15 +37,16 @@ struct SynchronizedBindings: View { } } -private class FeatureModel: ObservableObject { +@Observable +private class FeatureModel { enum Field: String { case username case password } - @Published var focusedField: Field? = .username - @Published var password: String = "" - @Published var username: String = "" + var focusedField: Field? = .username + var password: String = "" + var username: String = "" func signInButtonTapped() { if self.username.isEmpty { @@ -58,8 +59,6 @@ private class FeatureModel: ObservableObject { } } -struct SynchronizedBindings_Previews: PreviewProvider { - static var previews: some View { - SynchronizedBindings() - } +#Preview { + SynchronizedBindings() } diff --git a/Examples/CaseStudies/12-IfLet.swift b/Examples/CaseStudies/11-IfLet.swift similarity index 89% rename from Examples/CaseStudies/12-IfLet.swift rename to Examples/CaseStudies/11-IfLet.swift index 0798ecd17b..1f48f307fa 100644 --- a/Examples/CaseStudies/12-IfLet.swift +++ b/Examples/CaseStudies/11-IfLet.swift @@ -40,8 +40,6 @@ struct IfLetCaseStudy: View { } } -struct IfLetCaseStudy_EditStringView_Previews: PreviewProvider { - static var previews: some View { - IfLetCaseStudy() - } +#Preview { + IfLetCaseStudy() } diff --git a/Examples/CaseStudies/13-IfCaseLet.swift b/Examples/CaseStudies/12-IfCaseLet.swift similarity index 90% rename from Examples/CaseStudies/13-IfCaseLet.swift rename to Examples/CaseStudies/12-IfCaseLet.swift index 84a0ef6150..b29aedec9d 100644 --- a/Examples/CaseStudies/13-IfCaseLet.swift +++ b/Examples/CaseStudies/12-IfCaseLet.swift @@ -46,8 +46,6 @@ struct IfCaseLetCaseStudy: View { } } -struct IfCaseLetCaseStudy_EditStringView_Previews: PreviewProvider { - static var previews: some View { - IfCaseLetCaseStudy() - } +#Preview { + IfCaseLetCaseStudy() } diff --git a/Examples/CaseStudies/RootView.swift b/Examples/CaseStudies/RootView.swift index 5dec7adea1..165a236d20 100644 --- a/Examples/CaseStudies/RootView.swift +++ b/Examples/CaseStudies/RootView.swift @@ -3,7 +3,7 @@ import SwiftUINavigation struct RootView: View { var body: some View { - NavigationView { + NavigationStack { List { Section { NavigationLink("Optional-driven alerts") { @@ -40,9 +40,6 @@ struct RootView: View { NavigationLink("Optional navigation links") { OptionalNavigationLinks() } - NavigationLink("List of navigation links") { - ListOfNavigationLinks(model: ListOfNavigationLinksModel()) - } } header: { Text("Navigation links") } @@ -69,7 +66,6 @@ struct RootView: View { } .navigationTitle("Case studies") } - .navigationViewStyle(.stack) } } diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 36e74fd1ee..3eac9bf078 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - CA22CCC22967799600F52F6D /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA22CCC12967799600F52F6D /* Helpers.swift */; }; CA4737CF272F09600012CAC3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA4737CE272F09600012CAC3 /* Assets.xcassets */; }; CA4737F4272F09780012CAC3 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA4737F3272F09780012CAC3 /* SwiftUINavigation */; }; CA4737F9272F09D00012CAC3 /* ItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4737F5272F09D00012CAC3 /* ItemRow.swift */; }; @@ -22,60 +21,19 @@ CA473838272F0D860012CAC3 /* 03-Sheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA473832272F0D860012CAC3 /* 03-Sheets.swift */; }; CA473839272F0D860012CAC3 /* 01-Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA473833272F0D860012CAC3 /* 01-Alerts.swift */; }; CA47383B272F0DD60012CAC3 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA47383A272F0DD60012CAC3 /* SwiftUINavigation */; }; - CA47383E272F0F9B0012CAC3 /* 10-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 10-CustomComponents.swift */; }; - CA53F7F1295BBDB700DE68FE /* EditStandupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA53F7F0295BBDB700DE68FE /* EditStandupTests.swift */; }; - CA53F806295BEE4F00DE68FE /* StandupsListUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA53F805295BEE4F00DE68FE /* StandupsListUITests.swift */; }; - CA64539A2968A06E00802931 /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = CA6453992968A06E00802931 /* Dependencies */; }; - CA70FED7274B1907005A0D53 /* 08-NavigationLinkList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA70FED6274B1907005A0D53 /* 08-NavigationLinkList.swift */; }; - CA93236B292BE733004B1130 /* 13-IfCaseLet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93236A292BE733004B1130 /* 13-IfCaseLet.swift */; }; - CAAA74E02956956B009A25CA /* OpenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAA74DF2956956B009A25CA /* OpenSettings.swift */; }; - CAAA74E429569F6C009A25CA /* StandupsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAA74E329569F6C009A25CA /* StandupsListTests.swift */; }; - CAAA74E62956A60A009A25CA /* RecordMeetingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAA74E52956A60A009A25CA /* RecordMeetingTests.swift */; }; - CAAA74E82956A658009A25CA /* StandupDetailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAA74E72956A658009A25CA /* StandupDetailTests.swift */; }; - CAAC0072292BDE660083F2FF /* 12-IfLet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAC0071292BDE660083F2FF /* 12-IfLet.swift */; }; - CABE9FC1272F2C0000AFC150 /* 09-Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABE9FC0272F2C0000AFC150 /* 09-Routing.swift */; }; - CADF861E2977652500B7695B /* SoundEffectClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADF861D2977652500B7695B /* SoundEffectClient.swift */; }; - CADF8621297765F000B7695B /* ding.wav in Resources */ = {isa = PBXBuildFile; fileRef = CADF8620297765F000B7695B /* ding.wav */; }; - DC5E07772947CCD700293F45 /* StandupsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07762947CCD700293F45 /* StandupsApp.swift */; }; - DC5E07792947CCD700293F45 /* StandupDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07782947CCD700293F45 /* StandupDetail.swift */; }; - DC5E077B2947CCD800293F45 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC5E077A2947CCD800293F45 /* Assets.xcassets */; }; - DC5E077E2947CCD800293F45 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC5E077D2947CCD800293F45 /* Preview Assets.xcassets */; }; - DC5E07A52947CFA000293F45 /* StandupForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07A42947CFA000293F45 /* StandupForm.swift */; }; - DC5E07A72947CFA600293F45 /* StandupsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07A62947CFA600293F45 /* StandupsList.swift */; }; - DC5E07A92947CFB700293F45 /* SpeechClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07A82947CFB700293F45 /* SpeechClient.swift */; }; - DC5E07AB2947CFCA00293F45 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07AA2947CFCA00293F45 /* Models.swift */; }; - DC5E07AD2947CFD300293F45 /* RecordMeeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07AC2947CFD300293F45 /* RecordMeeting.swift */; }; + CA47383E272F0F9B0012CAC3 /* 09-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */; }; + CA93236B292BE733004B1130 /* 12-IfCaseLet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93236A292BE733004B1130 /* 12-IfCaseLet.swift */; }; + CAAC0072292BDE660083F2FF /* 11-IfLet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAC0071292BDE660083F2FF /* 11-IfLet.swift */; }; + CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */; }; DC609AD6291D76150052647F /* 06-NavigationDestinations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC609AD5291D76150052647F /* 06-NavigationDestinations.swift */; }; - DC6A8411291F227400B3F6C9 /* 11-SynchronizedBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6A8410291F227400B3F6C9 /* 11-SynchronizedBindings.swift */; }; + DC6A8411291F227400B3F6C9 /* 10-SynchronizedBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6A8410291F227400B3F6C9 /* 10-SynchronizedBindings.swift */; }; DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */; }; DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */; }; DCD4E68B274180F500CDF3BD /* 07-NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E68A274180F500CDF3BD /* 07-NavigationLinks.swift */; }; - DCE73E022947D02A004EE92E /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCE73E012947D02A004EE92E /* SwiftUINavigation */; }; - DCE73E052947D063004EE92E /* Tagged in Frameworks */ = {isa = PBXBuildFile; productRef = DCE73E042947D063004EE92E /* Tagged */; }; - DCE73E082947D082004EE92E /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DCE73E072947D082004EE92E /* IdentifiedCollections */; }; DCE73E0A2947D090004EE92E /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DCE73E092947D090004EE92E /* IdentifiedCollections */; }; - DCE73E0C2947D163004EE92E /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE73E0B2947D163004EE92E /* DataManager.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - CA53F800295BEDBE00DE68FE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CA47378C272F08EF0012CAC3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DC5E07732947CCD700293F45; - remoteInfo = Standups; - }; - DC5E07842947CCD800293F45 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CA47378C272F08EF0012CAC3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DC5E07732947CCD700293F45; - remoteInfo = Standups; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ - CA22CCC12967799600F52F6D /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; CA4737C3272F090F0012CAC3 /* swiftui-navigation */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swiftui-navigation"; path = ..; sourceTree = ""; }; CA4737C8272F095F0012CAC3 /* Inventory.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Inventory.app; sourceTree = BUILT_PRODUCTS_DIR; }; CA4737CE272F09600012CAC3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -92,38 +50,15 @@ CA473832272F0D860012CAC3 /* 03-Sheets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "03-Sheets.swift"; sourceTree = ""; }; CA473833272F0D860012CAC3 /* 01-Alerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "01-Alerts.swift"; sourceTree = ""; }; CA47383C272F0F0D0012CAC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - CA47383D272F0F9B0012CAC3 /* 10-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "10-CustomComponents.swift"; sourceTree = ""; }; - CA53F7F0295BBDB700DE68FE /* EditStandupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStandupTests.swift; sourceTree = ""; }; - CA53F7FA295BEDBD00DE68FE /* StandupsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StandupsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - CA53F805295BEE4F00DE68FE /* StandupsListUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsListUITests.swift; sourceTree = ""; }; - CA53F808295CCA2E00DE68FE /* Readme.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = ""; }; - CA70FED6274B1907005A0D53 /* 08-NavigationLinkList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-NavigationLinkList.swift"; sourceTree = ""; }; - CA93236A292BE733004B1130 /* 13-IfCaseLet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "13-IfCaseLet.swift"; sourceTree = ""; }; - CAAA74DF2956956B009A25CA /* OpenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettings.swift; sourceTree = ""; }; - CAAA74E329569F6C009A25CA /* StandupsListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsListTests.swift; sourceTree = ""; }; - CAAA74E52956A60A009A25CA /* RecordMeetingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordMeetingTests.swift; sourceTree = ""; }; - CAAA74E72956A658009A25CA /* StandupDetailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupDetailTests.swift; sourceTree = ""; }; - CAAC0071292BDE660083F2FF /* 12-IfLet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "12-IfLet.swift"; sourceTree = ""; }; - CABE9FC0272F2C0000AFC150 /* 09-Routing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "09-Routing.swift"; sourceTree = ""; }; - CADF861D2977652500B7695B /* SoundEffectClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundEffectClient.swift; sourceTree = ""; }; - CADF8620297765F000B7695B /* ding.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding.wav; sourceTree = ""; }; - DC5E07742947CCD700293F45 /* Standups.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Standups.app; sourceTree = BUILT_PRODUCTS_DIR; }; - DC5E07762947CCD700293F45 /* StandupsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsApp.swift; sourceTree = ""; }; - DC5E07782947CCD700293F45 /* StandupDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupDetail.swift; sourceTree = ""; }; - DC5E077A2947CCD800293F45 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DC5E077D2947CCD800293F45 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - DC5E07832947CCD800293F45 /* StandupsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StandupsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DC5E07A42947CFA000293F45 /* StandupForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupForm.swift; sourceTree = ""; }; - DC5E07A62947CFA600293F45 /* StandupsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsList.swift; sourceTree = ""; }; - DC5E07A82947CFB700293F45 /* SpeechClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechClient.swift; sourceTree = ""; }; - DC5E07AA2947CFCA00293F45 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; - DC5E07AC2947CFD300293F45 /* RecordMeeting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordMeeting.swift; sourceTree = ""; }; + CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "09-CustomComponents.swift"; sourceTree = ""; }; + CA93236A292BE733004B1130 /* 12-IfCaseLet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "12-IfCaseLet.swift"; sourceTree = ""; }; + CAAC0071292BDE660083F2FF /* 11-IfLet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "11-IfLet.swift"; sourceTree = ""; }; + CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-Routing.swift"; sourceTree = ""; }; DC609AD5291D76150052647F /* 06-NavigationDestinations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-NavigationDestinations.swift"; sourceTree = ""; }; - DC6A8410291F227400B3F6C9 /* 11-SynchronizedBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "11-SynchronizedBindings.swift"; sourceTree = ""; }; + DC6A8410291F227400B3F6C9 /* 10-SynchronizedBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "10-SynchronizedBindings.swift"; sourceTree = ""; }; DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-FullScreenCovers.swift"; sourceTree = ""; }; DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-Popovers.swift"; sourceTree = ""; }; DCD4E68A274180F500CDF3BD /* 07-NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "07-NavigationLinks.swift"; sourceTree = ""; }; - DCE73E0B2947D163004EE92E /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -144,31 +79,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - CA53F7F7295BEDBD00DE68FE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E07712947CCD700293F45 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DCE73E052947D063004EE92E /* Tagged in Frameworks */, - DCE73E082947D082004EE92E /* IdentifiedCollections in Frameworks */, - DCE73E022947D02A004EE92E /* SwiftUINavigation in Frameworks */, - CA64539A2968A06E00802931 /* Dependencies in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E07802947CCD800293F45 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -180,7 +90,6 @@ CA4737F2272F09780012CAC3 /* Frameworks */, CA4737C9272F095F0012CAC3 /* Inventory */, CA473795272F08EF0012CAC3 /* Products */, - CA53F807295CC9A900DE68FE /* Standups */, ); sourceTree = ""; }; @@ -189,9 +98,6 @@ children = ( CA4737C8272F095F0012CAC3 /* Inventory.app */, CA473804272F0D330012CAC3 /* CaseStudies.app */, - DC5E07742947CCD700293F45 /* Standups.app */, - DC5E07832947CCD800293F45 /* StandupsTests.xctest */, - CA53F7FA295BEDBD00DE68FE /* StandupsUITests.xctest */, ); name = Products; sourceTree = ""; @@ -226,12 +132,11 @@ DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */, DC609AD5291D76150052647F /* 06-NavigationDestinations.swift */, DCD4E68A274180F500CDF3BD /* 07-NavigationLinks.swift */, - CA70FED6274B1907005A0D53 /* 08-NavigationLinkList.swift */, - CABE9FC0272F2C0000AFC150 /* 09-Routing.swift */, - CA47383D272F0F9B0012CAC3 /* 10-CustomComponents.swift */, - DC6A8410291F227400B3F6C9 /* 11-SynchronizedBindings.swift */, - CAAC0071292BDE660083F2FF /* 12-IfLet.swift */, - CA93236A292BE733004B1130 /* 13-IfCaseLet.swift */, + CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */, + CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */, + DC6A8410291F227400B3F6C9 /* 10-SynchronizedBindings.swift */, + CAAC0071292BDE660083F2FF /* 11-IfLet.swift */, + CA93236A292BE733004B1130 /* 12-IfCaseLet.swift */, CA473830272F0D860012CAC3 /* CaseStudiesApp.swift */, CA473831272F0D860012CAC3 /* FactClient.swift */, CA47382E272F0D860012CAC3 /* RootView.swift */, @@ -240,81 +145,6 @@ path = CaseStudies; sourceTree = ""; }; - CA53F7FB295BEDBE00DE68FE /* StandupsUITests */ = { - isa = PBXGroup; - children = ( - CA53F805295BEE4F00DE68FE /* StandupsListUITests.swift */, - ); - path = StandupsUITests; - sourceTree = ""; - }; - CA53F807295CC9A900DE68FE /* Standups */ = { - isa = PBXGroup; - children = ( - CA53F808295CCA2E00DE68FE /* Readme.md */, - CADF861F297765F000B7695B /* Resources */, - DC5E07752947CCD700293F45 /* Standups */, - DC5E07862947CCD800293F45 /* StandupsTests */, - CA53F7FB295BEDBE00DE68FE /* StandupsUITests */, - ); - path = Standups; - sourceTree = ""; - }; - CA53F809295CE9AD00DE68FE /* Dependencies */ = { - isa = PBXGroup; - children = ( - DCE73E0B2947D163004EE92E /* DataManager.swift */, - CAAA74DF2956956B009A25CA /* OpenSettings.swift */, - CADF861D2977652500B7695B /* SoundEffectClient.swift */, - DC5E07A82947CFB700293F45 /* SpeechClient.swift */, - ); - path = Dependencies; - sourceTree = ""; - }; - CADF861F297765F000B7695B /* Resources */ = { - isa = PBXGroup; - children = ( - CADF8620297765F000B7695B /* ding.wav */, - ); - path = Resources; - sourceTree = ""; - }; - DC5E07752947CCD700293F45 /* Standups */ = { - isa = PBXGroup; - children = ( - DC5E07A42947CFA000293F45 /* StandupForm.swift */, - DC5E07AA2947CFCA00293F45 /* Models.swift */, - DC5E07AC2947CFD300293F45 /* RecordMeeting.swift */, - DC5E07782947CCD700293F45 /* StandupDetail.swift */, - CA22CCC12967799600F52F6D /* Helpers.swift */, - DC5E07762947CCD700293F45 /* StandupsApp.swift */, - DC5E07A62947CFA600293F45 /* StandupsList.swift */, - DC5E077A2947CCD800293F45 /* Assets.xcassets */, - CA53F809295CE9AD00DE68FE /* Dependencies */, - DC5E077C2947CCD800293F45 /* Preview Content */, - ); - path = Standups; - sourceTree = ""; - }; - DC5E077C2947CCD800293F45 /* Preview Content */ = { - isa = PBXGroup; - children = ( - DC5E077D2947CCD800293F45 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - DC5E07862947CCD800293F45 /* StandupsTests */ = { - isa = PBXGroup; - children = ( - CA53F7F0295BBDB700DE68FE /* EditStandupTests.swift */, - CAAA74E52956A60A009A25CA /* RecordMeetingTests.swift */, - CAAA74E72956A658009A25CA /* StandupDetailTests.swift */, - CAAA74E329569F6C009A25CA /* StandupsListTests.swift */, - ); - path = StandupsTests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -359,67 +189,6 @@ productReference = CA473804272F0D330012CAC3 /* CaseStudies.app */; productType = "com.apple.product-type.application"; }; - CA53F7F9295BEDBD00DE68FE /* StandupsUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = CA53F802295BEDBE00DE68FE /* Build configuration list for PBXNativeTarget "StandupsUITests" */; - buildPhases = ( - CA53F7F6295BEDBD00DE68FE /* Sources */, - CA53F7F7295BEDBD00DE68FE /* Frameworks */, - CA53F7F8295BEDBD00DE68FE /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - CA53F801295BEDBE00DE68FE /* PBXTargetDependency */, - ); - name = StandupsUITests; - productName = StandupsUITests; - productReference = CA53F7FA295BEDBD00DE68FE /* StandupsUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; - DC5E07732947CCD700293F45 /* Standups */ = { - isa = PBXNativeTarget; - buildConfigurationList = DC5E079B2947CCD800293F45 /* Build configuration list for PBXNativeTarget "Standups" */; - buildPhases = ( - DC5E07702947CCD700293F45 /* Sources */, - DC5E07712947CCD700293F45 /* Frameworks */, - DC5E07722947CCD700293F45 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Standups; - packageProductDependencies = ( - DCE73E012947D02A004EE92E /* SwiftUINavigation */, - DCE73E042947D063004EE92E /* Tagged */, - DCE73E072947D082004EE92E /* IdentifiedCollections */, - CA6453992968A06E00802931 /* Dependencies */, - ); - productName = Standups; - productReference = DC5E07742947CCD700293F45 /* Standups.app */; - productType = "com.apple.product-type.application"; - }; - DC5E07822947CCD800293F45 /* StandupsTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = DC5E079C2947CCD800293F45 /* Build configuration list for PBXNativeTarget "StandupsTests" */; - buildPhases = ( - DC5E077F2947CCD800293F45 /* Sources */, - DC5E07802947CCD800293F45 /* Frameworks */, - DC5E07812947CCD800293F45 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - DC5E07852947CCD800293F45 /* PBXTargetDependency */, - ); - name = StandupsTests; - packageProductDependencies = ( - ); - productName = StandupsTests; - productReference = DC5E07832947CCD800293F45 /* StandupsTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -428,7 +197,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1310; + LastUpgradeCheck = 1510; TargetAttributes = { CA4737C7272F095F0012CAC3 = { CreatedOnToolsVersion = 13.1; @@ -438,17 +207,6 @@ CreatedOnToolsVersion = 13.1; LastSwiftMigration = 1310; }; - CA53F7F9295BEDBD00DE68FE = { - CreatedOnToolsVersion = 14.1; - TestTargetID = DC5E07732947CCD700293F45; - }; - DC5E07732947CCD700293F45 = { - CreatedOnToolsVersion = 14.1; - }; - DC5E07822947CCD800293F45 = { - CreatedOnToolsVersion = 14.1; - TestTargetID = DC5E07732947CCD700293F45; - }; }; }; buildConfigurationList = CA47378F272F08EF0012CAC3 /* Build configuration list for PBXProject "Examples" */; @@ -471,9 +229,6 @@ targets = ( CA473803272F0D330012CAC3 /* CaseStudies */, CA4737C7272F095F0012CAC3 /* Inventory */, - DC5E07732947CCD700293F45 /* Standups */, - DC5E07822947CCD800293F45 /* StandupsTests */, - CA53F7F9295BEDBD00DE68FE /* StandupsUITests */, ); }; /* End PBXProject section */ @@ -495,30 +250,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - CA53F7F8295BEDBD00DE68FE /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E07722947CCD700293F45 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CADF8621297765F000B7695B /* ding.wav in Resources */, - DC5E077E2947CCD800293F45 /* Preview Assets.xcassets in Resources */, - DC5E077B2947CCD800293F45 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E07812947CCD800293F45 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -537,77 +268,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CABE9FC1272F2C0000AFC150 /* 09-Routing.swift in Sources */, - DC6A8411291F227400B3F6C9 /* 11-SynchronizedBindings.swift in Sources */, + CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */, + DC6A8411291F227400B3F6C9 /* 10-SynchronizedBindings.swift in Sources */, CA473837272F0D860012CAC3 /* FactClient.swift in Sources */, CA473835272F0D860012CAC3 /* 02-ConfirmationDialogs.swift in Sources */, - CA47383E272F0F9B0012CAC3 /* 10-CustomComponents.swift in Sources */, - CA70FED7274B1907005A0D53 /* 08-NavigationLinkList.swift in Sources */, + CA47383E272F0F9B0012CAC3 /* 09-CustomComponents.swift in Sources */, CA473836272F0D860012CAC3 /* CaseStudiesApp.swift in Sources */, DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */, DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */, CA473834272F0D860012CAC3 /* RootView.swift in Sources */, CA473839272F0D860012CAC3 /* 01-Alerts.swift in Sources */, DCD4E68B274180F500CDF3BD /* 07-NavigationLinks.swift in Sources */, - CAAC0072292BDE660083F2FF /* 12-IfLet.swift in Sources */, + CAAC0072292BDE660083F2FF /* 11-IfLet.swift in Sources */, DC609AD6291D76150052647F /* 06-NavigationDestinations.swift in Sources */, - CA93236B292BE733004B1130 /* 13-IfCaseLet.swift in Sources */, + CA93236B292BE733004B1130 /* 12-IfCaseLet.swift in Sources */, CA473838272F0D860012CAC3 /* 03-Sheets.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - CA53F7F6295BEDBD00DE68FE /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CA53F806295BEE4F00DE68FE /* StandupsListUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E07702947CCD700293F45 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DC5E07792947CCD700293F45 /* StandupDetail.swift in Sources */, - DCE73E0C2947D163004EE92E /* DataManager.swift in Sources */, - DC5E07772947CCD700293F45 /* StandupsApp.swift in Sources */, - CADF861E2977652500B7695B /* SoundEffectClient.swift in Sources */, - CA22CCC22967799600F52F6D /* Helpers.swift in Sources */, - DC5E07A52947CFA000293F45 /* StandupForm.swift in Sources */, - DC5E07AB2947CFCA00293F45 /* Models.swift in Sources */, - DC5E07AD2947CFD300293F45 /* RecordMeeting.swift in Sources */, - DC5E07A92947CFB700293F45 /* SpeechClient.swift in Sources */, - CAAA74E02956956B009A25CA /* OpenSettings.swift in Sources */, - DC5E07A72947CFA600293F45 /* StandupsList.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DC5E077F2947CCD800293F45 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CAAA74E82956A658009A25CA /* StandupDetailTests.swift in Sources */, - CAAA74E62956A60A009A25CA /* RecordMeetingTests.swift in Sources */, - CA53F7F1295BBDB700DE68FE /* EditStandupTests.swift in Sources */, - CAAA74E429569F6C009A25CA /* StandupsListTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - CA53F801295BEDBE00DE68FE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DC5E07732947CCD700293F45 /* Standups */; - targetProxy = CA53F800295BEDBE00DE68FE /* PBXContainerItemProxy */; - }; - DC5E07852947CCD800293F45 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DC5E07732947CCD700293F45 /* Standups */; - targetProxy = DC5E07842947CCD800293F45 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ CA4737B6272F08F10012CAC3 /* Debug */ = { isa = XCBuildConfiguration; @@ -646,6 +326,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -660,7 +341,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -708,6 +388,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -716,7 +397,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -742,7 +422,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -772,7 +451,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -802,7 +480,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -833,7 +510,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -847,150 +523,6 @@ }; name = Release; }; - CA53F803295BEDBE00DE68FE /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Standups; - }; - name = Debug; - }; - CA53F804295BEDBE00DE68FE /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Standups; - }; - name = Release; - }; - DC5E07952947CCD800293F45 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Standups/Standups/Preview Content\""; - DEVELOPMENT_TEAM = VFRXY8HC3H; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "To transcribe meeting notes."; - INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "To transcribe meeting notes."; - 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"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Standups; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - DC5E07962947CCD800293F45 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Standups/Standups/Preview Content\""; - DEVELOPMENT_TEAM = VFRXY8HC3H; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "To transcribe meeting notes."; - INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "To transcribe meeting notes."; - 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"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Standups; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - DC5E07972947CCD800293F45 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = VFRXY8HC3H; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Standups.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Standups"; - }; - name = Debug; - }; - DC5E07982947CCD800293F45 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = VFRXY8HC3H; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Standups.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Standups"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1021,33 +553,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - CA53F802295BEDBE00DE68FE /* Build configuration list for PBXNativeTarget "StandupsUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CA53F803295BEDBE00DE68FE /* Debug */, - CA53F804295BEDBE00DE68FE /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DC5E079B2947CCD800293F45 /* Build configuration list for PBXNativeTarget "Standups" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DC5E07952947CCD800293F45 /* Debug */, - DC5E07962947CCD800293F45 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DC5E079C2947CCD800293F45 /* Build configuration list for PBXNativeTarget "StandupsTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DC5E07972947CCD800293F45 /* Debug */, - DC5E07982947CCD800293F45 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1086,25 +591,6 @@ isa = XCSwiftPackageProductDependency; productName = SwiftUINavigation; }; - CA6453992968A06E00802931 /* Dependencies */ = { - isa = XCSwiftPackageProductDependency; - package = CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */; - productName = Dependencies; - }; - DCE73E012947D02A004EE92E /* SwiftUINavigation */ = { - isa = XCSwiftPackageProductDependency; - productName = SwiftUINavigation; - }; - DCE73E042947D063004EE92E /* Tagged */ = { - isa = XCSwiftPackageProductDependency; - package = DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */; - productName = Tagged; - }; - DCE73E072947D082004EE92E /* IdentifiedCollections */ = { - isa = XCSwiftPackageProductDependency; - package = DCE73E062947D082004EE92E /* XCRemoteSwiftPackageReference "swift-identified-collections" */; - productName = IdentifiedCollections; - }; DCE73E092947D090004EE92E /* IdentifiedCollections */ = { isa = XCSwiftPackageProductDependency; package = DCE73E062947D082004EE92E /* XCRemoteSwiftPackageReference "swift-identified-collections" */; diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme index 4053f97674..746e3f0427 100644 --- a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Inventory/App.swift b/Examples/Inventory/App.swift index 6efbbb2e99..0f7856452a 100644 --- a/Examples/Inventory/App.swift +++ b/Examples/Inventory/App.swift @@ -28,9 +28,10 @@ struct InventoryApp: App { } } -class AppModel: ObservableObject { - @Published var inventoryModel: InventoryModel - @Published var selectedTab: Tab +@Observable +class AppModel { + var inventoryModel: InventoryModel + var selectedTab: Tab init( inventoryModel: InventoryModel, @@ -47,7 +48,7 @@ class AppModel: ObservableObject { } struct AppView: View { - @ObservedObject var model: AppModel + @State var model: AppModel var body: some View { TabView(selection: self.$model.selectedTab) { @@ -71,3 +72,7 @@ struct AppView: View { } } } + +#Preview { + AppView(model: AppModel(inventoryModel: InventoryModel())) +} diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index 01e060b1cd..5f60d3680e 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -2,11 +2,12 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigation -class InventoryModel: ObservableObject { - @Published var inventory: IdentifiedArrayOf { +@Observable +class InventoryModel { + var inventory: IdentifiedArrayOf { didSet { self.bind() } } - @Published var destination: Destination? + var destination: Destination? enum Destination: Equatable { case add(Item) @@ -69,7 +70,7 @@ class InventoryModel: ObservableObject { } struct InventoryView: View { - @ObservedObject var model: InventoryModel + @State var model: InventoryModel var body: some View { List { @@ -123,31 +124,33 @@ struct InventoryView: View { } } -struct InventoryView_Previews: PreviewProvider { - static var previews: some View { - let keyboard = Item(color: .blue, name: "Keyboard", status: .inStock(quantity: 100)) +#Preview { + let keyboard = Item( + color: .blue, + name: "Keyboard", + status: .inStock(quantity: 100) + ) - NavigationStack { - InventoryView( - model: InventoryModel( - inventory: [ - ItemRowModel( - item: keyboard - ), - ItemRowModel( - item: Item(color: .yellow, name: "Charger", status: .inStock(quantity: 20)) - ), - ItemRowModel( - item: Item(color: .green, name: "Phone", status: .outOfStock(isOnBackOrder: true)) - ), - ItemRowModel( - item: Item( - color: .green, name: "Headphones", status: .outOfStock(isOnBackOrder: false) - ) - ), - ] - ) + return NavigationStack { + InventoryView( + model: InventoryModel( + inventory: [ + ItemRowModel( + item: keyboard + ), + ItemRowModel( + item: Item(color: .yellow, name: "Charger", status: .inStock(quantity: 20)) + ), + ItemRowModel( + item: Item(color: .green, name: "Phone", status: .outOfStock(isOnBackOrder: true)) + ), + ItemRowModel( + item: Item( + color: .green, name: "Headphones", status: .outOfStock(isOnBackOrder: false) + ) + ), + ] ) - } + ) } } diff --git a/Examples/Inventory/Item.swift b/Examples/Inventory/Item.swift index 6e1ca7edf8..5503df1103 100644 --- a/Examples/Inventory/Item.swift +++ b/Examples/Inventory/Item.swift @@ -90,16 +90,8 @@ struct ItemView: View { } } -struct ItemView_Previews: PreviewProvider, View { - @State var item = Item(color: nil, name: "", status: .inStock(quantity: 1)) - - static var previews: some View { - NavigationStack { - ItemView_Previews() - } - } - - var body: some View { - ItemView(item: self.$item) +#Preview { + WithState(initialValue: Item(color: nil, name: "", status: .inStock(quantity: 1))) { $item in + ItemView(item: $item) } } diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index 83ba136935..869603e0fa 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -2,9 +2,10 @@ import SwiftUI import SwiftUINavigation import XCTestDynamicOverlay -class ItemRowModel: Identifiable, ObservableObject { - @Published var item: Item - @Published var destination: Destination? +@Observable +class ItemRowModel: Identifiable { + var item: Item + var destination: Destination? enum Destination: Equatable { case alert(AlertState) @@ -73,7 +74,7 @@ extension Item { } struct ItemRowView: View { - @ObservedObject var model: ItemRowModel + @State var model: ItemRowModel var body: some View { Button { @@ -144,3 +145,16 @@ struct ItemRowView: View { } } } + +#Preview { + List { + ItemRowView( + model: ItemRowModel( + item: Item( + name: "Keyboard", + status: .inStock(quantity: 42) + ) + ) + ) + } +} diff --git a/Examples/Standups/Readme.md b/Examples/Standups/Readme.md deleted file mode 100644 index 52dec1047c..0000000000 --- a/Examples/Standups/Readme.md +++ /dev/null @@ -1,77 +0,0 @@ -# Standups - -This project demonstrates how to build a complex, real world application that deals with many forms -of navigation (_e.g._, sheets, drill-downs, alerts), many side effects (timers, speech recognizer, -data persistence), and do so in a way that is testable and modular. - -This application was built over the course of [many episodes][modern-swiftui-collection] on -Point-Free, a video series exploring functional programming and the Swift language, hosted by -[Brandon Williams](https://twitter.com/mbrandonw) and [Stephen -Celis](https://twitter.com/stephencelis). - - - video poster image - - -## Overview - -The inspiration for this application comes Apple's [Scrumdinger][scrumdinger] tutorial: - -> This module guides you through the development of Scrumdinger, an iOS app that helps users manage -> their daily scrums. To help keep scrums short and focused, Scrumdinger uses visual and audio cues -> to indicate when and how long each attendee should speak. The app also displays a progress screen -> that shows the time remaining in the meeting and creates a transcript that users can refer to -> later. - -The Scrumdinger app is one of Apple's most interesting code samples as it deals with many real world -world problems that one faces in application development. It shows off many types of navigation, -it deals with complex effects such as timers and speech recognition, and it persists application -data to disk. - -However, it is not necessarily built in the most ideal way. It uses mostly fire-and-forget style -navigation, which means you can't easily deep link into any screen of the app, which is handy for -push notifications and opening URLs. It also uses uncontrolled dependencies, including file system -access, timers and a speech recognizer, which makes it nearly impossible to write automated tests -and even hinders the ability to preview the app in Xcode previews. - -But, the simplicity of Apple's Scrumdinger codebase is not a defect. In fact, it's a feature! -Apple's sample code is viewed by hundreds of thousands of developers across the world, and so its -goal is to be as approachable as possible in order to teach the basics of SwiftUI. But, that doesn't -mean there isn't room for improvement. - -## Modern SwiftUI - -Our Standups application is a rebuild of Apple's Scrumdinger application, but with a focus on -modern, best practices for SwiftUI development. We faithfully recreate the Scrumdinger, but with -some key additions: - - 1. Identifiers are made type safe using our [Tagged library][tagged-gh]. This prevents us from - writing non-sensical code, such as comparing a `Standup.ID` to a `Attendee.ID`. - 2. Instead of using bare arrays in feature logic we use an "identified" array from our - [IdentifiedCollections][identified-collections-gh] library. This allows you to read and modify - elements of the collection via their ID rather than positional index, which can be error prone - and lead to bugs or crashes. - 3. _All_ navigation is driven off of state, including sheets, drill-downs and alerts. This makes - it possible to deep link into any screen of the app by just constructing a piece of state and - handing it off to SwiftUI. - 4. Further, each view represents its navigation destinations as a single enum, which gives us - compile time proof that two destinations cannot be active at the same time. This cannot be - accomplished with default SwiftUI tools, but can be done with our [SwiftUINavigation - library][swiftui-nav-gh]. - 5. All side effects are controlled. This includes access to the file system for persistence, access - to time-based asynchrony for timers, access to speech recognition APIs, and even the creation - of dates and UUIDs. This allows us to run our application in specific execution contexts, which - is very useful in tests and Xcode previews. We accomplish this using our - [Dependencies][dependencies-gh] library. - 6. The project includes a full test suite. Since all of navigation is driven off of state, and - because we controlled all dependencies, we can write very comprehensive and nuanced tests. For - example, we can write a unit test that proves that when a standup meeting's timer runs out the - screen pops off the stack and a new transcript is added to the standup. Such a test would be - very difficult, if not impossible, without controlling dependencies. - -[modern-swiftui-collection]: https://www.pointfree.co/collections/swiftui/modern-swiftui -[scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger -[tagged-gh]: http://github.com/pointfreeco/swift-tagged -[identified-collections-gh]: http://github.com/pointfreeco/swift-identified-collections -[swiftui-nav-gh]: http://github.com/pointfreeco/swiftui-navigation -[dependencies-gh]: http://github.com/pointfreeco/swift-dependencies diff --git a/Examples/Standups/Resources/ding.wav b/Examples/Standups/Resources/ding.wav deleted file mode 100644 index 5831df269dda704add693c6ff8468400559e22de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535904 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

7{1zAqfq$7$2 z9=R`)2Vuf+v>1Z{8c>~#!WlR;2W7$)qYsm1P%yy|b4*)c3fk(d#Isxb5l#r+`Y z1R%!`pS&^96CK?UEFQWYUR%M$0){J*Z-Sr8khxg0?5woWY&MFf;+!T9t7DuBT!-Pn zK&XnFdv`SLj8AP)+Zne{RPkd4*OjuNh{**^ z-p2>K**=F#SzM9EE-4(J#L4k&873okmcP8^=?sca|h zc6V*qQJ|i!`PrH|Hk@b6Qd>!%(-&=YPE--?kgins;81U_^y9h!t`BCHaE^?Yc7n@E zY@f>g>8#u;xtHs6dF}w`9O1&_+*ZMhRlHuyR@Zst4v#)!^h+wfXT=vj{LN(w2yDFV zj_iT;{h>4zW-8FsK<6nio{i%=s9y|MW4tk!`MdWH2zN(_FV>0D^9YQJN9zqR-i)YB zeAPMSdTY%@Z&Zf-ol-mh`52n*P(wE&o0Sw!=cr9UxihtQGXJyqK~o!QAhCLAPn|N z%$i;f`ek9eV2U=Hb9P7JZiqB%S$U$w8Q*LXV1bjSa5TgMeRLBR%Go$L1P#1set2{lnAWIQ$dS->~pG_djIcdiK4+Iak=UhWpR*VFk-i@K6aS zAK~ePY@g4Dy*#vwqeX}NR!+&}w{%|K%sr`$-pHC{?oDD{q8x>ZNwtvw1koseJ?qx< z(|YD6upyC-$t>Q$t|_uLa%vi%ZJ~Y^ONHW`!}hy*JeQ;PGq!+cM_66LrDfbz$;W3I zT+L&b`KgWy^?d(;s?T`8fu$d~>?@D_;v!&e3+!l%kk0tq9aViXT^WfZq=}_g16L-a zyFiR;W4kW;8sNGycAI0QB^+%L(5fT zoKKF@zJSZ}cw+|}GHJVso0FuaN+*JggP7sVcn|(`mKRk`E84GSk{OjusAov~C2X%p zLvcIgdYO$lHGnGAf)Q3v~7#zw`(Y&}`;)^Gz@n@Di z_D{@X{Xx1FaYLyzD(If4;w8Sk&e8RZe8gQZ826S2|D(ze8i{M8Xr^d|NgZ&$D|Ylk z-vLsLxIY}l}HKN!LZ6#V}YZ4YWljIAewdt6;kZ0hZWdg&j6% zZHH#|cxR8_4ybZOYbW_V&Jj+IXzz&L4k)t2SX-${`3g3UB`Q`!c_nU{LeB&(jIhK2 zXY}B<5YM%-O$#rlW8GxfOu)mjI5ApwL#PjeK|gfu3BxWhZ->n-q1qG~e`I*!;s@Sr zpvE%}5Zwf~*zqb`)Ud}{zAR@%2~`fUgNP9BruH^Y-NODUR7>EgXj+A_DuA7RSnb9y zj+`ROdqcik!taY%uS2zY9Ir*snGBmo&&hnRNyQ1WxBa*}U#oGSDtn2@ z=NMU#aZ-g9Ds&!04^_@rqvBYaiArA$o}9p!6Zva0Q>XF4Oc^+>*QT=goaoVZDF-f> zhpyIZ_|S^W9oWW=>E8Uhj;W!18qL5&o=WAh4ED;QVlFic`12@3$~fx`Q)=Yrwog4@ z3ZsHhOhkTA=!1V5*bMXAAgVK-^hC)3bQp@z(J&l`5siw2_B?dg#Q+1$U5*dtcx-|5 zHt6JpMeb6vsqsf`5SoSKcNFY|1fPJx$!ME`iz0a~^7~t`C=+E_(9VW?W5tzdxoWo^ zPTMhcyDW6Rw-tZ0&@~G&8JL_dYYp>K5Vip~5)mGcchSg-)aR#5{k25r_rtM`q*0F0n zCq1Id3!Zwz$WJ`^os0f)e=`}(dD0OpyJJ{CTu?@vkyxpQ=M%7h8uaGizye$o!wO?r zt}%J7ENXAzikDtc3&6rK924iC^%%QRLawH4#f=>MW`!*;R!rCiHy_e zbPm@ppldAxE@8MZ*j&e*I()i;F}DzW8)0`)en)<{61LboP`ZOYw~>4k-EW}&I&`n$ zy}<8Wghe%8oI}gAuoDC7#u4R{BHSs2&VK0c#iAYX-zp6x{e){P0p`)D78s=fJn@3L z3+~urvjvRJaKR9F_3%_1nzQ8Y(^&(rM@wIpQsbnnLl@j@gOAM+{f}#Z@U>|Ae#67h znE!w+?r`RH_PfX)=Xg)ltd#LFL~*Go;EtnVg1kGI zr*O+=-rB-rTlq7Gt@lU*;hzIMewbw?Oe>?BfP0^#Wi4@y4!0yl=j~%=ykx>VntbNh zAGH5Rm1Zz$4fl?Cr6@@Sib`_6*JCtX#v)+?+D*liSuoPZoJNPnM?*|E#rW0mSql$a zoOgt+8{)n2L9hx%h9y|0as8sCHCi(shsAF@8DSgoAO-(Y;k6mx)9@-CBey_f3%s}B z>=p!z_Fj=$5M|$+Wzs<{6^a{iJ_*GM*b?r+b#_!|@K{!=ai|(6 zsqvH=eaCW$I$x^G5X9s0{G-YCllgH9Cr_vLESAmTeL=TdKsP;R8}PRgrnSe zvaQ^R13YNxOS?cW4rgL4mnZQ|DibpJzwO-LNS8fwgr~&HBQ#vgUl{+33Qe%RB{JIKRcEa2frx&HQAX`>1ggMOU2efIr^0X+7SDy94o)mW%@R2_ zv{?>AQ`oJ9*BUfii>p@fu|cpM>>QBlh+aA2a$27XjeK+{#GHWl;|1@(2$fAU25Ajq!Yj$&y z;KycCV>2_8IU=5>QEU=M<3M`*@{$KjT-e!>OKfSlR@TpHn{&?!-ZJK~Wo%)$UCV9P=~d6$kBFBndB^Wx*x?VYMHcu!ICa2FSqACv{FZRoHAUc2yf z4{Yybd9<5IqV|bO4t7@pd2X z@nSDlN3+pT9|}h$==H|kuGrijR|HW<0lI%l!TSElv^QM+f|ZZ>s-8YKn0JMrYWTT| zZ!0;qj5x-QMI0cg@B7&)muGhIZ;mXcxs}N;>3qML(>8Hg3XM0gDv7m;oSVqv1R{aq z>shs)83|mR$m5AJ>Cj^XC#SH}Chp$M{B$N}N;gh%4g+?vcrRn~SysRbVPh^~)d|K3 zKUEc-YdG}^$J}6iJ?B4ShZppC!>b>8X>V@u?OsYSH#0F4Ur` zM((q-E9xcJ!a$P6g<$tFBKeY#B?b2!3ag@x5w|6 zc-RE7zj^Bmd%ox6mmKnh{qC{i2JJ7i@B(em@RQJ9g#vq+P6v20m%VmTeH#~K@b4zh z6CvD0>5D!ZMfY$i^|lld(RFn9EuXcJbg0FGl+?!H-+lQ9X#uLs=8SUt*9F&kc#ZwUM*ZXqZX8Z9KhGT1`40Wc^`A zm$0mimZzzBfxeemd7b8Wxa1)no^#9_?)gajZ%p{ZLroDQx+mJBu#1GemG{Sr!SYhv zFa|p`Fn%&-&p`V*csn2W7vZ$%J}|~gQ!HEsJ4-~{V5|dlouTOl10g{8$YEor09+5m zlVH3Ig+VwvM<6Ex9+8+5g`g<>jKsu9R7Joy0-oWB4@Fck{tH5I0DAf3pEtgFz}O8z z&iG`H`_?#bfgdX|#1w~&khugAy11>4aawpf4Zk!oK^+f9!+IDz2ckzGoNXLgd~St2 z1zh_>^>6(9f$tmW`II5|c;W^lFEjW8|D9r=6Wm$E?1Ma-OQ#*|oW)<8sFTc^IF1o# zU~vTUlhwMPT-els@2sf2hPTXkYdIe;GM$F>|Ev}s@>o%rNq4^}HXfka) z&yHiJI)9F(@mRX4@v_Ktt8%p(U#fB5Sl$}T@9I3JL4VP`K9S**cw-8$PiN6A4w@s2 zhV>V4u^yWl(8G{ZOt{OOz836aLw5%@b>%28mix0?2nR)SSsVu>bK+(W%;Mo38tRV=u0GcSU`zNz1BU{6!BOJQnQg74`z}z8d zF$!*K=sF%rldxtw^tJF%8?P23SszAXK5UG>rkJo2#jCN-5^>hhv%`1?`wq?K?q#r);4 z5>WBQP|=0WeAv!`KGWehh{AK)mjQ1KqKxv!vL?v_Lx0={J48GD8e` z8aVDLcid;6+Z=d}FKXDQin$^SP|7Vuaz0>?#~Hh5x{YfyIAD|f)s@8aU^Femc`=B4 z1uN2r13bCMl`Ta+*N)jXQXpSmewF?riDJ0zWziGBA`1QLKz(P!dO^vfUP1X3M4H zNFEi%;IxQ^r5t>UQ_i!;C2H33dObrPv-K+)zh}W0uKZ2QrWo7`_8o9a5n+80JqV*m zV1X*!$D{pJoS2O!I;hY?<}w^#fjg^_V=ZTuUEL7ijkW<$3`JrjzQxKB){I8}hA^@U zPo3Cst9PS!9(D>x%>mdIV)PN%9>vaLoGO9AaeOPqy%YG+_+?v$dxDTwCdZHZrPz5K z_m07|1nNh@!x$$l?m}(c50^X`?Lpj5L~O@%;ocOt=~T25J?sg%9)orf7!eFle|+{r z2l3%?KznP{t;P*gcpD*OG4?M&*SXj?14AaGw+3#h$hwW=gP_q5&3d4+6V%#5p(Uc5 zV9Rgjf91-L41B|&7gTu6z`NXilg`)J`4anHpw=1AtKj7mR6NE%M&atAFMOT?{lR9@f{Shw;=+hhi{wUX| zy5IcR1nvIAgLVk)0()U{=#TzGFl-d=tKq@~3>VFtTCmqa6MdXphPxtGDGn;uD0RRe zSM2u2lmN^QMQJ4TrxoH@s+aD?3t zFnce{cd&al$8VupDjk!!KE^aMGktjr1j(;n#&m3N>;cO|nqhh+X+zulh(85X5mNZ>Z=7MFSKG+o#-SE^E z6I`*?1ub0AL)0rc;+#Eh+oGopx(lwa1)Nr4kr@(95Nw2e14)<PlH>Wbd{&8`QI5v zl`*84Ifb0NkJ&qEx0PqosJf9O*UJ#u)DX#}8s{zR2&X!5r3h(SaECenGvUT%Okd1S zi?~~dW9PAIHc!l;_B6JeA~PoL6Bw?+nd4}#PMxvrtwv{6hN-e%mETp_MU5xbsG!b} zar~mel@mB*A~Pn_Y8qS33D)Ff{}He-!Adk4gR<*yC<%`WbAwP`^rJT zc^){}0)5+IaTmPn0rh@3I|y5bWBM35s~j*9EvKRDZ2S>a|AmOuNAyx0H^x&_1gt`g z1>&sm*cM$Jk?xG0t~lk6-=6p@csRb;>L(o_zUvScfX?Co7J$^ouh0P867fKP==q_) zNELZwmKXl>K&-3WwTFuzWd}2BJhVW`Din$Nwh1&0@o_Pp>Ow^a?Hlc~I#Y0P0*usA zr2_Bacrgf<`(eBw#wcQa2OMk#F?~HxrvBotFRXvh!Un#4#-fM(cZU{rTy>e}s--{^ zRKeM$EIZ1$Lu_||i*jYawsaeNX3-*@fvMb{%!LUIi{qteo{nH`D7}JtAdsKd{hy<= z0LpS(qwuo3yA`_?yFoBeRJub@LQoVH5D5hVm6Q;rTT)7qRxAVyDH^mM+=t? zU@3@>R?xP`>kSgMam*bHys>I0tODU03b#nih(U6Klpu&qRH&I#lc!h%b8FE^E3E%8m~{{!EyAeLXV?x zDu-nWREnTi2xZY{$%dC$N2Xx6h>pad|33K&9vg&=U3j@e662e=!fzAQ9c0G9dKI)x zu}U9jHKD4Gl?wqST$+KglaW3S5hE}~0VDe&raMM-f@&M6G=sY^+x+6UFD!h|j;|Q_ zgs1M)_zok4TIe!QoM&kbKb+#dDyCNOc!^BRh8EH(k0V7;CsS^Uf2HzXGIu93EM9K< zoMY)6!%5Lx6h(z78b{G3ids>$j-qxn%VYRDmZ#(RPq>E@X_mr$Y4k~FQYQE3uuDE? z9OCyPwl3xO3aT8JN!_9vMxCeXWlp@#n%iu2pMH;d>;)&jqxxsI{=x5m=++qXTjFp# zRCdO#9@xw?kn)*90iVNi%!-e%#oLC1FSm#rFN)p~WH9Y^)5(Xwg?!mf zGHon2a<&6Y?dV{`+p9QpB}bU@KO@E(u$P`3D%NQ-Nih6ZaQSj}Uq+{flwGbmOVydK z&N0jAuv{*QI%rZsWFxhyDeBt>>~75ErX0DFIjcC{hK_bj6e3Yao^ayGt(@q|8Q!e( zlPbOYd$?yW)uXAC!0V~B5EQRGCLCt7GG09sK47cf#K8aPEVr17JP`y+-2hSPWOh<0*)nfwVa|sEqLo(0eidEWt>D zxzNNLZM@dS4H2a_f}#nYnc}h;_L-w{CH`4puO&(#RZ3%wQ_u zJjPHr#1B2R)s>4XV-5UO#~C&JScvB;n5={$v(axl7EG3G*p_3kK+K~QaH}8mdtseu zE_6UxE4VhrevvT!Mbj^ge#fyd`Q$N;@5%L$(G^J}aXQ1-$N675Zx+!mpIX_xlg4I= zG>_(-z0?tBm;iS2WBqn^aA%DRw{2pb1N+-?w6$!|cQ@x$WBTgTUc{(0Iei87)wx@Z zR*U#(0nJqzJ)bX>_+TCr=5fh928q|}T*9y-&$j>yVFI*I`V+5Xxul5ui znu%ZYuyp}cm*Tz#j_P8x5nzsAt8mB`X=^cK13GL*|1CJ_h7wOK6V(PEIm+-Awd(+s z1!6@oy6u5uD0+usUKrxTuqYhPVhs_7>!Iisihw-`48hGH6b0bOZj9W8(SDe}1C6(1 zjt4Tf!rcXFn^3(T1J=s1wzyySTnP_j{MAFW78I6awi>>w;NDyen~B9!WN+PO6nYFr zqk(wU8@swmZn;To%x(sCpu=yj{mO5`eDs<-pUHYo&w6&d!SXBgyvSR%(!7ve#TykI zUP?6q-7VmjgItqE|8(w1glFJ3$Bbwf^ycf@$B)&?eVg`q1(`WzYW8DBr#)8FxG}+!#?N>giZpi zQlXN8ZGtS93q>(cE<$t(zLa5PC4!IP#c@16iJ_;_yBZn-?_2}7T5PR_vYT`d*2&a;TNU;|Rta!k#=RMaU>Xi zyP@oZUfb~56)~G})dBT_CA$inOi`qd&6@bR1b0=@b{^W!z%E6+6r_%!xHkazdqJxU z%G;u_IYii7`!`z&b?iINe92Z%n0SvjZ*s;}Dqm!NE$^M;f5+IroKKGMOCg))v28YI zr1L@w?S+C5eh#N@m|O&02&Q8YYXWHz$g=?)6u<}mTo=Ga;=cxRQjp9N zyb0ljP+ErbT?Cu&V{H@*V`-7V*GZD@5}QHS95&9EHC5$eo++nl6?dOvK@EpoVCO4b za)W*Anf-u%Px_zpz)V%bjU z?t)iCk-;_){{qn>2=juFAB1Z`vdOzX5J3UBISsywup5gH!|`$us{5dE zckJqj{MMMz6xaUp%XiKcg7P=?dCGzJsUX_ES2_CvFIF?EiU-QrqKK+_oRi6+DU6Qe zuKk=FE>{C>#MXWX^910?m46#ll2g~x*p^A#W(OlWV&Bt4dEGf#u>m+|6K{!nAD z#ay8kWm;hp(hF`seD?4`^h$}Ci-=6qhC&+RHas7l9$)LhJ`YRnQGmF4`b!8_XA zrbit^7MU<-B|}$plr10FOQ2a#C!TQSXipYXsV;r3bfWjV_iw*%{GLwF(#Ozr#YflBGLkbEm3L- zPf;#jg)6HhASKLFDz@V+WTeO545v*bu`1sX!}PI4SROR7Yz1u8(LxOl3vfdjnsbpi z6KPX1SP}ci!f~V|S?wAKwLVbkj-8!wUj)2cpj~6^`@@~z8S#n3-g3!v?s>>9_4K{L zKY}fJp03sOJKLiIwsn&nJq_KakC}+uB3(;D^2*vh{HsZS)W7nnW)FB;sUG7KRTSN%LBR`B^c~_ z+^o+m0~#8!iwOfvInkVP7TmB}8d5^nP)Q)v)^qPBj& z{ZaBsE~2o%8$QZ=hj^isKF1`SFz_r7Uy|>afp;b4xb6kVzvtnvtpClEjc~dJI<-eo zSJd>zo`G_p;ULtpia0bK-R8ne1v+Y&umZbv;AjX#Gs#3Mvc-mV$aTaWCyd#OU7j*O z^VnCGk3^#8O9)<)ffJ6n_VxeQz}Fiej-JY=tdNk@k;2fAH;R znN#Zjiq%gTai33bGfuP!uh8KFuhj75DGm|q@=Au5@?;Uc3t5oIKZ0hH#mVVBk;>L7 z+>*qd3CxP4Wh_s|P&tO~(eeboJBr<-c_f;RVrVN`hq3G$&kG5hnZ)vB-WIv#bgsbJ*D$2n!cyo7kQ@>m-+$C zVAvY>8m7%@eK2VdVuxY!81zs?kLmIlEW*Rii!o(6eraQsA-v46V-?=m!ps5YoAAN~ zTivnJ3$y(YFG9<~&s z;bo|RQzfD*v9l7%l{ixgn@Si~U~oAmlwn0FZWQA{5f&VRc>#9k;ZqL6Gof<;fhovH z1mbXhKPn^8HWXumP$K*bJMhI5&Rg-s370lthCQ;Zkhv0jjqykqH7oE_4WX*|Iaj`J z4k_Z`7~B)doIx1e2lZVM+a9M{Lb(ad|FZjc{{6s!*HnH+-vgNlS-sj16kd%QMQ>chwwEzQxv z5+_z8+6Gy6uwI9t^_b;|CY#XA2^XDl*9E#;(9sp$wxVJyg0|wFD?(kd*%i;Xz{dqQ zoM5*Zy*I(o5g#0|Xf5*AV6KhSOi#Cfiz&_+VznM_X<_ejJXFK#1z4brr?YW&8rCU7 zYb;zxz;Q7C6P%nLQ098z*k(G+&awsMg%%mxRo5pRW$t1!`0ehtN2 zyO}A>MEOM@KXqWF1=|(4u@vnV!(CM(Uslb*OHmt~jKLGoXAB~T!&3n!{jk3$nsi0S z4v1-mE=}?KAN%~`y)T^jp206^_=KkS+4DAc{>RpJyjII^r}*m_&y>-nh{5^npToR# zz80a=c%F@7<4BGOW7}Xh_NTj_jLuqY=U5Lm-6{zx)h(>e}x;HUMp-zbX@-vxt39GTqdyNzvkNY9<`Zq5qg z;ZRPE!* zh;YF+H{@)Ck`LDH!m9v`50UbVnUOGxLYG)?`cO z2XRY8*>e${i@CX2dJvm)a6cQpvk{Sr^mN=y!>m*oB;!*8#)(>)FkbD$j|dcoN}#v4 zKkoa%atBrk3f5NmI>TPnkR6b<2I(R`Z4L!vcnBuBCVHvkf&eNi=*;1 z;RsQHc|RmKR9D0wuv1H@Ho?Ka^!UMtpK14=dtY()Qx-j-e?8-G()>SZ-OW75u{C@t za;n0>Qpvd@!&WS92)>0(7mY&EX3FN*OfJdbh6DVY%9*J$Dz7BqMak@u%vH(kn!=0} z{z+jjmVimPd$e(8 zc)+Zu{Pv2CKFF|~$xq(+$K6eFz7?*wM~|+k?S-HLGO}(p3Kzy>)fDubh0{uCu@Hlo zB0vKp^{`X8NLFHs73SGNYdxlIM)4Lr5Og>p5%ZN%$&4U;*#qwgXzWKx3{2y3H4*+P zP(J|w3@pmRg=}aY#H(D4%g6J4%q+m(0<ancoSoRF{q*X^;_8jUP)&=@ItSfPon zOR;MKF3!V@nQ)#A-*HeA0eA(N^+jrTBz45t*7(x|X8(BN2eUuX<_(>nvBg7X-{Gfg zY;=hZXX$X7;o^5&F4dy-`Hak=Q3g{}SRc=dXinJ2)Nq~@!-@b#?c!r!PToO{?cC(S z=58G4%6%@ZapJj6T)vT!j_l^hX&YoR=Yaz?*GsI{+4X$Bo`*KD>qc(gB#~u8zvAY? z^RDdbF6{tX+xgv_{rtFWH)jWON(ghq7%Y-?B7`5$r%8N~Mx88P%BA8VZYk!Ma#mOI z)MH$xxb+%E=w<-_-U@RFY% zbHP0xy-8kSiwpc)&8f#(RZf*7G|Q)LmK@<8O^}UCfycKCp{BoF40ZBiTX!nBuxz7z z{9d+Wq&3%D@~RoDj9F?xYhCu$qU#F!E#vm3Y^TOc3u&`}{;KlzI%>Xr;XYSlcj1~; zW>aM@6z^iuTT!92D$^H`3wdfWyDgQql$sT6uEj?>TxGy3#;iAE7cmI6<~loiJ8+GV zS~_#1o1_i*@a2!)d=$(V;Z%#_<9IGlrBx=cyyL(xO#8{Yf9&5BjauVq2i)t5=w68HkKRLIJ`#_G|5_0lQ?Y9n+9=_k3LF>7 ziKWE~bkxQmJ={0Ib7QnI$7KurS%s%ovUSqcPVOd#u0vM`v|A5_4e0DB-5En1(b7@6 zBNZI6d;|KemjPPUbr`l5`gWLSiy78%T8;JM)@&}X#;**qNFS?p@J$1YmmyjWQx+m; zK9c8R*-SK^3Qokov2Y)W^dY!C03LlXvIk5$V{|(VZ;99@7%WQMzj)^hFTSVBYo2?? zTMs24Se!haud-JiH=bqwX^uV4mX+*PDu*6*`OM5=Pw^B@Ek zo@uBs90=j!FeXOGC&QL#4vXjUB(_XrXa;L?Xj;G%Mf_MQ4H|P!GQ5VF7Z`GdyKb=m zUAB14yccZtj@v%d<|hx5h0UPR25mZFkU;77L(yP3h(6tT44EQ(J)xpky9mMRFxHYS z!qH|}yb9B7WqNs*IO1->8Fwio`|67tfAkJPbr@DgLN6Ng@sx`2B(#Z#ZM3`&yNAnlm3;s@?!;k{&GW>Rt*~^0;s#l`IBAU!77{z~PY>0a zc%_cQMd&&oMzhgk8oo?K#Tc|54wFG(AFw;VcEaYic-b5m8bMqo9Dh*dv+Pb)KVC;7VK4edNQUtF5UdNrtN@vP&G5x&Z#?rvog37)V8mwBt;fLz9JKx_ zIcDi?2sIrftborFoNlNc70r-Xn7na_8-WM~l=qb<{~n$2ur)lI!cvrUM3LbWx4hwx zr)>Lx!J_{BAGckkW`jyHvXVU;%KKqC(&+stneXCQwx5;ZGzpvYz^I|(+9^6I!KyDZD>AiAn@;Z)< zlNoh@=LOp*pDT-4Q_71+<&n&=d*WX|R1AjeNVy{XItdr2MA?eV!Kz^fX&JE!aMOq z-65CtqS0UOgAYDf>Vx$nqwWnCZ>;k|?KaQ@4(@oj72mhO#u){hpy-Hl2Yj|ilr4U& zmh>2+#bz$;wOf0g;*hu zyW{ycfj5QiE`@v3=#Vax^BZ%hoySgvym^?4rAI8SQO)TF+LRIY%y4j-X#br#@ZuTT!dAJVSETB z1&GXpy2X`80?DIo3Ua8+S{X2 zIDc0nN?h*r08QvDMaBXwS3>hySUm+B#v^eg<_y8({utj23%W=Elu1i;Yl5V|?DT{A zpQP{mqL5ZRX30I4-lF<7nX4>3&t5frbdsa1m?Y@;rPMFtCjnc|rCJV?GbM1$I+dEq zEJ$SU1TKr?u^66+=B#LDi}uQX+U)20{d^|=Gm7V;I86*LVq~|)ES_nCnUlo+DNGi% zL@~$6VoDCZ^7x^Usz*4tl=CY1mT|H9`g$iMb;qp*2AmQKc{>9{x>=aeMz+fEJZ8%o@v zI#4jc17mobBft_9tTGwsn|oO?H5%tmN!Mw~N(w&1NRUT(!ucLaIJaMV{%G~0${ z+i+qVu5N?!Ht2dH*aOWxFv?xh1+%uGkqdmCWct06BNjTKTr|$sz{(n3MP|<&-Gv3u z03CGENfRg4VYC=aRiUT^|5-4eicJ&IXe=~F;Ijf&_QQytSkeWZ+hcW09B7PbfBEgZ z6vsSy!`9EKEu!G{EWOS)m#KP=r%yAoijL)MeuN?UT#-$?11wBpSu8X5@t&Z@1=C&# zhI|>|MQ7n|c9mi>acSzbo@MrYWlIBVrmo_hm0WMea1*{U;v7S|>9dU_b6F2XM#9m15hnPWVJq+7ML3^BZVT(rX$?33| zjSchgT^V~+<&CM;5=>K<(b;%$Db_?YZD{JCy)FWDp`nKn`Y_eU3q82$VVNG9>7lo- zti7z(!bnZjuE4itIJOk4)i7-lZVQn3e9RXA3;Mhu@qvFVRg3I@fPd`dx+wtdo^@ywQvE&w0|D#16W6pAEH4hzUeg%t) zx#SRs=gFGwv;!|)@0 zrmy4hwe+^*2U}jT;W8Uuwq{eoaIm756^~ePiZz`C{l=OFHvDJHEq1K7XP0%di!{!W z$(y9W?r%eR;fsfq*6I4tXeaOZ^J}nlc|V9`VlA`pz0>w~F2}3~`u7*iUVaIR`3P(|x1g0hL!JQB^4S{PA)&yYFZk*bQ zm%cFB0gLVOQSfyu6u02|W_e|H7HkJQgp07WB_^2Ru_3PO;+Q5@EW_r-NLIn5dAK$c zKc}E#0xpe)!f*)}4ebXTF|_FdKhZC1C9^t1fPTMu;2YO9yo+1E35S# z|8e0Z)}Lpr=r2?==LAiUF{Xl(%UD<}n?9z6bjxRtJbE0YSGK(GjLBqP1|O%hSvo@x z@W=s9N~e1|GtxOcLk4ayX0c-ql@GEumyhzPeTa!gEGg!oG7hYyO%=mW(yN-qwcLAw z`z~{}ct_k~hr4up$R^LYD&vz|I6Pz(Q?wTrM&GJH^6lE-uW#l*#ZOCl#nVL(qQ!*7m~rE(mN7 zw^mr*6u19T{U>{T;jH&Gd&SXD`S?D+-Ih&|zE`MqfxBxs=oF_`adickm9W!cD(AE3 zLH5sL$pH>a;p{{<7o*DGvCseip*$I1Uu{U#IZCDC2`89%(D>U-9G=J_A2`b*=+sBbQ{1%o@{Pgj)pLUWM} zSCA{hwWD!)JbWi%!Zg^;LbZt4slaz3D%7xHnLNN;*M^!Nz8K(yF;Ghh{PkCKf7jN_NHHKZ{i?foMlU~Kza;6-iRsrYbFgTqpQ>YhDFYz0W zV3=^?h)-b`hYOP!;g6Fp44R%;WRX0dqXZa;usSunNe9#C@rbGnqRG1zlJB* zGDK8LH?q!&{;r(m!Q*aG;5to$F=@`{c z@kI@O~Rk4_$Z>|bC9njom0*Wk+m2@mg4L(4AMZICX@vLM+Y1AP_K_%1N1ON zjv<_laL)(@Mwn`ZbB6FS#5Ylb(8o$WwA95FZLHA3A`LuShW$(7q$a0o^HtGN8QHlC^<#+;%n69fc0TiDg@>#pRJn2AR^E4Ikt>h5GS`)RU0LtS_^q;jC^q}) z9=z&F@9h%3Qt87zeoWX!bCCrSDeF-7j9|BY%oC^Xc&dpudedbCBD4CsI$~M!+oc?<^%^+aeF0OmvfF7 z9~E<%xFa9t;6of%$n6FECJyBJtPmrJJig4MQ$A1UvrPf-7s#n*_F>*D;^bofD&eGZ z`c^Xgn8XFRpW@~-EIiAT7g%_Szpk?U221YH>puTH;`L`-{+gfObFjcK{NUz4bZG>G z<~ZG2)~`NvLAzcU)E{>hU^N`Q#z_D5=BXGy3*Ph4R~6$#)M~k$8-(bgxiQX~W9n-3 zu$9ZWn)SH52}fMeMNBO`;p&CcK6thhd;Re?5OYFM8w!O8)QfiEKIw-QpJS$|GsNPT zXphE80+Mk&rp6;P4sGJFKNf9c@hBQoqi{r|Z6lEq0go`W5<%!73*^45Wl9-^8n*Ac|S*T?S%4UWeHnXu;LgiPI71seb00ACCRu>yh)q8eE5(rp7O;@ zTD+4(^@{I2^qb;ed~{RPwM5@`c-|2OUGcpqRQkzDVA>E&8i6;X5jP$kCt>VV4446h z*_bDmt@DwgisK8>P7RSuQM(MwSIAWDJS{xeMps>&(}js18tEfdAHVcvyTPSlK3SuO z*?Q>Ta6huqf|4f6Rv>E`RxQPJHS}17y{b5)jKaBym<9joa#ej}A|8#!*OAa4hP?{V z>5q(F7}-r4+3LimparTMOCfCFPhS4QfcISfiuzCK|A4V~XnvioF7w_wc0EJQ6P#Yj zOU1lgNRwPX7c+`fo=K4G1%rJY8BVWYHuL8gKPGwet0()q(cFbCH#fLq9jIq7OQ5%` z_}?l%v|y<@Q%vP}xv4RK8S;Vw%k()=pWpQOLr=27&*-s-K0gZPvmrkkvdoBWO!(83 ztyc1!1%ril)`|tT++)v44!pmCKQ}SaMXI$@JvqReKLk_DpBsW19LAJL{)=X60=K1d zVkW~6az-Ini6CeNosY9=HGiC?&LzfQqyKF#xlfHJ-1U;B@0jzMtA23EU$$?Iu`Tdd z^iw(_pewX`L7_h^6fkTUx{boRaoDej*;C;(1NF1vse~^o__PrIVoR%zzsqq{6JNDa zt&6?-2sMPAG3rfl*A%DCP;HLeE8#9oSVG=vfm91Tut2B<@>inLT!!kM%rM0iA;!3G z2pt1d3xJglT(odsED@H;dDQ8}h**Hd^CdlC|7@7eK=c%xo(OZ{j2I1r;pjIQpZa55 z9|ZM)M;H8VkNDO&&>SZkBlsVi{i5GjIt$L^Td4!S_k^wjq+HL>H+knj?!P2ETy1LU zcbeCZv&&KXlu^Byiw<*S0UPJCMYd!hd{2`xjIl|)8qdeEG!WBBQ6k&VeUbFo%M%eC z5y92r%n0M!FrE)%pKuNl8N6`Lh@i_}rbaSqA)!0n4DPhT#ifpd|TwPE#>W5tU<6 zF#^X2qw4?^^~S93@a%-Vw&)_7?@e%?)cMU5-{|v+HShTA6+b=W%}1PapU>|w>Ly2D zxZ8?n;`_b}%bvE30G6|;FM3(DBBf=QJ;d5r2On01P$tJ$hnc2XwSam*E76ak?d zw7$bh_nGvF^-sC(CEedL{v!jwa*806|D|aonQiFN8n4>nV<+tHETuM+QXLF4z{Q5ZqyW^oT1it$)3xw+kURR@o1{(b z?iQ46MSwfPJh5&&UVEYI4*b}G7QX1`C)vNEj`ZFS9d=^LPPE$zB|l8{g`S7*lis43huZuKIDaaEK4e=q`D9PpZ z#i`Izg#TD*Hw-F&^uzRC7}^!P+v8tLJZgfU|9J8zM|@%2JNmz5))RW(XS>@x_8+V3 z*y$X#tEqKd&U<5u>3oPY@)(%Kt_RpTna=TYYx5zJEyGzF!cT#0>Cdx%65wy`#WmaH zBJY)G+;5?Ui=1w?c4F-&W(hpbM&5LkK^<*D19p^JhcO%ZVIy-k(RDM!oT%u+e_NQb zmE+ynMTBX$({cxkeED=Ii~Xg9&LWg55!BenE78n~r(-gk2{TS6HymVf0sj>7btz9( z^80b_u9nR~@h8#uD&1~yQ9bL#(&Pzmz2M@v(m^@(8>jx_@PAZl44>vW&>D7vsw7&3 z-QX??JN=M25ZlE4Xc(r9g5y|x84ndjG@613(=cHsFdMh$;ORUZQHF{N0#$KX5U3U+ zPW0{HT>*QLy<^dsKIVAsup4TBCKAB-U|?`A_2BxO87h%o#voqrd)iqoQg}6 zFnA&aiT(U2v>1W$Ls2>iU;DwNx7^Y1>H_}`Fl+;p7I0|-e^T=|uYIH8C(eAwKChTA zju{WAUC-7xdG)HSa_>LO-=}%YnfxmcebpuVMiN=Tl1|I$5`=^ z;JS;ZgB8zOv4b`DTeHN5W7g2wj=}b9Bxal9p6kfeO-yiN&n@gAbd~~|=f&ne)Y!?7 z{yY)P*f1`Ol-D%fc-~K@Wjgm{%Sb@lVOEzi_9*+Go>1Z|w^@8WGic^ab zppFwOP$$HldT_> za)gTyQ>&29`Rty@F1hS|kl(WTDN90z2WRn@7*qS%qHBW}?5wpO!goxid3Z>Er+n?O-CyW1kH6KuNUV{a@TfLDX@aRe@o#X?0KmxX z4>ON6;Vx!VM(ALU50-djEf;XT*5lGfjCO{fE4ql<=62a5SmlTIyU{EVzk@L&6u-mp zF9Iv}K}`f8qmdqiH?f!&j}Zwtm4Nn%n3@QeM3f|AZ6cx)a3&r~@raCrW-J1u9j#|-S5jE&alU&8$|)ZztEm-Mgc+xE}Vy*+Dom6emZbcr5gV_iZw4r(@}CJedd1N8^R^ zbI(;rsnAer;kpij^`UQs^Cq|;8cCuUVu{?d}Hm}2n zb?D%L!gc7j4(V&5y;g>8Z0+z#G>B~^xTCGew_C~uZiX3(O<-t*J^F|cd=E_tqwA@T zsKq#=iWSP}JQv4)E@RWj(O858jE}hXNLer{4(FkH*G?DyKtb7A)qV?K~tZL3_UJ-FIJr#|x0K5Wb5rb1}RZW9woVEXMN1=(iXli;%w% zQQ|eNic%HWHoSjR=7O`KJqx2}z-21_n}qO*7&RVa1<+{(jt<2Y1yl@xYF~Kw#LccS z5IB)`Xx17po8eR=eELh}U!3!e#UE+;mLV^B^eG)5@^!sT$v*v$uZ8vIJbh~Dc#3u+6%KJDzg3CgAUjV=Z8RX9ayLi)=<9&G9 zn=8B+FUW+RoarHnNq^mBI?H-1S8QeUR#t3f+*aNc>_ImcyEE5=t3BzyjRU>-$D3t7 zQpGMtBBB0N4C37osy0kEZtvrw7zV~OQN%n_xhaD|IdsV9pF^xD=D%|KSMlyC=^X5E zfrl?M^g1Ws;r{z<@`U$a$WY4JPgMQROTT#c^3rtgj+w1Re;|x)Q!T* z@n}35tER(oHq4datcsk)$Wq5x4Q$iKV12m@7+?mjlmWaPYvioKlC`jRfWrnvZN$mV zQs4a91tVOsZYv7ikn0Xj5A5;4We;e1!rv3aJf&CVi#w9tQSXMpt!V5j#W()WxVc$! z?e=Yen*fQ67|9yUw1L`c7+4_634+4$Mienl z#NHZyjte8z5q2tO{}R3~VO}Z!mPu4v!cmT|;=SW+bc!C;>{!dq=Xn1DlP~e|RSvwt zp|?5euAI*pJ>lRN%n~}7_w4nVOTW|UH?7G9O|Yc}9=5^Kj_~PNy|_y+Jjyp@&+xsr4{}-wM(+pl6|nz8$Gwf$^&*=k>G+a!nxxJV+WkE!&Vzi z5dh4U*l2=H28h>@9?UXzs4Pag3gYG=WEKWZgQg;q#zJQ#+75-;KzR1Sjqb4PjNk2G z)d~kh(i_PBO^5Gn^jUgU_>zyFk`MXnE)#Caa@NPo+;EZAq8f09(I;sr!XA|jE2CX8 zEf4dHz>DQE?4WG)-_PKzblys%YAREbxgv?qiLwgXH-T&8`6iCn;@Bge2I7t*_KEQn zebPgTJd?!aWS&mp<}|KJ=lD!M&El0Dc{{sQK>fo!BXam6{Z+wW(UUsC>eF(3x=2KZ zE=jgzuN(BfO}BfDdnB=Y1H`!XEq{LGsBfI~i(-^_z7Z}oLwm7S6S>(=uxj3sVt-FN_3Upd^NU?*jfSB(#6|X*S!=V$KXc3ue)D{+gzc(Q~G<`Ba)srS4RIn?n7m44umD z)962)+0&U9%)#O_n#GB8_(s@eQfyg3kN*vNTXxM4dDcJux| z>iX!I;f+A_j*dZDVL@dO`&IwbC!5*qe23*p}XaRP}ZRHaujcm`J!hPUs3>41F@^KDzg}M#rv)Z>;_XwgLFl`ZqV+gP=)0dM88;uZabrYi=V=Ny&5h*$;pyZ(T` z?x~#uZ?VQrzP!psmpI@&uM4B*G$)_nLfdcu=hFYTx9WOPPk6@TMP+jMFbB;@ysJWjpfY*UU|-_7u3#F&B0o4*tw7ki>dL2 z`QN#roGE|kq@g&3sdX_`faiksGg8L(oz0+Oi>{7X?}AJZ4D!O2HrUq=13M~v`w>w| z2C0R9fGh|GW5RG;9)*2lF>?ZXPe!+C_#TY5voJuob90e89|;SU#QD@>xGu$sWr$pk zjVs`?5(z6YdKHpZseRv}RT#1g9aiDPN*Jv~ofW9Q98H&@#!{?ZjAJ5$UVy;)SU(q? z=HS>&I0vKpG~Akun-ef+ES6X00X`1Fuz|SO4~s?P(i1PbqAUQ7{4w7TMXllH1*aBh zqsv+Qu!3a8C6x+$iL>{ws%%}TY^KEzacGA-Zn%6s9y%VDz;uB(u>3cFWh(i+rQ3&*w4TZ=DiRJ-%k8vI=it<_4o zVz3fgRb-WiGFbi}Y!_j`0@%#Ma^d03#MEGguKhL%+s32E7<3&8yJ46+2uJ=wZf~^g ziKSh!JOKV3@Yxryy%n2luNz98Fv$+q60R~=MWmAw(bvPhdRSN+uWO*OOmZsN=O$N^5<&!PJiEmkB-M|V?uH(v@pX%`pmQjGUgyV!j<^LMlN z9%}AolYJb!U&&K`9pvakY;c6;$LM^5HBYhPG>6M&`2sIoqUlwYzFrZkdI~~qIw~Bq z2Q+xhW3l`hPwOP^NTKgb{>tR493r1P-zpw*e|-us}sFOIgwUq38vk9KlY z>xhY+P&WX70`N(`FrBfiGv;;1n$9@Y8RI%5ED$dOU=)C9oiM8-iiOzI9-)5dFVK%R zF!w=BD_rozLJy?4BCWYHAvU#xtu?+|V1p^D8Do~Aa z*Uq5-48{htXD|l_Gc1_CGk9_ao6KbHOq$K+(mA~KFN@}K_k12*$gBV3jU|j&MxB+6 zt7@YR-pHSu`E46V?NVm6Ujnc_%*V%g<`h?)rR_zgTv7aiy0_^RL6az&KH{)gUPxg1 zb9$w5fS9dv_${9!MXgfIt)F?fl#OK$P|1IQwl(0S1%GXH(N%T$Eru{NM!G2$G(&WpDsv7;NVbjPb6_$^7QUa$_r z!XQ-djk&!syf=OY;cV5vsTYJ6z2MOk4n5%59W}aPZx_4`MAHDw?1=jvAnL=OZ568P zrnkyt$oM764Yh^e<%IqAm}3J6OHhV#O%Y?HnB*@SpkIAxXd_Ast~D`Lj*)-3=r)Y-d|AYaZ}~;sB01E{;{BJrl*;qZIXaOIp3?aVPe0<<2mBdHw+OxtXJ{Db zgwp8-e_y5k74Eu3trzgl{!B>D&XlN2A3$xv2Gb>i%jGX7gR$X*@V`Cetoz%R7D1-rid_ysWk@M<5Y9h za#!<&SRbUdMSKVR41ilV*!Dt9ALZ9@8H`WEFi|jrV-YUI@5yL59p7Z6JR7y=Vzp${ z7opW+SS&^AGHk1Y^UI|vbq&JS;_f)t^-$Z-bVppVMZS>C&EeA&7L8%6kIA|S)y7vXnAAk? zY6$=ekHxBRb2Z4G=r)At)~WtaVndX zR)GeZ)xxRTm|hQ!s~)`X4Y9!(UCr>l8G>z)DQTAG_~DAJEl|)34O(M(Ta|pS-w9@& zq16rFdLT0hPy1qXe-(%G8-ho})C+O^Xfz*-GMO$;z+)LfOi?3_1=A2Y9XY{RH3MZc zFl{E@&V*vvx4Ow5@Hvza(Q0|~*nJ{{YpVcJw!O~JNFxH17z;?@yH)+nqKOU_WV z8H~dN@UfKOf-J;8GNOpB3uu!^-B*lw$ssAUea3*N{9g=rJ>-%|9=^l2Vf=Z6 zW3KY?MgDh=k3(2=Qt{<|}{V@3t{wiwf%4Y-0WfwLo0Io)zoZ za~498_dXNd_m zcy6a8M4z2-${EXCG1?uPEzrUfU0W(u)nTO*>iYKXUPjUPVP zEDv^X-15S`R>*IOXir7)UF?oet{CT{?m_(|1Z$69HtOi>-wcP%kkJ&4nqZJ2mgr-o z9%|_zqYg4Ov9$)i07L#L_ned$D!!>v&+1|&M)56BBcNxm_%of&B_jKrbrab&j*}lV z>;Xqcs_lHcFtv8?ca?6J`2HOChj7VBE?O6wDzn>HLFmflSZ&w|E@6GBH%vr~g zYx!+8y;pJX3eH)swzu(18MA~sOVlWG(P9o*O#j6kvY5vf(|ZYjEa8QvoGjSq*=++Y(f-tQQ8ue3> znu!DOVIYhLquLO>9)iT7*g6bl!_Z(joQGrZa10!dFG6w{rq+K3L(yRfJO-;jvSHl8y@bCum}fh(@dwdw7_J+&v@RMvP!dY;e zK~n#}O~cz9a+|YnD==E^aK?x8f@ECpa%=?W-(yK6w?wh_L#~cieVIv5cqC2{Zr>&{ z;5o}vcsGszO0D%3Yv zcZPUtjK^l!XNk$S7$WOk7kF2NnXmX@c3br6pbj>>yWnScYzl&FKkOfXvx9Me7%E4i z*;v$@fV-2_dZ=WKI+WHuG#226Q$G9 zdn%?*!szh`mfqP&%o&D?L732A6^$?SLW}O`-x(b{s_llcFFJbTXA9)I!c0V&_GoL3 zZ{~R46m^YYX8>J2{MCkbZMf9L=xR9ehiT=k_JgCovEmbV7qhaE>!hpln*Yn;$SnSo z&Xq5imaNdq1qrIk*CJL$_1X%L^7f; zu`En|)&g6Q6#9d39uWe-UH*w+(@4eRiiqODhg|rGo-r(Z!fH>sErA+IG*0H=R2sje zQ6{rqaX~J9^4a4pcNOtcF?W69rLSyI#;Lz}t%5&AKB$33HBl;jByC&~!Epn%BHUtx znkFzcM;%Le+hCJD&O2eb3wpW3$rHL>$o0VjUnKj%Ua%@1Rp8vc3(yT&-I3o@S?_)N z;D2Hb?gwvaZ3|^(AW8<}zd>j*7@>nva|jv?!P}}S<-NfeD`2ZZxHb@)1JQE;8unLv zz~+4s-y447O6!S<-O;|Qde3SHAY6Pd?cvlG`&y%>HyX6WeRr&O!73+=x5rm&bZ&-A zrs!;pyN2r9TTd50+L)?^kQ$Kf$*oH6`o(kK`RNObN))hSV*z9GSUp=kuHU4xB8fc` zcp#Qf1cngB-uL({oC|MJ_XgKqq5DPJo?~DL+nl7!F-||sj|VwtKkMw}rCpr3gHyKg z$`)2`qWeZ}UeEsP*n2Gl*3fb_yRBl~F@RVkfy)o`;0 z$~7^nE-vWcjvkKcW3?gXH$giS2*YHMFc7Rj8~EF+UVkr1>^NhuD+aqG!$b9~zY6oA zCA8!b>V>x6SnZ8?Z&>=k%LkvlF~wVbUZ=e9rxkv+M2siyx4<0_1i53gD^i@{*&O2> z)!E+37O$ogcut+Mm z@uAd<1joIdnmf3C2fcQ(?JmyU&7(5g+{@Pc`TYQI{Kv(IIqIlFxP+giMF{_^8f%=p zr0lfIZYa>!xNrtXFz!BsqIph!;Bg$DsG@h8Y22BiSSvyc(R|CFMT{uniZ5LHU45%V zD!BsaQ3HLnFkBl$bm6RzH-@-rjPrt^u|z#vEO$^M-N$Zd?}~ARKzkVe8;;r|v2hfBj#eIU!*R$Nhr{F1djg(KfX+l1 zPeg->(4B}^6EJ9ksv-Os2gh;fHWp0^;&?HZH=Zy&hNon={)j=3sQZYQLE#iw0jQyxQiBenekrK*p zJ`!?fHLR?GKbn|d7dbi@E~LRmIBNt4DWsUg!wMg5vCkL8I!;yYCNU%2l>7=^HzJ=EY(< z7BR1Y*I#pKj`B%4rqlHWM_kZ-Rdx&6_2*q zH*Rptb>-klxyqhb`TYtnU*Vc7$_TLH3UjY;+Eu>0%8>$$z0QF*xL%gUp^Ul3ez&Q2 zN3DjJ-eXrOEk0nV5ELKNC6>!XKAy;|ByLTia~eO(%O#8DIjnfiPX+u_NQ+`l6ouJW zrhI3Sz(u75{g=7b@Lfi-S{P6lCv}jjhXj2*GQ{g9s5C*kIrds&xFp*}D(Z-ToRR7Z zO%Hf@VqGicA(Ay~YhP5h#l3b2?0~I8M3He*CnN_TG7!@{Bd4>9O-hipSX{kbutsd8 zUDQ_bN@qL{#Nhxu?1Ud3@wjSXw7ebuw8d;+tZA)G87*6(hAbuBF~}96g70>KgB_Mx z<47~i5O;=*DU9&j0LL4^Q*82e5uk;L8faY&*Z&Z|>G*@2zp>tD_ATb5LjL!L`gux- zoG6v{RJA26PTov0wvZ)-!A!AFrj+TAo;=blwkEbKYt$Sk0)_tXxg! zHQXqHjkWYxM+0%Rv~FAVT!g*aaBUjh6vWjR6PvU zL2zB1*TR*Wn4|$?vEEctubg=@=lMpv&uk#$=%T8)PCngpY5j^;88k@a?_{+PLe=Yd zNsM~5ev0DQNY;qpxo~9)F2Bk2>uQ^Cc7^_z_`eG*Imd%%IsOdoLip_zD^GI3NhY39 zMWKVoIrTXIJ|pF*Nm(B;PgtEB<1B&mj2+i z-`x3^BQ&5@3)iZa2<`QtZ-Bdv(V;1xnc<8jhS{n&mwZxd-C*H~kzN?x8f)9)c6-Ej z#N|ND?h3meNa}@2eek*;TnAw2Aj}$q4a2Zx1e%XR)Mx~bMZ!2(O3Gp)940|;GKwc- z?G)6WiV;(lC@^9wo=78jstVpbpMrk!ke>`lS$LvK#%7Pl>2Yuyi-V)_cO-g`Q0>Lb zL$Ga-I&a(l1Mx(h4?;x`4C;ow&KMnl|M_ECI~?>yj@Uq2A*}^YyJ4T4_Z+dv4jrvQ z3njg}V2tgJG06aiA_>w#N?l}X;d)K1(!f0N-d8fCoL_$MS1DV4<>^m!DB)3&G8FP) z0q5s)W*&Ry(CQW4GkHWV$S*nNg+e}V6kGap&P?LJ&$uj+;R*Ch;OBU5i>H_Dcj6fo z&x7&&6wl7W3{T*ZL~eb?O-bDToL7=*kgD{i!(Or=o%=IYSvyU(17d1^!@h5sRmdqH zsP&O2#gh4r_GPsCMb8S_ON^l!Txwv0CerHQT|JcPVyHgy3}Iu84yM>*fl4b3v%_Xb zJaR^*8>~Gs)C>DtW2b1c+heSBRRYn!tCECH>xCwLaHk(k2Ecz1CJw>zVK_em%SNHq z7$lCxPytI$K(k43n+%gF(3+}Ndt0XA?KCu>j(O8@XgW4ehtG5+37s+xc~fCD74}oA zKK+SmGC5>CZiwD)G#o}@;s}fx2BRT}9f)Pp`s{~wy^-1zWs(i(f_4GuDT|DD_|OI+ z-k9GKo*syBfrSu(?9^2^(gM12X=;L-jTG@|cYP#O?fqrG@evsOM@{NJmT|^68h=)~ zrY4eZDo_>pz#KNs;+L0vn##22id)z{p37sI^qBudtD1ncI449X2dmv}j=06MH~H>5 zeXlWDV5FBBEY>qgy*9yTRob|-M7MSY+Gj|+yMH3e+YmTRmD6~hS zEzXFN*b+l5kX`jm4l#m>A;#)sksg-nU_)K3)546J2-JWEd8I;;2AfySnFBuY=?Bg! zq;Ua{=TRq{2Q%oK#++m>e5U64=bmu%Ber`$!$_vxVMrM7++=|iDKD!KMMr@!gs{&k zm4}i0OP#~i_)ql+q)pax4;Sp>UwH*@SJtFzB4XamKbtsi6T>(1`v$Jwz;PQmaRY~K z;GhlMyMZbDVX-OgFUiKz&b~Yl(_h@bkt7AKY(^*fv=23y-!akleDLI^`OPtg0RS+M#PZ zG--!pez5ez?zX7Y7QKAYr;XCNHu6zY8@cb82+_0!;DHft*yDn2%@sXhjXl=dqMgWY zn&E%uus21xu__$xYJ`G@=&XlRI`~`{hP4q;3oEPRKJY~#R~2miiv!A(dL!pE4L;KS zgF1R87O-7D_viBED|XCexhx7_D7Zm%lG@E(i07d=ZV^M=V3gEX>Dgjbi(frs(<0>3kM$JGh!XE-xlkwu)qRcOi|Mq`G&~VN0J^c>R?q}r7u4t$PQr|klB^A`pqdn8Tp;I z-;@3}rli4MO|DVu(~>eYr(rVisXc;gI#)9 zt&fR@aA<;JdFq-Wv>7&9s}gsX19msZSXbzIV2P*N70S7{fiDL7A-KIl4;6I+1CiZD z*^?LdfJZNtP3_hPm-^yMKQ!(S_W|&!3Qry#1fRi35m((1^dF*L+0%w#;1HFfyf7He z24mJB+z^z?0Oa<^!GDw%M<&T3y%mT_t0$bhV@y{J>kM7NdibM%dv$x7C#q0yJZq^I zRe^5k=?rs6l-MHP3KuLe&J@K>FwYQH`gp2~uJv%fHVkXwyav3Ku~HL(h0Ap4Vb|=`nL3vQ?D&sn)v7jkjrai*Ii5iCmzr zFh;5+7wB=GA!q4xhMA|i{uIZYMi=qS$|Wn+=8 z9HZrNmK|H1zY6U z;fFm^9WdJwR!&HB!as6vYL1xZc-vg{zdp$Gyg6Do#}g;?aKd#*^_>|tvS5SFxmtwjj_rI3k@;M0Dh8_)Wv)qHHHe4%9tjW)j(1;IFqX@8C|aW zgN3Cu`pT}KIHs6Gi#X&hN98N`jl|@RW^hd!Z=|r+bJ|q3idsD3(MQyL$iVwt5y3a% zToFc>P~N@HE?22{nKdr5_IcJm%jzMtKgF#lXndTTk17r2sY5&`2!(^(FT@5>Htgpz z+1~Esk-Z$YmtK3>c`xVg<-NUZxR1;C(NioQ2h>e`twI!@I?jJYQFmH> zRoY0n>jI|lgRRuTw|~(yzmE8~6aE)~Re|W%8Bv1P z>4Fd$i*<#C=)$`quL~l(U`ZEzt7@#Ak;g&+N;{#fBT|IL)B*PGk>rPdZE>azUirYp z8(mvrkSF}*B<6-6GB|a@DhD*P!zFq6SmKfRn@n-b7&VPByb;nHqLUt$>)>u(G_9>l zuj$ocQw{(7%d`r1Eob}>w*Jl&5(xjS0ERn0(7TA4ZyEB2A+LElm;1B1Ka0^BG*0J& zG`>t_j}*R_@J5n4p^cI~asqqDbL&&J@fj?mtyuPtRd_)CSeA)Z_6bd6IX9M{Vijoa zO&kwA<%)Q&PvELVZhEGklxaA_V-=JQ7Z2NyEpJ)KK< z?i0Jow&=Us5zHy4PbED_2MyGzi84);*FkL^nCrn+A0CGAl`XjmR+wX_CAQdLwLSJb z;jRm=x+B2G5|51F<1x*Jy16YefnTuKgCZ-9f)&-v0$ijfYuv{ z9i!Ck#%nBAkHhWpcrXELC&G6U$|m8=WYs|Qs=Defo`N+~FlY)2Cu8(vMFyBP3411D z{RGS%kGbP8U@TsbM!+bX5MRb{j1slkVDuP>mi=MVPsyEE^+L@aSlt!qjG>)yxP$r{ zd~BnvYHqFID|{6<)RaVrgPOsA2RWTm#J?FS2c6-Jy ziENQTAGuFGrPfofjbmjjj|zz{mIvkM7R#%#)QaN*(TG3giKiSIPv-=hC$iNu&P`I$ zuXo8Dm`bfQ-jW(`2CHZB>nm#Hvf69be8XmM70mhOd+L;Mm>@O2&{WtdW$gQlw|~>= zk3?&^R|9r6u~QSKb(GLeZa~@%uvj1ZhPZ5`5?R|#@xvU0Epg2nKW$;{fB{aJ>5OTv znBt&QR{`}<)|JKSoI7ai1#a(qWL?Sza@m=SO>oChrE#PVsZk$B+OVz-(^~khf!AbKC6me(fpByw z{l3uWBb$BT$9D{S!`pehpUrcbJn)joQyKc4&l6cAp8m0%`IuK8(kY5}@6ki*MYmPQ zq0LQNT<7mA{Bns#7dh}eFPvq4DSw@2(^GtNf{Di&eT;XHGW`fWk1*^oJr6VT5a%D_ z^h2C?hX7~tP|8er5Kn;LfHQ-P0#c01x~%hu~)eE znyPY#hcYLOzIS*zf{yoj;sGrl@p23&#?dE%CC|7eSyfs4rL$!g%d+`c&?0Ym{2iCR zXTK6!edh14d?pr#pB(j@_5LWFj?nEiYhtY?-qc1%ZKZXqr3c4`@H0R+Lv%Mn3uF9k ziW7poGsis(^@Lnvg~QgkZi5H5xL}7V_Rw^|HV4#q#2lfbIU>Xn7ag(15d$2d>4;qp zcyEvP_SkEOFuCf=lSH~gRxqm)_;2a6S(sL;3Gb8rSa~aP8E#g z1;(9ct@B)Pj=#=w?pbP|Wy%>|J;TT|%s9i+Gs^KTEpL&Y+SHlEp&Wg+*-Yk%UU9|1-7_jyen!uV~Hc`+pBL^H!EDS z08KHcDazzsY=}^OXjFw}leE#j4o+&~M-9lR_PaQ`tFFe&f6?Fv^GbR9E5}Ps?<4CK z)8#$=3fbu`N5A2X*L28J_E__5-pt~ZOm@p)$8-k1WRMINUa&ZoGg1`^ZBYu_rO-{@ zs=@(C;n);jl^VVbAyYY29KP}%Pvf(fY6oPJ$&Ojv@`?>}cqNwsulXjQX9~FO9rqOR zzYjcJ!st(mGcci)k!7^}#Z^)q`@>689@J3DoY|VNlWwdwF4b40!RiJ`G{mVUm}vqN z`O!7Q25StmLoG*jEwXaOQ+JH>M3FE6eQ>D_>iJ=|#LoSZ*a=C2_|yf}x}#VOCcWU_ z8-FAi(+^GhWAFeR9EjpUYNDt;6lp_oaG0`!HW;CX(ho*JZzP6{#Eg;XH4Il;Klt>;`YJ9~iR7=k!@Mhg2db6Rc7F_R z4=EZ84W^b4HnxJ6Cnk#%(*^IGU?m`HnW9@E)DfYP%;J`8N-_?jdJA#-^6x{g zil*%&r8pe^m{u_~d%||H>=VZsPkBe)%Za@BjK0sAlFUV^?3Tth>9o&e{a0+9!?t<+ zH=m~q6wc14q)hY1RYOvB1l2;87QE_WRXx0_kDvw! z(MMV%)G&grF@j7mOUSDh3c0LjjZzytvcmueJa$C;=6K)?Yge?D;gtvhJ(M%f*%K+A zs@L$PB_>D~y%kDZp}ZA}TdCW${OX6b!t<60Xo+x774vA)0(B+*C9_ReH3#eBjEC|W zIbyFpYS_Wa292y>*bLt0@H9oPF%}rX(-1%Pv9SS4>LaioBJ03c3+seiu7L`&orJxA z^WsmYedqVDYGof-!sYLI;2p2MVNo9Kb2u(b5rAL4pi2r@B=Ktk#|w`23AD zT&0dgCc+sTrnF*tH&}d))>palGJjp<_zNm#DSXJjXVebSL+&-F*!d*ao#2n-Ty&hD zk8#d1b~(nNW6Jk!d5rCk@#-;JALo|i)HuN_C%Ew>|96UWPIGLCqPx62%iiaC>4IWT z9+Oe_Rd%WZbUY7bLKsWJsUJZp%k+=p?uV@Xn2Vk;J&s-pyelX9WR|6J@k{CnKjan1 z=dx+Oiop*rq{au{E#bV+G!{U8RXiZOoJ%WNt?JXys`~UVX`xd74(nk;eP!Cc&=BDU zIA(|`@>gw)IwpuPg_k+DTi|yyOtQi$Ya9~9oGmulVVXVM9T4pRXGiRJgqD*!u=RI> zhZC%w;NXN}3HUkUf&;uA@W37!c1q+CXM=oeJhs9#OVnn;0zw_U3H2A_@AE{Z)<3$|z zjtvU9<29Sh=`>rlR}QChSQ=ZU(mR>ml2ia@gj{6em?=%t7@mE^M$tU=fI))AxX+4v zOpjp7UH%sMhVgnB|AcY!ZN9$ELE-!r&KGwWf0yAB z!@I})Ncu%FnpF8a-O8B%Gtk?vK4(%4WTt~Q4{;>U|2m^=;BKQq#58!V+<4fmn_lD zG1(G^HrQi_-ws&V9KT&K+#LrcAJr0Xy;S<$!WY&35Z4ZSI$&5w1?X5Ch(dutcExJB zMs-JO4}|u__Ff1Mf=h2i^oCU*tP!J1A6WK`eR9Zob*Gym~GoAE|a5|inxF9K$aUK1!&wH z9*%fzhod$aXN5l&SZW3%6C}zjsWE7P^+NC0LzE6?Nq({pj%uP)P1LQ9bYQ<^;49hu zHw}JK=Lhw^E77X?7j}{neF;B*U`xsE6w*%6X9Zj=7nXc}&*P6=*2$$&4!z_V{)z=z zoR`J=S==Kp=u8G=s#W`-OpeIp$xOAJI+Vr6uXynlXJ>Oj4!h*Ca~?;&=I(sHexm^F z58tt?L}9BQyu;+d`{whEnCdCN2nuy3nk1IVea@Ko1m8XlaeeVfD3txsq90nyZc$u0 zeG%Fh1Ny2Dx{a{rdaL;5-(INbi3dHfvpZ&VL$j{f+Zn|H7}5z*{-}`2q#vru0-_D7 z`yi_o_IaYC2TEMA%NedtxM+{sw)ocy-z+dfRO3zY(g>D@Sgwx*J^a>zu{H+QMrbYk zshV#L{L9l7tW(bEg6Sw_#TSnFL`a?IJ!=&z9j9GBs|y?~o6ED1FmA7bhGl%etLoya%OIQ=;dQq&PH zrD_Z^CzFF;aY7DP=5c2}4-|0EJ0^(yx0t;?GUzj>eC7R88vdZ~FLwCNX_btS5f)Ir zI%?H~jV9U&;k+(7)PqfZ)X+nI13cEp%0_6~7#EEcYejf9|CnNp86KOXvCuv((aQ>s z)~IEJVjJACMSvY{*ul&mTkTO`kLnJnv`4W$a_!YMRx-&_RqJDiBwN_qqO%Q#SYxym zhFU7$$5(UgGJ~Bd&NqdfF_s(Qh9Qy-P*-}plIhh&Sv{oJ#g*Eaqp3(5=^B_KwTr(z zSwXLI9{)k@?_5*$@?BcO7w_p=$SVbO%;$eq59H~Y{3Li8q2J4=ChLGirae_7h(R%I z^oaQnm?aF8d;A~~<#0B+&7rp#d6OPDc>0=ZYd*QGy7Ox6aWewg%_)|N)wnw^yVxzTp!Y66@HHWP;f}L^9 z8M)4A>;g9z)OAq=9{CLpcUJbm>CN%TN%?$dIVvtns69^FDYx%d8%(oSr>s-W;A4RT zGfX$d!=`{S`Wqq25PuESI?SYjDm>1Vl#Vt=*TFwp=vfQhYal=aX29=1EUjRPpk;sZ zY8j7}a?e+8klXP`9+05;d+smfZK-vTfAzBL_KBDQ)a}efrCvPb>e6s)Q&h>#wn@RtvGeRzA3fDc)pBh zgm7;XRYPrKlIr8vPT{mv)yfx?*`0LG&1B10tjOk*Twai!;2VB?%bG>3_ko>Cc;geT zzj8_`L(9~e77 zn%iTSBdR%Lm@AIDBV9JItzha6_o_ghnICei9@KL>!aM+b1M#J*4I14I<=s^i?Nd)B z70n33n%*#zl~5lT_r)Sfu=mBAzQ~m%d|$-$#eu$X?Th0gPw9i3g24%bP7sWGDOr3+ zk@9rMqpk>+NM2_=2!MAdtno)md$_h!B@5xl9{0f+FPs%Fhoo`bG20cboKfFNttzhC zVx)wjEpf#h3r*EmG1Ca6BtL0@k_I@h3x6GC)y3u7IIM}uHDOU5g~02-{9eh=znS}s z**{pni~~w}|2;bu@zXm#e#`3xN)xs%p9iHLp2x;{oSVy(90uet zEt|WtIXate*(yt4Gn*dSoR-bg*$Up$JBRNiZjnpRJf=%IQD)uw>T?Vica=CG-mz~H zZ@gEY-##S@nX>&eTYOcC?&)#>{z0=})D(nu1w;N&Drk4A;X!qkI+HWild2Wr-+HK{ zi$;>o&_~xs2yTppO)y0e_oixHzO5PNS}9w16FbEOmbUM~=Bn^p_J^N8zIMPa zX~A^BX4xRL1O2M%!)?(;WFKvi?1O{eYCb7@Ko?JJ6}*5OTDd9?@MNhiIAFXS%f(u2 zg)$2qH^U4Qv^2)U#_(u_TMaQt4<$P2sg0wxp(`t}8j96Fmb_L;gWufqlX}vb{l{_-F& zR(7T}&_vE8nz$}Oh`M;AjlDW>*2NJ$b>t4x#{~m?Y6P9eFg1c%6EreLg)yEr#c~rE znBt@<+L+;@8Qz;g-yB`cF{kQ(LFOXQ%p6%gQn__Ya4?`_}dsl zQpjqA)dp&ro6-O)^Z=Po=%~`Jab3(31fM1{Wpz>=+QO`sp#EQWsAOXquKuF&4;p-D z_E#SJ%yl2RqL^df)2fi)3YeKs>8D)E;igxdl*u0AVvt^cDyJrMP!cC6sv}KB9L;0t zCrqSAbc|MJoO(jekEFvr)|IaJ9c~Nf$lIJ1#tXNY7Rnz&+P}$*H`r0uwbwcGI?b-L z#&v35XT$3>zD~dE+;d$)gE!uw)=i$c$?2i&AxRH`^WUa_ILF^%=v^A!!7q9FxM$FZlE&4Kq1L)}1;0n#U1unD&*O;weY+a3ME`80}#NLO4~Q5 zf?LXYWec+cOE-9q*I~Er3@*B>~r}t~R=P@vsql847 z%{^7Op&?mJ&7`jk7BW=hFDQdeGx$E8$>}T;aC8PoWpJ-V;4;+Lsa}>UC;Gf%$t&h$ ztEY5&E~E2U@S2WqxV(S~Z)sY{ZAG;Gz?5Q6mJ9o5-v7cq-?-{KC;Xt-FFJ}4wvt2s zawITa18Zs^ycRx6QN1oY*TdZUIID-^hHz?xv5m3539dF(*mDmH^p||BHKy4rRbPxF z?l;F?>EXIzg9jFP!lM;(yi{p7PhNm6yd5$O(eM9lzb1;U^+Y`UOPS0r@B zylyZMWMy}>>w&#JlvG6$tu1?^Ur%Lu5Ng)F9$3>uF&U0`$MNKhCL<-GDbrqyb^$tyo?&4nl2vJ!)5{0*M@$!~{SeSqIm^yPC+< z#2s0A*F~3lFszSKUBop|Koz@2$TY-ZBek-5(-i$pQD}yF7OLhSXoXGI$h3i_ox0sj zl`*d)#>p_MxvI}M5)hUPy1L?vD>k{|qZ?e@F<-#x?s)HxLU*LNVFfJXy6k*eJ=w{+6mIcNTJ zJrTrv^}YA~-usR3|NcLWJu(jG?6c3_PhD%Sx#k-D9oH=8U5j|!LMDkS^EH1E7kxfs z<=B47v%cV)pR=!u$9=|OpYfMZ`N*fd@l($El;b|-h2pM%%3aE2`HYW$#;DKv#OFNW z3nqy)@+DtzTkwZp8?!yIfUOpB;$lmGyFf|DOL&OXYk%Uszwq#7+_Ibxt+bkSXBP1A zbyiojb|cF-GqjbFJB$*rb2lsZGDT&7OWCfR(G^UtGU9Y!*&Yq7Y2>y5SBCg8aJ?5< zev|I}P9{Q&ho)m^8tzO($28od{Ge1El8Wi=@waxE-xi;>!7GXe ziU*sF6O(KN-jINP@n!~0jlnljNLS(Ya3qCctPgj2@D156#EX>>+Gr9wU(_;ODPvVU zy@KbK^NLb_Sj@UYt4A+<@3@_&=~uXw8C!U~R@x1WS|DNxC$D(i9>05J1pHuv95$|2dE(AedXT*wy|@%+Wi_?FwgOB zeknBr?QdQj>c?5CLKumqF4D9s9z}_WOh%8^R>?ik4l7f!C=DNF;Ob1Ib;jZ@mIu_U zhmpyJT1@YaRRWXy;hg@MorT-9o-0Ln0HzE?_d)o15N72fbugZBfrf#3xHb=8=3z}9 zzLR&8hdc8yDG%v+cw16DgYj}MdgbDVL3nu}77svm4pMRumyNysF~1)k>Wg#wAh9=I z>xsNWu&6su>4rsJ(9;oR3of)M%DAJTE^7y)+%?``87+xVsuNw%@gcFx+)h1;3AgD>x} zvL;cx%-x#^>S_Cg7!3xG&jqG={gup!OK5?Md)#23}NvptDsc%;=8q zdKhD8q~oq%*AKz|xH=n+Ic5kg9fWw%oAYqP5PUP#GKoGu3{M}9Cx+qfBQX3(tT+;r zh9gb_gCkHo0$C%GKN25~MA1mAai)CjCr0AVkr+D?r6WuexMnzRRtd`^ambN4^$6TL z4DTO~FO`^ZC~h5!^M+tR9x8M3&>(aki1$^!CEG+1)B52O(K&h{x+hNTVdT%Zy5NgW z*x1oZ>~)cjqSlahXx9dZILIq05%qDXk3p0a7b8(3$jgVV9z+Qy2{9(X$VLvS=MA+M zWcj#~h5LA1IX{qjQ^K>9p1GG7?V)EkAKA&lJ6Nn;aw}im!lyP{8FbMvg({2MIz-F+ zI>RVVUd!47-e15j1>CrX-?-zeHCCGTyESZ8z&T2hC}8SZ&Rxr#bu3xO$Jg_)4UAGL zflUloVxW#KOx?z_q*u0sTXwSRZqC`mjT#I^+)>OkOZjsd^Y^iBB|i~kwuW13`D8uM zZlou`zXf?J>GPN%=TxaugyXFUoDyZq*w4lyBOcEspld6#GZ?p{4J9udnKa#5>w<@_9I*U%-O$i-%?Im&hJ-PRnx7j zZRBLF<5%lBb|d?2re`a^+{Vc}*mIYaIWFJJ&x<&-gd@t>eIKJMxw?uEiK$g*D5XaR zi8H_bz>gl>=tH6?Tj5BJG=(Kqy1y?LTjS6*0hcFYzEm8O5KOl6l-@RoYm2hBwt=3j z!bz!EAr_ysJ~|k)>%lZ^PeYeT21PbT;)w|K3db{k)O#`6gMH-W zkO3NvjphJusN<2fmeszn(tx11l}jkU%Q)3p(e10by@reG__f29mCHPv9PhytA0{Ya zCjyj zLaRPFuP+|yho}4FzAW6CZRYW11JHXQ3RIX?u5K>A%ti0PxOFgA4@QqXOv$riWXedo zDi2pnIXn+Jc~~aG-eB~XNNg^0a!sOV_dpyu5cdqQ1$jv}R%Bs&e-tVKxi4PogBiUs zq8Hk@?4)tsQP&kubuj^@WsWZ`D{f#KzLAaH9#^)-`ql=;EKNesR=6wy->K|EEWVDm z?3GE%ybHq^Kel=C7Z1Kx6~YjA26#u4iRgY^VnQ%lrvZ53au1 zTbws^pNj`=jKqB^k0M!>I9!l`j;%~wWnu~{lp@_07q+)tm7CMx&%omjz7Tb%OIKXq z4SN*XIRsxS^y518n7+8HA71H?hqEl9?CKm`?68aCf%xkn49qod#^}L#e=x%GaAF=F zb9U6)Jd=-}x8I=3%ft7BF+t}H#(lZiHpp^OCk@001F$;>CD|y*!cYCNq8~o(i#ZB9 z^v3r+jV&-lxyId)(iM+)MvqQ-NQzkLNKM0RtrgmB+hSyEOiDHzV|M~l<1s)gi_w@J ziMPYC*^hu1k~KJnyeY^pUAQ4t8A-K#tD2EjJk#M_jitP$#K>3bFZIYByE$wZ8+P#Z z?L2E6yE&6PV>4q^cXb0}H}K5$e0m+fTT4Nj(+Vi8|IRh^uHlnnGp=Tz)r^qZ$tr%l zieIm?)be(UQ>o|Trdu*^e^4w^QTsc0aaHoNO2gi=u86cOtc1kJ_*++ zi}JR(zn!IeNeH5(J^H2Mp;YWrVp<0TqzRB}89)+9*_4XgQqd>X&UvZ5!JC)1!zz_{ zXp6VnSb4S0DW)CTCmH>d(6$vy6Yyg^UWqelg-$V87=>|>s1L_uVd$s=C|(4B%QU1_ zO(VdU8d+V>zqmM2cr}l!$V?S{D5{AC#Vc+w~?@ZnO`ES|r zTfVxOV;6J4V$&W}7RjPTT)2pP7McF%af?m<=m!aOe9N_}rS%SJWWC5AGq}g zzAFv7pSbvEZuo_pma<}*t^aqe5 z-OS(1UPat2J(*Gq3;yUJFX3_P z8|EX*v`nuv6YzARi8<^}!uDiTq#)256>Ut|;r4bor9F~Uu`t!juoiT{X=zxVh9T*= zF&!Vs;nj4@FeX)YhVe5)>1L-bPRCX0NJ_^$iYd#iQsA!x-b#fp6%*TA`niUUzb(?+ z;KbILmV*3b3{OIKE9A%rPcVP{l~_yOKQ^25<444Zs-)Qf%|EGDOg*geR# zO`I)dvj)zq+oe z?V;QF+E!z{U$up$n|a%29;OkpiH~eDX_O;2arh<<+{7N6c<3fWGC$o!NX=t2U)jt~ zTP)-0Okt8+dFwWwxt+syuNhrJg%OL8#u9vounTU;<-SD2XFf<@l#nvKPX`~8ac7%GQ5$16I-Du2~VWptTynr z!`-2pGAS;F1&j_BLT{HBPm=JSdc6;h>Ok<5D;lX-q0e5k6y{qVNrgG#^^wNG7y=AaNQtmAA~%Wq?Bl3E}jvnsEA%JUdqKKx#%Wp%^5;sq5 z_5qgKCKUR)YjL+ZSE>pjB`4xC;-l2;ijd=q}YVl~n7D;hj*>5pyz- zD)6KOhNog)I~?IQ-%C=kMWJDpSxLa;cr?UfW{inWToP&cq>}ds`!U~(i#-Y_Vq=K6 zNdi5<>PAyM`?Q|7)tTt`8PX7|=G9fap^|r3@bi6)*k_6s50r6RDNifq+7ez=!V<0O z#XO|g>ZC6%GJsR=UJ>h5X}Xv%6tkw7=c&3@2`8#La~WSRxxMgtFQad@?Tx;@O-b_7-WcB-dA*H` zySbOCK%CbLn|k7+o~S(pHync1J&@c3XLQH&-LRo6_H{vNXY5fm5b4f!#5WnZH66#L z+43&>&9&_i-4>6x#z1E~{iPK?Ni=#dGE4d@cBLjS+ zk+BUtr;e}Jux&M;sH9hA8p^q>loyn+w1{&G>DkLCc5~D&VyB_O*KFgkttKKJvzd{b z7}~%}MGDqi3dx;o4PUBS!!y=!?P{L0n#+~%xr+W(T(*+WtmH*2jb+heB}c3@71PNp zdHqVG*Y;e+TUK%1DjvR?-#RNgu7EETaKu_hu4CCc{lw2Z*~^- z*V|dYgPFTHVmD_hT}A3%MLfQkx0i5nDU-@MV;{e*FmvLBYCc)R+FG7c&$$hp-$-A8 zX9oGCN->gKfM_p9DzVgWvwm7Qu8P3Jl8uVOlxPf&u}lz89G1l4(s(2#;43BjCt7CT z1+DN?E951avW>!sosw~aB2XfICF9LxypW9Bk})kArzWFwGFBvEb`sK(u&5Q(M_(qQ zM99$7$n9ZI~s>YAvY5JBG5k^eZvqgBf*PTJr*6fh-@3;cVaS2 zo4t{d4Sc7LCpy#jqbgof$%qO*Th0^97*Wbki+Mp2y@mXE53kzILwB)e2j6%7c-mIx zZegp-B(-(A7M(oK?#!*X+fE?j1*5Sy3s z3Hho^Ic6!RF6B&j+_aPnma@k()983@8N-$v31s*RE?8l;@M>4`r&WAqHK(sJ8TRf{ zWLjs$ywMwY&PHCX<$W`;l}Bzf5KJ@xIk_5swR?D~oVy}p|NdNRBC!khad;({RdGrU zt8002z429UYGS(}p9wJ;xZ7h)!iW6^(N9;oK}t(EOMM>H7^kjt@LV`<3Nt7#)rUe4z5*ned`rkivex0~ zCi)uLSkE1GmY4UPP@rnoR@kdcNGtheW>JYXpojdwJPj-nf@9?B(*kc7h@rpGr2rh}RXd+HJ3$O89w+ zG2*m7uPWn<<-AONRAJC)mn!yFnt2U7)-t<}W9luM`DP=3Y@#Q~<3oIcECAxXIL~Lo zP@TdtBLbgBA}$(N$6$9XhQ#Cg1QaCVs3g3TjOrBhZDUFO+uI>K6=OSCXyChaM0GT7 z`H7uyUT0j^1-Ep?z1?tKcZ}_U!`*@$6ko0vCiO;4AAHmY7xu+Y7X_Xt>bt^={js<| zDx{l~g=4cUb9_=3PRz0ZaY~lSA1UeJ)cz=TM65pj@N!>7_Qm8r7NG3W+t?af@LzMQ zcT;ynbw_zulStaA@LwkjDw!5m1`^V-tplD+#qjn9u)Nv^V_PFE1&<~nrxms;?%y1M*e8z_pylACAHHJUuUsGN!y)O&Cyky zP|5o$_{Bb!m9u-f>4|+^%Is3USVHL(DUTq%n5&9-dyxebH>h-4AwLkaO98$@URTIx z3i*8@D+`$_euWBY71{X`G2U6svrAa41l3aZ)iujFP27@w^i|kO`#>e9DoRq#V`_L< zEqm25t)7_;R^ezwlOa!98tpemf+##~VTpQ!VH-rvn;0V1foi8hWSW zzI1F#N2?4Rk%5ad@K;q)&%iSoxF-YGyLTkr6ns^lyrQYW=d8cbU>F5cq-M% zU}v?*J?-#wTeOkKv%tkJActwObvDKVa#jY-(Kck#|%2-*#4~ltN5eFBV7{a#QT%asq z6*u3(+1q*fHr_35v@LveGvC={q4_==dBFxgrm~*vc$RRdwdTEkP{5%D+%8SXH9Tew z>sIr#)qHL>pIFU1S6etw8v8G;X2ELu*0A>)&Rk>Rs#6L$uYhUdK(4i_J%#Id?|Sy! zV2P+7Y&3&>@@8gjVS)?*)eqgyc01_b$@pCyxZ9+1L@v6j&%1_OeTFrEGEQZm-CUg#^clk{4D`R3CK>wHHmmT5#J_aWg^xm+FGh(VWRob z=O$u^*8BuKm|zuN7Q|zsqE~S^IS%hg&^XpPWhxdKZ9jKUBz{s}Sh)3LN*Hpboauv- z98dA!DByVUy+zRpva4HlC1IykcUv95tmV@+ytmpyLNhCQQUwR>WB+nyi-=fi^G;BP zCRD#d9=?|&_wcOUyk(aq(k@-rT z-^eW+`0NHw-@wy0SjyPpB5An4XDEm-HR%m3lholx-r$%Uqc`!3O&qkD3peweEevhp zi(7f(HhQ*m`F6g&gE#Et3A@;THwW*rl=DLhEhT?QF~=y=MD&|7gIHA?rM1JjZgLn` zc`c8Wwco(68X0Kf@E{)xaSz$mgR{JN)`u!ThK6HG1RjgTGWDGl|CCV}hil{Ue1dhy z`c~MOgo0$Ols-voENX+-+T!wd7}*}JQ?Wf24?CQBNtz|EMP}f(4D8Op@g4C+M`UE; z{7l@IiBB?7l!>@bXx$0n%4f+$O(wR8W|N706@$oxlxgqli0&QnY6f~`;6rie(hZ;% zIz6ld9&?b0fXBbIGv?VtS}Pg z5hw{qK^W%yG0$h3DhoaM9C)34Bg7wq+!0_=6Dt~-+{oh^cuhV3R>xJfOp&cp!-dtx z>C(jCT*>n*xkDt?3fAo7{rfmVvAlAAs3>zePbg>Ka)TLD%8l(KgSK}$$CPufJ3$M( zPJUNJpbAc_;MxjKu4GshAFEePE*!NBL_E`7N0*ze~8hF z3IRbCAM>KU&!Y23hFJ#L`EJrZ6NOKr@vU^*obxN@M&CqhlVM3XBN-Q`;L_H(qzx`= zi;LPBXn9&H`gK4|8p_h}b2=W+z}X#`zYB#*x6+OFJ zeYDQn=-m$6yAy8B#EOpaD4RP2XQg9q8s6-H4^r_(d;F|QRc$R5PLS8&8uo4q^98=64a<7Ukg}ktk zuNQKBA^RxZUTG194DMy=UKZ_TSRr$qt)-fApU4+0;<-iqq=;>cd0jE9ig|en#gTiw zls(G0pp3K1nI+(HA0HI_9Hb$#~LGjqD@!Mli8DoCR4ROeg$NG57tyVboqePsb z(hRMP%=V`9gOu8{HW?ihp-90?DOi_cHk-dS+KB@xoqWNVtx+jun-n}N#mE%&5-~Cv zbCS_68J{QF82h9ZdbPq+iHJ?KT(O7Tv~5v)dRJhqXwzwIaM54$vzcK-N$52u`=7X zM5qfZW=auz7xKuxJarGJ?dD^P6E;}QR%IK7Z(u-qof`~N9HXeY~e@{L`*Vzlfs6p!g7 zxAvjhX9>m6h2g?*w2Q!U=MImG!e*6aiAJQ%vlzono^*ck%s6b0!(b&o#9QKjWjqEZ zSX^UPf*I{|6HFRHd+xQeloHTKG7$+T2st<2hR}{UJP>E*?&q}mIlIL4g>s7xda5tv<%Q&4-lhq- zmrw5D2!&a9^Xc9E%Wfv`=B{0)T>9KD&fUdpc5&7&-mr^Li37ijzwBaY7mrjn@oxSi zMW#KxQTZc#c+p;N-pdOVBXA759>ul@%n~43%H5@WuZ-7~bCkmO6>O^Do=PsS;=F1; zU&Ci>`9d9^6RE6$4>p?a)5igR9po1wE|PjQP~|~Ln7m6hN()DSrSL>rdHUYb$c?dt z6t!*pctj?c<)e7|kF6{db80e1rJ#LlY-o*p+u$&X7AjTT?Y}>^$LLfPrkZY&{FD)C zctw>C(vhEz*V3&7r0CGcXW+^V)A!IK|6B%M$uLapz6?ysFfmLy%SGvU%Q3b5=|<@s zlZNUJxJI1nRNSJ#M5-~nHnzjic6i>w(Pz7$|0cH%T%L^glCZHA;#yfTTrss?ipTeH zsEkEwERKpXOy;gAydG(|mMk=Ri^DZg{g~^+H(u0vOy1#GWtEc41qCRPFv$A@+}^|^ zn)rMpYa4iS1HV+RRz1J3V{RS)Udv-@SzTjs^WHV4x_o~%$5*poH7l!3g6u7Y6ss%| z=AkOit>P=;lgJ@LpFB`1=<{A%zRs5Iq&l$S*cy(nB1}a0eg{Jh*$+eQjKc@< zHk(dPM4wjhCE=?iTR8_SYTp|3TVq@s_}Ut_aArH~QyKL3rh9O0D*PRAg{qr%K(>qE zm!?_xep)(Skz{(hg%8p)&?f_Z1$4N-9WpFyX{7+qbTh-m(-3=Ej<&wOx&tojfQD4e zOvT>z$XDUZb{NtQcecgmHaN5m?v_!Pg1Tg63js^Q(+=<2AmQS8i+T)+Gl_|_V=yrq zS4Nr8(<4e=2*)E~c*~DZoI#~jj>W)gvPh{YK{CL&0DCp@1ZD9!gX-BjuBfG7kU|YF zsJ&M085M0DzMT{=u z-a@W)n1(#x2MhUZA>VUOv2s6EzeXmppy~UH_^pcqPbfAPZbk{OE#bBjo?FU|r94B6 ziF0{mnB678oC>~Q!ILWqRbf*=vzkF?JYQ1Fo^{+@$NTGfLIV@z3^wwwB5MWg8NUh| zo-=}!7{!Z{SM{K+7e9J2&4&a(KKCO(4E}ID5pH>f^CED3BvwV@geZI*g+9@EEgG%F zqfk1JG+JY=P|J&|%NdKhSoDa)IdQl`wH@7YM;!hdXZ!fsaTp+8t_V@mD2p{tW@fA< z1pPGzJ={`!d^9#hVZ0XqNTa-d9)WHVxHsH*Q!*Fl`cdFRSD&%Fg-@;p5`Ys0wukse zkiP_2+r%zSJhhRRHt?Q$i-fLoz1Bq#LA7b=NP_bF3YMzA+dd8}=S{L?N?BLJo+X@G zYz?kj$rB6t{$94(%X{`Pw4391bNMcwt&(;-&3}pBX|cJJcQASf7i{Ou?G~Snk)q%> z{-Ws8Ha@q z5|c#q&OKI|O4wI75e5o*sRSpB&BIk1@;L&T%ec0Tua@)LeLS|pl9abqS~lqOGVeth zt!0NghU&OgMO7MjeIw6o;*bE7g4__~OCin@jR8njogfeH@!|v@qWoCk$BZx}hg%ly zxCnR^7mh@SD8m+nVa^kCAO>?KjHc-miv_Vrbe;HOoE1aY*b|4p#hFTmuCOo`m&GC_7H`Mkuo#PpTpW#xC}ZmG zPzjhw>=0)x0w0E3DB-R!)cFmEe8Yz@A0~P6od>CII~Fk`mApK}pMy*b8joElU!Nx4 z*~s+*f*SaAJ!9&vkoVGB4v|oHjfDGmj$jmCf&)WukW&-HNz5TztwaRO$#6p~Euc;W9YN`vR?eSrI%xI6a_LeL*wVe?l#VyHci!a-lK+3|_26{`( zR+PwxlTA;4tWs8mhqtmSqCYEslz`b1yme#a>{vt2D`o#jTR7>PNW?~BOaxvD$1Vj) z!*GKitEH;s!$dE>@F2s3YZPlE`;!lctm$To^ANC%{v}O3xrqghJV(s%243F4_HHje zub!yq$925Cj)Ur0Q_HVv%}}3K%TvW@t!4XK#wic6mPxgiYMoijLA5-+mRHr9jr2n; z0}{}yoiF=E4@c;^{H9aUI~?DI+aos#gpag@?zK^v77bqv9*;rqSbPy{>LtI# zVYH;qt+5Yzo?>U~4koPR5lASR`Xp5^mHGO2VdAn9~XkiMS;ZDS}B= z@gxB^#-k$6ATj}E*|E4oMMI)7G}`DTn{D&#T#?n%7rxdnHFzS`Dep3O=`w z>HGLZInz{zqm0LDdY1B*Oc?vef)AC53S%=6+F6< zODcIr6}JeEt7ce@1p)ij^6OfjUB|jQK3LB~6b)=JqHLWM`=#_A;Aa8;CCK6+ZwfJ9 z1DMPdSPcwO6(mV_dhvJV==t!nfNNrcGXs_$E4S!x zjb>~J1^T6)17w%hF}#kGYx!dh2iEYVY9?3nhAI}S;#(!xRB*I&l!hyWT5jR{b)s38 zvAUFVOWCWGKS?*qSyPLPc|$P|D`v2W^Cci$WRmH564Q2`)Fx+EDXz4J5@Hcic1vfkP2=MU#{U_RB*i3aJCcbjg|Cv120m5+fDf?0d@`=)^VNUCgfW3 zcBM1OT=C!r<-GeaU+pQ5Y#1I6GuUTdxN&t>MPRhNj7S_4W!c1HAio$5#9&g4Nqopm zx>yvxSOj7Xm$)Vluf}0%92&%@jmHu3I4&Ni#T$=H8?K;cz1EuwKBmQ^HqMsTr{b)- zw6uCewD=&_f_;ZL(@Q9~w)h{SZH0+ZdZS9qMPZRDzeGY?j9}G$;kY8)#@n@FpdVMO zIG4|ShgvUYX=CyjZ&pd%lY!mjMAcLcabn0QTUP}c3h)*c;|TEeCLZ3zpBqiY&)>+` z8q6Fmm(p^*>7R$!8*F%G9dqi)T7xcMs^zR&o>0r2TBg-9td?ap+*ZR~H7u^N5&%^- zW{oSxH>}opAUBBiR%=A`c6G)^mNz7zEVG`o>$#+!X$_nsHg5xu)+XD?Hch;(i6Q44 zRRwrykflM+36UY*Ny?;r3h1lkZn3_-*rj3bL!2M4`Ehs{mWN?tI0E6OMcGr8Wt=@+ z;MQkvjEUaI#^T{vbQHBI4jm;K9glc1>k<%1Kz<^=S23(sc&HU>TjAIwJRl-g5(c=H z_C3X9laZE!lTvU+3La0v`zcr=l8huGQw+Z`xB;ZBKrN-mgIK!D0`Dz=BI~A&q#>{B! zio$78_#zVBBkcjhBTS=jRhT)6@|8OJal2c+&+(cKd5j11fUdx^WCqQ_6jk7{ATxq| zGr+^#{(G)8(zVt#GS``8H#Qg~xLasRz17CbsOQc)eo)6d>v&-u2iLJ}9er*`-d)Qz zwOpxkkWwwKrJ?|h+K%f?3Fo{zUR%c(>kMCtb>rvedVXF{pD<@>W-2kTfrmHp=|%<{ zd6t`!Z3Dbfn~TbDi~kv9Z>?9VvY~Lc^L_FGfg6gpeDA@bUcBx#&i`Z1JigqIU4EP% zhDBk>5--tp{e=-0A6FCI9Ek!4fZQB~RZ{Pa#yCagqEQr$o-w#E2KUEcehi9Z%+}3| z#iRTsAqAbD+g=9qFt#IUpkYP5y-{x!NuI2;IVE8hF-t``9#CUwzZ}fs zD&8zL=_-Cu$+IfybKCJ574%D>MwQ^?Ub*0c)^lymirZ>YRd69x#yw>$DC75K{H%;0 zmhn>;*!`)@)_hslvb0sQ=frZ(DYvlxvT`GI9kY*j?z5!S80ExO@ZAcclBZSjbro`| zve3qgDh{mXv(=2N;dO3(I7fR;El1VyE7^GUe65}x8~9uUGa4<=E~<&QG|?O2bpcic zm>=ZEAV-JzeTcbMTbS{{wZJY3@p|x@$6^+;HWfim@mZMlT^|(8INpyt{aC14X?_e5 zlgF*+pNC<)^O`$`BR3qQ!f|RiE(pg(?(Z2=J_*N&aP$i|pQ|PetHUra%zT_NVdxo# z3WWgtxXEwD;uV^d%QMM`4n8cBPL9{ybA^5;dJyJ;9QF~C&LFQPlgL*?JTk>+6&*QdK0d1WKx8~I8Dhc&Q34pBX$)FyR2zK+GUoLkE*O|%-`<#y+-!tkqk zay6r?`E3<%sp2tJR*__DCEu>(^_4ucl7lLlQpvgsZm!@G@$M@4f$}3N_?kPUUGt5A z5%+0Lg@yADt>hSGYFF~TN<&UEtIQ`7z_qfo#af+*}%q*|C@vizzC77>na$jf>tP78JQL z2J=)IL4sYPUFqx5_)>XB(KyP@>-kY8&d@_Y8i~I~;?PKJjlf+I$c@0}a9kIT4&nGB z4CBJ!3A2n9g^5?XjaU%++v3`JvD;($q&-D-0LBB6zE$a5Qctu#Oy`Eet+H!`J(qnbFgiLW$qXOj)Z$pZfsrVMa!katQLC&(@# z+jD;jv6mGf|}z4B&UekQ9zN;aC@r zLnH8{uvuq5y&s9_C|u|eDka>V6OA{bQ5TJ2F}Nkhiot1jJ~kFN#+tKSB2si5PK?8> zI6R`DKpeKjp-vJm&T7hz$Kml9<&KdK#L$1U+(!}4rrH>XPveZqd1;&_o41d{o>;se zi)&*sG8SR6SSUVZ3UMk5%7*P?Jn6oQdfNl=4LK*#M4;!0W&L>BEG zX0Xheew6!gr_VN{d4g-a*e+MsV}+jI21M!C+`rDPp<~-79I!OBUbDH>E z6L&YUcYt$Lj6i(#ATJE^Et!Z)73zx%4Zp5uPF7^3%?ihb!_b-O-X z3X_*Ir3YiBq3yv2MO(ZEm?$ws>$#G8vV9ou2EzwFZ1lnBM|VGl`*EIgmhbiB1wY=A z(uE&i`?182U;Oydk41ibBW)1(k@fntF_Nn)^ zRRx$6V5ys%lO5c#PjE*g=QQ##StgF({-Q#P4YuF4X<%7B7uE9_AtA1Ll?i@`WGd^~ zSjQ5F?aK^atVnAezp1mFKD}O|XhoedGi&5`yIGu5&r^jo)*A|;IlI2zsJyuimT~_; zgH1# z@|%ieVG7vsV5bKIytv7W6<$N^FH@Yw2frVu`tg(!>-^{zhRI=gGYsWn=o^kn;dnkA z>tz>7DlGzYBJffKRz`r%(mpQI@&fKtK3AmCYZV=+j6|4<)VR2O-zW@-vNAnlGV925 zK5X|WD}Ip>WwgetsK{6cfTTxSndx5~axz=djRA`5Lz z7@l%_?jd0)@#9TDrufZ-*yoJEi+$+n93t(%7kklD3_}mz@L;N-8wZm79hd_g1_<%{ zo_v5jm+VHX8?_+B2VFA`3NbEZ5jr_0@}4da@|++K4Kh7QUy$WuU2204@Y4X_3Yg54 z*7nB&d?dh!0(>;UX99dBz<1mUO9EDFNiJ=8kR9Dc&I|GyG0$DE?Flk5#34c;LwrC{ z>=3I$b~C!AzmngP8p4B>Zw0&s6iW5TVs<$o_O69#Yia`NiUI z+~POY>qbAa!!Sw4Ntm^pic!iRnH-MC1t^7^TB+9CBO)xh@?Lo>(oBoMP8V@df^gqR z40915b%tmb3>&am>tU$om_0H-JcXRx&LGv!<{1!`I*e!8_pgXyk zl$iXKrU1_Y2p)2{Hn>)025?)1voxnvFR@Z z+$+VlpeZ(75wwK;RY8WifpJO5k`#UnF%YsvOiS_oq=GEs7Il<^3`5#qcWbiOV+e`b zN4xXO9!vKw^B~!afo=!9%!|Kzt$LI^ruAOS1PPb@CXH4f2Kq3Oj|$erJzRr>fwc5-HLL4(26_e*cK!HT~RZeVHy(ZHg5u62v{ zd-Z&w-UQnV#%VM>8u&kI6y!Jw7(-0KXgqz3kBux(U! znk<2r-6}0)BgJ{>W1KnnQ6slXmS17MCQfnPy`+it4p<%&FduMnfOQIFJ6r3~pm7D8 zg3Jmr-*xoQA%d2yB-BoJmlmjc@(|!m;8x%RXMja`kma$^@GZ^}ZtmzrFLIO_Erka! z?s7Zhm#Tv2#U9Csc@gh3kh6~udHehNOdrm5U3{TKB-GvN_k+7zqe5dtBP7yiX{x(a zV!(?}y?D`Uk;<7~Q$!i!HID$!4F1NgOV>Lq>rjv7_ErEt1FtFfSxz623rP1)mgGY6 z8S)BoXvscgI4vLc^AI0ZJTzpMXJ1k5LWaQqDA+2;Nxv=U*G(+yI{q zm}r&yMXS0D_1K_M>t!2g5Gn%le30J+xzo+ZUheiJ&-I)Va6*)MED`Tx$(iKS`B2BW1{B}pN3UVF=}Jjc0NnvxGIBQi%;1sLq-A`v z@I60XbjH#qzZI&8kwj-0^1^Up7%mX!&4IuID;^0mzSb*31r;N3w(-aAub_;-hvA(t z{LSr~T5|7nUlBGUd_%^wfW#hQrmCUMVuv%v{_cki_^bRlSNN(QnObE1mI3;?4^Q}T zl@Dk5(BEhIH5y%lL9_@c456{tTdRs{<#_O!2YTM=&K~Qojn-pm-bb+L(oDB%^;WtE zP)7btD`25DZ8mw1TgJ8h#nSe{jUg@xah~%;{;FtH$fW;d!c-39GwQ{1AC{=Br&P#%7RYu#(MC;K34xtq^c0C_`O z-3`3u#e=TP&XJ(g{wciGgZa*+zt&?AjiOzxJ*Whhy471|zu=1_+ybr@Nwl1I$$KQt zL94;1cGIf0DP*)Xp}5y7i#EieA*O3JQ}YJ7IB0B;`<*FuL6AoUEu`LFbIQ5KTLK1s zeBn%{7n~7wd%)@%Tpr-`04D}GHb5<&a>~vPSk}4buU-fs)$S<}L+9vBov)oj2H5i| z>l5JTA|5ykS@=d%fC)iM?;RB6aY5<^W(Rq9kT1&D3)+rTB)cwT%wCO_lR~C9ae4@v6El&5~OtI(>`a9ZVFu44hQEr$GRvl`O*?}EA zmI@@)u>mMnHZJgsY#|}ZqC^7H-@DocfKGG0)(dFkEM6_Y>bve&XlQ~_hc7X}Sj)yAr|Vr+<7v>$Yq#sasn z)Vf&1A>?q$L5r{A>}|D)EUWcee6$3aWHJr~Mgvpb7WSB1(ibR81yEPYG*|npBh(`b zyQs?q0?!eH+Jgr?c-Dj06^n6mN+FD89u&wBaxR$s5k=^$z5Vm+oBTH?mTkHBYc zr4oXt`Fpv9a8={UVbnQ5CLlyTWE6F7CVz2jotEO~+`zcWdCf9Hv?j|=JHpu{YOZvx zgk+SP@AV;;Deog>Cfo)`k6Gp{RV~`zhPXJyZ``2 zhYwc^E0g)BdE~~S)|39^p>A(I-3`%89k4dn&AT_qPn~JB&H+=JebH`R()z4OicI2h zz$}LVsLQlas}X*Zbepyt*K3I$boLn2W26U~T;n~M^;DvFx5Vppzp)db2NE1DOS4J;&#_!8WPn~ym$M!)>19b zac*5raSfa5=9JdzjvgZw>(C1w8EygB3arL&>(=uGZTB8TcpxuK2{rYwt+U95s|1;@ zaoec6X1=q6-U40(o&@d(#6!5oIr!?P^Cg<Vqww!&Sy?atGAPb~ecCzcQC2pz; zk^39@l$))$l2^O_n&6;=Q{1L3OD0>sgXk;H&x&`Jb|^$GedTUbli9N|#MN#-YF28_ zYN~$e7XOdk-X}EaoeIlY`tvYE;xXd{P4=#(5bU8yT(vdjna{^-z-zcjM$Z=ZF5qfxwy<8Z&D43xTo9 ztamG9zT3g|xB1YTIo^GwRYj*=0GtP$?L0DdmPXx)RvHu|-9D>d$u-4U^p$k8+dvWlGqREm7b2I2XQd3E-r>@s>t;uz?7+h|m4RbT@PyzUE_GtnRcE=!ETvdOY|Eu?h zxN|gY^c5}m>XD;J&3c&!Cpl+LOYzyRgT}f(nnF%@zRwjd;(4>XL)nCyzmK{**Zsfl z48?imXXG~y(bAKya7OhOayMBbg(nYe4PR88`&!}seo;?NcH6Y({$&o|yTaK&!W?c;?yXx5ZgF$%4!0`a>1=qJ@prqU`Gwxp zp%ZR%zP8TS7xi^r(5nWK-q>F3v>Q^*>^^3>4UstbS#6Z zzWv>;rCvR*mf!2ReddU4=5a6Ve zRywNp|J4f}YDP6?^U>U}%@gXt>w`bi>wkD(UvIubb9XfVZN75z4>kW{^RGAGhHgnO zns2Q6CJ)@UUL5Ge=C0JE1%7`Vc<|;%&|e+;*ZkMLw0>`Xp}(%der-Lqq5d_uWb^BT z|9ha3n@?!|??1eG;Jg3u!d=*|+88h=U2lX4!cYIcMc4lsFzwCj7 za&xo$WaeaL=k(9gk=r+W;Lt(2Lvsda{`#-;uxPzHZ03ZCLr)uZ^shgs{~g|W^6c4{ z4(;FniYu<@cg28yGiFZepFL#A5W7rHPG6nTch*(YXOFwG@AO$+nlG~dI-~Mujh{K? z(%DmHOwY9Ij=N~aoY{wW?rcAF;NP#$HSN*^KQw(-KlhCN#?P46|H^Tf_RsE@)&Ibi z^~J-~CPPos(@z*Td)%mTv-5}Pxd!yj%Iljo@bv5D=Fb zz4-_HE?8i)0_$2#aST^rgR{=@tK#^axK(tq6m)28+RqlTL``>4y+ zroZ;lA2!{r)33TTzyE3Zvu4bhIX?fW%k!tt?()Z%IxYXV?*2C}rPF`w@n8Gpq?3jo zH+|OZanr}=A2({48o%F^DHGHY1M{+v8Z|I0FDENEFFQLgCujIkSw|1d%E>)?aPFw) zZuH%vt%?g@QY$ z@3))!$1hF!gD(4*58r(4gTDIb*S}(N{`6lbX?EY7+|#r3hGyps9jHl~?IvmSrQBJ6 zevw%-CeFTM+|2yplQas3HLrHV=S-O5)|Zi^b2AS)Y25fJ(`V0^HM!^hC;$C-{`}hG zCy$#xDSyJS{7KkK@I1BMP5(9(5o68(o~ z-M?PZ{y*7u|98)}!6S#{4j-63#;D2qQJjm_K%HHqxHD9X5qWsS|14kb{aPWWa zzx|K9?l+IUrR(I<|L6R;f9ZPvf3oZT*A~iVukQCA`F;@LpFMcPj|^SNpE*U&_R%wE zO#3$-cI}12PKF-jI`0Qc{vFrtAHMoK&-B+TPxxo7SO4hBGyW%VN%JrK z!6knCrTwjaP^0d5vHxRe+l2840JBTy%$({V%?acC=TFTyd})^O#_ZqYjT6QXoj7CW zv~jbCjT7E5qyLqCuNZgvVf}x5``7#bkKLh$!>B*Q1E)0uqNA>wK5p8S@h9a^m?9K- ze;dixvQateq@kzJ6x=-Pp!e+~gC$3doAmp4_kZx;#haRcXSV&$$>XNw4;wir|1UFy zzhs^?ZssNVGiSN3)Sdix#&6x+Y!m)=x9%JRx$G*x`=WdE&%ZcvrhfV~!+<9a%**-} z@YRq0_WqxLcUtp({!?o--`t;qbHClYHRpe`)qeYObMpx*ojLpTDQdoE5LCDR+gpEd z>;L$D4Fa{<tzHso2Xz>?X{Dl^Op=Doa*%w;&g_eE6wLnWe>)*`TJ$St6bxXUowA=57M2l_N zVq3P@md#V*U(AK(kAJVSa9}R&&szSo3vt zVLxvADRXA;e~#ZW+y5X1``6$9Em{5FO=AD!R%m|r_X+;{@8k5}=>asK`H#w_&F@P7 zRm!vd4@l4cNq+o+9{bO{_=DE@&s_N6tF-*c!L9KxU$->Zzigs|PiSebgInWYzHVu* qf7wI_pU~1=2e-z*eBIJq|FVe=KH+`2evHl84)Bit5{{x2r diff --git a/Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897008..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3ee1..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json deleted file mode 100644 index 73c00596a7..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json deleted file mode 100644 index 849c4cbfca..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.820", - "green" : "0.502", - "red" : "0.933" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.820", - "green" : "0.502", - "red" : "0.933" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json deleted file mode 100644 index 92c0b5a884..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.588", - "green" : "0.945", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.588", - "green" : "0.945", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json deleted file mode 100644 index d9daea3e96..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.000", - "red" : "0.212" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.000", - "red" : "0.212" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json deleted file mode 100644 index f95edce012..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.808", - "red" : "0.812" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.808", - "red" : "0.812" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json deleted file mode 100644 index b20bdf59ea..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.467", - "green" : "0.075", - "red" : "0.647" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.467", - "green" : "0.075", - "red" : "0.647" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json deleted file mode 100644 index 821f22f7de..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.255", - "green" : "0.078", - "red" : "0.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.255", - "green" : "0.078", - "red" : "0.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json deleted file mode 100644 index 863c8c7235..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.259", - "green" : "0.545", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.259", - "green" : "0.545", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json deleted file mode 100644 index 0821af29b5..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.043", - "green" : "0.027", - "red" : "0.290" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.043", - "green" : "0.027", - "red" : "0.290" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json deleted file mode 100644 index 8d29c91c76..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.510", - "red" : "0.525" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.510", - "red" : "0.525" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json deleted file mode 100644 index d6a984fc34..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.369", - "green" : "0.369", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.369", - "green" : "0.369", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json deleted file mode 100644 index b19089a131..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.949", - "green" : "0.294", - "red" : "0.569" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.949", - "green" : "0.294", - "red" : "0.569" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json deleted file mode 100644 index 39065d2a9f..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.898", - "green" : "0.918", - "red" : "0.796" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.898", - "green" : "0.918", - "red" : "0.796" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json deleted file mode 100644 index 91e8248243..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.573", - "red" : "0.431" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.573", - "red" : "0.431" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json deleted file mode 100644 index e42a6726cf..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.494", - "green" : "0.608", - "red" : "0.761" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.494", - "green" : "0.608", - "red" : "0.761" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json deleted file mode 100644 index a43d657749..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.620", - "green" : "0.561", - "red" : "0.133" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.620", - "green" : "0.561", - "red" : "0.133" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json deleted file mode 100644 index ce3b3be843..0000000000 --- a/Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.302", - "green" : "0.875", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.302", - "green" : "0.875", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/Dependencies/DataManager.swift b/Examples/Standups/Standups/Dependencies/DataManager.swift deleted file mode 100644 index 1f63ff858b..0000000000 --- a/Examples/Standups/Standups/Dependencies/DataManager.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Dependencies -import Foundation - -struct DataManager: Sendable { - var load: @Sendable (URL) throws -> Data - var save: @Sendable (Data, URL) throws -> Void -} - -extension DataManager: DependencyKey { - static let liveValue = DataManager( - load: { url in try Data(contentsOf: url) }, - save: { data, url in try data.write(to: url) } - ) - - static let testValue = DataManager( - load: unimplemented("DataManager.load"), - save: unimplemented("DataManager.save") - ) -} - -extension DependencyValues { - var dataManager: DataManager { - get { self[DataManager.self] } - set { self[DataManager.self] = newValue } - } -} - -extension DataManager { - static func mock(initialData: Data? = nil) -> DataManager { - let data = LockIsolated(initialData) - return DataManager( - load: { _ in - guard let data = data.value - else { - struct FileNotFound: Error {} - throw FileNotFound() - } - return data - }, - save: { newData, _ in data.setValue(newData) } - ) - } - - static let failToWrite = DataManager( - load: { url in Data() }, - save: { data, url in - struct SaveError: Error {} - throw SaveError() - } - ) - - static let failToLoad = DataManager( - load: { _ in - struct LoadError: Error {} - throw LoadError() - }, - save: { newData, url in } - ) -} diff --git a/Examples/Standups/Standups/Dependencies/OpenSettings.swift b/Examples/Standups/Standups/Dependencies/OpenSettings.swift deleted file mode 100644 index 835fe9b876..0000000000 --- a/Examples/Standups/Standups/Dependencies/OpenSettings.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Dependencies -import UIKit - -extension DependencyValues { - var openSettings: @Sendable () async -> Void { - get { self[OpenSettingsKey.self] } - set { self[OpenSettingsKey.self] = newValue } - } - - private enum OpenSettingsKey: DependencyKey { - typealias Value = @Sendable () async -> Void - - static let liveValue: @Sendable () async -> Void = { - await MainActor.run { - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) - } - } - } -} diff --git a/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift b/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift deleted file mode 100644 index d09aa6c5ce..0000000000 --- a/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift +++ /dev/null @@ -1,45 +0,0 @@ -import AVFoundation -import Dependencies - -struct SoundEffectClient { - var load: @Sendable (String) -> Void - var play: @Sendable () -> Void -} - -extension SoundEffectClient: DependencyKey { - static var liveValue: Self { - let player = LockIsolated(AVPlayer()) - return Self( - load: { fileName in - player.withValue { - guard let url = Bundle.main.url(/service/forresource: fileName, withExtension: "") - else { return } - $0.replaceCurrentItem(with: AVPlayerItem(url: url)) - } - }, - play: { - player.withValue { - $0.seek(to: .zero) - $0.play() - } - } - ) - } - - static let testValue = Self( - load: unimplemented("SoundEffectClient.load"), - play: unimplemented("SoundEffectClient.play") - ) - - static let noop = Self( - load: { _ in }, - play: {} - ) -} - -extension DependencyValues { - var soundEffectClient: SoundEffectClient { - get { self[SoundEffectClient.self] } - set { self[SoundEffectClient.self] = newValue } - } -} diff --git a/Examples/Standups/Standups/Dependencies/SpeechClient.swift b/Examples/Standups/Standups/Dependencies/SpeechClient.swift deleted file mode 100644 index dc8a3e7d5b..0000000000 --- a/Examples/Standups/Standups/Dependencies/SpeechClient.swift +++ /dev/null @@ -1,193 +0,0 @@ -import Dependencies -@preconcurrency import Speech - -struct SpeechClient { - var authorizationStatus: @Sendable () -> SFSpeechRecognizerAuthorizationStatus - var requestAuthorization: @Sendable () async -> SFSpeechRecognizerAuthorizationStatus - var startTask: - @Sendable (SFSpeechAudioBufferRecognitionRequest) async -> AsyncThrowingStream< - SpeechRecognitionResult, Error - > -} - -extension SpeechClient: DependencyKey { - static var liveValue: SpeechClient { - let speech = Speech() - return SpeechClient( - authorizationStatus: { SFSpeechRecognizer.authorizationStatus() }, - requestAuthorization: { - await withUnsafeContinuation { continuation in - SFSpeechRecognizer.requestAuthorization { status in - continuation.resume(returning: status) - } - } - }, - startTask: { request in - await speech.startTask(request: request) - } - ) - } - - static var previewValue: SpeechClient { - let isRecording = ActorIsolated(false) - return Self( - authorizationStatus: { .authorized }, - requestAuthorization: { .authorized }, - startTask: { _ in - AsyncThrowingStream { continuation in - Task { @MainActor in - await isRecording.setValue(true) - var finalText = """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute \ - irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui \ - officia deserunt mollit anim id est laborum. - """ - var text = "" - while await isRecording.value { - let word = finalText.prefix { $0 != " " } - try await Task.sleep(for: .milliseconds(word.count * 50 + .random(in: 0...200))) - finalText.removeFirst(word.count) - if finalText.first == " " { - finalText.removeFirst() - } - text += word + " " - continuation.yield( - SpeechRecognitionResult( - bestTranscription: Transcription( - formattedString: text - ), - isFinal: false - ) - ) - } - } - } - } - ) - } - - static let testValue = SpeechClient( - authorizationStatus: unimplemented("SpeechClient.authorizationStatus", placeholder: .denied), - requestAuthorization: unimplemented("SpeechClient.requestAuthorization", placeholder: .denied), - startTask: unimplemented("SpeechClient.startTask") - ) - - static func fail(after duration: Duration) -> Self { - return Self( - authorizationStatus: { .authorized }, - requestAuthorization: { .authorized }, - startTask: { request in - AsyncThrowingStream { continuation in - Task { @MainActor in - let start = ContinuousClock.now - do { - for try await result in await Self.previewValue.startTask(request) { - if ContinuousClock.now - start > duration { - struct SpeechRecognitionFailed: Error {} - continuation.finish(throwing: SpeechRecognitionFailed()) - break - } else { - continuation.yield(result) - } - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - } - } - ) - } -} - -extension DependencyValues { - var speechClient: SpeechClient { - get { self[SpeechClient.self] } - set { self[SpeechClient.self] = newValue } - } -} - -struct SpeechRecognitionResult: Equatable { - var bestTranscription: Transcription - var isFinal: Bool -} - -struct Transcription: Equatable { - var formattedString: String -} - -extension SpeechRecognitionResult { - init(_ speechRecognitionResult: SFSpeechRecognitionResult) { - self.bestTranscription = Transcription(speechRecognitionResult.bestTranscription) - self.isFinal = speechRecognitionResult.isFinal - } -} - -extension Transcription { - init(_ transcription: SFTranscription) { - self.formattedString = transcription.formattedString - } -} - -private actor Speech { - private var audioEngine: AVAudioEngine? = nil - private var recognitionTask: SFSpeechRecognitionTask? = nil - private var recognitionContinuation: - AsyncThrowingStream.Continuation? - - func startTask( - request: SFSpeechAudioBufferRecognitionRequest - ) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - self.recognitionContinuation = continuation - let audioSession = AVAudioSession.sharedInstance() - do { - try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - } catch { - continuation.finish(throwing: error) - return - } - - self.audioEngine = AVAudioEngine() - let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))! - self.recognitionTask = speechRecognizer.recognitionTask(with: request) { result, error in - switch (result, error) { - case let (.some(result), _): - continuation.yield(SpeechRecognitionResult(result)) - case (_, .some): - continuation.finish(throwing: error) - case (.none, .none): - fatalError("It should not be possible to have both a nil result and nil error.") - } - } - - continuation.onTermination = { [audioEngine, recognitionTask] _ in - _ = speechRecognizer - audioEngine?.stop() - audioEngine?.inputNode.removeTap(onBus: 0) - recognitionTask?.finish() - } - - self.audioEngine?.inputNode.installTap( - onBus: 0, - bufferSize: 1024, - format: self.audioEngine?.inputNode.outputFormat(forBus: 0) - ) { buffer, when in - request.append(buffer) - } - - self.audioEngine?.prepare() - do { - try self.audioEngine?.start() - } catch { - continuation.finish(throwing: error) - return - } - } - } -} diff --git a/Examples/Standups/Standups/Helpers.swift b/Examples/Standups/Standups/Helpers.swift deleted file mode 100644 index 0480f3594e..0000000000 --- a/Examples/Standups/Standups/Helpers.swift +++ /dev/null @@ -1,47 +0,0 @@ -import SwiftUI - -// NB: This is only used for previews. -struct Preview: View { - let content: Content - let message: String - init( - message: String, - @ViewBuilder content: () -> Content - ) { - self.content = content() - self.message = message - } - - var body: some View { - VStack { - DisclosureGroup { - Text(self.message) - .frame(maxWidth: .infinity) - } label: { - HStack { - Image(systemName: "info.circle.fill") - .font(.title3) - Text("About this preview") - } - } - .padding() - - self.content - } - } -} - -struct Preview_Previews: PreviewProvider { - static var previews: some View { - Preview( - message: - """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt \ - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation \ - ullamco laboris nisi ut aliquip ex ea commodo consequat. - """ - ) { - StandupDetailView(model: StandupDetailModel(standup: .mock)) - } - } -} diff --git a/Examples/Standups/Standups/Models.swift b/Examples/Standups/Standups/Models.swift deleted file mode 100644 index 3c2f537985..0000000000 --- a/Examples/Standups/Standups/Models.swift +++ /dev/null @@ -1,117 +0,0 @@ -import IdentifiedCollections -import SwiftUI -import Tagged - -struct Standup: Equatable, Identifiable, Codable { - let id: Tagged - var attendees: IdentifiedArrayOf = [] - var duration = Duration.seconds(60 * 5) - var meetings: IdentifiedArrayOf = [] - var theme: Theme = .bubblegum - var title = "" - - var durationPerAttendee: Duration { - self.duration / self.attendees.count - } -} - -struct Attendee: Equatable, Identifiable, Codable { - let id: Tagged - var name = "" -} - -struct Meeting: Equatable, Identifiable, Codable { - let id: Tagged - let date: Date - var transcript: String -} - -enum Theme: String, CaseIterable, Equatable, Hashable, Identifiable, Codable { - case bubblegum - case buttercup - case indigo - case lavender - case magenta - case navy - case orange - case oxblood - case periwinkle - case poppy - case purple - case seafoam - case sky - case tan - case teal - case yellow - - var id: Self { self } - - var accentColor: Color { - switch self { - case .bubblegum, .buttercup, .lavender, .orange, .periwinkle, .poppy, .seafoam, .sky, .tan, - .teal, .yellow: - return .black - case .indigo, .magenta, .navy, .oxblood, .purple: - return .white - } - } - - var mainColor: Color { Color(self.rawValue) } - - var name: String { self.rawValue.capitalized } -} - -extension Standup { - static let mock = Self( - id: Standup.ID(UUID()), - attendees: [ - Attendee(id: Attendee.ID(UUID()), name: "Blob"), - Attendee(id: Attendee.ID(UUID()), name: "Blob Jr"), - Attendee(id: Attendee.ID(UUID()), name: "Blob Sr"), - Attendee(id: Attendee.ID(UUID()), name: "Blob Esq"), - Attendee(id: Attendee.ID(UUID()), name: "Blob III"), - Attendee(id: Attendee.ID(UUID()), name: "Blob I"), - ], - duration: .seconds(60), - meetings: [ - Meeting( - id: Meeting.ID(UUID()), - date: Date().addingTimeInterval(-60 * 60 * 24 * 7), - transcript: """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ - mollit anim id est laborum. - """ - ) - ], - theme: .orange, - title: "Design" - ) - - static let engineeringMock = Self( - id: Standup.ID(UUID()), - attendees: [ - Attendee(id: Attendee.ID(UUID()), name: "Blob"), - Attendee(id: Attendee.ID(UUID()), name: "Blob Jr"), - ], - duration: .seconds(60 * 10), - meetings: [], - theme: .periwinkle, - title: "Engineering" - ) - - static let designMock = Self( - id: Standup.ID(UUID()), - attendees: [ - Attendee(id: Attendee.ID(UUID()), name: "Blob Sr"), - Attendee(id: Attendee.ID(UUID()), name: "Blob Jr"), - ], - duration: .seconds(60 * 30), - meetings: [], - theme: .poppy, - title: "Product" - ) -} diff --git a/Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7..0000000000 --- a/Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Standups/Standups/RecordMeeting.swift b/Examples/Standups/Standups/RecordMeeting.swift deleted file mode 100644 index 35bb6f16e5..0000000000 --- a/Examples/Standups/Standups/RecordMeeting.swift +++ /dev/null @@ -1,403 +0,0 @@ -import Clocks -import Dependencies -import Speech -import SwiftUI -import SwiftUINavigation -import XCTestDynamicOverlay - -@MainActor -class RecordMeetingModel: ObservableObject { - @Published var destination: Destination? - @Published var isDismissed = false - @Published var secondsElapsed = 0 - @Published var speakerIndex = 0 - let standup: Standup - private var transcript = "" - - @Dependency(\.continuousClock) var clock - @Dependency(\.soundEffectClient) var soundEffectClient - @Dependency(\.speechClient) var speechClient - - var onMeetingFinished: (String) async -> Void = unimplemented( - "RecordMeetingModel.onMeetingFinished") - - enum Destination { - case alert(AlertState) - } - - enum AlertAction { - case confirmSave - case confirmDiscard - } - - init( - destination: Destination? = nil, - standup: Standup - ) { - self.destination = destination - self.standup = standup - } - - var durationRemaining: Duration { - self.standup.duration - .seconds(self.secondsElapsed) - } - - var isAlertOpen: Bool { - switch destination { - case .alert: - return true - case .none: - return false - } - } - - func nextButtonTapped() { - guard self.speakerIndex < self.standup.attendees.count - 1 - else { - self.destination = .alert(.endMeeting(isDiscardable: false)) - return - } - - self.speakerIndex += 1 - self.soundEffectClient.play() - self.secondsElapsed = - self.speakerIndex * Int(self.standup.durationPerAttendee.components.seconds) - } - - func endMeetingButtonTapped() { - self.destination = .alert(.endMeeting(isDiscardable: true)) - } - - func alertButtonTapped(_ action: AlertAction?) async { - switch action { - case .confirmSave?: - await self.finishMeeting() - case .confirmDiscard?: - self.isDismissed = true - case nil: - break - } - } - - func task() async { - self.soundEffectClient.load("ding.wav") - - let authorization = - await self.speechClient.authorizationStatus() == .notDetermined - ? self.speechClient.requestAuthorization() - : self.speechClient.authorizationStatus() - - await withTaskGroup(of: Void.self) { group in - if authorization == .authorized { - group.addTask { - await self.startSpeechRecognition() - } - } - group.addTask { - await self.startTimer() - } - } - } - - private func finishMeeting() async { - self.isDismissed = true - await self.onMeetingFinished(self.transcript) - } - - private func startSpeechRecognition() async { - do { - let speechTask = await self.speechClient.startTask(SFSpeechAudioBufferRecognitionRequest()) - for try await result in speechTask { - self.transcript = result.bestTranscription.formattedString - } - } catch { - if !self.transcript.isEmpty { - self.transcript += " ❌" - } - self.destination = .alert(.speechRecognizerFailed) - } - } - - private func startTimer() async { - for await _ in self.clock.timer(interval: .seconds(1)) where !self.isAlertOpen { - guard !self.isDismissed - else { break } - - self.secondsElapsed += 1 - - let secondsPerAttendee = Int(self.standup.durationPerAttendee.components.seconds) - if self.secondsElapsed.isMultiple(of: secondsPerAttendee) { - if self.speakerIndex == self.standup.attendees.count - 1 { - await self.finishMeeting() - break - } - self.speakerIndex += 1 - self.soundEffectClient.play() - } - } - } -} - -extension AlertState where Action == RecordMeetingModel.AlertAction { - static func endMeeting(isDiscardable: Bool) -> Self { - Self { - TextState("End meeting?") - } actions: { - ButtonState(action: .confirmSave) { - TextState("Save and end") - } - if isDiscardable { - ButtonState(role: .destructive, action: .confirmDiscard) { - TextState("Discard") - } - } - ButtonState(role: .cancel) { - TextState("Resume") - } - } message: { - TextState("You are ending the meeting early. What would you like to do?") - } - } - - static let speechRecognizerFailed = Self { - TextState("Speech recognition failure") - } actions: { - ButtonState(role: .cancel) { - TextState("Continue meeting") - } - ButtonState(role: .destructive, action: .confirmDiscard) { - TextState("Discard meeting") - } - } message: { - TextState( - """ - The speech recognizer has failed for some reason and so your meeting will no longer be \ - recorded. What do you want to do? - """) - } -} - -struct RecordMeetingView: View { - @Environment(\.dismiss) var dismiss - @ObservedObject var model: RecordMeetingModel - - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 16) - .fill(self.model.standup.theme.mainColor) - - VStack { - MeetingHeaderView( - secondsElapsed: self.model.secondsElapsed, - durationRemaining: self.model.durationRemaining, - theme: self.model.standup.theme - ) - MeetingTimerView( - standup: self.model.standup, - speakerIndex: self.model.speakerIndex - ) - MeetingFooterView( - standup: self.model.standup, - nextButtonTapped: { self.model.nextButtonTapped() }, - speakerIndex: self.model.speakerIndex - ) - } - } - .padding() - .foregroundColor(self.model.standup.theme.accentColor) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("End meeting") { - self.model.endMeetingButtonTapped() - } - } - } - .navigationBarBackButtonHidden(true) - .alert( - unwrapping: self.$model.destination, - case: /RecordMeetingModel.Destination.alert - ) { action in - await self.model.alertButtonTapped(action) - } - .task { await self.model.task() } - .onChange(of: self.model.isDismissed) { _ in self.dismiss() } - } -} - -struct MeetingHeaderView: View { - let secondsElapsed: Int - let durationRemaining: Duration - let theme: Theme - - var body: some View { - VStack { - ProgressView(value: self.progress) - .progressViewStyle(MeetingProgressViewStyle(theme: self.theme)) - HStack { - VStack(alignment: .leading) { - Text("Time Elapsed") - .font(.caption) - Label( - Duration.seconds(self.secondsElapsed).formatted(.units()), - systemImage: "hourglass.bottomhalf.fill" - ) - } - Spacer() - VStack(alignment: .trailing) { - Text("Time Remaining") - .font(.caption) - Label(self.durationRemaining.formatted(.units()), systemImage: "hourglass.tophalf.fill") - .font(.body.monospacedDigit()) - .labelStyle(.trailingIcon) - } - } - } - .padding([.top, .horizontal]) - } - - private var totalDuration: Duration { - .seconds(self.secondsElapsed) + self.durationRemaining - } - - private var progress: Double { - guard totalDuration > .seconds(0) else { return 0 } - return Double(self.secondsElapsed) / Double(self.totalDuration.components.seconds) - } -} - -struct MeetingProgressViewStyle: ProgressViewStyle { - var theme: Theme - - func makeBody(configuration: Configuration) -> some View { - ZStack { - RoundedRectangle(cornerRadius: 10.0) - .fill(theme.accentColor) - .frame(height: 20.0) - - ProgressView(configuration) - .tint(theme.mainColor) - .frame(height: 12.0) - .padding(.horizontal) - } - } -} - -struct MeetingTimerView: View { - let standup: Standup - let speakerIndex: Int - - var body: some View { - Circle() - .strokeBorder(lineWidth: 24) - .overlay { - VStack { - Group { - if self.speakerIndex < self.standup.attendees.count { - Text(self.standup.attendees[self.speakerIndex].name) - } else { - Text("Someone") - } - } - .font(.title) - Text("is speaking") - Image(systemName: "mic.fill") - .font(.largeTitle) - .padding(.top) - } - .foregroundStyle(self.standup.theme.accentColor) - } - .overlay { - ForEach(Array(self.standup.attendees.enumerated()), id: \.element.id) { index, attendee in - if index < self.speakerIndex + 1 { - SpeakerArc(totalSpeakers: self.standup.attendees.count, speakerIndex: index) - .rotation(Angle(degrees: -90)) - .stroke(self.standup.theme.mainColor, lineWidth: 12) - } - } - } - .padding(.horizontal) - } -} - -struct SpeakerArc: Shape { - let totalSpeakers: Int - let speakerIndex: Int - - func path(in rect: CGRect) -> Path { - let diameter = min(rect.size.width, rect.size.height) - 24.0 - let radius = diameter / 2.0 - let center = CGPoint(x: rect.midX, y: rect.midY) - return Path { path in - path.addArc( - center: center, - radius: radius, - startAngle: self.startAngle, - endAngle: self.endAngle, - clockwise: false - ) - } - } - - private var degreesPerSpeaker: Double { - 360.0 / Double(self.totalSpeakers) - } - private var startAngle: Angle { - Angle(degrees: self.degreesPerSpeaker * Double(self.speakerIndex) + 1.0) - } - private var endAngle: Angle { - Angle(degrees: self.startAngle.degrees + self.degreesPerSpeaker - 1.0) - } -} - -struct MeetingFooterView: View { - let standup: Standup - var nextButtonTapped: () -> Void - let speakerIndex: Int - - var body: some View { - VStack { - HStack { - if self.speakerIndex < self.standup.attendees.count - 1 { - Text("Speaker \(self.speakerIndex + 1) of \(self.standup.attendees.count)") - } else { - Text("No more speakers.") - } - Spacer() - Button(action: self.nextButtonTapped) { - Image(systemName: "forward.fill") - } - } - } - .padding([.bottom, .horizontal]) - } -} - -struct RecordMeeting_Previews: PreviewProvider { - static var previews: some View { - NavigationStack { - RecordMeetingView( - model: RecordMeetingModel(standup: .mock) - ) - } - .previewDisplayName("Happy path") - - Preview( - message: """ - This preview demonstrates how the feature behaves when the speech recognizer emits a \ - failure after 2 seconds of transcribing. - """ - ) { - NavigationStack { - RecordMeetingView( - model: withDependencies { - $0.speechClient = .fail(after: .seconds(2)) - } operation: { - RecordMeetingModel(standup: .mock) - } - ) - } - } - .previewDisplayName("Speech failure after 2 secs") - } -} diff --git a/Examples/Standups/Standups/StandupDetail.swift b/Examples/Standups/Standups/StandupDetail.swift deleted file mode 100644 index 17d4b6ff9e..0000000000 --- a/Examples/Standups/Standups/StandupDetail.swift +++ /dev/null @@ -1,412 +0,0 @@ -import Clocks -import CustomDump -import Dependencies -import SwiftUI -import SwiftUINavigation -import XCTestDynamicOverlay - -@MainActor -class StandupDetailModel: ObservableObject { - @Published var destination: Destination? { - didSet { self.bind() } - } - @Published var isDismissed = false - @Published var standup: Standup - - @Dependency(\.continuousClock) var clock - @Dependency(\.date.now) var now - @Dependency(\.openSettings) var openSettings - @Dependency(\.speechClient.authorizationStatus) var authorizationStatus - @Dependency(\.uuid) var uuid - - var onConfirmDeletion: () -> Void = unimplemented("StandupDetailModel.onConfirmDeletion") - - enum Destination { - case alert(AlertState) - case edit(StandupFormModel) - case meeting(Meeting) - case record(RecordMeetingModel) - } - enum AlertAction { - case confirmDeletion - case continueWithoutRecording - case openSettings - } - - init( - destination: Destination? = nil, - standup: Standup - ) { - self.destination = destination - self.standup = standup - self.bind() - } - - func deleteMeetings(atOffsets indices: IndexSet) { - self.standup.meetings.remove(atOffsets: indices) - } - - func meetingTapped(_ meeting: Meeting) { - self.destination = .meeting(meeting) - } - - func deleteButtonTapped() { - self.destination = .alert(.deleteStandup) - } - - func alertButtonTapped(_ action: AlertAction?) async { - switch action { - case .confirmDeletion?: - self.onConfirmDeletion() - self.isDismissed = true - - case .continueWithoutRecording?: - self.destination = .record( - withDependencies(from: self) { - RecordMeetingModel(standup: self.standup) - } - ) - - case .openSettings?: - await self.openSettings() - - case nil: - break - } - } - - func editButtonTapped() { - self.destination = .edit( - withDependencies(from: self) { - StandupFormModel(standup: self.standup) - } - ) - } - - func cancelEditButtonTapped() { - self.destination = nil - } - - func doneEditingButtonTapped() { - guard case let .edit(model) = self.destination - else { return } - - self.standup = model.standup - self.destination = nil - } - - func startMeetingButtonTapped() { - switch self.authorizationStatus() { - case .notDetermined, .authorized: - self.destination = .record( - withDependencies(from: self) { - RecordMeetingModel(standup: self.standup) - } - ) - - case .denied: - self.destination = .alert(.speechRecognitionDenied) - - case .restricted: - self.destination = .alert(.speechRecognitionRestricted) - - @unknown default: - break - } - } - - private func bind() { - switch destination { - case let .record(recordMeetingModel): - recordMeetingModel.onMeetingFinished = { [weak self] transcript async in - guard let self else { return } - - let didCancel = nil == (try? await self.clock.sleep(for: .milliseconds(400))) - withAnimation(didCancel ? nil : .default) { - self.standup.meetings.insert( - Meeting( - id: Meeting.ID(self.uuid()), - date: self.now, - transcript: transcript - ), - at: 0 - ) - self.destination = nil - } - } - - case .edit, .meeting, .alert, .none: - break - } - } -} - -struct StandupDetailView: View { - @Environment(\.dismiss) var dismiss - @ObservedObject var model: StandupDetailModel - - var body: some View { - List { - Section { - Button { - self.model.startMeetingButtonTapped() - } label: { - Label("Start Meeting", systemImage: "timer") - .font(.headline) - .foregroundColor(.accentColor) - } - HStack { - Label("Length", systemImage: "clock") - Spacer() - Text(self.model.standup.duration.formatted(.units())) - } - - HStack { - Label("Theme", systemImage: "paintpalette") - Spacer() - Text(self.model.standup.theme.name) - .padding(4) - .foregroundColor(self.model.standup.theme.accentColor) - .background(self.model.standup.theme.mainColor) - .cornerRadius(4) - } - } header: { - Text("Standup Info") - } - - if !self.model.standup.meetings.isEmpty { - Section { - ForEach(self.model.standup.meetings) { meeting in - Button { - self.model.meetingTapped(meeting) - } label: { - HStack { - Image(systemName: "calendar") - Text(meeting.date, style: .date) - Text(meeting.date, style: .time) - } - } - } - .onDelete { indices in - self.model.deleteMeetings(atOffsets: indices) - } - } header: { - Text("Past meetings") - } - } - - Section { - ForEach(self.model.standup.attendees) { attendee in - Label(attendee.name, systemImage: "person") - } - } header: { - Text("Attendees") - } - - Section { - Button("Delete") { - self.model.deleteButtonTapped() - } - .foregroundColor(.red) - .frame(maxWidth: .infinity) - } - } - .navigationTitle(self.model.standup.title) - .toolbar { - Button("Edit") { - self.model.editButtonTapped() - } - } - .navigationDestination( - unwrapping: self.$model.destination, - case: /StandupDetailModel.Destination.meeting - ) { $meeting in - MeetingView(meeting: meeting, standup: self.model.standup) - } - .navigationDestination( - unwrapping: self.$model.destination, - case: /StandupDetailModel.Destination.record - ) { $model in - RecordMeetingView(model: model) - } - .alert( - unwrapping: self.$model.destination, - case: /StandupDetailModel.Destination.alert - ) { action in - await self.model.alertButtonTapped(action) - } - .sheet( - unwrapping: self.$model.destination, - case: /StandupDetailModel.Destination.edit - ) { $editModel in - NavigationStack { - StandupFormView(model: editModel) - .navigationTitle(self.model.standup.title) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - self.model.cancelEditButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - self.model.doneEditingButtonTapped() - } - } - } - } - } - .onChange(of: self.model.isDismissed) { _ in self.dismiss() } - } -} - -extension AlertState where Action == StandupDetailModel.AlertAction { - static let deleteStandup = Self { - TextState("Delete?") - } actions: { - ButtonState(role: .destructive, action: .confirmDeletion) { - TextState("Yes") - } - ButtonState(role: .cancel) { - TextState("Nevermind") - } - } message: { - TextState("Are you sure you want to delete this meeting?") - } - - static let speechRecognitionDenied = Self { - TextState("Speech recognition denied") - } actions: { - ButtonState(action: .continueWithoutRecording) { - TextState("Continue without recording") - } - ButtonState(action: .openSettings) { - TextState("Open settings") - } - ButtonState(role: .cancel) { - TextState("Cancel") - } - } message: { - TextState(""" - You previously denied speech recognition and so your meeting meeting will not be \ - recorded. You can enable speech recognition in settings, or you can continue without \ - recording. - """) - } - - static let speechRecognitionRestricted = Self { - TextState("Speech recognition restricted") - } actions: { - ButtonState(action: .continueWithoutRecording) { - TextState("Continue without recording") - } - ButtonState(role: .cancel) { - TextState("Cancel") - } - } message: { - TextState(""" - Your device does not support speech recognition and so your meeting will not be recorded. - """) - } -} - -struct MeetingView: View { - let meeting: Meeting - let standup: Standup - - var body: some View { - ScrollView { - VStack(alignment: .leading) { - Divider() - .padding(.bottom) - Text("Attendees") - .font(.headline) - ForEach(self.standup.attendees) { attendee in - Text(attendee.name) - } - Text("Transcript") - .font(.headline) - .padding(.top) - Text(self.meeting.transcript) - } - } - .navigationTitle(Text(self.meeting.date, style: .date)) - .padding() - } -} - -struct StandupDetail_Previews: PreviewProvider { - static var previews: some View { - Preview( - message: """ - This preview demonstrates the "happy path" of the application where everything works \ - perfectly. You can start a meeting, wait a few moments, end the meeting, and you will \ - see that a new transcription was added to the past meetings. The transcript will consist \ - of some "lorem ipsum" text because a mock speech recongizer is used for Xcode previews. - """ - ) { - NavigationStack { - StandupDetailView(model: StandupDetailModel(standup: .mock)) - } - } - .previewDisplayName("Happy path") - - Preview( - message: """ - This preview demonstrates an "unhappy path" of the application where the speech \ - recognizer mysteriously fails after 2 seconds of recording. This gives us an opportunity \ - to see how the application deals with this rare occurence. To see the behavior, run the \ - preview, tap the "Start Meeting" button and wait 2 seconds. - """ - ) { - NavigationStack { - StandupDetailView( - model: withDependencies { - $0.speechClient = .fail(after: .seconds(2)) - } operation: { - StandupDetailModel(standup: .mock) - } - ) - } - } - .previewDisplayName("Speech recognition failed") - - Preview( - message: """ - This preview demonstrates how the feature behaves when access to speech recognition has \ - been previously denied by the user. Tap the "Start Meeting" button to see how we handle \ - that situation. - """ - ) { - NavigationStack { - StandupDetailView( - model: withDependencies { - $0.speechClient.authorizationStatus = { .denied } - } operation: { - StandupDetailModel(standup: .mock) - } - ) - } - } - .previewDisplayName("Speech recognition denied") - - Preview( - message: """ - This preview demonstrates how the feature behaves when the device restricts access to \ - speech recognition APIs. Tap the "Start Meeting" button to see how we handle that \ - situation. - """ - ) { - NavigationStack { - StandupDetailView( - model: withDependencies { - $0.speechClient.authorizationStatus = { .restricted } - } operation: { - StandupDetailModel(standup: .mock) - } - ) - } - } - .previewDisplayName("Speech recognition restricted") - } -} diff --git a/Examples/Standups/Standups/StandupForm.swift b/Examples/Standups/Standups/StandupForm.swift deleted file mode 100644 index 5aa9698e9e..0000000000 --- a/Examples/Standups/Standups/StandupForm.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Dependencies -import SwiftUI -import SwiftUINavigation - -class StandupFormModel: ObservableObject { - @Published var focus: Field? - @Published var standup: Standup - - @Dependency(\.uuid) var uuid - - enum Field: Hashable { - case attendee(Attendee.ID) - case title - } - - init( - focus: Field? = .title, - standup: Standup - ) { - self.focus = focus - self.standup = standup - if self.standup.attendees.isEmpty { - self.standup.attendees.append(Attendee(id: Attendee.ID(self.uuid()))) - } - } - - func deleteAttendees(atOffsets indices: IndexSet) { - self.standup.attendees.remove(atOffsets: indices) - if self.standup.attendees.isEmpty { - self.standup.attendees.append(Attendee(id: Attendee.ID(self.uuid()))) - } - guard let firstIndex = indices.first - else { return } - let index = min(firstIndex, self.standup.attendees.count - 1) - self.focus = .attendee(self.standup.attendees[index].id) - } - - func addAttendeeButtonTapped() { - let attendee = Attendee(id: Attendee.ID(self.uuid())) - self.standup.attendees.append(attendee) - self.focus = .attendee(attendee.id) - } -} - -struct StandupFormView: View { - @FocusState var focus: StandupFormModel.Field? - @ObservedObject var model: StandupFormModel - - var body: some View { - Form { - Section { - TextField("Title", text: self.$model.standup.title) - .focused(self.$focus, equals: .title) - HStack { - Slider(value: self.$model.standup.duration.seconds, in: 5...30, step: 1) { - Text("Length") - } - Spacer() - Text(self.model.standup.duration.formatted(.units())) - } - ThemePicker(selection: self.$model.standup.theme) - } header: { - Text("Standup Info") - } - Section { - ForEach(self.$model.standup.attendees) { $attendee in - TextField("Name", text: $attendee.name) - .focused(self.$focus, equals: .attendee(attendee.id)) - } - .onDelete { indices in - self.model.deleteAttendees(atOffsets: indices) - } - - Button("New attendee") { - self.model.addAttendeeButtonTapped() - } - } header: { - Text("Attendees") - } - } - .bind(self.$model.focus, to: self.$focus) - } -} - -struct ThemePicker: View { - @Binding var selection: Theme - - var body: some View { - Picker("Theme", selection: $selection) { - ForEach(Theme.allCases) { theme in - ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(theme.mainColor) - Label(theme.name, systemImage: "paintpalette") - .padding(4) - } - .foregroundColor(theme.accentColor) - .fixedSize(horizontal: false, vertical: true) - .tag(theme) - } - } - } -} - -extension Duration { - fileprivate var seconds: Double { - get { Double(self.components.seconds / 60) } - set { self = .seconds(newValue * 60) } - } -} - -struct StandupForm_Previews: PreviewProvider { - static var previews: some View { - NavigationStack { - StandupFormView(model: StandupFormModel(standup: .mock)) - } - .previewDisplayName("Edit") - - Preview( - message: """ - This preview shows how we can start the screen if a very specific state, where the 4th \ - attendee is already focused. - """ - ) { - NavigationStack { - StandupFormView( - model: StandupFormModel( - focus: .attendee(Standup.mock.attendees[3].id), - standup: .mock - ) - ) - } - } - .previewDisplayName("4th attendee focused") - } -} diff --git a/Examples/Standups/Standups/StandupsApp.swift b/Examples/Standups/Standups/StandupsApp.swift deleted file mode 100644 index 518fe6d814..0000000000 --- a/Examples/Standups/Standups/StandupsApp.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Dependencies -import SwiftUI - -@main -struct StandupsApp: App { - var body: some Scene { - WindowGroup { - // NB: This conditional is here only to facilitate UI testing so that we can mock out certain - // dependencies for the duration of the test (e.g. the data manager). We do not really - // recommend performing UI tests in general, but we do want to demonstrate how it can be - // done. - if ProcessInfo.processInfo.environment["UITesting"] == "true" { - UITestingView() - } else { - StandupsList(model: StandupsListModel()) - } - } - } -} - -struct UITestingView: View { - var body: some View { - withDependencies { - $0.dataManager = .mock() - } operation: { - StandupsList(model: StandupsListModel()) - } - } -} diff --git a/Examples/Standups/Standups/StandupsList.swift b/Examples/Standups/Standups/StandupsList.swift deleted file mode 100644 index 0fbb946993..0000000000 --- a/Examples/Standups/Standups/StandupsList.swift +++ /dev/null @@ -1,350 +0,0 @@ -import Combine -import Dependencies -import IdentifiedCollections -import SwiftUI -import SwiftUINavigation - -@MainActor -final class StandupsListModel: ObservableObject { - @Published var destination: Destination? { - didSet { self.bind() } - } - @Published var standups: IdentifiedArrayOf - - private var destinationCancellable: AnyCancellable? - private var cancellables: Set = [] - - @Dependency(\.dataManager) var dataManager - @Dependency(\.mainQueue) var mainQueue - @Dependency(\.uuid) var uuid - - enum Destination { - case add(StandupFormModel) - case alert(AlertState) - case detail(StandupDetailModel) - } - enum AlertAction { - case confirmLoadMockData - } - - init( - destination: Destination? = nil - ) { - defer { self.bind() } - self.destination = destination - self.standups = [] - - do { - self.standups = try JSONDecoder().decode( - IdentifiedArray.self, - from: self.dataManager.load(.standups) - ) - } catch is DecodingError { - self.destination = .alert(.dataFailedToLoad) - } catch { - } - - self.$standups - .dropFirst() - .debounce(for: .seconds(1), scheduler: self.mainQueue) - .sink { [weak self] standups in - try? self?.dataManager.save(JSONEncoder().encode(standups), .standups) - } - .store(in: &self.cancellables) - } - - func addStandupButtonTapped() { - self.destination = .add( - withDependencies(from: self) { - StandupFormModel(standup: Standup(id: Standup.ID(self.uuid()))) - } - ) - } - - func dismissAddStandupButtonTapped() { - self.destination = nil - } - - func confirmAddStandupButtonTapped() { - defer { self.destination = nil } - - guard case let .add(standupFormModel) = self.destination - else { return } - var standup = standupFormModel.standup - - standup.attendees.removeAll { attendee in - attendee.name.allSatisfy(\.isWhitespace) - } - if standup.attendees.isEmpty { - standup.attendees.append(Attendee(id: Attendee.ID(self.uuid()))) - } - self.standups.append(standup) - } - - func standupTapped(standup: Standup) { - self.destination = .detail( - withDependencies(from: self) { - StandupDetailModel(standup: standup) - } - ) - } - - private func bind() { - switch self.destination { - case let .detail(standupDetailModel): - standupDetailModel.onConfirmDeletion = { [weak self, id = standupDetailModel.standup.id] in - withAnimation { - self?.standups.remove(id: id) - self?.destination = nil - } - } - - self.destinationCancellable = standupDetailModel.$standup - .sink { [weak self] standup in - self?.standups[id: standup.id] = standup - } - - case .add, .alert, .none: - break - } - } - - func alertButtonTapped(_ action: AlertAction?) { - switch action { - case .confirmLoadMockData?: - withAnimation { - self.standups = [ - .mock, - .designMock, - .engineeringMock, - ] - } - case nil: - break - } - } -} - -extension AlertState where Action == StandupsListModel.AlertAction { - static let dataFailedToLoad = Self { - TextState("Data failed to load") - } actions: { - ButtonState(action: .confirmLoadMockData) { - TextState("Yes") - } - ButtonState(role: .cancel) { - TextState("No") - } - } message: { - TextState( - """ - Unfortunately your past data failed to load. Would you like to load some mock data to play \ - around with? - """) - } -} - -struct StandupsList: View { - @ObservedObject var model: StandupsListModel - - var body: some View { - NavigationStack { - List { - ForEach(self.model.standups) { standup in - Button { - self.model.standupTapped(standup: standup) - } label: { - CardView(standup: standup) - } - .listRowBackground(standup.theme.mainColor) - } - } - .toolbar { - Button { - self.model.addStandupButtonTapped() - } label: { - Image(systemName: "plus") - } - } - .navigationTitle("Daily Standups") - .sheet( - unwrapping: self.$model.destination, - case: /StandupsListModel.Destination.add - ) { $model in - NavigationStack { - StandupFormView(model: model) - .navigationTitle("New standup") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Dismiss") { - self.model.dismissAddStandupButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Add") { - self.model.confirmAddStandupButtonTapped() - } - } - } - } - } - .navigationDestination( - unwrapping: self.$model.destination, - case: /StandupsListModel.Destination.detail - ) { $detailModel in - StandupDetailView(model: detailModel) - } - .alert( - unwrapping: self.$model.destination, - case: /StandupsListModel.Destination.alert - ) { - self.model.alertButtonTapped($0) - } - } - } -} - -struct CardView: View { - let standup: Standup - - var body: some View { - VStack(alignment: .leading) { - Text(self.standup.title) - .font(.headline) - Spacer() - HStack { - Label("\(self.standup.attendees.count)", systemImage: "person.3") - Spacer() - Label(self.standup.duration.formatted(.units()), systemImage: "clock") - .labelStyle(.trailingIcon) - } - .font(.caption) - } - .padding() - .foregroundColor(self.standup.theme.accentColor) - } -} - -struct TrailingIconLabelStyle: LabelStyle { - func makeBody(configuration: Configuration) -> some View { - HStack { - configuration.title - configuration.icon - } - } -} - -extension LabelStyle where Self == TrailingIconLabelStyle { - static var trailingIcon: Self { Self() } -} - -extension URL { - fileprivate static let standups = Self.documentsDirectory.appending(component: "standups.json") -} - -struct StandupsList_Previews: PreviewProvider { - static var previews: some View { - Preview( - message: """ - This preview demonstrates how to start the app in a state with a few standups \ - pre-populated. Since the initial standups are loaded from disk we cannot simply pass some \ - data to the StandupsList model. But, we can override the DataManager dependency so that \ - when its load endpoint is called it will load whatever data we want. - """ - ) { - StandupsList( - model: withDependencies { - $0.dataManager = .mock( - initialData: try! JSONEncoder().encode([ - Standup.mock, - .engineeringMock, - .designMock, - ]) - ) - } operation: { - StandupsListModel() - } - ) - } - .previewDisplayName("Mocking initial standups") - - Preview( - message: """ - This preview demonstrates how to test the flow of loading bad data from disk, in which \ - case an alert should be shown. This can be done by overridding the DataManager dependency \ - so that its initial data does not properly decode into a collection of standups. - """ - ) { - StandupsList( - model: withDependencies { - $0.dataManager = .mock( - initialData: Data("!@#$% bad data ^&*()".utf8) - ) - } operation: { - StandupsListModel() - } - ) - } - .previewDisplayName("Load data failure") - - Preview( - message: """ - The preview demonstrates how you can start the application navigated to a very specific \ - screen just by constructing a piece of state. In particular we will start the app drilled \ - down to the detail screen of a standup, and then further drilled down to the record screen \ - for a new meeting. - """ - ) { - StandupsList( - model: withDependencies { - $0.dataManager = .mock( - initialData: try! JSONEncoder().encode([ - Standup.mock, - .engineeringMock, - .designMock, - ]) - ) - } operation: { - StandupsListModel( - destination: .detail( - StandupDetailModel( - destination: .record( - RecordMeetingModel(standup: .mock) - ), - standup: .mock - ) - ) - ) - } - ) - } - .previewDisplayName("Deep link record flow") - - Preview( - message: """ - The preview demonstrates how you can start the application navigated to a very specific \ - screen just by constructing a piece of state. In particular we will start the app with the \ - "Add standup" screen opened and with the last attendee text field focused. - """ - ) { - StandupsList( - model: withDependencies { - $0.dataManager = .mock() - } operation: { - var standup = Standup.mock - let lastAttendee = Attendee(id: Attendee.ID()) - let _ = standup.attendees.append(lastAttendee) - return StandupsListModel( - destination: .add( - StandupFormModel( - focus: .attendee(lastAttendee.id), - standup: standup - ) - ) - ) - } - ) - } - .previewDisplayName("Deep link add flow") - } -} diff --git a/Examples/Standups/StandupsTests/EditStandupTests.swift b/Examples/Standups/StandupsTests/EditStandupTests.swift deleted file mode 100644 index 2063f05156..0000000000 --- a/Examples/Standups/StandupsTests/EditStandupTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -import CustomDump -import Dependencies -import XCTest - -@testable import Standups - -@MainActor -final class StandupFormTests: XCTestCase { - func testAddAttendee() { - let model = withDependencies { - $0.uuid = .incrementing - } operation: { - StandupFormModel( - standup: Standup( - id: Standup.ID(), - attendees: [], - title: "Engineering" - ) - ) - } - - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000000")!) - ] - ) - - model.addAttendeeButtonTapped() - - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000000")!), - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!), - ] - ) - } - - func testFocus_AddAttendee() { - let model = withDependencies { - $0.uuid = .incrementing - } operation: { - StandupFormModel( - standup: Standup( - id: Standup.ID(), - attendees: [], - title: "Engineering" - ) - ) - } - - XCTAssertEqual(model.focus, .title) - - model.addAttendeeButtonTapped() - - XCTAssertEqual( - model.focus, - .attendee(Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ) - } - - func testFocus_RemoveAttendee() { - let model = withDependencies { - $0.uuid = .incrementing - } operation: { - @Dependency(\.uuid) var uuid - - return StandupFormModel( - standup: Standup( - id: Standup.ID(), - attendees: [ - Attendee(id: Attendee.ID(uuid())), - Attendee(id: Attendee.ID(uuid())), - Attendee(id: Attendee.ID(uuid())), - Attendee(id: Attendee.ID(uuid())), - ], - title: "Engineering" - ) - ) - } - - model.deleteAttendees(atOffsets: [0]) - - XCTAssertNoDifference( - model.focus, - .attendee(Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ) - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!), - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000002")!), - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000003")!), - ] - ) - - model.deleteAttendees(atOffsets: [1]) - - XCTAssertNoDifference( - model.focus, - .attendee(Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000003")!) - ) - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!), - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000003")!), - ] - ) - - model.deleteAttendees(atOffsets: [1]) - - XCTAssertNoDifference( - model.focus, - .attendee(Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ) - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ] - ) - - model.deleteAttendees(atOffsets: [0]) - - XCTAssertNoDifference( - model.focus, - .attendee(Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000004")!) - ) - XCTAssertNoDifference( - model.standup.attendees, - [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000004")!) - ] - ) - } -} diff --git a/Examples/Standups/StandupsTests/RecordMeetingTests.swift b/Examples/Standups/StandupsTests/RecordMeetingTests.swift deleted file mode 100644 index 519ba727ab..0000000000 --- a/Examples/Standups/StandupsTests/RecordMeetingTests.swift +++ /dev/null @@ -1,345 +0,0 @@ -import CasePaths -import CustomDump -import Dependencies -import XCTest - -@testable import Standups - -@MainActor -final class RecordMeetingTests: XCTestCase { - func testTimer() async throws { - let clock = TestClock() - let soundEffectPlayCount = LockIsolated(0) - - let model = withDependencies { - $0.continuousClock = clock - $0.soundEffectClient = .noop - $0.soundEffectClient.play = { soundEffectPlayCount.withValue { $0 += 1 } } - $0.speechClient.authorizationStatus = { .denied } - } operation: { - RecordMeetingModel( - standup: Standup( - id: Standup.ID(), - attendees: [ - Attendee(id: Attendee.ID()), - Attendee(id: Attendee.ID()), - Attendee(id: Attendee.ID()), - ], - duration: .seconds(3) - ) - ) - } - - let onMeetingFinishedExpectation = self.expectation(description: "onMeetingFinished") - model.onMeetingFinished = { - XCTAssertEqual($0, "") - onMeetingFinishedExpectation.fulfill() - } - - let task = Task { - await model.task() - } - - // NB: This should not be necessary, but it doesn't seem like there is a better way to - // guarantee that the timer has started up. See this forum discussion for more information - // on the difficulties of testing async code in Swift: - // https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304 - try await Task.sleep(for: .milliseconds(300)) - - XCTAssertEqual(model.speakerIndex, 0) - XCTAssertEqual(model.durationRemaining, .seconds(3)) - - await clock.advance(by: .seconds(1)) - XCTAssertEqual(model.speakerIndex, 1) - XCTAssertEqual(model.durationRemaining, .seconds(2)) - XCTAssertEqual(soundEffectPlayCount.value, 1) - - await clock.advance(by: .seconds(1)) - XCTAssertEqual(model.speakerIndex, 2) - XCTAssertEqual(model.durationRemaining, .seconds(1)) - XCTAssertEqual(soundEffectPlayCount.value, 2) - - await clock.advance(by: .seconds(1)) - XCTAssertEqual(model.speakerIndex, 2) - XCTAssertEqual(model.durationRemaining, .seconds(0)) - XCTAssertEqual(soundEffectPlayCount.value, 2) - - await task.value - - self.wait(for: [onMeetingFinishedExpectation], timeout: 0) - XCTAssertEqual(model.isDismissed, true) - XCTAssertEqual(soundEffectPlayCount.value, 2) - } - - func testRecordTranscript() async throws { - let model = withDependencies { - $0.continuousClock = ImmediateClock() - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .authorized } - $0.speechClient.startTask = { _ in - AsyncThrowingStream { continuation in - continuation.yield( - SpeechRecognitionResult( - bestTranscription: Transcription(formattedString: "I completed the project"), - isFinal: true - ) - ) - continuation.finish() - } - } - } operation: { - RecordMeetingModel( - standup: Standup( - id: Standup.ID(), - attendees: [Attendee(id: Attendee.ID())], - duration: .seconds(3) - ) - ) - } - - let onMeetingFinishedExpectation = self.expectation(description: "onMeetingFinished") - model.onMeetingFinished = { - XCTAssertEqual($0, "I completed the project") - onMeetingFinishedExpectation.fulfill() - } - - await model.task() - - self.wait(for: [onMeetingFinishedExpectation], timeout: 0) - XCTAssertEqual(model.isDismissed, true) - } - - func testEndMeetingSave() async throws { - let clock = TestClock() - - let model = withDependencies { - $0.continuousClock = clock - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .denied } - } operation: { - RecordMeetingModel(standup: .mock) - } - - let onMeetingFinishedExpectation = self.expectation(description: "onMeetingFinished") - model.onMeetingFinished = { - XCTAssertEqual($0, "") - onMeetingFinishedExpectation.fulfill() - } - - let task = Task { - await model.task() - } - - model.endMeetingButtonTapped() - - let alert = try XCTUnwrap(model.destination, case: /RecordMeetingModel.Destination.alert) - - XCTAssertNoDifference(alert, .endMeeting(isDiscardable: true)) - - await clock.advance(by: .seconds(5)) - - XCTAssertEqual(model.speakerIndex, 0) - XCTAssertEqual(model.durationRemaining, .seconds(60)) - - await model.alertButtonTapped(.confirmSave) - - self.wait(for: [onMeetingFinishedExpectation], timeout: 0) - XCTAssertEqual(model.isDismissed, true) - - task.cancel() - await task.value - } - - func testEndMeetingDiscard() async throws { - let clock = TestClock() - - let model = withDependencies { - $0.continuousClock = clock - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .denied } - } operation: { - RecordMeetingModel(standup: .mock) - } - - model.onMeetingFinished = { _ in XCTFail() } - - let task = Task { - await model.task() - } - - model.endMeetingButtonTapped() - - let alert = try XCTUnwrap(model.destination, case: /RecordMeetingModel.Destination.alert) - - XCTAssertNoDifference(alert, .endMeeting(isDiscardable: true)) - - await model.alertButtonTapped(.confirmDiscard) - - XCTAssertEqual(model.isDismissed, true) - - task.cancel() - await task.value - } - - func testNextSpeaker() async throws { - let clock = TestClock() - let soundEffectPlayCount = LockIsolated(0) - - let model = withDependencies { - $0.continuousClock = clock - $0.soundEffectClient = .noop - $0.soundEffectClient.play = { soundEffectPlayCount.withValue { $0 += 1 } } - $0.speechClient.authorizationStatus = { .denied } - - } operation: { - RecordMeetingModel( - standup: Standup( - id: Standup.ID(), - attendees: [ - Attendee(id: Attendee.ID()), - Attendee(id: Attendee.ID()), - Attendee(id: Attendee.ID()), - ], - duration: .seconds(3) - ) - ) - } - - let onMeetingFinishedExpectation = self.expectation(description: "onMeetingFinished") - model.onMeetingFinished = { - XCTAssertEqual($0, "") - onMeetingFinishedExpectation.fulfill() - } - - let task = Task { - await model.task() - } - - model.nextButtonTapped() - - XCTAssertEqual(model.speakerIndex, 1) - XCTAssertEqual(model.durationRemaining, .seconds(2)) - XCTAssertEqual(soundEffectPlayCount.value, 1) - - model.nextButtonTapped() - - XCTAssertEqual(model.speakerIndex, 2) - XCTAssertEqual(model.durationRemaining, .seconds(1)) - XCTAssertEqual(soundEffectPlayCount.value, 2) - - model.nextButtonTapped() - - let alert = try XCTUnwrap(model.destination, case: /RecordMeetingModel.Destination.alert) - - XCTAssertNoDifference(alert, .endMeeting(isDiscardable: false)) - - await clock.advance(by: .seconds(5)) - - XCTAssertEqual(model.speakerIndex, 2) - XCTAssertEqual(model.durationRemaining, .seconds(1)) - XCTAssertEqual(soundEffectPlayCount.value, 2) - - await model.alertButtonTapped(.confirmSave) - - self.wait(for: [onMeetingFinishedExpectation], timeout: 0) - XCTAssertEqual(model.isDismissed, true) - XCTAssertEqual(soundEffectPlayCount.value, 2) - - task.cancel() - await task.value - } - - func testSpeechRecognitionFailure_Continue() async throws { - let model = withDependencies { - $0.continuousClock = ImmediateClock() - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .authorized } - $0.speechClient.startTask = { _ in - AsyncThrowingStream { - $0.yield( - SpeechRecognitionResult( - bestTranscription: Transcription(formattedString: "I completed the project"), - isFinal: true - ) - ) - struct SpeechRecognitionFailure: Error {} - $0.finish(throwing: SpeechRecognitionFailure()) - } - } - } operation: { - RecordMeetingModel( - standup: Standup( - id: Standup.ID(), - attendees: [Attendee(id: Attendee.ID())], - duration: .seconds(3) - ) - ) - } - - let onMeetingFinishedExpectation = self.expectation(description: "onMeetingFinished") - model.onMeetingFinished = { transcript in - XCTAssertEqual(transcript, "I completed the project ❌") - onMeetingFinishedExpectation.fulfill() - } - - let task = Task { - await model.task() - } - - // NB: This should not be necessary, but it doesn't seem like there is a better way to - // guarantee that the timer has started up. See this forum discussion for more information - // on the difficulties of testing async code in Swift: - // https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304 - try await Task.sleep(for: .milliseconds(100)) - - let alert = try XCTUnwrap(model.destination, case: /RecordMeetingModel.Destination.alert) - XCTAssertEqual(alert, .speechRecognizerFailed) - - model.destination = nil // NB: Simulate SwiftUI closing alert. - XCTAssertEqual(model.isDismissed, false) - - await task.value - - XCTAssertEqual(model.secondsElapsed, 3) - self.wait(for: [onMeetingFinishedExpectation], timeout: 0) - } - - func testSpeechRecognitionFailure_Discard() async throws { - let model = withDependencies { - $0.continuousClock = ImmediateClock() - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .authorized } - $0.speechClient.startTask = { _ in - struct SpeechRecognitionFailure: Error {} - return AsyncThrowingStream.finished(throwing: SpeechRecognitionFailure()) - } - } operation: { - RecordMeetingModel( - standup: Standup( - id: Standup.ID(), - attendees: [Attendee(id: Attendee.ID())], - duration: .seconds(3) - ) - ) - } - - let task = Task { - await model.task() - } - - // NB: This should not be necessary, but it doesn't seem like there is a better way to - // guarantee that the timer has started up. See this forum discussion for more information - // on the difficulties of testing async code in Swift: - // https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304 - try await Task.sleep(for: .milliseconds(100)) - - let alert = try XCTUnwrap(model.destination, case: /RecordMeetingModel.Destination.alert) - XCTAssertEqual(alert, .speechRecognizerFailed) - - await model.alertButtonTapped(.confirmDiscard) - model.destination = nil // NB: Simulate SwiftUI closing alert. - XCTAssertEqual(model.isDismissed, true) - - await task.value - } -} diff --git a/Examples/Standups/StandupsTests/StandupDetailTests.swift b/Examples/Standups/StandupsTests/StandupDetailTests.swift deleted file mode 100644 index 3ce8e61752..0000000000 --- a/Examples/Standups/StandupsTests/StandupDetailTests.swift +++ /dev/null @@ -1,166 +0,0 @@ -import CasePaths -import CustomDump -import Dependencies -import XCTest - -@testable import Standups - -@MainActor -final class StandupDetailTests: XCTestCase { - func testSpeechRestricted() throws { - let model = withDependencies { - $0.speechClient.authorizationStatus = { .restricted } - } operation: { - StandupDetailModel(standup: .mock) - } - - model.startMeetingButtonTapped() - - let alert = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.alert) - - XCTAssertNoDifference(alert, .speechRecognitionRestricted) - } - - func testSpeechDenied() async throws { - let model = withDependencies { - $0.speechClient.authorizationStatus = { .denied } - } operation: { - StandupDetailModel(standup: .mock) - } - - model.startMeetingButtonTapped() - - let alert = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.alert) - - XCTAssertNoDifference(alert, .speechRecognitionDenied) - } - - func testOpenSettings() async { - let settingsOpened = LockIsolated(false) - let model = withDependencies { - $0.openSettings = { settingsOpened.setValue(true) } - } operation: { - StandupDetailModel( - destination: .alert(.speechRecognitionDenied), - standup: .mock - ) - } - - await model.alertButtonTapped(.openSettings) - - XCTAssertEqual(settingsOpened.value, true) - } - - func testContinueWithoutRecording() async throws { - let model = StandupDetailModel( - destination: .alert(.speechRecognitionDenied), - standup: .mock - ) - - await model.alertButtonTapped(.continueWithoutRecording) - - let recordModel = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.record) - - XCTAssertEqual(recordModel.standup, model.standup) - } - - func testSpeechAuthorized() async throws { - let model = withDependencies { - $0.speechClient.authorizationStatus = { .authorized } - } operation: { - StandupDetailModel(standup: .mock) - } - - model.startMeetingButtonTapped() - - let recordModel = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.record) - - XCTAssertEqual(recordModel.standup, model.standup) - } - - func testRecordWithTranscript() async throws { - let model = withDependencies { - $0.continuousClock = ImmediateClock() - $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) - $0.soundEffectClient = .noop - $0.speechClient.authorizationStatus = { .authorized } - $0.speechClient.startTask = { _ in - AsyncThrowingStream { continuation in - continuation.yield( - SpeechRecognitionResult( - bestTranscription: Transcription(formattedString: "I completed the project"), - isFinal: true - ) - ) - continuation.finish() - } - } - $0.uuid = .incrementing - } operation: { - StandupDetailModel( - destination: .record(RecordMeetingModel(standup: .mock)), - standup: Standup( - id: Standup.ID(), - attendees: [ - .init(id: Attendee.ID()), - .init(id: Attendee.ID()), - ], - duration: .seconds(10), - title: "Engineering" - ) - ) - } - - let recordModel = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.record) - - await recordModel.task() - - XCTAssertNil(model.destination) - XCTAssertNoDifference( - model.standup.meetings, - [ - Meeting( - id: Meeting.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - date: Date(timeIntervalSince1970: 1_234_567_890), - transcript: "I completed the project" - ) - ] - ) - } - - func testEdit() throws { - let model = withDependencies { - $0.uuid = .incrementing - } operation: { - @Dependency(\.uuid) var uuid - - return StandupDetailModel( - standup: Standup( - id: Standup.ID(uuid()), - title: "Engineering" - ) - ) - } - - model.editButtonTapped() - - let editModel = try XCTUnwrap(model.destination, case: /StandupDetailModel.Destination.edit) - - editModel.standup.title = "Engineering" - editModel.standup.theme = .lavender - model.doneEditingButtonTapped() - - XCTAssertNil(model.destination) - XCTAssertEqual( - model.standup, - Standup( - id: Standup.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - attendees: [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ], - theme: .lavender, - title: "Engineering" - ) - ) - } -} diff --git a/Examples/Standups/StandupsTests/StandupsListTests.swift b/Examples/Standups/StandupsTests/StandupsListTests.swift deleted file mode 100644 index 16683f9cc5..0000000000 --- a/Examples/Standups/StandupsTests/StandupsListTests.swift +++ /dev/null @@ -1,208 +0,0 @@ -import CasePaths -import CustomDump -import Dependencies -import IdentifiedCollections -import XCTest - -@testable import Standups - -@MainActor -final class StandupsListTests: XCTestCase { - let mainQueue = DispatchQueue.test - - func testAdd() async throws { - let savedData = LockIsolated(Data?.none) - - let model = withDependencies { - $0.dataManager = .mock() - $0.dataManager.save = { data, _ in savedData.setValue(data) } - $0.mainQueue = mainQueue.eraseToAnyScheduler() - $0.uuid = .incrementing - } operation: { - StandupsListModel() - } - - model.addStandupButtonTapped() - - let addModel = try XCTUnwrap(model.destination, case: /StandupsListModel.Destination.add) - - addModel.standup.title = "Engineering" - addModel.standup.attendees[0].name = "Blob" - addModel.addAttendeeButtonTapped() - addModel.standup.attendees[1].name = "Blob Jr." - model.confirmAddStandupButtonTapped() - - XCTAssertNil(model.destination) - - XCTAssertNoDifference( - model.standups, - [ - Standup( - id: Standup.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - attendees: [ - Attendee( - id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!, - name: "Blob" - ), - Attendee( - id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000002")!, - name: "Blob Jr." - ), - ], - title: "Engineering" - ) - ] - ) - - await self.mainQueue.run() - XCTAssertEqual( - try JSONDecoder().decode(IdentifiedArrayOf.self, from: XCTUnwrap(savedData.value)), - model.standups - ) - } - - func testAdd_ValidatedAttendees() async throws { - let model = withDependencies { - $0.dataManager = .mock() - $0.mainQueue = mainQueue.eraseToAnyScheduler() - $0.uuid = .incrementing - } operation: { - StandupsListModel( - destination: .add( - StandupFormModel( - standup: Standup( - id: Standup.ID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!, - attendees: [ - Attendee(id: Attendee.ID(), name: ""), - Attendee(id: Attendee.ID(), name: " "), - ], - title: "Design" - ) - ) - ) - ) - } - - model.confirmAddStandupButtonTapped() - - XCTAssertNil(model.destination) - XCTAssertNoDifference( - model.standups, - [ - Standup( - id: Standup.ID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!, - attendees: [ - Attendee( - id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - name: "" - ) - ], - title: "Design" - ) - ] - ) - } - - func testDelete() async throws { - let model = try withDependencies { dependencies in - dependencies.dataManager = .mock( - initialData: try JSONEncoder().encode([Standup.mock]) - ) - dependencies.mainQueue = mainQueue.eraseToAnyScheduler() - } operation: { - StandupsListModel() - } - - model.standupTapped(standup: model.standups[0]) - - let detailModel = try XCTUnwrap(model.destination, case: /StandupsListModel.Destination.detail) - - detailModel.deleteButtonTapped() - - let alert = try XCTUnwrap(detailModel.destination, case: /StandupDetailModel.Destination.alert) - - XCTAssertNoDifference(alert, .deleteStandup) - - await detailModel.alertButtonTapped(.confirmDeletion) - - XCTAssertNil(model.destination) - XCTAssertEqual(model.standups, []) - XCTAssertEqual(detailModel.isDismissed, true) - } - - func testDetailEdit() async throws { - let model = try withDependencies { dependencies in - dependencies.dataManager = .mock( - initialData: try JSONEncoder().encode([ - Standup( - id: Standup.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - attendees: [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ] - ) - ]) - ) - dependencies.mainQueue = mainQueue.eraseToAnyScheduler() - } operation: { - StandupsListModel() - } - - model.standupTapped(standup: model.standups[0]) - - let detailModel = try XCTUnwrap(model.destination, case: /StandupsListModel.Destination.detail) - - detailModel.editButtonTapped() - - let editModel = try XCTUnwrap( - detailModel.destination, case: /StandupDetailModel.Destination.edit) - - editModel.standup.title = "Design" - detailModel.doneEditingButtonTapped() - - XCTAssertNil(detailModel.destination) - XCTAssertEqual( - model.standups, - [ - Standup( - id: Standup.ID(uuidString: "00000000-0000-0000-0000-000000000000")!, - attendees: [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) - ], - title: "Design" - ) - ] - ) - } - - func testLoadingDataDecodingFailed() async throws { - let model = withDependencies { - $0.mainQueue = .immediate - $0.dataManager = .mock( - initialData: Data("!@#$ BAD DATA %^&*()".utf8) - ) - } operation: { - StandupsListModel() - } - - let alert = try XCTUnwrap(model.destination, case: /StandupsListModel.Destination.alert) - - XCTAssertNoDifference(alert, .dataFailedToLoad) - - model.alertButtonTapped(.confirmLoadMockData) - - XCTAssertNoDifference(model.standups, [.mock, .designMock, .engineeringMock]) - } - - func testLoadingDataFileNotFound() async throws { - let model = withDependencies { - $0.dataManager.load = { _ in - struct FileNotFound: Error {} - throw FileNotFound() - } - } operation: { - StandupsListModel() - } - - XCTAssertNil(model.destination) - } -} diff --git a/Examples/Standups/StandupsUITests/StandupsListUITests.swift b/Examples/Standups/StandupsUITests/StandupsListUITests.swift deleted file mode 100644 index ad25792f17..0000000000 --- a/Examples/Standups/StandupsUITests/StandupsListUITests.swift +++ /dev/null @@ -1,49 +0,0 @@ -import XCTest - -// This test case demonstrates how one can write UI tests using the swift-dependencies library. We -// do not really recommend writing UI tests in general as they are slow and flakey, but if you must -// then this shows how. -// -// The key to doing this is to set a launch environment variable on your XCUIApplication instance, -// and then check for that value in the entry point of the application. If the environment value -// exists, you can use 'withDependencies' to override dependencies to be used in the UI test. -final class StandupsListUITests: XCTestCase { - var app: XCUIApplication! - - override func setUpWithError() throws { - self.continueAfterFailure = false - self.app = XCUIApplication() - app.launchEnvironment = [ - "UITesting": "true" - ] - } - - // This test demonstrates the simple flow of tapping the "Add" button, filling in some fields in - // the form, and then adding the standup to the list. It's a very simple test, but it takes - // approximately 10 seconds to run, and it depends on a lot of internal implementation details to - // get right, such as tapping a button with the literal label "Add". - // - // This test is also written in the simpler, "unit test" style in StandupsListTests.swift, where - // it takes 0.025 seconds (400 times faster) and it even tests more. It further confirms that when - // the standup is added to the list its data will be persisted to disk so that it will be - // available on next launch. - func testAdd() throws { - app.launch() - app.navigationBars["Daily Standups"].buttons["Add"].tap() - let collectionViews = app.collectionViews - let titleTextField = collectionViews.textFields["Title"] - let nameTextField = collectionViews.textFields["Name"] - - titleTextField.typeText("Engineering") - - nameTextField.tap() - nameTextField.typeText("Blob") - - collectionViews.buttons["New attendee"].tap() - app.typeText("Blob Jr.") - - app.navigationBars["New standup"].buttons["Add"].tap() - - XCTAssertEqual(collectionViews.staticTexts["Engineering"].exists, true) - } -} diff --git a/README.md b/README.md index d3daa833d1..597f4235d4 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ reading these articles: Learn how to present alerts and confirmation dialogs in a concise and testable manner. * **[Bindings][bindings]**: - Learn how to manage certain view state, such as `@FocusState` directly in your observable object. + Learn how to manage certain view state, such as `@FocusState` directly in your observable classes. ## Examples diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md index 075e90c7d0..506b82452d 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -15,7 +15,8 @@ your model, as well as an enum that describes every action that can happen in th ```swift -class FeatureModel: ObservableObject { +@Observable +class FeatureModel { var alert: AlertState? enum AlertAction { case deletionConfirmed @@ -110,7 +111,8 @@ In such a case: ```swift -class FeatureModel: ObservableObject { +@Observable +class FeatureModel { var destination: Destination? enum Destination { case alert(AlertState) @@ -145,7 +147,8 @@ of alerts. For example, the model for a delete confirmation could look like this: ```swift -class FeatureModel: ObservableObject { +@Observable +class FeatureModel { var dialog: ConfirmationDialogState? enum DialogAction { case deletionConfirmed diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index 99c674c0fd..ce67c4547b 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -1,12 +1,12 @@ # Bindings -Learn how to manage certain view state, such as `@FocusState` directly in your observable object. +Learn how to manage certain view state, such as `@FocusState` directly in your observable classes. ## Overview SwiftUI comes with many property wrappers that can be used in views to drive view state, such as `@FocusState`. Unfortunately, these property wrappers _must_ be used in views. It's not possible -to extract this logic to an observable object and integrate it with the rest of the model's +to extract this logic to an `@Observable` class and integrate it with the rest of the model's business logic, and be in a better position to test this state. We can work around these limitations by introducing a published field to your observable @@ -17,13 +17,14 @@ For example, suppose you have a sign in flow where if the API request to sign in to refocus the email field. The model can be implemented like so: ```swift -class SignInModel: ObservableObject { - @Published var email: String - @Published var password: String - @Published var focus: Field? +@Observable +class SignInModel { + var email: String + var password: String + var focus: Field? enum Field { case email, password } - func signInButtonTapped() async { + func signInButtonTapped() async throws { do { try await self.apiClient.signIn(self.email, self.password) } catch { @@ -33,9 +34,9 @@ class SignInModel: ObservableObject { } ``` -Notice that we store the focus as a `@Published` property in the model rather than `@FocusState`. +Notice that we store the focus as a regular `var` property in the model rather than `@FocusState`. This is because `@FocusState` only works when installed directly in a view. It cannot be used in -an observable object. +an observable class. You can implement the view as you would normally, except you must also use `@FocusState` for the focus _and_ use the `bind` helper to make sure that changes to the model's focus are replayed to diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md index 1e3996e2d4..bbc4435a7a 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -65,8 +65,9 @@ For example, suppose you have a list of items, and when one is tapped you want t sheet for editing the item: ```swift -class FeatureModel: ObservableObject { - @Published var editingItem: Item? +@Observable +class FeatureModel { + var editingItem: Item? func tapped(item: Item) { self.editingItem = item } @@ -142,11 +143,12 @@ item and duplicate an item, and you can navigate to a help screen. That can tech represented as four optionals: ```swift -class FeatureModel: ObservableObject { - @Published var addItem: Item? - @Published var duplicateItem: Item? - @Published var editingItem: Item? - @Published var help: Help? +@Observable +class FeatureModel { + var addItem: Item? + var duplicateItem: Item? + var editingItem: Item? + var help: Help? // ... } ``` @@ -166,8 +168,9 @@ each destination, and then hold onto a single optional value to represent which is currently active: ```swift -class FeatureModel: ObservableObject { - @Published var destination: Destination? +@Observable +class FeatureModel { + var destination: Destination? // ... enum Destination { @@ -222,8 +225,9 @@ Similar APIs are defined for popovers, covers, and more. For example, consider a feature model that has 3 different destinations that can be navigated to: ```swift -class FeatureModel: ObservableObject { - @Published var destination: Destination? +@Observable +class FeatureModel { + var destination: Destination? // ... enum Destination { diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index f0bfa52ff7..757fa19fe8 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -13,7 +13,8 @@ /// alerts as an enum: /// /// ```swift - /// class HomeScreenModel: ObservableObject { + /// @Observable + /// class HomeScreenModel { /// enum AlertAction { /// case delete /// case removeFromHomeScreen @@ -22,12 +23,13 @@ /// } /// ``` /// - /// Then you hold onto optional `AlertState` as a `@Published` field in your model, which can + /// Then you hold onto optional `AlertState` as a field in your model, which can /// start off as `nil`: /// /// ```swift - /// class HomeScreenModel: ObservableObject { - /// @Published var alert: AlertState? + /// @Observable + /// class HomeScreenModel { + /// var alert: AlertState? /// // ... /// } /// ``` @@ -35,7 +37,8 @@ /// And you define an endpoint for handling each alert action: /// /// ```swift - /// class HomeScreenModel: ObservableObject { + /// @Observable + /// class HomeScreenModel { /// // ... /// func alertButtonTapped(_ action: AlertAction?) { /// switch action { @@ -54,7 +57,8 @@ /// represent the alert: /// /// ```swift - /// class HomeScreenModel: ObservableObject { + /// @Observable + /// class HomeScreenModel { /// // ... /// func deleteAppButtonTapped() { /// self.alert = AlertState { diff --git a/Sources/SwiftUINavigationCore/Bind.swift b/Sources/SwiftUINavigationCore/Bind.swift index c530c606a1..76271685dc 100644 --- a/Sources/SwiftUINavigationCore/Bind.swift +++ b/Sources/SwiftUINavigationCore/Bind.swift @@ -4,17 +4,17 @@ extension View { /// Synchronizes model state to view state via two-way bindings. /// - /// SwiftUI comes with many property wrappers that can be used in views to drive view state, like - /// field focus. Unfortunately, these property wrappers _must_ be used in views. It's not possible - /// to extract this logic to an observable object and integrate it with the rest of the model's - /// business logic, and be in a better position to test this state. + /// SwiftUI comes with many property wrappers that can be used in views to drive view state, + /// like field focus. Unfortunately, these property wrappers _must_ be used in views. It's not + /// possible to extract this logic to an `@Observable` class and integrate it with the rest of + /// the model's business logic, and be in a better position to test this state. /// /// We can work around these limitations by introducing a published field to your observable /// object and synchronizing it to view state with this view modifier. /// /// - Parameters: - /// - modelValue: A binding from model state. _E.g._, a binding derived from a published field - /// on an observable object. + /// - modelValue: A binding from model state. _E.g._, a binding derived from a field + /// on an observable class. /// - viewValue: A binding from view state. _E.g._, a focus binding. @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) public func bind( diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index e5c13e3a6e..97a9af21d9 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -12,7 +12,8 @@ /// To use this API, you describe all of a dialog's actions as cases in an enum: /// /// ```swift - /// class FeatureModel: ObservableObject { + /// @Observable + /// class FeatureModel { /// enum ConfirmationDialogAction { /// case delete /// case favorite @@ -24,9 +25,10 @@ /// You model the state for showing the alert in as a published field, which can start off `nil`: /// /// ```swift - /// class FeatureModel: ObservableObject { + /// @Observable + /// class FeatureModel { /// // ... - /// @Published var dialog: ConfirmationDialogState? + /// var dialog: ConfirmationDialogState? /// // ... /// } /// ``` @@ -34,7 +36,8 @@ /// And you define an endpoint for handling each alert action: /// /// ```swift - /// class FeatureModel: ObservableObject { + /// @Observable + /// class FeatureModel { /// // ... /// func dialogButtonTapped(_ action: ConfirmationDialogAction) { /// switch action { @@ -51,7 +54,8 @@ /// ``ConfirmationDialogState`` value to represent it: /// /// ```swift - /// class FeatureModel: ObservableObject { + /// @Observable + /// class FeatureModel { /// // ... /// func infoButtonTapped() { /// self.dialog = ConfirmationDialogState( diff --git a/Sources/SwiftUINavigationCore/TextState.swift b/Sources/SwiftUINavigationCore/TextState.swift index c7ceda3cd2..f5b1cfc18a 100644 --- a/Sources/SwiftUINavigationCore/TextState.swift +++ b/Sources/SwiftUINavigationCore/TextState.swift @@ -16,8 +16,9 @@ /// ``TextState``: /// /// ```swift - /// class Model: Equatable { - /// @Published var label = TextState("") + /// @Observable + /// class Model { + /// var label = TextState("") /// } /// ``` /// diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme b/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme index 43ae5ec349..bc729f65cc 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme +++ b/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 16 Oct 2023 20:19:27 +0000 Subject: [PATCH 020/124] Run swift-format --- Sources/SwiftUINavigationCore/Bind.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigationCore/Bind.swift b/Sources/SwiftUINavigationCore/Bind.swift index 76271685dc..68da31575f 100644 --- a/Sources/SwiftUINavigationCore/Bind.swift +++ b/Sources/SwiftUINavigationCore/Bind.swift @@ -4,7 +4,7 @@ extension View { /// Synchronizes model state to view state via two-way bindings. /// - /// SwiftUI comes with many property wrappers that can be used in views to drive view state, + /// SwiftUI comes with many property wrappers that can be used in views to drive view state, /// like field focus. Unfortunately, these property wrappers _must_ be used in views. It's not /// possible to extract this logic to an `@Observable` class and integrate it with the rest of /// the model's business logic, and be in a better position to test this state. From 5eb58994fa88f63e18d2b12f101907e8e57706f6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 2 Nov 2023 12:46:15 -0700 Subject: [PATCH 021/124] Use `Text.init(verbatim:)` to avoid localization warnings (#131) --- Sources/SwiftUINavigation/Alert.swift | 12 ++++++------ Sources/SwiftUINavigation/ConfirmationDialog.swift | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index b771ddce30..699aa64c64 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -61,7 +61,7 @@ @ViewBuilder message: (Value) -> M ) -> some View { self.alert( - value.wrappedValue.map(title) ?? Text(""), + value.wrappedValue.map(title) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: actions, @@ -122,7 +122,7 @@ action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: { @@ -155,7 +155,7 @@ action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: { @@ -228,7 +228,7 @@ action handler: @escaping (Value?) -> Void ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: { @@ -246,7 +246,7 @@ action handler: @escaping (Value?) async -> Void ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: { @@ -263,7 +263,7 @@ unwrapping value: Binding?> ) -> some View { self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, actions: { diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index cd5a776c35..cdec553ae0 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -66,7 +66,7 @@ @ViewBuilder message: (Value) -> M ) -> some View { self.confirmationDialog( - value.wrappedValue.map(title) ?? Text(""), + value.wrappedValue.map(title) ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: titleVisibility, presenting: value.wrappedValue, @@ -130,7 +130,7 @@ action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, @@ -164,7 +164,7 @@ action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, @@ -240,7 +240,7 @@ action handler: @escaping (Value?) -> Void ) -> some View { self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, @@ -259,7 +259,7 @@ action handler: @escaping (Value?) async -> Void ) -> some View { self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, From a3aa5d46e8bf174d773d23b030f1c103999398ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 13 Nov 2023 09:40:55 -0800 Subject: [PATCH 022/124] Case key paths (#132) * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * cleanup * wip * wip * wip * wip * wip * Don't bother running windows tests --- .github/workflows/ci.yml | 23 +- .github/workflows/format.yml | 10 +- .spi.yml | 5 + Examples/CaseStudies/08-Routing.swift | 12 +- .../CaseStudies/09-CustomComponents.swift | 2 +- Examples/CaseStudies/11-IfLet.swift | 29 +- Examples/CaseStudies/12-IfCaseLet.swift | 30 +- Examples/CaseStudies/RootView.swift | 4 +- Examples/Inventory/Inventory.swift | 11 +- Examples/Inventory/Item.swift | 22 +- Examples/Inventory/ItemRow.swift | 13 +- Package.resolved | 4 +- Package.swift | 2 +- README.md | 23 +- Sources/SwiftUINavigation/Alert.swift | 278 +-- Sources/SwiftUINavigation/Binding.swift | 150 +- .../ConfirmationDialog.swift | 284 +-- .../Articles/AlertsDialogs.md | 36 +- .../Documentation.docc/Articles/Bindings.md | 33 +- .../Articles/DestructuringViews.md | 164 -- .../Documentation.docc/Articles/Navigation.md | 35 +- .../Articles/SheetsPopoversCovers.md | 34 +- .../Articles/WhatIsNavigation.md | 81 +- .../Extensions/Deprecations.md | 46 + .../Documentation.docc/Extensions/Switch.md | 8 + .../Documentation.docc/SwiftUINavigation.md | 5 +- .../SwiftUINavigation/FullScreenCover.swift | 30 - Sources/SwiftUINavigation/IfCaseLet.swift | 94 - Sources/SwiftUINavigation/IfLet.swift | 89 - .../Internal/Deprecations.swift | 1976 +++++++++++++++-- .../NavigationDestination.swift | 29 +- .../SwiftUINavigation/NavigationLink.swift | 91 +- Sources/SwiftUINavigation/Popover.swift | 54 +- Sources/SwiftUINavigation/Sheet.swift | 45 +- Sources/SwiftUINavigation/Switch.swift | 1118 ---------- Sources/SwiftUINavigation/WithState.swift | 2 +- .../SwiftUINavigationCore/AlertState.swift | 7 +- .../SwiftUINavigationCore/ButtonState.swift | 8 +- .../ConfirmationDialogState.swift | 12 +- .../SwiftUINavigationCore.md | 12 + Sources/SwiftUINavigationCore/TextState.swift | 203 +- .../xcshareddata/swiftpm/Package.resolved | 231 +- Tests/SwiftUINavigationTests/AlertTests.swift | 2 +- 43 files changed, 2533 insertions(+), 2814 deletions(-) create mode 100644 .spi.yml delete mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Extensions/Switch.md delete mode 100644 Sources/SwiftUINavigation/IfCaseLet.swift delete mode 100644 Sources/SwiftUINavigation/IfLet.swift delete mode 100644 Sources/SwiftUINavigation/Switch.swift create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 954ae0690f..f6600c27c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,19 @@ concurrency: jobs: library: - runs-on: macos-12 + runs-on: macos-13 strategy: matrix: - xcode: ['13.4.1', '14.1'] + xcode: + - '15.0' + - '14.3.1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: Skip macro validation + run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES - name: Run tests run: make test @@ -38,15 +42,8 @@ jobs: steps: - uses: compnerd/gha-setup-swift@main with: - branch: swift-5.8.1-release - tag: 5.8.1-RELEASE - - uses: actions/checkout@v3 + branch: swift-5.9.1-release + tag: 5.9.1-RELEASE + - uses: actions/checkout@v4 - name: Build run: swift build -c ${{ matrix.config }} - - name: Run tests (debug only) - # There is an issue that exists in the 5.8.1 toolchain - # which fails on release configuration testing, but - # this issue is fixed 5.9 so we can remove the if once - # that is generally available. - if: ${{ matrix.config == 'debug' }} - run: swift test \ No newline at end of file diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index dc82144b9d..c1c794ca99 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -8,15 +8,13 @@ on: jobs: swift_format: name: swift-format - runs-on: macOS-11 + runs-on: macOS-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_13.0.app - - name: Tap - run: brew tap pointfreeco/formulae + run: sudo xcode-select -s /Applications/Xcode_15.0.app - name: Install - run: brew install Formulae/swift-format@5.5 + run: brew install swift-format - name: Format run: make format - uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000000..980ef4497e --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [SwiftUINavigation, SwiftUINavigationCore] + swift_version: 5.9 diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift index d6546a4076..7921f25d11 100644 --- a/Examples/CaseStudies/08-Routing.swift +++ b/Examples/CaseStudies/08-Routing.swift @@ -11,6 +11,7 @@ private let readMe = """ in this library. """ +@CasePathable enum Destination { case alert(AlertState) case confirmationDialog(ConfirmationDialogState) @@ -80,7 +81,7 @@ struct Routing: View { } } .navigationTitle("Routing") - .alert(unwrapping: self.$destination, case: /Destination.alert) { action in + .alert(self.$destination.alert) { action in switch action { case .randomize?: self.count = .random(in: 0...1_000) @@ -90,10 +91,7 @@ struct Routing: View { break } } - .confirmationDialog( - unwrapping: self.$destination, - case: /Destination.confirmationDialog - ) { action in + .confirmationDialog(self.$destination.confirmationDialog) { action in switch action { case .decrement?: self.count -= 1 @@ -103,13 +101,13 @@ struct Routing: View { break } } - .navigationDestination(unwrapping: self.$destination, case: /Destination.link) { $count in + .navigationDestination(unwrapping: self.$destination.link) { $count in Form { Stepper("Count: \(count)", value: $count) } .navigationTitle("Routing link") } - .sheet(unwrapping: self.$destination, case: /Destination.sheet) { $count in + .sheet(unwrapping: self.$destination.sheet) { $count in NavigationStack { Form { Stepper("Count: \(count)", value: $count) diff --git a/Examples/CaseStudies/09-CustomComponents.swift b/Examples/CaseStudies/09-CustomComponents.swift index fb37764b0b..329b8e61c0 100644 --- a/Examples/CaseStudies/09-CustomComponents.swift +++ b/Examples/CaseStudies/09-CustomComponents.swift @@ -106,7 +106,7 @@ extension View { fileprivate func bottomMenu( unwrapping value: Binding, - case casePath: CasePath, + case casePath: AnyCasePath, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { diff --git a/Examples/CaseStudies/11-IfLet.swift b/Examples/CaseStudies/11-IfLet.swift index 1f48f307fa..caa116580e 100644 --- a/Examples/CaseStudies/11-IfLet.swift +++ b/Examples/CaseStudies/11-IfLet.swift @@ -2,8 +2,7 @@ import SwiftUI import SwiftUINavigation private let readMe = """ - This demonstrates to use the IfLet view to unwrap a binding of an optional into a binding of \ - an honest value. + This demonstrates how to unwrap a binding of an optional into a binding of an honest value. Tap the "Edit" button to put the form into edit mode. Then you can make changes to the message \ and either commit the changes by tapping "Save", or discard the changes by tapping "Discard". @@ -18,25 +17,29 @@ struct IfLetCaseStudy: View { Section { Text(readMe) } - IfLet(self.$editableString) { $string in - TextField("Edit string", text: $string) - HStack { - Button("Discard") { - self.editableString = nil - } - Button("Save") { - self.string = string - self.editableString = nil + Binding(unwrapping: self.$editableString).map { $string in + VStack { + TextField("Edit string", text: $string) + HStack { + Button("Discard") { + self.editableString = nil + } + Spacer() + Button("Save") { + self.string = string + self.editableString = nil + } } } - } else: { + } + if self.editableString == nil { Text("\(self.string)") Button("Edit") { self.editableString = self.string } } - .buttonStyle(.borderless) } + .buttonStyle(.borderless) } } diff --git a/Examples/CaseStudies/12-IfCaseLet.swift b/Examples/CaseStudies/12-IfCaseLet.swift index b29aedec9d..47f4df3499 100644 --- a/Examples/CaseStudies/12-IfCaseLet.swift +++ b/Examples/CaseStudies/12-IfCaseLet.swift @@ -3,8 +3,7 @@ import SwiftUI import SwiftUINavigation private let readMe = """ - This demonstrates to use the IfCaseLet view to destructure a binding of an enum into a binding \ - of one of its cases. + This demonstrates how to destructure a binding of an enum into a binding of one of its cases. Tap the "Edit" button to put the form into edit mode. Then you can make changes to the message \ and either commit the changes by tapping "Save", or discard the changes by tapping "Discard". @@ -14,6 +13,7 @@ struct IfCaseLetCaseStudy: View { @State var string: String = "Hello" @State var editableString: EditableString = .inactive + @CasePathable enum EditableString { case active(String) case inactive @@ -24,25 +24,29 @@ struct IfCaseLetCaseStudy: View { Section { Text(readMe) } - IfCaseLet(self.$editableString, pattern: /EditableString.active) { $string in - TextField("Edit string", text: $string) - HStack { - Button("Discard") { - self.editableString = .inactive - } - Button("Save") { - self.string = string - self.editableString = .inactive + self.$editableString.active.map { $string in + VStack { + TextField("Edit string", text: $string) + HStack { + Button("Discard", role: .cancel) { + self.editableString = .inactive + } + Spacer() + Button("Save") { + self.string = string + self.editableString = .inactive + } } } - } else: { + } + if !self.editableString.is(\.active) { Text("\(self.string)") Button("Edit") { self.editableString = .active(self.string) } } - .buttonStyle(.borderless) } + .buttonStyle(.borderless) } } diff --git a/Examples/CaseStudies/RootView.swift b/Examples/CaseStudies/RootView.swift index 165a236d20..d409623de9 100644 --- a/Examples/CaseStudies/RootView.swift +++ b/Examples/CaseStudies/RootView.swift @@ -54,10 +54,10 @@ struct RootView: View { NavigationLink("Synchronized bindings") { SynchronizedBindings() } - NavigationLink("IfLet view") { + NavigationLink("Optional bindings") { IfLetCaseStudy() } - NavigationLink("IfCaseLet view") { + NavigationLink("Enum bindings") { IfCaseLetCaseStudy() } } header: { diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index 5f60d3680e..1ec1a260e7 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -9,6 +9,7 @@ class InventoryModel { } var destination: Destination? + @CasePathable enum Destination: Equatable { case add(Item) case edit(Item) @@ -84,10 +85,7 @@ struct InventoryView: View { } } .navigationTitle("Inventory") - .navigationDestination( - unwrapping: self.$model.destination, - case: /InventoryModel.Destination.edit - ) { $item in + .navigationDestination(unwrapping: self.$model.destination.edit) { $item in ItemView(item: $item) .navigationBarTitle("Edit") .navigationBarBackButtonHidden(true) @@ -104,10 +102,7 @@ struct InventoryView: View { } } } - .sheet( - unwrapping: self.$model.destination, - case: /InventoryModel.Destination.add - ) { $itemToAdd in + .sheet(unwrapping: self.$model.destination.add) { $itemToAdd in NavigationStack { ItemView(item: $itemToAdd) .navigationTitle("Add") diff --git a/Examples/Inventory/Item.swift b/Examples/Inventory/Item.swift index 5503df1103..19f6097d45 100644 --- a/Examples/Inventory/Item.swift +++ b/Examples/Inventory/Item.swift @@ -7,14 +7,10 @@ struct Item: Equatable, Identifiable { var name: String var status: Status + @CasePathable enum Status: Equatable { case inStock(quantity: Int) case outOfStock(isOnBackOrder: Bool) - - var isInStock: Bool { - guard case .inStock = self else { return false } - return true - } } struct Color: Equatable, Hashable { @@ -62,26 +58,32 @@ struct ItemView: View { } } - Switch(self.$item.status) { - CaseLet(/Item.Status.inStock) { $quantity in - Section(header: Text("In stock")) { + switch self.item.status { + case .inStock: + self.$item.status.inStock.map { $quantity in + Section { Stepper("Quantity: \(quantity)", value: $quantity) Button("Mark as sold out") { withAnimation { self.item.status = .outOfStock(isOnBackOrder: false) } } + } header: { + Text("In stock") } .transition(.opacity) } - CaseLet(/Item.Status.outOfStock) { $isOnBackOrder in - Section(header: Text("Out of stock")) { + case .outOfStock: + self.$item.status.outOfStock.map { $isOnBackOrder in + Section { Toggle("Is on back order?", isOn: $isOnBackOrder) Button("Is back in stock!") { withAnimation { self.item.status = .inStock(quantity: 1) } } + } header: { + Text("Out of stock") } .transition(.opacity) } diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index 869603e0fa..bc30cf5ca3 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -7,6 +7,7 @@ class ItemRowModel: Identifiable { var item: Item var destination: Destination? + @CasePathable enum Destination: Equatable { case alert(AlertState) case duplicate(Item) @@ -113,17 +114,11 @@ struct ItemRowView: View { .padding(.leading) } .buttonStyle(.plain) - .foregroundColor(self.model.item.status.isInStock ? nil : Color.gray) - .alert( - unwrapping: self.$model.destination, - case: /ItemRowModel.Destination.alert - ) { + .foregroundColor(self.model.item.status.is(\.inStock) ? nil : Color.gray) + .alert(self.$model.destination.alert) { self.model.alertButtonTapped($0) } - .popover( - unwrapping: self.$model.destination, - case: /ItemRowModel.Destination.duplicate - ) { $item in + .popover(unwrapping: self.$model.destination.duplicate) { $item in NavigationStack { ItemView(item: $item) .navigationBarTitle("Duplicate") diff --git a/Package.resolved b/Package.resolved index 924fe3c0b9..97d64e88c2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version": "1.0.0" + "revision": "40773cbaf8d71ed5357f297b1ba4073f5b24faaa", + "version": "1.1.0" } }, { diff --git a/Package.swift b/Package.swift index 2a3e9803ca..f81e1388e2 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], diff --git a/README.md b/README.md index 597f4235d4..e8c1eeecd1 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ fall in two categories: piece of data, handing it to a SwiftUI view, and letting SwiftUI handle the rest. Navigation that is "state-driven" is the more powerful form of navigation, albeit slightly more -complicated. Unfortunately, SwiftUI does not ship with all of the tools necessary to model our domains with -enums and make use of navigation APIs. This library bridges that gap by providing APIs that allow -you to model your navigation destinations as an enum, and then drive navigation by a binding -to that enum. +complicated. Unfortunately, SwiftUI does not ship with all of the tools necessary to model our +domains with enums and make use of navigation APIs. This library bridges that gap by providing APIs +that allow you to model your navigation destinations as an enum, and then drive navigation by a +binding to that enum. Explore all of the tools this library comes with by checking out the [documentation][docs], and reading these articles: @@ -75,8 +75,10 @@ alerts, all driven by state and deep-linkable. ## Learn More -SwiftUI Navigation's tools were motivated and designed over the course of many episodes on [Point-Free](https://www.pointfree.co), a video series exploring functional programming and the -Swift language, hosted by [Brandon Williams](https://twitter.com/mbrandonw) and [Stephen Celis](https://twitter.com/stephencelis). +SwiftUI Navigation's tools were motivated and designed over the course of many episodes on +[Point-Free](https://www.pointfree.co), a video series exploring functional programming and the +Swift language, hosted by [Brandon Williams](https://twitter.com/mbrandonw) and +[Stephen Celis](https://twitter.com/stephencelis). You can watch all of the episodes [here](https://www.pointfree.co/collections/swiftui/navigation). @@ -90,8 +92,10 @@ If you want to discuss this library or have a question about how to use it to so a particular problem, there are a number of places you can discuss with fellow [Point-Free](http://www.pointfree.co) enthusiasts: -* For long-form discussions, we recommend the [discussions](http://github.com/pointfreeco/swiftui-navigation/discussions) tab of this repo. -* For casual chat, we recommend the [Point-Free Community slack](http://pointfree.co/slack-invite). + * For long-form discussions, we recommend the + [discussions](http://github.com/pointfreeco/swiftui-navigation/discussions) tab of this repo. + * For casual chat, we recommend the + [Point-Free Community slack](http://pointfree.co/slack-invite). ## Installation @@ -110,7 +114,8 @@ dependencies: [ ## Documentation -The latest documentation for the SwiftUI Navigation APIs is available [here](http://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/). +The latest documentation for the SwiftUI Navigation APIs is available +[here](http://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/). ## License diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 699aa64c64..bf27a0128c 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -69,239 +69,67 @@ ) } - /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case. + /// Presents an alert from a binding to optional alert state. /// - /// A version of `alert(unwrapping:)` that works with enum state. - /// - /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths + /// See for more information on how to use this API. /// /// - Parameters: - /// - title: A closure returning the alert's title given the current alert state. - /// - enum: A binding to an optional enum that holds alert state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and then pass it to the modifier's closures. You can use it to populate the fields - /// of an alert that the system displays to the user. When the user presses or taps one of the - /// alert's actions, the system sets this value to `nil` and dismisses the alert. - /// - casePath: A case path that identifies a particular case that holds alert state. - /// - actions: A view builder returning the alert's actions given the current alert state. - /// - message: A view builder returning the message for the alert given the current alert - /// state. + /// - state: A binding to optional alert state that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to + /// populate the fields of an alert that the system displays to the user. When the user + /// presses or taps one of the alert's actions, the system sets this value to `nil` and + /// dismisses the alert, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - title: (Case) -> Text, - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder actions: (Case) -> A, - @ViewBuilder message: (Case) -> M + public func alert( + _ state: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.alert( - title: title, - unwrapping: `enum`.case(casePath), - actions: actions, - message: message + (state.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: state.isPresent(), + presenting: state.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } ) } - #if swift(>=5.7) - /// Presents an alert from a binding to optional ``AlertState``. - /// - /// See for more information on how to use this API. - /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to - /// populate the fields of an alert that the system displays to the user. When the user - /// presses or taps one of the alert's actions, the system sets this value to `nil` and - /// dismisses the alert, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents an alert from a binding to optional ``AlertState``. - /// - /// See for more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to - /// populate the fields of an alert that the system displays to the user. When the user - /// presses or taps one of the alert's actions, the system sets this value to `nil` and - /// dismisses the alert, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case of ``AlertState``. - /// - /// A version of `alert(unwrapping:)` that works with enum state. See for - /// more information on how to use this API. - /// - /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds alert state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an alert that the system displays to the user. - /// When the user presses or taps one of the alert's actions, the system sets this value to - /// `nil` and dismisses the alert, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds alert state. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - - /// Presents an alert from a binding to an optional enum, and a [case path][case-paths-gh] to a - /// specific case of ``AlertState``. - /// - /// A version of `alert(unwrapping:)` that works with enum state. See for - /// more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds alert state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an alert that the system displays to the user. - /// When the user presses or taps one of the alert's actions, the system sets this value to - /// `nil` and dismisses the alert, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds alert state. - /// - handler: A closure that is called with an action from a particular alert button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - #else - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping value: Binding?> - ) -> some View { - self.alert( - (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0) { _ in } - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: handler) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath> - ) -> some View { - self.alert(unwrapping: `enum`.case(casePath)) { (_: Never?) in } - } - #endif - - // TODO: support iOS <15? + /// Presents an alert from a binding to optional alert state. + /// + /// See for more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - state: A binding to optional alert state that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and used to + /// populate the fields of an alert that the system displays to the user. When the user + /// presses or taps one of the alert's actions, the system sets this value to `nil` and + /// dismisses the alert, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular alert button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + _ state: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.alert( + (state.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: state.isPresent(), + presenting: state.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index a0074920f6..24a035c93a 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -1,7 +1,55 @@ #if canImport(SwiftUI) + import CasePaths import SwiftUI extension Binding { + #if swift(>=5.9) + /// Returns a binding to the associated value of a given case key path. + /// + /// Useful for producing bindings to values held in enum state. + /// + /// - Parameter keyPath: A case key path to a specific associated value. + /// - Returns: A new binding. + public subscript( + dynamicMember keyPath: CaseKeyPath + ) -> Binding? + where Value: CasePathable { + Binding( + unwrapping: Binding( + get: { self.wrappedValue[case: keyPath] }, + set: { newValue, transaction in + guard let newValue else { return } + self.transaction(transaction).wrappedValue[case: keyPath] = newValue + } + ) + ) + } + + /// Returns a binding to the associated value of a given case key path. + /// + /// Useful for driving navigation off an optional enumeration of destinations. + /// + /// - Parameter keyPath: A case key path to a specific associated value. + /// - Returns: A new binding. + public subscript( + dynamicMember keyPath: CaseKeyPath + ) -> Binding + where Value == Enum? { + return Binding( + get: { self.wrappedValue[case: (\Enum?.Cases.some).appending(path: keyPath)] }, + set: { newValue, transaction in + guard let newValue else { + self.transaction(transaction).wrappedValue = nil + return + } + self.transaction(transaction).wrappedValue[ + case: (\Enum?.Cases.some).appending(path: keyPath) + ] = newValue + } + ) + } + #endif + /// Creates a binding by projecting the base value to an unwrapped value. /// /// Useful for producing non-optional bindings from optional ones. @@ -17,52 +65,7 @@ /// - Parameter base: A value to project to an unwrapped value. /// - Returns: A new binding or `nil` when `base` is `nil`. public init?(unwrapping base: Binding) { - self.init(unwrapping: base, case: /Optional.some) - } - - /// Creates a binding by projecting the base enum value to an unwrapped case. - /// - /// Useful for extracting bindings of non-optional state from the case of an enum. - /// - /// See ``IfCaseLet`` for a view builder-friendly version of this initializer. - /// - /// - Parameters: - /// - enum: An enum to project to a particular case. - /// - casePath: A case path that identifies a particular case to unwrap. - /// - Returns: A new binding or `nil` when `base` is `nil`. - public init?(unwrapping enum: Binding, case casePath: CasePath) { - guard var `case` = casePath.extract(from: `enum`.wrappedValue) - else { return nil } - - self.init( - get: { - `case` = casePath.extract(from: `enum`.wrappedValue) ?? `case` - return `case` - }, - set: { - guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } - `case` = $0 - `enum`.transaction($1).wrappedValue = casePath.embed($0) - } - ) - } - - /// Creates a binding by projecting the current optional enum value to the value at a particular - /// case. - /// - /// > Note: This method is constrained to optionals so that the projected value can write `nil` - /// > back to the parent, which is useful for navigation, particularly dismissal. - /// - /// - Parameter casePath: A case path that identifies a particular case to unwrap. - /// - Returns: A binding to an enum case. - public func `case`(_ casePath: CasePath) -> Binding - where Value == Enum? { - .init( - get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, - set: { newValue, transaction in - self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) - } - ) + self.init(unwrapping: base, case: AnyCasePath(\.some)) } /// Creates a binding by projecting the current optional value to a boolean describing if it's @@ -83,61 +86,6 @@ ) } - /// Creates a binding by projecting the current optional enum value to a boolean describing - /// whether or not it matches the given case path. - /// - /// Writing `false` to the binding will `nil` out the base enum value. Writing `true` does - /// nothing. - /// - /// Useful for interacting with APIs that take a binding of a boolean that you want to drive with - /// with an enum case that has no associated data. - /// - /// For example, a view may model all of its presentations in a single destination enum to prevent - /// the invalid states that can be introduced by holding onto many booleans and optionals, - /// instead. Even the simple case of two booleans driving two alerts introduces a potential - /// runtime state where both alerts are presented at the same time. By modeling these alerts - /// using a two-case enum instead of two booleans, we can eliminate this invalid state at compile - /// time. Then we can transform a binding to the destination enum into a boolean binding using - /// `isPresent`, so that it can be passed to various presentation APIs. - /// - /// ```swift - /// enum Destination { - /// case deleteAlert - /// ... - /// } - /// - /// struct ProductView: View { - /// @State var destination: Destination? - /// @State var product: Product - /// - /// var body: some View { - /// Button("Delete") { - /// self.model.destination = .deleteAlert - /// } - /// // SwiftUI's vanilla alert modifier - /// .alert( - /// self.product.name - /// isPresented: self.$model.destination.isPresent(/Destination.deleteAlert), - /// actions: { - /// Button("Delete", role: .destructive) { - /// self.model.deleteConfirmationButtonTapped() - /// } - /// }, - /// message: { - /// Text("Are you sure you want to delete this product?") - /// } - /// ) - /// } - /// } - /// ``` - /// - /// - Parameter casePath: A case path that identifies a particular case to match. - /// - Returns: A binding to a boolean. - public func isPresent(_ casePath: CasePath) -> Binding - where Value == Enum? { - self.case(casePath).isPresent() - } - /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. /// /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index cdec553ae0..7f48fc8509 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -75,243 +75,69 @@ ) } - /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case. + /// Presents a confirmation dialog from a binding to optional confirmation dialog state. /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See - /// for more information on how to use this API. + /// See for more information on how to use this API. /// /// - Parameters: - /// - title: A closure returning the dialog's title given the current dialog case. - /// - titleVisibility: The visibility of the dialog's title. - /// - enum: A binding to an optional enum that holds dialog state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and then pass it to the modifier's closures. You can use it to populate the fields - /// of a dialog that the system displays to the user. When the user presses or taps one of - /// the dialog's actions, the system sets this value to `nil` and dismisses the dialog. - /// - casePath: A case path that identifies a particular dialog case to handle. - /// - actions: A view builder returning the dialog's actions given the current dialog case. - /// - message: A view builder returning the message for the dialog given the current dialog - /// case. + /// - state: A binding to optional state that determines whether a confirmation dialog should + /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used + /// to populate the fields of a dialog that the system displays to the user. When the user + /// presses or taps one of the dialog's actions, the system sets this value to `nil` and + /// dismisses the dialog, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - title: (Case) -> Text, - titleVisibility: Visibility = .automatic, - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder actions: (Case) -> A, - @ViewBuilder message: (Case) -> M + public func confirmationDialog( + _ state: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.confirmationDialog( - title: title, - titleVisibility: titleVisibility, - unwrapping: `enum`.case(casePath), - actions: actions, - message: message + state.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), + isPresented: state.isPresent(), + titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: state.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } ) } - #if swift(>=5.7) - /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. - /// - /// See for more information on how to use this API. - /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether a confirmation dialog should - /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used - /// to populate the fields of a dialog that the system displays to the user. When the user - /// presses or taps one of the dialog's actions, the system sets this value to `nil` and - /// dismisses the dialog, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents a confirmation dialog from a binding to optional ``ConfirmationDialogState``. - /// - /// See for more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - value: A binding to an optional value that determines whether a confirmation dialog should - /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used - /// to populate the fields of a dialog that the system displays to the user. When the user - /// presses or taps one of the dialog's actions, the system sets this value to `nil` and - /// dismisses the dialog, and the action is fed to the `action` closure. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case of ``ConfirmationDialogState``. - /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See - /// for more information on how to use this API. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds dialog state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an dialog that the system displays to the user. - /// When the user presses or taps one of the dialog's actions, the system sets this value to - /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds dialog state. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void = { (_: Never?) in } - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } - - /// Presents a confirmation dialog from a binding to an optional enum, and a case path to a - /// specific case of ``ConfirmationDialogState``. - /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. See - /// for more information on how to use this API. - /// - /// > Warning: Async closures cannot be performed with animation. If the underlying action is - /// > animated, a runtime warning will be emitted. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds dialog state at a particular case. When - /// the binding is updated with a non-`nil` enum, the case path will attempt to extract this - /// state and use it to populate the fields of an dialog that the system displays to the user. - /// When the user presses or taps one of the dialog's actions, the system sets this value to - /// `nil` and dismisses the dialog, and the action is fed to the `action` closure. - /// - casePath: A case path that identifies a particular case that holds dialog state. - /// - handler: A closure that is called with an action from a particular dialog button when - /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } - #else - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.confirmationDialog( - value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), - titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: value.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping value: Binding?> - ) -> some View { - self.confirmationDialog(unwrapping: value) { _ in } - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) -> Void - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value?) async -> Void - ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: handler - ) - } - - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath> - ) -> some View { - self.confirmationDialog(unwrapping: `enum`.case(casePath)) { _ in } - } - #endif - - // TODO: support iOS <15? + /// Presents a confirmation dialog from a binding to optional confirmation dialog state. + /// + /// See for more information on how to use this API. + /// + /// > Warning: Async closures cannot be performed with animation. If the underlying action is + /// > animated, a runtime warning will be emitted. + /// + /// - Parameters: + /// - state: A binding to optional state that determines whether a confirmation dialog should + /// be presented. When the binding is updated with non-`nil` value, it is unwrapped and used + /// to populate the fields of a dialog that the system displays to the user. When the user + /// presses or taps one of the dialog's actions, the system sets this value to `nil` and + /// dismisses the dialog, and the action is fed to the `action` closure. + /// - handler: A closure that is called with an action from a particular dialog button when + /// tapped. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + _ state: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.confirmationDialog( + state.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), + isPresented: state.isPresent(), + titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: state.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } + ) + } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md index 506b82452d..25d07b3c47 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -60,10 +60,12 @@ equatability. This makes it possible to write tests against these values. Next you can provide an endpoint that will be called when the alert is interacted with: ```swift -func alertButtonTapped(_ action: AlertAction) { +func alertButtonTapped(_ action: AlertAction?) { switch action { case .deletionConfirmed: // NB: Perform deletion logic here + case nil: + // NB: Perform cancel button logic here } } ``` @@ -79,7 +81,7 @@ struct ContentView: View { List { // ... } - .alert(unwrapping: self.$model.alert) { action in + .alert(self.$model.alert) { action in self.model.alertButtonTapped(action) } } @@ -91,7 +93,7 @@ it: ```swift func testDelete() { - let model = FeatureModel(…) + let model = FeatureModel(/* ... */) model.deleteButtonTapped() XCTAssertEqual(model.alert?.title, TextState("Are you sure?")) @@ -105,19 +107,21 @@ This works because all of the types for describing an alert are `Equatable`, inc `TextState`, and even the buttons. Sometimes it is not optimal to model the alert as an optional. In particular, if a feature can -navigate to multiple, mutually exclusive screens, then an enum is more appropriate. +navigate to multiple, mutually exclusive screens, then a "case-pathable" enum is more appropriate. In such a case: - ```swift @Observable class FeatureModel { var destination: Destination? + + @CasePathable enum Destination { case alert(AlertState) // NB: Other destinations } + enum AlertAction { case deletionConfirmed } @@ -130,7 +134,7 @@ With this kind of set up you can use an alternative `alert` view modifier that t argument for specifying which case of the enum drives the presentation of the alert: ```swift -.alert(unwrapping: self.$model.destination, case: /Destination.alert) { action in +.alert(self.$model.destination.alert) { action in self.model.alertButtonTapped(action) } ``` @@ -166,10 +170,12 @@ class FeatureModel { ) } - func dialogButtonTapped(_ action: DialogAction) { + func dialogButtonTapped(_ action: DialogAction?) { switch action { case .deletionConfirmed: // NB: Perform deletion logic here + case nil: + // NB: Perform cancel button logic here } } } @@ -185,9 +191,23 @@ struct ContentView: View { List { // ... } - .confirmationDialog(unwrapping: self.$model.dialog) { action in + .confirmationDialog(self.$model.dialog) { action in self.dialogButtonTapped(action) } } } ``` + +## Topics + +### Alert and dialog modifiers + +- ``SwiftUI/View/alert(title:unwrapping:actions:message:)`` +- ``SwiftUI/View/confirmationDialog(title:titleVisibility:unwrapping:actions:message:)`` + +### Alert state and dialog state + +- ``SwiftUI/View/alert(_:action:)-sgyk`` +- ``SwiftUI/View/alert(_:action:)-1gtsa`` +- ``SwiftUI/View/confirmationDialog(_:action:)-9alh7`` +- ``SwiftUI/View/confirmationDialog(_:action:)-7mxx7`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index ce67c4547b..ba7575b66b 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -5,13 +5,13 @@ Learn how to manage certain view state, such as `@FocusState` directly in your o ## Overview SwiftUI comes with many property wrappers that can be used in views to drive view state, such as -`@FocusState`. Unfortunately, these property wrappers _must_ be used in views. It's not possible -to extract this logic to an `@Observable` class and integrate it with the rest of the model's -business logic, and be in a better position to test this state. -We can work around these limitations by introducing a published field to your observable -object and synchronizing it to view state with the `bind` view modifier that ships with this -library. +`@FocusState`. Unfortunately, these property wrappers _must_ be used in views. It's not possible to +extract this logic to an `@Observable` class and integrate it with the rest of the model's business +logic, and be in a better position to test this state. + +We can work around these limitations by introducing a published field to your observable object and +synchronizing it to view state with the `bind` view modifier that ships with this library. For example, suppose you have a sign in flow where if the API request to sign in fails, you want to refocus the email field. The model can be implemented like so: @@ -64,3 +64,24 @@ struct SignInView: View { } } ``` + +## Topics + +### Dynamic case lookup + +- ``SwiftUI/Binding/subscript(dynamicMember:)-9akk`` +- ``SwiftUI/Binding/subscript(dynamicMember:)-9okch`` + +### Unwrapping bindings + +- ``SwiftUI/Binding/init(unwrapping:)`` + +### Binding transformations + +- ``SwiftUI/Binding/isPresent()`` +- ``SwiftUI/Binding/removeDuplicates()`` +- ``SwiftUI/Binding/removeDuplicates(by:)`` + +### Supporting views + +- ``WithState`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md deleted file mode 100644 index d099b277b6..0000000000 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md +++ /dev/null @@ -1,164 +0,0 @@ -# Destructuring views - -Learn how to use ``IfLet``, ``IfCaseLet`` and ``Switch`` views in order to destructure bindings into -smaller parts. - -## Overview - -Often our views can hold bindings of optional and enum state, and we will want to derive a binding -to its underlying wrapped value or a particular case. SwiftUI does not come with tools to do this, -but this library has a few views for accomplishing this. - -### IfLet - -The ``IfLet`` view allows one to derive a binding of an honest value from a binding of an optional -value. For example, suppose you had an interface that could editing a single piece of text in the -UI, and further those changes can be either saved or discarded. - -Using ``IfLet`` you can model the state of being in editing mode as an optional string: - -```swift -struct EditView: View { - @State var string: String = "" - @State var editableString: String? - - var body: some View { - Form { - IfLet(self.$editableString) { $string in - TextField("Edit string", text: $string) - HStack { - Button("Cancel") { - self.editableString = nil - } - Button("Save") { - self.string = string - self.editableString = nil - } - } - } else: { - Text(self.string) - Button("Edit") { - self.editableString = self.string - } - } - .buttonStyle(.borderless) - } - } -} -``` - -This is the most optimal way to model this domain. Without the ability to derive a -`Binding` from a `Binding` we would have had to hold onto extra state to represent -whether or not we are in editing mode: - -```swift -struct EditView: View { - @State var string: String = "" - @State var editableString: String - @State var isEditing = false - - // ... -} -``` - -This is non-optimal because we have to make sure to clean up `editableString` before or after -showing the editable `TextField`. If we forget to do that we can introduce bugs into our -application, such as showing the _previous_ editing string when entering edit mode. - -### IfCaseLet - -The ``IfCaseLet`` view is similar to ``IfLet`` (see [above](#IfLet)), except it can derive a binding -to a particular case of an enum. - -For example, using the sample code from [above](#IfLet), what if you didn't want to use an optional -string for `editableState`, but instead use a custom enum so that you can describe the two states -more clearly: - -```swift -enum EditableString { - case active(String) - case inactive -} -``` - -You cannot use ``IfLet`` with this because it's an enum, but you can use ``IfCaseLet``: - -```swift -struct EditView: View { - @State var string: String = "" - @State var editableString: EditableString = .inactive - - var body: some View { - Form { - IfCaseLet(self.$editableString, pattern: /EditableString.active) { $string in - TextField("Edit string", text: $string) - HStack { - Button("Cancel") { - self.editableString = .inactive - } - Button("Save") { - self.string = string - self.editableString = .inactive - } - } - } else: { - Text(self.string) - Button("Edit") { - self.editableString = .active(self.string) - } - } - .buttonStyle(.borderless) - } - } -} -``` - -The "pattern" for the ``IfCaseLet`` is expressed by what is known as a "[case path][case-paths-gh]". -A case path is like a key path, except it is specifically tuned for abstracting over the -shape of enums rather than structs. A key path abstractly bundles up the functionality of getting -and setting a property on a struct, whereas a case path bundles up the functionality of "extracting" -a value from an enum and "embedding" a value into an enum. They are an indispensable tool for -transforming bindings. - -### Switch and CaseLet - -The ``Switch`` and ``CaseLet`` generalize the ``IfLet`` and ``IfCaseLet`` views, allowing you to -destructure a binding of an enum into bindings of each case, and provides some runtime exhaustivity -checking. - -For example, a warehousing application may model the status of an inventory item using an enum -with cases that distinguish in-stock and out-of-stock statuses. ``Switch`` and ``CaseLet`` can -be used to produce bindings to the associated values of each case. - -```swift -enum ItemStatus { - case inStock(quantity: Int) - case outOfStock(isOnBackOrder: Bool) -} - -struct InventoryItemView: View { - @State var status: ItemStatus - - var body: some View { - Switch(self.$status) { - CaseLet(/ItemStatus.inStock) { $quantity in - HStack { - Text("Quantity: \(quantity)") - Stepper("Quantity", value: $quantity) - } - Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } - } - CaseLet(/ItemStatus.outOfStock) { $isOnBackOrder in - Toggle("Is on back order?", isOn: $isOnBackOrder) - Button("In stock") { self.status = .inStock(quantity: 1) } - } - } - } -} -``` - -In debug builds, exhaustivity is handled at runtime: if the `Switch` encounters an -unhandled case, and no ``Default`` view is present, a runtime warning is issued and a warning -view is presented. - -[case-paths-gh]: http://github.com/pointfreeco/swift-case-paths diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md index e9c8486e3a..c3bce8c5b6 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -5,7 +5,7 @@ manner. ## Overview -The library comes with new tools for driving drill-down navigation with optional and enum state. +The library comes with new tools for driving drill-down navigation with optional and enum values. This includes new initializers on `NavigationLink` and new overloads of the `navigationDestination` view modifier. @@ -64,12 +64,17 @@ Suppose that in addition to be able to drill down to a counter view that one can sheet with some text. We can model those destinations as an enum: ```swift +@CasePathable enum Destination { case counter(Int) case text(String) } ``` +> Note: We have applied the `@CasePathable` macro from +> [CasePaths](https://github.com/pointfreeco.swift-case-paths), which allows the navigation binding +> to use "dynamic case lookup" to a particular enum case. + And we can hold an optional destination in state to represent whether or not we are navigated to one of these destinations: @@ -77,15 +82,13 @@ one of these destinations: @State var destination: Destination? ``` -With this set up you can make use of the `init(unwrapping:case:)` initializer on `NavigationLink` -in order to specify a binding to the optional destination, and further specify which case of the -enum you want driving navigation: +With this set up you can make use of the +``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` initializer on +`NavigationLink` in order to specify a binding to the optional destination, and further specify +which case of the enum you want driving navigation: ```swift -NavigationLink( - unwrapping: self.$destination, - case: /Destination.counter -) { isActive in +NavigationLink(unwrapping: self.$destination.counter) { isActive in self.destination = isActive ? .counter(42) : nil } destination: { $number in CounterView(number: $number) @@ -94,7 +97,7 @@ NavigationLink( } ``` -And similarly for `navigationDestination`: +And similarly for ``SwiftUI/View/navigationDestination(unwrapping:destination:)``: ```swift Button { @@ -102,14 +105,14 @@ Button { } label: { Text("Go to counter") } -.navigationDestination( - unwrapping: self.$model.destination, - case: /Destination.counter -) { $number in +.navigationDestination(unwrapping: self.$model.destination.counter) { $number in CounterView(number: $number) } ``` -Note that the `case` argument is specified via a concept known as "case paths", which are like -key paths except tuned specifically for enums and cases rather than structs and properties. See - for more information. +## Topics + +### Navigation views and modifiers + +- ``SwiftUI/View/navigationDestination(unwrapping:destination:)`` +- ``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md index c60a0ba950..72d67c7224 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md @@ -46,11 +46,12 @@ Sometimes it is not optimal to model presentation destinations as optionals. In feature can navigate to multiple, mutually exclusive screens, then an enum is more appropriate. There is an additional overload of the `sheet` for this situation. If you model your destinations -as an enum: +as a "case-pathable" enum: ```swift @State var destination: Destination? +@CasePathable enum Destination { var counter(Int) // More destinations @@ -64,10 +65,7 @@ var body: some View { List { // ... } - .sheet( - unwrapping: self.$destination, - case: /Destination.counter - ) { $number in + .sheet(unwrapping: self.$destination.counter) { $number in CounterView(number: $number) } } @@ -93,11 +91,13 @@ struct ContentView: View { } ``` -And if the popover state is represented as an enum, then you can do the following: +And if the popover state is represented as a "case-pathable" enum, then you can do the following: ```swift struct ContentView: View { @State var destination: Destination? + + @CasePathable enum Destination { case counter(Int) // More destinations @@ -107,10 +107,7 @@ struct ContentView: View { List { // ... } - .popover( - unwrapping: self.$destination, - case: /Destination.counter - ) { $number in + .popover(unwrapping: self.$destination.counter) { $number in CounterView(number: $number) } } @@ -137,11 +134,13 @@ struct ContentView: View { } ``` -And if the cover's' state is represented as an enum, then you can do the following: +And if the covers' state is represented as a "case-pathable" enum, then you can do the following: ```swift struct ContentView: View { @State var destination: Destination? + + @CasePathable enum Destination { case counter(Int) // More destinations @@ -151,12 +150,17 @@ struct ContentView: View { List { // ... } - .fullscreenCover( - unwrapping: self.$destination, - case: /Destination.counter - ) { $number in + .fullscreenCover(unwrapping: self.$destination.counter) { $number in CounterView(number: $number) } } } ``` + +## Topics + +### Presentation modifiers + +- ``SwiftUI/View/fullScreenCover(unwrapping:onDismiss:content:)`` +- ``SwiftUI/View/popover(unwrapping:attachmentAnchor:arrowEdge:content:)`` +- ``SwiftUI/View/sheet(unwrapping:onDismiss:content:)`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md index bbc4435a7a..3eea4ab2c0 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -29,16 +29,16 @@ be dismissed. Driving navigation from state like this can be incredibly powerful: -* It guarantees that your model will always be in sync with the visual representation of the UI. -It shouldn't be possible for a piece of state to be non-`nil` and not have the corresponding view -present. -* It easily enables deep linking capabilities. If all forms of navigation in your application are -driven off of state, then you can instantly open your application into any state imaginable by -simply constructing a piece of state, handing it to SwiftUI, and letting it do its thing. -* It also allows you to write unit tests for navigation logic without resorting to UI tests, which -can be slow, flakey and introduce instability into your test suite. If you write a unit test that -shows when a user performs an action that a piece of state went from `nil` to non-`nil`, then you -can be assured that the user would be navigated to the next screen. + * It guarantees that your model will always be in sync with the visual representation of the UI. + It shouldn't be possible for a piece of state to be non-`nil` and not have the corresponding + view present. + * It easily enables deep linking capabilities. If all forms of navigation in your application are + driven off of state, then you can instantly open your application into any state imaginable by + simply constructing a piece of state, handing it to SwiftUI, and letting it do its thing. + * It also allows you to write unit tests for navigation logic without resorting to UI tests, which + can be slow, flakey and introduce instability into your test suite. If you write a unit test + that shows when a user performs an action that a piece of state went from `nil` to non-`nil`, + then you can be assured that the user would be navigated to the next screen. So, this is why state-driven navigation is so great. So, what tools does SwiftUI gives us to embrace this pattern? @@ -193,27 +193,26 @@ and more from a particular case of that enum. ## SwiftUINavigation's tools The tools that ship with this library aim to solve the problems discussed above, and more. There are -new APIs for sheets, popovers, covers, alerts, confirmation dialogs _and_ navigation links that +new APIs for sheets, popovers, covers, alerts, confirmation dialogs _and_ navigation links that allow you to model destinations as an enum and drive navigation by a particular case of the enum. All of the APIs for these seemingly disparate forms of navigation are unified by a single pattern. -You first specify a binding to the optional enum driving navigation, and then you specify the case -of the enum that you want to isolate. +You first specify a binding to an optional value driving navigation, and then you specify some +content that takes a binding to a non-optional value. -For example, the new sheet API now takes a binding to an optional enum, and something known as a -[`CasePath`][case-paths-gh]: +For example, the new sheet API now takes a binding to an optional: ```swift -func sheet( - unwrapping: Binding, - case: CasePath, - content: @escaping (Binding) -> Content -) -> some View where Content : View +func sheet( + unwrapping: Binding, + content: @escaping (Binding) -> Content +) -> some View ``` -This allows you to drive the presentation and dismiss of a sheet from a particular case of an enum. +This single API allows you to not only drive the presentation and dismiss of a sheet from an +optional value, but also from a particular case of an enum. -In order to isolate a specific case of an enum we must make use of our [CasePaths][case-paths-gh] +In order to isolate a specific case of an enum we make use of our [CasePaths][case-paths-gh] library. A case path is like a key path, except it is specifically tuned for abstracting over the shape of enums rather than structs. A key path abstractly bundles up the functionality of getting and setting a property on a struct, whereas a case path bundles up the functionality of "extracting" @@ -230,6 +229,7 @@ class FeatureModel { var destination: Destination? // ... + @CasePathable enum Destination { case add(Item) case duplicate(Item) @@ -238,45 +238,35 @@ class FeatureModel { } ``` +We apply that `@CasePathable` macro to the enum in order to enable "dynamic case lookup" for SwiftUI +bindings, which will allow an optional binding to an enum chain into a particular case. + Suppose we want the `add` destination to be shown in a sheet, the `duplicate` destination to be shown in a popover, and the `edit` destination in a drill-down. We can do so easily using the APIs that ship with this library: ```swift -.popover( - unwrapping: self.$model.destination, - case: /FeatureModel.Destination.duplicate -) { $item in +.popover(unwrapping: self.$model.destination.duplicate) { $item in DuplicateItemView(item: $item) } -.sheet( - unwrapping: self.$model.destination, - case: /FeatureModel.Destination.add -) { $item in +.sheet(unwrapping: self.$model.destination.add) { $item in AddItemView(item: $item) } -.navigationDestination( - unwrapping: self.$model.destination, - case: /FeatureModel.Destination.edit -) { $item in +.navigationDestination(unwrapping: self.$model.destination.edit) { $item in EditItemView(item: $item) } ``` Even though all 3 forms of navigation are visually quite different, describing how to present them is very consistent. You simply provide the binding to the optional enum held in the model, and then -you construct a case path for a particular case, which can be done by prefixing the case with a -forward slash. +you dot-chain into a particular case. -The above code uses the `navigationDestination` view modifier, which is only available in iOS 16. -If you must support iOS 15 and earlier, you can use the following initializer on `NavigationLink`, -which also has a very similar API to the above: +The above code uses the `navigationDestination` view modifier, which is only available in iOS 16 and +later. If you must support iOS 15 and earlier, you can use the following initializer on +`NavigationLink`, which also has a very similar API to the above: ```swift -NavigationLink( - unwrapping: self.$model.destination, - case: /FeatureModel.Destination.edit -) { isActive in +NavigationLink(unwrapping: self.$model.destination.edit) { isActive in self.model.setEditIsActive(isActive) } destination: { $item in EditItemView(item: $item) @@ -285,8 +275,8 @@ NavigationLink( } ``` -That is the basics of using this library's APIs for driving navigation off of state. Learn more -by reading the articles below. +That is the basics of using this library's APIs for driving navigation off of state. Learn more by +reading the articles below. ## Topics @@ -298,7 +288,6 @@ alerts, dialogs, sheets, popovers, covers, and navigation links all from binding - - - -- - [case-paths-gh]: http://github.com/pointfreeco/swift-case-paths diff --git a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md new file mode 100644 index 0000000000..4f37a20049 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md @@ -0,0 +1,46 @@ +# Deprecations + +Review unsupported SwiftUI Navigation APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Views + +- ``IfLet`` +- ``IfCaseLet`` +- ``SwiftUI/NavigationLink/init(unwrapping:case:onNavigate:destination:label:)`` +- ``SwiftUI/NavigationLink/init(unwrapping:destination:onNavigate:label:)`` +- ``SwiftUI/NavigationLink/init(unwrapping:case:destination:onNavigate:label:)`` +- ``Switch`` + +### View modifiers + +- ``SwiftUI/View/alert(title:unwrapping:case:actions:message:)`` +- ``SwiftUI/View/alert(unwrapping:action:)-7da26`` +- ``SwiftUI/View/alert(unwrapping:action:)-6y2fk`` +- ``SwiftUI/View/alert(unwrapping:action:)-867h5`` +- ``SwiftUI/View/alert(unwrapping:case:action:)-14fwn`` +- ``SwiftUI/View/alert(unwrapping:case:action:)-3yw6u`` +- ``SwiftUI/View/alert(unwrapping:case:action:)-4w3oq`` +- ``SwiftUI/View/confirmationDialog(title:titleVisibility:unwrapping:case:actions:message:)`` +- ``SwiftUI/View/confirmationDialog(unwrapping:action:)-9465l`` +- ``SwiftUI/View/confirmationDialog(unwrapping:action:)-4f8ze`` +- ``SwiftUI/View/confirmationDialog(unwrapping:action:)-29s77`` +- ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-uncl`` +- ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-2ddxv`` +- ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-7oi9`` +- ``SwiftUI/View/fullScreenCover(unwrapping:case:onDismiss:content:)`` +- ``SwiftUI/View/navigationDestination(unwrapping:case:destination:)`` +- ``SwiftUI/View/popover(unwrapping:case:attachmentAnchor:arrowEdge:content:)`` +- ``SwiftUI/View/sheet(unwrapping:case:onDismiss:content:)`` + +### Bindings + +- ``SwiftUI/Binding/init(unwrapping:case:)`` +- ``SwiftUI/Binding/case(_:)`` +- ``SwiftUI/Binding/isPresent(_:)`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Switch.md b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Switch.md new file mode 100644 index 0000000000..fa849e2bf3 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Switch.md @@ -0,0 +1,8 @@ +# ``Switch`` + +## Topics + +### Supporting views + +- ``CaseLet`` +- ``Default`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md index 53dc495143..77cbfdfcca 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md @@ -46,9 +46,12 @@ to that enum. - - - -- - +### Deprecated interfaces + +- + ## See Also The collection of videos from [Point-Free](https://www.pointfree.co) that dive deep into the diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index 88049a377d..317b40e733 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -58,35 +58,5 @@ Binding(unwrapping: value).map(content) } } - - /// Presents a full-screen cover using a binding and case path as a data source for the sheet's - /// content. - /// - /// A version of `fullScreenCover(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` - /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or - /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the sheet. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @available(iOS 14, tvOS 14, watchOS 7, *) - @available(macOS, unavailable) - public func fullScreenCover( - unwrapping enum: Binding, - case casePath: CasePath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.fullScreenCover( - unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) - } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift deleted file mode 100644 index e2af61ba3b..0000000000 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ /dev/null @@ -1,94 +0,0 @@ -#if canImport(SwiftUI) - import SwiftUI - - /// A view that computes content by extracting a case from a binding to an enum and passing a - /// non-optional binding to the case's associated value to its content closure. - /// - /// Useful when working with enum state and building views that require the associated value at a - /// particular case. - /// - /// For example, a warehousing application may model the status of an inventory item using an enum. - /// ``IfCaseLet`` can be used to produce bindings to the associated values of each case. - /// - /// ```swift - /// enum ItemStatus { - /// case inStock(quantity: Int) - /// case outOfStock(isOnBackOrder: Bool) - /// } - /// - /// struct InventoryItemView: View { - /// @State var status: ItemStatus - /// - /// var body: some View { - /// IfCaseLet(self.$status, pattern: /ItemStatus.inStock) { $quantity in - /// HStack { - /// Text("Quantity: \(quantity)") - /// Stepper("Quantity", value: $quantity) - /// } - /// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } - /// } - /// IfCaseLet(self.$status, pattern: /ItemStatus.outOfStock) { $isOnBackOrder in - /// Toggle("Is on back order?", isOn: $isOnBackOrder) - /// Button("In stock") { self.status = .inStock(quantity: 1) } - /// } - /// } - /// } - /// ``` - /// - /// To exhaustively handle every case of a binding to an enum, see ``Switch``. Or, to unwrap a - /// binding to an optional, see ``IfLet``. - public struct IfCaseLet: View - where IfContent: View, ElseContent: View { - public let `enum`: Binding - public let casePath: CasePath - public let ifContent: (Binding) -> IfContent - public let elseContent: ElseContent - - /// Computes content by extracting a case from a binding to an enum and passing a non-optional - /// binding to the case's associated value to its content closure. - /// - /// - Parameters: - /// - enum: A binding to an enum that holds the source of truth for the content at a particular - /// case. When `casePath` successfully extracts a value from `enum`, a non-optional binding to - /// the value is passed to the `content` closure. The closure can use this binding to produce - /// its content and write changes back to the source of truth. Upstream changes to the case's - /// value will also be instantly reflected in the presented content. If `enum` becomes a - /// different case, nothing is computed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the content. - /// - ifContent: A closure for computing content when `enum` matches a particular case. - /// - elseContent: A closure for computing content when `enum` does not match the case. - public init( - _ `enum`: Binding, - pattern casePath: CasePath, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder else elseContent: () -> ElseContent - ) { - self.casePath = casePath - self.elseContent = elseContent() - self.enum = `enum` - self.ifContent = ifContent - } - - public var body: some View { - if let $case = Binding(unwrapping: self.enum, case: self.casePath) { - self.ifContent($case) - } else { - self.elseContent - } - } - } - - extension IfCaseLet where ElseContent == EmptyView { - public init( - _ `enum`: Binding, - pattern casePath: CasePath, - @ViewBuilder ifContent: @escaping (Binding) -> IfContent - ) { - self.casePath = casePath - self.elseContent = EmptyView() - self.enum = `enum` - self.ifContent = ifContent - } - } -#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift deleted file mode 100644 index e4576f0c10..0000000000 --- a/Sources/SwiftUINavigation/IfLet.swift +++ /dev/null @@ -1,89 +0,0 @@ -#if canImport(SwiftUI) - import SwiftUI - - /// A view that computes content by unwrapping a binding to an optional and passing a non-optional - /// binding to its content closure. - /// - /// Useful when working with optional state and building views that require non-optional state. - /// - /// For example, a warehousing application may model the quantity of an inventory item using an - /// optional integer, where a `nil` value denotes an item that is out-of-stock. In order to produce - /// a binding to a non-optional integer for a stepper, ``IfLet`` can be used to safely unwrap the - /// optional binding. - /// - /// ```swift - /// struct InventoryItemView: View { - /// @State var quantity: Int? - /// - /// var body: some View { - /// IfLet(self.$quantity) { $quantity in - /// HStack { - /// Text("Quantity: \(quantity)") - /// Stepper("Quantity", value: $quantity) - /// } - /// Button("Out of stock") { self.quantity = nil } - /// } else: { - /// Button("In stock") { self.quantity = 1 } - /// } - /// } - /// } - /// ``` - /// - /// To unwrap a particular case of a binding to an enum, see ``IfCaseLet``, or, to exhaustively - /// handle every case, see ``Switch``. - public struct IfLet: View - where IfContent: View, ElseContent: View { - public let value: Binding - public let ifContent: (Binding) -> IfContent - public let elseContent: ElseContent - - /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to - /// its content closure. - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the content. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The - /// closure can use this binding to produce its content and write changes back to the source - /// of truth. Upstream changes to `value` will also be instantly reflected in the presented - /// content. If `value` becomes `nil`, the `elseContent` closure is used to produce content - /// instead. - /// - ifContent: A closure for computing content when `value` is non-`nil`. - /// - elseContent: A closure for computing content when `value` is `nil`. - public init( - _ value: Binding, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder else elseContent: () -> ElseContent - ) { - self.value = value - self.ifContent = ifContent - self.elseContent = elseContent() - } - - public var body: some View { - if let $value = Binding(unwrapping: self.value) { - self.ifContent($value) - } else { - self.elseContent - } - } - } - - extension IfLet where ElseContent == EmptyView { - /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to - /// its content closure. - /// - /// - Parameters: - /// - value: A binding to an optional source of truth for the content. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `ifContent` closure. The - /// closure can use this binding to produce its content and write changes back to the source - /// of truth. Upstream changes to `value` will also be instantly reflected in the presented - /// content. If `value` becomes `nil`, nothing is computed. - /// - ifContent: A closure for computing content when `value` is non-`nil`. - public init( - _ value: Binding, - @ViewBuilder then ifContent: @escaping (Binding) -> IfContent - ) { - self.init(value, then: ifContent, else: { EmptyView() }) - } - } -#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 33f11fe933..fa3ccf1375 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,175 +1,1873 @@ #if canImport(SwiftUI) import SwiftUI + @_spi(RuntimeWarn) import SwiftUINavigationCore - // NB: Deprecated after 0.5.0 + // NB: Deprecated after 1.0.2 @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) extension View { - #if swift(>=5.7) - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ + @available(*, deprecated, renamed: "alert(_:action:)") + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } ) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.alert(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) + } + + @available(*, deprecated, renamed: "alert(_:action:)") + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.alert( + (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) } - } - } + }, + message: { $0.message.map { Text($0) } } + ) + } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ + @available(*, deprecated, renamed: "confirmationDialog(_:action:)") + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + }, + message: { $0.message.map { Text($0) } } ) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) + } + + @available(*, deprecated, renamed: "confirmationDialog(_:action:)") + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.confirmationDialog( + value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), + isPresented: value.isPresent(), + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: handler) } + }, + message: { $0.message.map { Text($0) } } + ) + } + } + + extension View { + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func alert( + title: (Case) -> Text, + unwrapping enum: Binding, + case casePath: AnyCasePath, + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M + ) -> some View { + self.alert( + title: title, + unwrapping: `enum`.case(casePath), + actions: actions, + message: message + ) + } + + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func alert( + unwrapping `enum`: Binding, + case casePath: AnyCasePath>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.alert(`enum`.case(casePath), action: handler) + } + + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func alert( + unwrapping `enum`: Binding, + case casePath: AnyCasePath>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.alert(`enum`.case(casePath), action: handler) + } + + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func confirmationDialog( + title: (Case) -> Text, + titleVisibility: Visibility = .automatic, + unwrapping enum: Binding, + case casePath: AnyCasePath, + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M + ) -> some View { + self.confirmationDialog( + title: title, + titleVisibility: titleVisibility, + unwrapping: `enum`.case(casePath), + actions: actions, + message: message + ) + } + + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: AnyCasePath>, + action handler: @escaping (Value?) -> Void = { (_: Never?) in } + ) -> some View { + self.confirmationDialog( + `enum`.case(casePath), + action: handler + ) + } + + @available( + iOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 12, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 8, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: AnyCasePath>, + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + ) -> some View { + self.confirmationDialog( + `enum`.case(casePath), + action: handler + ) + } + + @available( + iOS, introduced: 14, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available(macOS, unavailable) + @available( + tvOS, introduced: 14, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 7, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func fullScreenCover( + unwrapping enum: Binding, + case casePath: AnyCasePath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.fullScreenCover( + unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + } + + @available( + iOS, introduced: 16, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 13, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 16, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 9, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func navigationDestination( + unwrapping enum: Binding, + case casePath: AnyCasePath, + @ViewBuilder destination: (Binding) -> Destination + ) -> some View { + self.navigationDestination(unwrapping: `enum`.case(casePath), destination: destination) + } + + @available( + iOS, introduced: 13, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 10.15, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + unwrapping enum: Binding, + case casePath: AnyCasePath, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View where Content: View { + self.popover( + unwrapping: `enum`.case(casePath), + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge, + content: content + ) + } + + @available( + iOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @MainActor + public func sheet( + unwrapping enum: Binding, + case casePath: AnyCasePath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + } + } + + extension Binding { + @available( + iOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public init?(unwrapping enum: Binding, case casePath: AnyCasePath) { + guard var `case` = casePath.extract(from: `enum`.wrappedValue) + else { return nil } + + self.init( + get: { + `case` = casePath.extract(from: `enum`.wrappedValue) ?? `case` + return `case` + }, + set: { + guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } + `case` = $0 + `enum`.transaction($1).wrappedValue = casePath.embed($0) } + ) + } + + @available( + iOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func `case`(_ casePath: AnyCasePath) -> Binding + where Value == Enum? { + .init( + get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, + set: { newValue, transaction in + self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) + } + ) + } + + @available( + iOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, deprecated: 9999, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public func isPresent(_ casePath: AnyCasePath) -> Binding + where Value == Enum? { + self.case(casePath).isPresent() + } + } + + public struct IfCaseLet: View + where IfContent: View, ElseContent: View { + public let `enum`: Binding + public let casePath: AnyCasePath + public let ifContent: (Binding) -> IfContent + public let elseContent: ElseContent + + @available( + iOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + public init( + _ `enum`: Binding, + pattern casePath: AnyCasePath, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent + ) { + self.casePath = casePath + self.elseContent = elseContent() + self.enum = `enum` + self.ifContent = ifContent + } + + public var body: some View { + if let $case = Binding(unwrapping: self.enum, case: self.casePath) { + self.ifContent($case) + } else { + self.elseContent } + } + } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ + @available( + iOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + macOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + tvOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + watchOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + extension IfCaseLet where ElseContent == EmptyView { + public init( + _ `enum`: Binding, + pattern casePath: AnyCasePath, + @ViewBuilder ifContent: @escaping (Binding) -> IfContent + ) { + self.casePath = casePath + self.elseContent = EmptyView() + self.enum = `enum` + self.ifContent = ifContent + } + } + + public struct IfLet: View + where IfContent: View, ElseContent: View { + public let value: Binding + public let ifContent: (Binding) -> IfContent + public let elseContent: ElseContent + + @available( + iOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + ) + public init( + _ value: Binding, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent + ) { + self.value = value + self.ifContent = ifContent + self.elseContent = elseContent() + } + + public var body: some View { + if let $value = Binding(unwrapping: self.value) { + self.ifContent($value) + } else { + self.elseContent + } + } + } + + @available( + iOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + macOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + tvOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + @available( + watchOS, deprecated: 9999, + message: "Use '$enum.case.map { $case in … }' with a '@CasePathable' enum, instead." + ) + extension IfLet where ElseContent == EmptyView { + public init( + _ value: Binding, + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent + ) { + self.init(value, then: ifContent, else: { EmptyView() }) + } + } + + extension NavigationLink { + @available( + iOS, introduced: 13, deprecated: 16, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + macOS, introduced: 10.15, deprecated: 13, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + tvOS, introduced: 13, deprecated: 16, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + @available( + watchOS, introduced: 6, deprecated: 9, + message: + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + ) + public init( + unwrapping enum: Binding, + case casePath: AnyCasePath, + onNavigate: @escaping (Bool) -> Void, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + unwrapping: `enum`.case(casePath), + onNavigate: onNavigate, + destination: destination, + label: label ) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.confirmationDialog(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) - } + } + } + + @available( + iOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + public struct Switch: View { + public let `enum`: Binding + public let content: Content + + private init( + enum: Binding, + @ViewBuilder content: () -> Content + ) { + self.enum = `enum` + self.content = content() + } + + public var body: some View { + self.content + .environmentObject(BindingObject(binding: self.enum)) + } + } + + @available( + iOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + public struct CaseLet: View + where Content: View { + @EnvironmentObject private var `enum`: BindingObject + public let casePath: AnyCasePath + public let content: (Binding) -> Content + + public init( + _ casePath: AnyCasePath, + @ViewBuilder then content: @escaping (Binding) -> Content + ) { + self.casePath = casePath + self.content = content + } + + public var body: some View { + Binding(unwrapping: self.enum.wrappedValue, case: self.casePath).map(self.content) + } + } + + @available( + iOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + public struct Default: View { + private let content: Content + + public init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + public var body: some View { + self.content + } + } + + @available( + iOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + macOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + tvOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + @available( + watchOS, deprecated: 9999, + message: + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + ) + extension Switch { + public init( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + CaseLet, + Default + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else { + content.1 } } + } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ - ) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } - ) -> some View { - self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) - } + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> CaseLet + ) + where + Content == _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + { + self.init(`enum`) { + content() + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else { + content.2 } } - #else - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ - ) - public func alert( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.alert(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) - } + } + + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default<_ExhaustivityCheckView> + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default + > + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else { + content.3 } } + } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ - ) - public func alert( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) - } + public init( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + Default + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else { + content.4 } } + } - @_disfavoredOverload - @available( - *, - deprecated, - message: - """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. - """ - ) - public func confirmationDialog( - unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.confirmationDialog(unwrapping: value) { (value: Value?) in - if let value = value { - await handler(value) - } + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + Default<_ExhaustivityCheckView> + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + CaseLet, + Default + > + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else if content.4.casePath.extract(from: `enum`.wrappedValue) != nil { + content.4 + } else { + content.5 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default + > + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else if content.4.casePath.extract(from: `enum`.wrappedValue) != nil { + content.4 + } else if content.5.casePath.extract(from: `enum`.wrappedValue) != nil { + content.5 + } else { + content.6 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default + > + > + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else if content.4.casePath.extract(from: `enum`.wrappedValue) != nil { + content.4 + } else if content.5.casePath.extract(from: `enum`.wrappedValue) != nil { + content.5 + } else if content.6.casePath.extract(from: `enum`.wrappedValue) != nil { + content.6 + } else { + content.7 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + Default + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else if content.4.casePath.extract(from: `enum`.wrappedValue) != nil { + content.4 + } else if content.5.casePath.extract(from: `enum`.wrappedValue) != nil { + content.5 + } else if content.6.casePath.extract(from: `enum`.wrappedValue) != nil { + content.6 + } else if content.7.casePath.extract(from: `enum`.wrappedValue) != nil { + content.7 + } else { + content.8 + } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + Default<_ExhaustivityCheckView> + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + Case9, Content9, + DefaultContent + >( + _ enum: Binding, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default + > + > + { + self.init(enum: `enum`) { + let content = content().value + if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { + content.0 + } else if content.1.casePath.extract(from: `enum`.wrappedValue) != nil { + content.1 + } else if content.2.casePath.extract(from: `enum`.wrappedValue) != nil { + content.2 + } else if content.3.casePath.extract(from: `enum`.wrappedValue) != nil { + content.3 + } else if content.4.casePath.extract(from: `enum`.wrappedValue) != nil { + content.4 + } else if content.5.casePath.extract(from: `enum`.wrappedValue) != nil { + content.5 + } else if content.6.casePath.extract(from: `enum`.wrappedValue) != nil { + content.6 + } else if content.7.casePath.extract(from: `enum`.wrappedValue) != nil { + content.7 + } else if content.8.casePath.extract(from: `enum`.wrappedValue) != nil { + content.8 + } else { + content.9 } } + } + + public init< + Case1, Content1, + Case2, Content2, + Case3, Content3, + Case4, Content4, + Case5, Content5, + Case6, Content6, + Case7, Content7, + Case8, Content8, + Case9, Content9 + >( + _ enum: Binding, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(`enum`) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + content.value.8 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + } + + public struct _ExhaustivityCheckView: View { + @EnvironmentObject private var `enum`: BindingObject + let file: StaticString + let line: UInt - @_disfavoredOverload - @available( - *, - deprecated, - message: + public var body: some View { + #if DEBUG + let message = """ + Warning: Switch.body@\(self.file):\(self.line) + + "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" + + Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ + provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. """ - 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. + VStack(spacing: 17) { + self.exclamation() + .font(.largeTitle) + + Text(message) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.white) + .padding() + .background(Color.red.edgesIgnoringSafeArea(.all)) + .onAppear { runtimeWarn(message, file: self.file, line: self.line) } + #else + EmptyView() + #endif + } + + func exclamation() -> some View { + #if os(macOS) + return Text("⚠️") + #else + return Image(systemName: "exclamationmark.triangle.fill") + #endif + } + } + + private class BindingObject: ObservableObject { + let wrappedValue: Binding + + init(binding: Binding) { + self.wrappedValue = binding + } + } + + private func describeCase(_ enum: Enum) -> String { + let mirror = Mirror(reflecting: `enum`) + let `case`: String + if mirror.displayStyle == .enum, let child = mirror.children.first, let label = child.label { + let childMirror = Mirror(reflecting: child.value) + let associatedValuesMirror = + childMirror.displayStyle == .tuple + ? childMirror + : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) + `case` = """ + \(label)(\ + \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ + ) """ - ) - public func confirmationDialog( - unwrapping `enum`: Binding, - case casePath: CasePath>, - action handler: @escaping (Value) async -> Void - ) -> some View { - self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { - await handler(value) - } + } else { + `case` = "\(`enum`)" + } + var type = String(reflecting: Enum.self) + if let index = type.firstIndex(of: ".") { + type.removeSubrange(...index) + } + return "\(type).\(`case`)" + } + + // NB: Deprecated after 0.5.0 + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension View { + @_disfavoredOverload + @available( + *, + deprecated, + message: + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + ) + public func alert( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.alert(value) { (value: Value?) in + if let value = value { + await handler(value) + } + } + } + + @_disfavoredOverload + @available( + *, + deprecated, + message: + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + ) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } + } + } + + @_disfavoredOverload + @available( + *, + deprecated, + message: + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + ) + public func confirmationDialog( + unwrapping value: Binding?>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.confirmationDialog(unwrapping: value) { (value: Value?) in + if let value = value { + await handler(value) } } - #endif + } + + @_disfavoredOverload + @available( + *, + deprecated, + message: + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + ) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action handler: @escaping (Value) async -> Void = { (_: Void) async in } + ) -> some View { + self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in + if let value = value { + await handler(value) + } + } + } } // NB: Deprecated after 0.3.0 diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index d131222bf7..56e534060b 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -1,4 +1,4 @@ -#if swift(>=5.7) && canImport(SwiftUI) +#if canImport(SwiftUI) import SwiftUI @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) @@ -56,31 +56,6 @@ } } } - - /// Pushes a view onto a `NavigationStack` using a binding and case path as a data source for - /// the destination's content. - /// - /// A version of `View.navigationDestination(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the destination - /// at a particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a - /// value, a non-optional binding to the value is passed to the `content` closure. You use - /// this binding to produce content that the system pushes to the user in a navigation - /// stack. Changes made to the destination's binding will be reflected back in the source of - /// truth. Likewise, changes to `enum` at the given case are instantly reflected in the - /// destination. If `enum` becomes `nil`, or becomes a case other than the one identified by - /// `casePath`, the destination is popped. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the destination. - /// - destination: A closure returning the content of the destination. - public func navigationDestination( - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder destination: (Binding) -> Destination - ) -> some View { - self.navigationDestination(unwrapping: `enum`.case(casePath), destination: destination) - } } // NB: This view modifier works around a bug in SwiftUI's built-in modifier: @@ -107,4 +82,4 @@ else { return true } return false }() -#endif // swift(>=5.7) && canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index 3b387f9d51..b604a7919e 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -2,7 +2,12 @@ import SwiftUI extension NavigationLink { - /// Creates a navigation link that presents the destination view when a bound value is non-`nil`. + /// Creates a navigation link that presents the destination view when a bound value is + /// non-`nil`. + /// + /// > Note: This interface is deprecated to match the availability of the corresponding SwiftUI + /// > API. If you are targeting iOS 16 or later, use + /// > ``SwiftUI/View/navigationDestination(unwrapping:destination:)``, instead. /// /// This allows you to drive navigation to a destination from an optional value. When the /// optional value becomes non-`nil` a binding to an honest value is derived and passed to the @@ -35,14 +40,14 @@ /// /// - Parameters: /// - value: A binding to an optional source of truth for the destination. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. The - /// destination can use this binding to produce its content and write changes back to the - /// source of truth. Upstream changes to `value` will also be instantly reflected in the + /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. + /// The destination can use this binding to produce its content and write changes back to + /// the source of truth. Upstream changes to `value` will also be instantly reflected in the /// destination. If `value` becomes `nil`, the destination is dismissed. /// - onNavigate: A closure that executes when the link becomes active or inactive with a - /// boolean that describes if the link was activated or not. Use this closure to populate the - /// source of truth when it is passed a value of `true`. When passed `false`, the system will - /// automatically write `nil` to `value`. + /// boolean that describes if the link was activated or not. Use this closure to populate + /// the source of truth when it is passed a value of `true`. When passed `false`, the system + /// will automatically write `nil` to `value`. /// - destination: A view for the navigation link to present. /// - label: A view builder to produce a label describing the `destination` to present. @available(iOS, introduced: 13, deprecated: 16) @@ -61,77 +66,5 @@ label: label ) } - - /// Creates a navigation link that presents the destination view when a bound enum is non-`nil` - /// and matches a particular case. - /// - /// This allows you to drive navigation to a destination from an enum of values. When the - /// optional value becomes non-`nil` _and_ matches a particular case of the enum, a binding to an - /// honest value is derived and passed to the destination. Any edits made to the binding in the - /// destination are automatically reflected in the parent. - /// - /// ```swift - /// struct ContentView: View { - /// @State var destination: Destination? - /// @State var posts: [Post] - /// - /// enum Destination { - /// case edit(Post) - /// /* other destinations */ - /// } - /// - /// var body: some View { - /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$destination, case: /Destination.edit) { isActive in - /// self.destination = isActive ? .edit(post) : nil - /// } destination: { $draft in - /// EditPostView(post: $draft) - /// } label: { - /// Text(post.title) - /// } - /// } - /// } - /// } - /// - /// struct EditPostView: View { - /// @Binding var post: Post - /// var body: some View { ... } - /// } - /// ``` - /// - /// See `NavigationLink.init(unwrapping:destination:onNavigate:label)` for a version of this - /// initializer that works with optional state instead of enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional source of truth for the destination. When `enum` is - /// non-`nil`, and `casePath` successfully extracts a value, a non-optional binding to the - /// value is passed to the `destination` closure. The destination can use this binding to - /// produce its content and write changes back to the source of truth. Upstream changes to - /// `enum` will also be instantly reflected in the destination. If `enum` becomes `nil`, the - /// destination is dismissed. - /// - onNavigate: A closure that executes when the link becomes active or inactive with a - /// boolean that describes if the link was activated or not. Use this closure to populate the - /// source of truth when it is passed a value of `true`. When passed `false`, the system will - /// automatically write `nil` to `enum`. - /// - destination: A view for the navigation link to present. - /// - label: A view builder to produce a label describing the `destination` to present. - @available(iOS, introduced: 13, deprecated: 16) - @available(macOS, introduced: 10.15, deprecated: 13) - @available(tvOS, introduced: 13, deprecated: 16) - @available(watchOS, introduced: 6, deprecated: 9) - public init( - unwrapping enum: Binding, - case casePath: CasePath, - onNavigate: @escaping (Bool) -> Void, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - @ViewBuilder label: () -> Label - ) where Destination == WrappedDestination? { - self.init( - unwrapping: `enum`.case(casePath), - onNavigate: onNavigate, - destination: destination, - label: label - ) - } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 7c5fde4e0d..43ce6238c9 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -5,9 +5,9 @@ /// Presents a popover using a binding as a data source for the popover's content. /// /// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content - /// closure. This value, however, is completely static, which prevents the popover from modifying - /// it. + /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the + /// content closure. This value, however, is completely static, which prevents the popover from + /// modifying it. /// /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This /// gives the popover the ability to write changes back to its source of truth. @@ -36,12 +36,13 @@ /// /// - Parameters: /// - value: A binding to an optional source of truth for the popover. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You use - /// this binding to produce content that the system presents to the user in a popover. Changes - /// made to the popover's binding will be reflected back in the source of truth. Likewise, - /// changes to `value` are instantly reflected in the popover. If `value` becomes `nil`, the - /// popover is dismissed. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. + /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You + /// use this binding to produce content that the system presents to the user in a popover. + /// Changes made to the popover's binding will be reflected back in the source of truth. + /// Likewise, changes to `value` are instantly reflected in the popover. If `value` becomes + /// `nil`, the popover is dismissed. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the + /// popover. /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's /// arrow. /// - content: A closure returning the content of the popover. @@ -61,40 +62,5 @@ Binding(unwrapping: value).map(content) } } - - /// Presents a popover using a binding and case path as the data source for the popover's content. - /// - /// A version of `popover(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the popover at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a popover. Changes made to the - /// popover's binding will be reflected back in the source of truth. Likewise, changes to - /// `enum` at the given case are instantly reflected in the popover. If `enum` becomes `nil`, - /// or becomes a case other than the one identified by `casePath`, the popover is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the popover. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow. - /// - content: A closure returning the content of the popover. - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public func popover( - unwrapping enum: Binding, - case casePath: CasePath, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View where Content: View { - self.popover( - unwrapping: `enum`.case(casePath), - attachmentAnchor: attachmentAnchor, - arrowEdge: arrowEdge, - content: content - ) - } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index bf871d9a22..6823b78e3b 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -11,9 +11,9 @@ /// Presents a sheet using a binding as a data source for the sheet's content. /// /// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the content - /// closure. This value, however, is completely static, which prevents the sheet from modifying - /// it. + /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the + /// content closure. This value, however, is completely static, which prevents the sheet from + /// modifying it. /// /// This overload differs in that it passes a _binding_ to the content closure, instead. This /// gives the sheet the ability to write changes back to its source of truth. @@ -41,12 +41,12 @@ /// ``` /// /// - Parameters: - /// - value: A binding to an optional source of truth for the sheet. When `value` is non-`nil`, - /// a non-optional binding to the value is passed to the `content` closure. You use this - /// binding to produce content that the system presents to the user in a sheet. Changes made - /// to the sheet's binding will be reflected back in the source of truth. Likewise, changes - /// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is - /// dismissed. + /// - value: A binding to an optional source of truth for the sheet. When `value` is + /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You + /// use this binding to produce content that the system presents to the user in a sheet. + /// Changes made to the sheet's binding will be reflected back in the source of truth. + /// Likewise, changes to `value` are instantly reflected in the sheet. If `value` becomes + /// `nil`, the sheet is dismissed. /// - onDismiss: The closure to execute when dismissing the sheet. /// - content: A closure returning the content of the sheet. @MainActor @@ -60,32 +60,5 @@ Binding(unwrapping: value).map(content) } } - - /// Presents a sheet using a binding and case path as the data source for the sheet's content. - /// - /// A version of `View.sheet(unwrapping:)` that works with enum state. - /// - /// - Parameters: - /// - enum: A binding to an optional enum that holds the source of truth for the sheet at a - /// particular case. When `enum` is non-`nil`, and `casePath` successfully extracts a value, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to `enum` - /// at the given case are instantly reflected in the sheet. If `enum` becomes `nil`, or - /// becomes a case other than the one identified by `casePath`, the sheet is dismissed. - /// - casePath: A case path that identifies a case of `enum` that holds a source of truth for - /// the sheet. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - @MainActor - public func sheet( - unwrapping enum: Binding, - case casePath: CasePath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) - } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift deleted file mode 100644 index 2bcfbfbc8a..0000000000 --- a/Sources/SwiftUINavigation/Switch.swift +++ /dev/null @@ -1,1118 +0,0 @@ -#if canImport(SwiftUI) - import SwiftUI - @_spi(RuntimeWarn) import SwiftUINavigationCore - - /// A view that can switch over a binding of enum state and exhaustively handle each case. - /// - /// Useful for computing a view from enum state where every case should be handled (using a - /// ``CaseLet`` view), or where there should be a default fallback view (using a ``Default`` view). - /// - /// For example, a warehousing application may model the status of an inventory item using an enum - /// with cases that distinguish in-stock and out-of-stock statuses. ``Switch`` (and ``CaseLet``) can - /// be used to produce bindings to the associated values of each case. - /// - /// ```swift - /// enum ItemStatus { - /// case inStock(quantity: Int) - /// case outOfStock(isOnBackOrder: Bool) - /// } - /// - /// struct InventoryItemView: View { - /// @State var status: ItemStatus - /// - /// var body: some View { - /// Switch(self.$status) { - /// CaseLet(/ItemStatus.inStock) { $quantity in - /// HStack { - /// Text("Quantity: \(quantity)") - /// Stepper("Quantity", value: $quantity) - /// } - /// Button("Out of stock") { self.status = .outOfStock(isOnBackOrder: false) } - /// } - /// CaseLet(/ItemStatus.outOfStock) { $isOnBackOrder in - /// Toggle("Is on back order?", isOn: $isOnBackOrder) - /// Button("In stock") { self.status = .inStock(quantity: 1) } - /// } - /// } - /// } - /// } - /// ``` - /// - /// To unwrap an individual case of a binding to an enum (_i.e._, if exhaustivity is not needed), - /// use ``IfCaseLet``, instead. Or, to unwrap a binding to an optional, use ``IfLet``. - /// - /// > Note: In debug builds, exhaustivity is handled at runtime: if the `Switch` encounters an - /// > unhandled case, and no ``Default`` view is present, a runtime warning is issued and a warning - /// > view is presented. - public struct Switch: View { - public let `enum`: Binding - public let content: Content - - private init( - enum: Binding, - @ViewBuilder content: () -> Content - ) { - self.enum = `enum` - self.content = content() - } - - public var body: some View { - self.content - .environmentObject(BindingObject(binding: self.enum)) - } - } - - /// A view that handles a specific case of enum state in a ``Switch``. - public struct CaseLet: View - where Content: View { - @EnvironmentObject private var `enum`: BindingObject - public let casePath: CasePath - public let content: (Binding) -> Content - - /// Computes content for a particular case of an enum handled by a ``Switch``. - /// - /// - Parameters: - /// - casePath: A case path that identifies a case of the ``Switch``'s enum that holds a source - /// of truth for the content. - /// - content: A closure returning the content to be computed from a binding to an enum case. - public init( - _ casePath: CasePath, - @ViewBuilder then content: @escaping (Binding) -> Content - ) { - self.casePath = casePath - self.content = content - } - - public var body: some View { - Binding(unwrapping: self.enum.wrappedValue, case: self.casePath).map(self.content) - } - } - - /// A view that covers any cases that aren't explicitly addressed in a ``Switch``. - /// - /// If you wish to use ``Switch`` in a non-exhaustive manner (_i.e._, you do not want to provide a - /// ``CaseLet`` for every case of the enum), then you must insert a ``Default`` view at the end of - /// the ``Switch``'s body, or use ``IfCaseLet`` instead. - public struct Default: View { - private let content: Content - - /// Initializes a ``Default`` view that computes content depending on if a binding to enum state - /// does not match a particular case. - /// - /// - Parameter content: A function that returns a view that is visible only when the switch - /// view's state does not match a preceding ``CaseLet`` view. - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - public var body: some View { - self.content - } - } - - extension Switch { - public init( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - CaseLet, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - if content.0.casePath ~= `enum`.wrappedValue { - content.0 - } else { - content.1 - } - } - } - - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> CaseLet - ) - where - Content == _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - { - self.init(`enum`) { - content() - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - default: - content.2 - } - } - } - - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - default: - content.3 - } - } - } - - public init( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - default: - content.4 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - default: - content.5 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default - > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - default: - content.6 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default - > - > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - default: - content.7 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - Default - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - case content.7.casePath: - content.7 - default: - content.8 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - Default<_ExhaustivityCheckView> - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9, - DefaultContent - >( - _ enum: Binding, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - _ConditionalContent< - CaseLet, - Default - > - > - { - self.init(enum: `enum`) { - let content = content().value - switch `enum`.wrappedValue { - case content.0.casePath: - content.0 - case content.1.casePath: - content.1 - case content.2.casePath: - content.2 - case content.3.casePath: - content.3 - case content.4.casePath: - content.4 - case content.5.casePath: - content.5 - case content.6.casePath: - content.6 - case content.7.casePath: - content.7 - case content.8.casePath: - content.8 - default: - content.9 - } - } - } - - public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9 - >( - _ enum: Binding, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - >, - _ConditionalContent< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > - > - >, - _ConditionalContent< - CaseLet, - Default<_ExhaustivityCheckView> - > - > - { - let content = content() - self.init(`enum`) { - content.value.0 - content.value.1 - content.value.2 - content.value.3 - content.value.4 - content.value.5 - content.value.6 - content.value.7 - content.value.8 - Default { _ExhaustivityCheckView(file: file, line: line) } - } - } - } - - public struct _ExhaustivityCheckView: View { - @EnvironmentObject private var `enum`: BindingObject - let file: StaticString - let line: UInt - - public var body: some View { - #if DEBUG - let message = """ - Warning: Switch.body@\(self.file):\(self.line) - - "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" - - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ - provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. - """ - VStack(spacing: 17) { - self.exclamation() - .font(.largeTitle) - - Text(message) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .foregroundColor(.white) - .padding() - .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { runtimeWarn(message, file: self.file, line: self.line) } - #else - EmptyView() - #endif - } - - func exclamation() -> some View { - #if os(macOS) - return Text("⚠️") - #else - return Image(systemName: "exclamationmark.triangle.fill") - #endif - } - } - - private class BindingObject: ObservableObject { - let wrappedValue: Binding - - init(binding: Binding) { - self.wrappedValue = binding - } - } - - private func describeCase(_ enum: Enum) -> String { - let mirror = Mirror(reflecting: `enum`) - let `case`: String - if mirror.displayStyle == .enum, let child = mirror.children.first, let label = child.label { - let childMirror = Mirror(reflecting: child.value) - let associatedValuesMirror = - childMirror.displayStyle == .tuple - ? childMirror - : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) - `case` = """ - \(label)(\ - \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ - ) - """ - } else { - `case` = "\(`enum`)" - } - var type = String(reflecting: Enum.self) - if let index = type.firstIndex(of: ".") { - type.removeSubrange(...index) - } - return "\(type).\(`case`)" - } -#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/WithState.swift b/Sources/SwiftUINavigation/WithState.swift index 13c2f9d0ec..5d7b339728 100644 --- a/Sources/SwiftUINavigation/WithState.swift +++ b/Sources/SwiftUINavigation/WithState.swift @@ -10,7 +10,7 @@ /// /// ```swift /// struct TextField_Previews: PreviewProvider { - /// @State static var text = "" // ⚠️ @State static does not work. + /// @State static var text = "" // ⚠️ @State static does not work. /// /// static var previews: some View { /// TextField("Test", text: self.$text) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index 757fa19fe8..aff3f6ed53 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -53,7 +53,7 @@ /// } /// ``` /// - /// Then, whenever you need to show an alert you can simply construct an ``AlertState`` value to + /// Then, whenever you need to show an alert you can simply construct an `AlertState` value to /// represent the alert: /// /// ```swift @@ -79,8 +79,7 @@ /// } /// ``` /// - /// And in your view you can use the `.alert(unwrapping:action:)` view modifier to present the - /// alert: + /// And in your view you can use the `.alert(_:action:)` view modifier to present the alert: /// /// ```swift /// struct FeatureView: View { @@ -92,7 +91,7 @@ /// self.model.deleteAppButtonTapped() /// } /// } - /// .alert(unwrapping: self.$model.alert) { action in + /// .alert(self.$model.alert) { action in /// self.model.alertButtonTapped(action) /// } /// } diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index faac4cbc74..91f062a7ea 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -228,11 +228,9 @@ } } - #if swift(>=5.7) - extension ButtonStateAction: Sendable where Action: Sendable {} - extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} - extension ButtonState: Sendable where Action: Sendable {} - #endif + extension ButtonStateAction: Sendable where Action: Sendable {} + extension ButtonStateAction._ActionType: Sendable where Action: Sendable {} + extension ButtonState: Sendable where Action: Sendable {} // MARK: - SwiftUI bridging diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 97a9af21d9..ef9f86a10d 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -270,13 +270,11 @@ } } - #if swift(>=5.7) - @available(iOS 13, *) - @available(macOS 12, *) - @available(tvOS 13, *) - @available(watchOS 6, *) - extension ConfirmationDialogState: Sendable where Action: Sendable {} - #endif + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + extension ConfirmationDialogState: Sendable where Action: Sendable {} // MARK: - SwiftUI bridging diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md new file mode 100644 index 0000000000..9807e67d2d --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md @@ -0,0 +1,12 @@ +# ``SwiftUINavigationCore`` + +A few core types included in SwiftUI Navigation. + +## Topics + +### State + +- ``TextState`` +- ``AlertState`` +- ``ConfirmationDialogState`` +- ``ButtonState`` diff --git a/Sources/SwiftUINavigationCore/TextState.swift b/Sources/SwiftUINavigationCore/TextState.swift index f5b1cfc18a..09ac2ee987 100644 --- a/Sources/SwiftUINavigationCore/TextState.swift +++ b/Sources/SwiftUINavigationCore/TextState.swift @@ -78,17 +78,15 @@ case expanded case standard - #if swift(>=5.7.1) - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - var toSwiftUI: SwiftUI.Font.Width { - switch self { - case .compressed: return .compressed - case .condensed: return .condensed - case .expanded: return .expanded - case .standard: return .standard - } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + var toSwiftUI: SwiftUI.Font.Width { + switch self { + case .compressed: return .compressed + case .condensed: return .condensed + case .expanded: return .expanded + case .standard: return .standard } - #endif + } } public enum LineStylePattern: String, Equatable, Hashable, Sendable { @@ -313,40 +311,36 @@ public enum AccessibilityTextContentType: String, Equatable, Hashable, Sendable { case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing - #if compiler(>=5.5.1) - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - var toSwiftUI: SwiftUI.AccessibilityTextContentType { - switch self { - case .console: return .console - case .fileSystem: return .fileSystem - case .messaging: return .messaging - case .narrative: return .narrative - case .plain: return .plain - case .sourceCode: return .sourceCode - case .spreadsheet: return .spreadsheet - case .wordProcessing: return .wordProcessing - } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.AccessibilityTextContentType { + switch self { + case .console: return .console + case .fileSystem: return .fileSystem + case .messaging: return .messaging + case .narrative: return .narrative + case .plain: return .plain + case .sourceCode: return .sourceCode + case .spreadsheet: return .spreadsheet + case .wordProcessing: return .wordProcessing } - #endif + } } public enum AccessibilityHeadingLevel: String, Equatable, Hashable, Sendable { case h1, h2, h3, h4, h5, h6, unspecified - #if compiler(>=5.5.1) - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - var toSwiftUI: SwiftUI.AccessibilityHeadingLevel { - switch self { - case .h1: return .h1 - case .h2: return .h2 - case .h3: return .h3 - case .h4: return .h4 - case .h5: return .h5 - case .h6: return .h6 - case .unspecified: return .unspecified - } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.AccessibilityHeadingLevel { + switch self { + case .h1: return .h1 + case .h2: return .h2 + case .h3: return .h3 + case .h4: return .h4 + case .h5: return .h5 + case .h6: return .h6 + case .unspecified: return .unspecified } - #endif + } } } @@ -430,92 +424,65 @@ } self = state.modifiers.reduce(text) { text, modifier in switch modifier { - #if compiler(>=5.5.1) - case let .accessibilityHeading(level): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.accessibilityHeading(level.toSwiftUI) - } else { - return text - } - case let .accessibilityLabel(value): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - switch value.storage { - case let .verbatim(string): - return text.accessibilityLabel(string) - case let .localized(key, tableName, bundle, comment): - return text.accessibilityLabel( - Text(key, tableName: tableName, bundle: bundle, comment: comment)) - case .concatenated(_, _): - assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") - return text - } - } else { - return text - } - case let .accessibilityTextContentType(type): - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return text.accessibilityTextContentType(type.toSwiftUI) - } else { + case let .accessibilityHeading(level): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.accessibilityHeading(level.toSwiftUI) + } else { + return text + } + case let .accessibilityLabel(value): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + switch value.storage { + case let .verbatim(string): + return text.accessibilityLabel(string) + case let .localized(key, tableName, bundle, comment): + return text.accessibilityLabel( + Text(key, tableName: tableName, bundle: bundle, comment: comment)) + case .concatenated(_, _): + assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") return text } - #else - case .accessibilityHeading, - .accessibilityLabel, - .accessibilityTextContentType: + } else { return text - #endif + } + case let .accessibilityTextContentType(type): + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return text.accessibilityTextContentType(type.toSwiftUI) + } else { + return text + } case let .baselineOffset(baselineOffset): return text.baselineOffset(baselineOffset) case let .bold(isActive): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.bold(isActive) - } else { - return text.bold() - } - #else - _ = isActive + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.bold(isActive) + } else { return text.bold() - #endif + } case let .font(font): return text.font(font) case let .fontDesign(design): - #if swift(>=5.7.1) - if #available(iOS 16.1, macOS 13, tvOS 16.1, watchOS 9.1, *) { - return text.fontDesign(design) - } else { - return text - } - #else - _ = design + if #available(iOS 16.1, macOS 13, tvOS 16.1, watchOS 9.1, *) { + return text.fontDesign(design) + } else { return text - #endif + } case let .fontWeight(weight): return text.fontWeight(weight) case let .fontWidth(width): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.fontWidth(width?.toSwiftUI) - } else { - return text - } - #else - _ = width + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.fontWidth(width?.toSwiftUI) + } else { return text - #endif + } case let .foregroundColor(color): return text.foregroundColor(color) case let .italic(isActive): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - return text.italic(isActive) - } else { - return text.italic() - } - #else - _ = isActive + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + return text.italic(isActive) + } else { return text.italic() - #endif + } case let .kerning(kerning): return text.kerning(kerning) case .monospacedDigit: @@ -549,29 +516,19 @@ return text } case let .strikethrough(isActive, pattern, color): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { - return text.strikethrough(isActive, pattern: pattern.toSwiftUI, color: color) - } else { - return text.strikethrough(isActive, color: color) - } - #else - _ = pattern + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { + return text.strikethrough(isActive, pattern: pattern.toSwiftUI, color: color) + } else { return text.strikethrough(isActive, color: color) - #endif + } case let .tracking(tracking): return text.tracking(tracking) case let .underline(isActive, pattern, color): - #if swift(>=5.7.1) - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { - return text.underline(isActive, pattern: pattern.toSwiftUI, color: color) - } else { - return text.underline(isActive, color: color) - } - #else - _ = pattern - return text.strikethrough(isActive, color: color) - #endif + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), let pattern = pattern { + return text.underline(isActive, pattern: pattern.toSwiftUI, color: color) + } else { + return text.underline(isActive, color: color) + } } } } diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index b4100c0bea..bbe623d4e7 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,115 +1,122 @@ { - "object": { - "pins": [ - { - "package": "combine-schedulers", - "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", - "state": { - "branch": null, - "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version": "1.0.0" - } - }, - { - "package": "swift-case-paths", - "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", - "state": { - "branch": null, - "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version": "1.0.0" - } - }, - { - "package": "swift-clocks", - "repositoryURL": "https://github.com/pointfreeco/swift-clocks", - "state": { - "branch": null, - "revision": "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version": "1.0.0" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections", - "state": { - "branch": null, - "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version": "1.0.4" - } - }, - { - "package": "swift-concurrency-extras", - "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", - "state": { - "branch": null, - "revision": "ea631ce892687f5432a833312292b80db238186a", - "version": "1.0.0" - } - }, - { - "package": "swift-custom-dump", - "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", - "state": { - "branch": null, - "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version": "1.0.0" - } - }, - { - "package": "swift-dependencies", - "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", - "state": { - "branch": null, - "revision": "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version": "1.0.0" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", - "version": "1.3.0" - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": null, - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": "1.0.0" - } - }, - { - "package": "swift-identified-collections", - "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", - "state": { - "branch": null, - "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version": "1.0.0" - } - }, - { - "package": "swift-tagged", - "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", - "state": { - "branch": null, - "revision": "3907a9438f5b57d317001dc99f3f11b46882272b", - "version": "0.10.0" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version": "1.0.2" - } + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "40773cbaf8d71ed5357f297b1ba4073f5b24faaa", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "ea631ce892687f5432a833312292b80db238186a", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "http://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections.git", + "state" : { + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged.git", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version" : "1.0.2" + } + } + ], + "version" : 2 } diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 9690784768..78f07d3627 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -106,7 +106,7 @@ var body: some View { Text("") - .alert(unwrapping: self.$alert) { + .alert(self.$alert) { await self.alertButtonTapped($0) } } From 78f9d72cf667adb47e2040aa373185c88c63f0dc Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 16 Nov 2023 11:49:13 -0800 Subject: [PATCH 023/124] Add `HashableObject` protocol (#133) SwiftUI's built-in navigation tools requires hashability and identifiability of objects, and while objects get identity for free, we must manually equate and hash objects by their object identity. Instead, the library can vend a protocol with default conformances. --- .../Documentation.docc/Articles/Navigation.md | 4 ++++ .../SwiftUINavigation/HashableObject.swift | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Sources/SwiftUINavigation/HashableObject.swift diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md index c3bce8c5b6..14bf6a481c 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -116,3 +116,7 @@ Button { - ``SwiftUI/View/navigationDestination(unwrapping:destination:)`` - ``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` + +### Supporting types + +- ``HashableObject`` diff --git a/Sources/SwiftUINavigation/HashableObject.swift b/Sources/SwiftUINavigation/HashableObject.swift new file mode 100644 index 0000000000..29a08cedd3 --- /dev/null +++ b/Sources/SwiftUINavigation/HashableObject.swift @@ -0,0 +1,20 @@ +/// A protocol that adds a default implementation of `Hashable` to an object based off its object +/// identity. +/// +/// SwiftUI's navigation tools requires `Identifiable` and `Hashable` conformances throughout its +/// APIs, for example `sheet(item:)` requires `Identifiable`, while `navigationDestination(item:)` +/// and `NavigationLink.init(value:)` require `Hashable`. While `Identifiable` conformances come for +/// free on objects based on object identity, there is no such mechanism for `Hashable`. This +/// protocol addresses this shortcoming by providing default implementations of `==` and +/// `hash(into:)`. +public protocol HashableObject: AnyObject, Hashable {} + +extension HashableObject { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} From 80593221414e53877433c75d2e5bb8dd7d93711f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=96hman?= Date: Thu, 23 Nov 2023 17:28:00 +0100 Subject: [PATCH 024/124] Fix typo in SheetsPopoversCovers documentation (#135) --- .../Documentation.docc/Articles/SheetsPopoversCovers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md index 72d67c7224..7e03c029b8 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md @@ -53,7 +53,7 @@ as a "case-pathable" enum: @CasePathable enum Destination { - var counter(Int) + case counter(Int) // More destinations } ``` From a7592b62e808b922c40fef5981cdbb9725ced0b2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 3 Jan 2024 09:59:51 -0800 Subject: [PATCH 025/124] Improve dynamic "case" lookup performance (#137) When writing algorithms against dynamic "case" lookup it's more performant to work directly with the case-pathable type rather than use case key paths, which resolve lazily and have a higher cost. --- Examples/CaseStudies/08-Routing.swift | 8 +- Package.resolved | 17 +- Sources/SwiftUINavigation/Binding.swift | 27 +- .../xcshareddata/swiftpm/Package.resolved | 240 +++++++++--------- 4 files changed, 153 insertions(+), 139 deletions(-) diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift index 7921f25d11..0b75f9c649 100644 --- a/Examples/CaseStudies/08-Routing.swift +++ b/Examples/CaseStudies/08-Routing.swift @@ -5,10 +5,10 @@ private let readMe = """ This case study demonstrates how to power multiple forms of navigation from a single destination \ enum that describes all of the possible destinations one can travel to from this screen. - The screen has four navigation destinations: an alert, a confirmation dialog, a navigation link to a count stepper, \ - and a modal sheet to a count stepper. The state for each of these destinations is held as \ - associated data of an enum, and bindings to the cases of that enum are derived using the tools \ - in this library. + The screen has four navigation destinations: an alert, a confirmation dialog, a navigation link \ + to a count stepper, and a modal sheet to a count stepper. The state for each of these \ + destinations is held as associated data of an enum, and bindings to the cases of that enum are \ + derived using the tools in this library. """ @CasePathable diff --git a/Package.resolved b/Package.resolved index 97d64e88c2..bc0e7355d1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "40773cbaf8d71ed5357f297b1ba4073f5b24faaa", - "version": "1.1.0" + "revision": "bba1111185863c9288c5f047770f421c3b7793a4", + "version": "1.1.3" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", "state": { "branch": null, - "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version": "1.0.0" + "revision": "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version": "1.1.2" } }, { @@ -37,6 +37,15 @@ "version": "1.0.0" } }, + { + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", + "state": { + "branch": null, + "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version": "509.0.2" + } + }, { "package": "xctest-dynamic-overlay", "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index 24a035c93a..edfce57644 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -11,15 +11,16 @@ /// - Parameter keyPath: A case key path to a specific associated value. /// - Returns: A new binding. public subscript( - dynamicMember keyPath: CaseKeyPath + dynamicMember keyPath: KeyPath> ) -> Binding? where Value: CasePathable { - Binding( + let casePath = Value.allCasePaths[keyPath: keyPath] + return Binding( unwrapping: Binding( - get: { self.wrappedValue[case: keyPath] }, + get: { casePath.extract(from: self.wrappedValue) }, set: { newValue, transaction in guard let newValue else { return } - self.transaction(transaction).wrappedValue[case: keyPath] = newValue + self.transaction(transaction).wrappedValue = casePath.embed(newValue) } ) ) @@ -31,20 +32,22 @@ /// /// - Parameter keyPath: A case key path to a specific associated value. /// - Returns: A new binding. - public subscript( - dynamicMember keyPath: CaseKeyPath - ) -> Binding + public subscript( + dynamicMember keyPath: KeyPath> + ) -> Binding where Value == Enum? { - return Binding( - get: { self.wrappedValue[case: (\Enum?.Cases.some).appending(path: keyPath)] }, + let casePath = Enum.allCasePaths[keyPath: keyPath] + return Binding( + get: { + guard let wrappedValue = self.wrappedValue else { return nil } + return casePath.extract(from: wrappedValue) + }, set: { newValue, transaction in guard let newValue else { self.transaction(transaction).wrappedValue = nil return } - self.transaction(transaction).wrappedValue[ - case: (\Enum?.Cases.some).appending(path: keyPath) - ] = newValue + self.transaction(transaction).wrappedValue = casePath.embed(newValue) } ) } diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index bbe623d4e7..51a14302a3 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,122 +1,124 @@ { - "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "object": { + "pins": [ + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version": "1.0.0" + } + }, + { + "package": "swift-case-paths", + "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", + "state": { + "branch": null, + "revision": "bba1111185863c9288c5f047770f421c3b7793a4", + "version": "1.1.3" + } + }, + { + "package": "swift-clocks", + "repositoryURL": "https://github.com/pointfreeco/swift-clocks", + "state": { + "branch": null, + "revision": "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version": "1.0.2" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "d029d9d39c87bed85b1c50adee7c41795261a192", + "version": "1.0.6" + } + }, + { + "package": "swift-concurrency-extras", + "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", + "state": { + "branch": null, + "revision": "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version": "1.1.0" + } + }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version": "1.1.2" + } + }, + { + "package": "swift-dependencies", + "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", + "state": { + "branch": null, + "revision": "c31b1445c4fae49e6fdb75496b895a3653f6aefc", + "version": "1.1.5" + } + }, + { + "package": "SwiftDocCPlugin", + "repositoryURL": "https://github.com/apple/swift-docc-plugin", + "state": { + "branch": null, + "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", + "version": "1.3.0" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", + "version": "1.0.0" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", + "state": { + "branch": null, + "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version": "1.0.0" + } + }, + { + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", + "state": { + "branch": null, + "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version": "509.0.2" + } + }, + { + "package": "swift-tagged", + "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", + "state": { + "branch": null, + "revision": "3907a9438f5b57d317001dc99f3f11b46882272b", + "version": "0.10.0" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version": "1.0.2" + } } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "40773cbaf8d71ed5357f297b1ba4073f5b24faaa", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "http://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections.git", - "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", - "version" : "509.0.0" - } - }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged.git", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" - } - } - ], - "version" : 2 + ] + }, + "version": 1 } From d9e72f3083c08375794afa216fb2f89c0114f303 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 30 Jan 2024 13:06:44 -0800 Subject: [PATCH 026/124] Use better binding transformations where possible (#141) * Use better binding transformations where possible `Binding.init(get:set)` is handy but buggy when it comes to preserving animations and passing in the correct transaction. We can preserve the binding's transaction in a less buggy manner by leveraging dynamic member lookup instead, a trick we've employed in TCA, but never here. * wip --- Package.swift | 2 +- Sources/SwiftUINavigation/Binding.swift | 102 ++++++++++++------ .../xcshareddata/swiftpm/Package.resolved | 4 +- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/Package.swift b/Package.swift index f81e1388e2..ff10a97ad2 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index edfce57644..9d65e4f25a 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -14,16 +14,7 @@ dynamicMember keyPath: KeyPath> ) -> Binding? where Value: CasePathable { - let casePath = Value.allCasePaths[keyPath: keyPath] - return Binding( - unwrapping: Binding( - get: { casePath.extract(from: self.wrappedValue) }, - set: { newValue, transaction in - guard let newValue else { return } - self.transaction(transaction).wrappedValue = casePath.embed(newValue) - } - ) - ) + Binding(unwrapping: self[keyPath]) } /// Returns a binding to the associated value of a given case key path. @@ -36,20 +27,7 @@ dynamicMember keyPath: KeyPath> ) -> Binding where Value == Enum? { - let casePath = Enum.allCasePaths[keyPath: keyPath] - return Binding( - get: { - guard let wrappedValue = self.wrappedValue else { return nil } - return casePath.extract(from: wrappedValue) - }, - set: { newValue, transaction in - guard let newValue else { - self.transaction(transaction).wrappedValue = nil - return - } - self.transaction(transaction).wrappedValue = casePath.embed(newValue) - } - ) + self[keyPath] } #endif @@ -68,7 +46,8 @@ /// - Parameter base: A value to project to an unwrapped value. /// - Returns: A new binding or `nil` when `base` is `nil`. public init?(unwrapping base: Binding) { - self.init(unwrapping: base, case: AnyCasePath(\.some)) + guard let value = base.wrappedValue else { return nil } + self = base[default: DefaultSubscript(value)] } /// Creates a binding by projecting the current optional value to a boolean describing if it's @@ -79,14 +58,7 @@ /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. public func isPresent() -> Binding where Value == Wrapped? { - .init( - get: { self.wrappedValue != nil }, - set: { isPresent, transaction in - if !isPresent { - self.transaction(transaction).wrappedValue = nil - } - } - ) + self._isPresent } /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. @@ -139,4 +111,68 @@ ) } } + + extension Optional { + fileprivate var _isPresent: Bool { + get { self != nil } + set { + guard !newValue else { return } + self = nil + } + } + + fileprivate subscript(default defaultSubscript: DefaultSubscript) -> Wrapped { + get { + defaultSubscript.value = self ?? defaultSubscript.value + return defaultSubscript.value + } + set { + defaultSubscript.value = newValue + if self != nil { self = newValue } + } + } + } + + private final class DefaultSubscript: Hashable { + var value: Value + init(_ value: Value) { + self.value = value + } + static func == (lhs: DefaultSubscript, rhs: DefaultSubscript) -> Bool { + lhs === rhs + } + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + } + + extension CasePathable { + fileprivate subscript( + keyPath: KeyPath> + ) -> Member? { + get { Self.allCasePaths[keyPath: keyPath].extract(from: self) } + set { + guard let newValue else { return } + self = Self.allCasePaths[keyPath: keyPath].embed(newValue) + } + } + } + + 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) + } + } + } #endif // canImport(SwiftUI) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 51a14302a3..a6a4bc1692 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "bba1111185863c9288c5f047770f421c3b7793a4", - "version": "1.1.3" + "revision": "8cc3bc05d0cc956f7374c6c208a11f66a7cac3db", + "version": "1.2.2" } }, { From 3fff7901a7813f544a707abaf00c8c42177515d5 Mon Sep 17 00:00:00 2001 From: David Peterson Date: Sat, 6 Apr 2024 02:14:03 +1000 Subject: [PATCH 027/124] Alert with binding (#145) * Updated `alert` and `confirmationDialog` wrapper functions - Deprecated the `alert(title:unwrapping:...)` and `confirmationDialog(title:unwrapping:...)` functions in `SwiftUINavigation` - Deprecated the read-only `Bindable.isPresent()` in `SwiftUINavigation. - Added `alert(_:title:actions:message)` functions to `SwiftUINavigationCore` to expose them to TCA. - Added `confirmationDialog(_:title:titleVisibility:actions:message)` functions to `SwiftUINavigationCore` to expose them to TCA. - Added read/write `Bindable.isPresent()` in `SwiftUINavigationCore`. - Added/updated documentation * Fixed bug when updating whether a binding is present. * Updated Alert/ConfirmationDialog methods - Reordered parameters to more closely match the built-in ones. - Added overloads without `message` closures. - Updated documentation * Removed dialog documentation. * Fixed bug in deprectaions call. * Removed static title overloads. * Reverted Binding implementation, moved into `SwiftUINavigationCore` * Update Sources/SwiftUINavigation/Internal/Deprecations.swift * Update Sources/SwiftUINavigation/Internal/Deprecations.swift * Restore SwiftUINavigationCore.md * Delete Sourcs/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md * Create SwiftUINavigationCore.md --------- Co-authored-by: Stephen Celis --- Package.resolved | 4 +- Sources/SwiftUINavigation/Alert.swift | 73 +--- Sources/SwiftUINavigation/Binding.swift | 21 +- .../ConfirmationDialog.swift | 73 ---- .../Documentation.docc/Articles/Bindings.md | 4 +- .../Extensions/Deprecations.md | 1 + .../Internal/Deprecations.swift | 387 +++++++++--------- Sources/SwiftUINavigationCore/Alert.swift | 141 +++++++ Sources/SwiftUINavigationCore/Binding.swift | 27 ++ .../ConfirmationDialog.swift | 150 +++++++ .../ButtonStateTests.swift | 2 +- 11 files changed, 529 insertions(+), 354 deletions(-) create mode 100644 Sources/SwiftUINavigationCore/Alert.swift create mode 100644 Sources/SwiftUINavigationCore/Binding.swift create mode 100644 Sources/SwiftUINavigationCore/ConfirmationDialog.swift diff --git a/Package.resolved b/Package.resolved index bc0e7355d1..5962320b06 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "bba1111185863c9288c5f047770f421c3b7793a4", - "version": "1.1.3" + "revision": "e593aba2c6222daad7c4f2732a431eed2c09bb07", + "version": "1.3.0" } }, { diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index bf27a0128c..45b1a2e689 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -1,73 +1,8 @@ #if canImport(SwiftUI) import SwiftUI - - extension View { - /// Presents an alert from a binding to an optional value. - /// - /// SwiftUI's `alert` view modifiers are driven by two disconnected pieces of state: an - /// `isPresented` binding to a boolean that determines if the alert should be presented, and - /// optional alert `data` that is used to customize its actions and message. - /// - /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: - /// - /// * `isPresented` can be `true`, but `data` can be `nil`. - /// * `isPresented` can be `false`, but `data` can be non-`nil`. - /// - /// On top of that, SwiftUI's `alert` modifiers take static titles, which means the title cannot - /// be dynamically computed from the alert data. - /// - /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the - /// invalid runtime states at compile time by driving the alert's presentation from a single, - /// optional binding. When this binding is non-`nil`, the alert will be presented. Further, the - /// title can be customized from the alert data. - /// - /// ```swift - /// struct AlertDemo: View { - /// @State var randomMovie: Movie? - /// - /// var body: some View { - /// Button("Pick a random movie", action: self.getRandomMovie) - /// .alert( - /// title: { Text($0.title) }, - /// unwrapping: self.$randomMovie, - /// actions: { _ in - /// Button("Pick another", action: self.getRandomMovie) - /// }, - /// message: { Text($0.summary) } - /// ) - /// } - /// - /// func getRandomMovie() { - /// self.randomMovie = Movie.allCases.randomElement() - /// } - /// } - /// ``` - /// - /// - Parameters: - /// - title: A closure returning the alert's title given the current alert state. - /// - value: A binding to an optional value that determines whether an alert should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed - /// to the modifier's closures. You can use this data to populate the fields of an alert - /// that the system displays to the user. When the user presses or taps one of the alert's - /// actions, the system sets this value to `nil` and dismisses the alert. - /// - actions: A view builder returning the alert's actions given the current alert state. - /// - message: A view builder returning the message for the alert given the current alert - /// state. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func alert( - title: (Value) -> Text, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M - ) -> some View { - self.alert( - value.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - presenting: value.wrappedValue, - actions: actions, - message: message - ) - } + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension View { /// Presents an alert from a binding to optional alert state. /// @@ -81,7 +16,6 @@ /// dismisses the alert, and the action is fed to the `action` closure. /// - handler: A closure that is called with an action from a particular alert button when /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( _ state: Binding?>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } @@ -114,7 +48,6 @@ /// dismisses the alert, and the action is fed to the `action` closure. /// - handler: A closure that is called with an action from a particular alert button when /// tapped. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( _ state: Binding?>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index 9d65e4f25a..fd9012e7f6 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -50,17 +50,6 @@ self = base[default: DefaultSubscript(value)] } - /// Creates a binding by projecting the current optional value to a boolean describing if it's - /// non-`nil`. - /// - /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing. - /// - /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. - public func isPresent() -> Binding - where Value == Wrapped? { - self._isPresent - } - /// Creates a binding that ignores writes to its wrapped value when equivalent to the new value. /// /// Useful to minimize writes to bindings passed to SwiftUI APIs. For example, [`NavigationLink` @@ -113,14 +102,6 @@ } extension Optional { - fileprivate var _isPresent: Bool { - get { self != nil } - set { - guard !newValue else { return } - self = nil - } - } - fileprivate subscript(default defaultSubscript: DefaultSubscript) -> Wrapped { get { defaultSubscript.value = self ?? defaultSubscript.value @@ -175,4 +156,4 @@ } } } -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 7f48fc8509..6543e2b9f9 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -2,79 +2,6 @@ import SwiftUI extension View { - /// Presents a confirmation dialog from a binding to an optional value. - /// - /// SwiftUI's `confirmationDialog` view modifiers are driven by two disconnected pieces of - /// state: an `isPresented` binding to a boolean that determines if the dialog should be - /// presented, and optional dialog `data` that is used to customize its actions and message. - /// - /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: - /// - /// * `isPresented` can be `true`, but `data` can be `nil`. - /// * `isPresented` can be `false`, but `data` can be non-`nil`. - /// - /// On top of that, SwiftUI's `confirmationDialog` modifiers take static titles, which means the - /// title cannot be dynamically computed from the dialog data. - /// - /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the - /// invalid runtime states at compile time by driving the dialog's presentation from a single, - /// optional binding. When this binding is non-`nil`, the dialog will be presented. Further, the - /// title can be customized from the dialog data. - /// - /// ```swift - /// struct DialogDemo: View { - /// @State var randomMovie: Movie? - /// - /// var body: some View { - /// Button("Pick a random movie", action: self.getRandomMovie) - /// .confirmationDialog( - /// title: { Text($0.title) }, - /// titleVisibility: .always, - /// unwrapping: self.$randomMovie, - /// actions: { _ in - /// Button("Pick another", action: self.getRandomMovie) - /// }, - /// message: { Text($0.summary) } - /// ) - /// } - /// - /// func getRandomMovie() { - /// self.randomMovie = Movie.allCases.randomElement() - /// } - /// } - /// ``` - /// - /// See for more information on how to use this API. - /// - /// - Parameters: - /// - title: A closure returning the dialog's title given the current dialog state. - /// - titleVisibility: The visibility of the dialog's title. - /// - value: A binding to an optional value that determines whether a dialog should be - /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed - /// to the modifier's closures. You can use this data to populate the fields of a dialog - /// that the system displays to the user. When the user presses or taps one of the dialog's - /// actions, the system sets this value to `nil` and dismisses the dialog. - /// - actions: A view builder returning the dialog's actions given the current dialog state. - /// - message: A view builder returning the message for the dialog given the current dialog - /// state. - @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( - title: (Value) -> Text, - titleVisibility: Visibility = .automatic, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M - ) -> some View { - self.confirmationDialog( - value.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: value.isPresent(), - titleVisibility: titleVisibility, - presenting: value.wrappedValue, - actions: actions, - message: message - ) - } - /// Presents a confirmation dialog from a binding to optional confirmation dialog state. /// /// See for more information on how to use this API. diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index ba7575b66b..5dae2c5dfd 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -69,8 +69,8 @@ struct SignInView: View { ### Dynamic case lookup -- ``SwiftUI/Binding/subscript(dynamicMember:)-9akk`` -- ``SwiftUI/Binding/subscript(dynamicMember:)-9okch`` +- ``SwiftUI/Binding/subscript(dynamicMember:)-9abgy`` +- ``SwiftUI/Binding/subscript(dynamicMember:)-8vc80`` ### Unwrapping bindings diff --git a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md index 4f37a20049..ed5b569664 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md @@ -21,6 +21,7 @@ instead. ### View modifiers - ``SwiftUI/View/alert(title:unwrapping:case:actions:message:)`` +- ``SwiftUI/View/alert(title:unwrapping:actions:message:)`` - ``SwiftUI/View/alert(unwrapping:action:)-7da26`` - ``SwiftUI/View/alert(unwrapping:action:)-6y2fk`` - ``SwiftUI/View/alert(unwrapping:action:)-867h5`` diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index fa3ccf1375..18b686c553 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -2,6 +2,39 @@ import SwiftUI @_spi(RuntimeWarn) import SwiftUINavigationCore + // NB: Deprecated after 1.2.1 + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + extension View { + @available(*, deprecated, renamed: "alert(item:title:actions:message:)") + public func alert( + title: (Value) -> Text, + unwrapping value: Binding, + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M + ) -> some View { + alert(item: value, title: title, actions: actions, message: message) + } + + @available(*, deprecated, renamed: "confirmationDialog(item:textVisibility:title:actions:message:)") + public func confirmationDialog( + title: (Value) -> Text, + titleVisibility: Visibility = .automatic, + unwrapping value: Binding, + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M + ) -> some View { + self.confirmationDialog( + value.wrappedValue.map(title) ?? Text(verbatim: ""), + isPresented: value.isPresent(), + titleVisibility: titleVisibility, + presenting: value.wrappedValue, + actions: actions, + message: message + ) + } + } + // NB: Deprecated after 1.0.2 @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) @@ -11,7 +44,7 @@ unwrapping value: Binding?>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.alert( + alert( (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, @@ -29,7 +62,7 @@ unwrapping value: Binding?>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.alert( + alert( (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), isPresented: value.isPresent(), presenting: value.wrappedValue, @@ -47,7 +80,7 @@ unwrapping value: Binding?>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.confirmationDialog( + confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, @@ -66,7 +99,7 @@ unwrapping value: Binding?>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.confirmationDialog( + confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), isPresented: value.isPresent(), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, @@ -85,22 +118,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( title: (Case) -> Text, @@ -109,9 +142,9 @@ @ViewBuilder actions: (Case) -> A, @ViewBuilder message: (Case) -> M ) -> some View { - self.alert( + alert( + item: `enum`.case(casePath), title: title, - unwrapping: `enum`.case(casePath), actions: actions, message: message ) @@ -120,78 +153,78 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: AnyCasePath>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.alert(`enum`.case(casePath), action: handler) + alert(`enum`.case(casePath), action: handler) } @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: AnyCasePath>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.alert(`enum`.case(casePath), action: handler) + alert(`enum`.case(casePath), action: handler) } @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( title: (Case) -> Text, @@ -201,10 +234,10 @@ @ViewBuilder actions: (Case) -> A, @ViewBuilder message: (Case) -> M ) -> some View { - self.confirmationDialog( - title: title, + confirmationDialog( + item: `enum`.case(casePath), titleVisibility: titleVisibility, - unwrapping: `enum`.case(casePath), + title: title, actions: actions, message: message ) @@ -213,29 +246,29 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: AnyCasePath>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.confirmationDialog( + confirmationDialog( `enum`.case(casePath), action: handler ) @@ -244,29 +277,29 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: AnyCasePath>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.confirmationDialog( + confirmationDialog( `enum`.case(casePath), action: handler ) @@ -275,18 +308,18 @@ @available( iOS, introduced: 14, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available(macOS, unavailable) @available( tvOS, introduced: 14, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 7, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func fullScreenCover( unwrapping enum: Binding, @@ -294,48 +327,48 @@ onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View - where Content: View { - self.fullScreenCover( + where Content: View { + fullScreenCover( unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } @available( iOS, introduced: 16, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 13, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 16, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 9, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func navigationDestination( unwrapping enum: Binding, case casePath: AnyCasePath, @ViewBuilder destination: (Binding) -> Destination ) -> some View { - self.navigationDestination(unwrapping: `enum`.case(casePath), destination: destination) + navigationDestination(unwrapping: `enum`.case(casePath), destination: destination) } @available( iOS, introduced: 13, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 10.15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available(tvOS, unavailable) @available(watchOS, unavailable) @@ -346,7 +379,7 @@ arrowEdge: Edge = .top, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { - self.popover( + popover( unwrapping: `enum`.case(casePath), attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge, @@ -357,22 +390,22 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @MainActor public func sheet( @@ -381,8 +414,8 @@ onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View - where Content: View { - self.sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) + where Content: View { + sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } } @@ -390,22 +423,22 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public init?(unwrapping enum: Binding, case casePath: AnyCasePath) { guard var `case` = casePath.extract(from: `enum`.wrappedValue) @@ -427,25 +460,25 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func `case`(_ casePath: AnyCasePath) -> Binding - where Value == Enum? { + where Value == Enum? { .init( get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, set: { newValue, transaction in @@ -457,31 +490,31 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func isPresent(_ casePath: AnyCasePath) -> Binding - where Value == Enum? { + where Value == Enum? { self.case(casePath).isPresent() } } public struct IfCaseLet: View - where IfContent: View, ElseContent: View { + where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: AnyCasePath public let ifContent: (Binding) -> IfContent @@ -490,25 +523,25 @@ @available( iOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( macOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( tvOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( watchOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) public init( - _ `enum`: Binding, + _ enum: Binding, pattern casePath: AnyCasePath, @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, @ViewBuilder else elseContent: () -> ElseContent @@ -546,19 +579,19 @@ ) extension IfCaseLet where ElseContent == EmptyView { public init( - _ `enum`: Binding, + _ enum: Binding, pattern casePath: AnyCasePath, @ViewBuilder ifContent: @escaping (Binding) -> IfContent ) { self.casePath = casePath - self.elseContent = EmptyView() + elseContent = EmptyView() self.enum = `enum` self.ifContent = ifContent } } public struct IfLet: View - where IfContent: View, ElseContent: View { + where IfContent: View, ElseContent: View { public let value: Binding public let ifContent: (Binding) -> IfContent public let elseContent: ElseContent @@ -566,22 +599,22 @@ @available( iOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( macOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( tvOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( watchOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) public init( _ value: Binding, @@ -631,22 +664,22 @@ @available( iOS, introduced: 13, deprecated: 16, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 10.15, deprecated: 13, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 13, deprecated: 16, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 6, deprecated: 9, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public init( unwrapping enum: Binding, @@ -667,22 +700,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct Switch: View { public let `enum`: Binding @@ -705,25 +738,25 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct CaseLet: View - where Content: View { + where Content: View { @EnvironmentObject private var `enum`: BindingObject public let casePath: AnyCasePath public let content: (Binding) -> Content @@ -744,22 +777,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct Default: View { private let content: Content @@ -776,22 +809,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) extension Switch { public init( @@ -803,12 +836,11 @@ ) > ) - where + where Content == _ConditionalContent< CaseLet, Default - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -825,12 +857,11 @@ line: UInt = #line, @ViewBuilder content: () -> CaseLet ) - where + where Content == _ConditionalContent< CaseLet, Default<_ExhaustivityCheckView> - > - { + > { self.init(`enum`) { content() Default { _ExhaustivityCheckView(file: file, line: line) } @@ -847,15 +878,14 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -879,15 +909,14 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default<_ExhaustivityCheckView> - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -912,7 +941,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, @@ -922,8 +951,7 @@ CaseLet, Default > - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -950,7 +978,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, @@ -960,8 +988,7 @@ CaseLet, Default<_ExhaustivityCheckView> > - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -989,7 +1016,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1002,8 +1029,7 @@ > >, Default - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1038,7 +1064,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1051,8 +1077,7 @@ > >, Default<_ExhaustivityCheckView> - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1083,7 +1108,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1099,8 +1124,7 @@ CaseLet, Default > - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1139,7 +1163,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1155,8 +1179,7 @@ CaseLet, Default<_ExhaustivityCheckView> > - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1190,7 +1213,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1209,8 +1232,7 @@ >, Default > - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1253,7 +1275,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1272,8 +1294,7 @@ >, Default<_ExhaustivityCheckView> > - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1310,7 +1331,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1332,8 +1353,7 @@ Default > > - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1380,7 +1400,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1402,8 +1422,7 @@ Default<_ExhaustivityCheckView> > > - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1443,7 +1462,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1468,8 +1487,7 @@ > >, Default - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1520,7 +1538,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1545,8 +1563,7 @@ > >, Default<_ExhaustivityCheckView> - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1589,7 +1606,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1617,8 +1634,7 @@ CaseLet, Default > - > - { + > { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1673,7 +1689,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1701,8 +1717,7 @@ CaseLet, Default<_ExhaustivityCheckView> > - > - { + > { let content = content() self.init(`enum`) { content.value.0 @@ -1727,13 +1742,13 @@ public var body: some View { #if DEBUG let message = """ - Warning: Switch.body@\(self.file):\(self.line) + Warning: Switch.body@\(self.file):\(self.line) - "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" + "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ - provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. - """ + Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ + provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. + """ VStack(spacing: 17) { self.exclamation() .font(.largeTitle) @@ -1763,7 +1778,7 @@ let wrappedValue: Binding init(binding: Binding) { - self.wrappedValue = binding + wrappedValue = binding } } @@ -1774,13 +1789,13 @@ let childMirror = Mirror(reflecting: child.value) let associatedValuesMirror = childMirror.displayStyle == .tuple - ? childMirror - : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) + ? childMirror + : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) `case` = """ - \(label)(\ - \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ - ) - """ + \(label)(\ + \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ + ) + """ } else { `case` = "\(`enum`)" } @@ -1800,13 +1815,13 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func alert( unwrapping value: Binding?>, action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { - self.alert(value) { (value: Value?) in + alert(value) { (value: Value?) in if let value = value { await handler(value) } @@ -1818,14 +1833,14 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func alert( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: CasePath>, action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { - self.alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in + alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in if let value = value { await handler(value) } @@ -1837,13 +1852,13 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func confirmationDialog( unwrapping value: Binding?>, action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { - self.confirmationDialog(unwrapping: value) { (value: Value?) in + confirmationDialog(unwrapping: value) { (value: Value?) in if let value = value { await handler(value) } @@ -1855,14 +1870,14 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func confirmationDialog( - unwrapping `enum`: Binding, + unwrapping enum: Binding, case casePath: CasePath>, action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { - self.confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in + confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in if let value = value { await handler(value) } @@ -1875,7 +1890,7 @@ @available(*, deprecated, renamed: "init(_:pattern:then:else:)") extension IfCaseLet { public init( - _ `enum`: Binding, + _ enum: Binding, pattern casePath: CasePath, @ViewBuilder ifContent: @escaping (Binding) -> IfContent, @ViewBuilder elseContent: () -> ElseContent @@ -1917,4 +1932,4 @@ ) } } -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Alert.swift b/Sources/SwiftUINavigationCore/Alert.swift new file mode 100644 index 0000000000..448cda9f86 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Alert.swift @@ -0,0 +1,141 @@ +#if canImport(SwiftUI) + import SwiftUI + + // MARK: - Alert with dynamic title + extension View { + /// Presents an alert from a binding to an optional value. + /// + /// SwiftUI's `alert` view modifiers are driven by two disconnected pieces of state: an + /// `isPresented` binding to a boolean that determines if the alert should be presented, and + /// optional alert `data` that is used to customize its actions and message. + /// + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: + /// + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `alert` modifiers take static titles, which means the title cannot + /// be dynamically computed from the alert data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the alert's presentation from a single, + /// optional binding. When this binding is non-`nil`, the alert will be presented. Further, the + /// title can be customized from the alert data. + /// + /// ```swift + /// struct AlertDemo: View { + /// @State var randomMovie: Movie? + /// + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .alert(item: self.$randomMovie) { + /// Text($0.title) + /// } actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// Button("I'm done", action: self.clearRandomMovie) + /// } message: { + /// Text($0.summary) + /// } + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// + /// func clearRandomMovie() { + /// self.randomMovie = nil + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - item: A binding to an optional value that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of an alert + /// that the system displays to the user. When the user presses or taps one of the alert's + /// actions, the system sets this value to `nil` and dismisses the alert. + /// - title: A closure returning the alert's title given the current alert state. + /// - actions: A view builder returning the alert's actions given the current alert state. + /// - message: A view builder returning the message for the alert given the current alert + /// state. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + item: Binding, + title: (Item) -> Text, + @ViewBuilder actions: (Item) -> A, + @ViewBuilder message: (Item) -> M + ) -> some View { + alert( + item.wrappedValue.map(title) ?? Text(verbatim: ""), + isPresented: item.isPresent(), + presenting: item.wrappedValue, + actions: actions, + message: message + ) + } + + /// Presents an alert from a binding to an optional value. + /// + /// SwiftUI's `alert` view modifiers are driven by two disconnected pieces of state: an + /// `isPresented` binding to a boolean that determines if the alert should be presented, and + /// optional alert `data` that is used to customize its actions and message. + /// + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `alert` modifiers take static titles, which means the title cannot + /// be dynamically computed from the alert data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the alert's presentation from a single, + /// optional binding. When this binding is non-`nil`, the alert will be presented. Further, the + /// title can be customized from the alert data. + /// + /// ```swift + /// struct AlertDemo: View { + /// @State var randomMovie: Movie? + /// + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .alert(item: self.$randomMovie) { + /// Text($0.title) + /// } actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// Button("I'm done", action: self.clearRandomMovie) + /// } + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// + /// func clearRandomMovie() { + /// self.randomMovie = nil + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - item: A binding to an optional value that determines whether an alert should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of an alert + /// that the system displays to the user. When the user presses or taps one of the alert's + /// actions, the system sets this value to `nil` and dismisses the alert. + /// - title: A closure returning the alert's title given the current alert state. + /// - actions: A view builder returning the alert's actions given the current alert state. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + item: Binding, + title: (Item) -> Text, + @ViewBuilder actions: (Item) -> A + ) -> some View { + alert( + item.wrappedValue.map(title) ?? Text(verbatim: ""), + isPresented: item.isPresent(), + presenting: item.wrappedValue, + actions: actions + ) + } + } +#endif diff --git a/Sources/SwiftUINavigationCore/Binding.swift b/Sources/SwiftUINavigationCore/Binding.swift new file mode 100644 index 0000000000..22de85a1ab --- /dev/null +++ b/Sources/SwiftUINavigationCore/Binding.swift @@ -0,0 +1,27 @@ +#if canImport(SwiftUI) + import SwiftUI + + extension Binding { + /// Creates a binding by projecting the current optional value to a boolean describing if it's + /// non-`nil`. + /// + /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing. + /// + /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. + public func isPresent() -> Binding + where Value == Wrapped? { + self._isPresent + } + } + + extension Optional { + fileprivate var _isPresent: Bool { + get { self != nil } + set { + guard !newValue else { return } + self = nil + } + } + } + +#endif diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialog.swift b/Sources/SwiftUINavigationCore/ConfirmationDialog.swift new file mode 100644 index 0000000000..f84f139280 --- /dev/null +++ b/Sources/SwiftUINavigationCore/ConfirmationDialog.swift @@ -0,0 +1,150 @@ +#if canImport(SwiftUI) + import SwiftUI + + // MARK: - ConfirmationDialog with dynamic title + + extension View { + /// Presents a confirmation dialog from a binding to an optional value. + /// + /// SwiftUI's `confirmationDialog` view modifiers are driven by two disconnected pieces of + /// state: an `isPresented` binding to a boolean that determines if the dialog should be + /// presented, and optional dialog `data` that is used to customize its actions and message. + /// + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: + /// + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `confirmationDialog` modifiers take static titles, which means the + /// title cannot be dynamically computed from the dialog data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the dialog's presentation from a single, + /// optional binding. When this binding is non-`nil`, the dialog will be presented. Further, the + /// title can be customized from the dialog data. + /// + /// ```swift + /// struct DialogDemo: View { + /// @State var randomMovie: Movie? + /// + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .confirmationDialog(item: self.$randomMovie, titleVisibility: .always) { + /// Text($0.title) + /// } actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// Button("I'm done", action: self.clearRandomMovie) + /// } message: { + /// Text($0.summary) + /// } + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// + /// func clearRandomMovie() { + /// self.randomMovie = nil + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - item: A binding to an optional value that determines whether a dialog should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of a dialog + /// that the system displays to the user. When the user presses or taps one of the dialog's + /// actions, the system sets this value to `nil` and dismisses the dialog. + /// - title: A closure returning the dialog's title given the current dialog state. + /// - titleVisibility: The visibility of the dialog's title. (default: .automatic) + /// - actions: A view builder returning the dialog's actions given the current dialog state. + /// - message: A view builder returning the message for the dialog given the current dialog + /// state. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + item: Binding, + titleVisibility: Visibility = .automatic, + title: (Item) -> Text, + @ViewBuilder actions: (Item) -> A, + @ViewBuilder message: (Item) -> M + ) -> some View { + confirmationDialog( + item.wrappedValue.map(title) ?? Text(verbatim: ""), + isPresented: item.isPresent(), + titleVisibility: titleVisibility, + presenting: item.wrappedValue, + actions: actions, + message: message + ) + } + + /// Presents a confirmation dialog from a binding to an optional value. + /// + /// SwiftUI's `confirmationDialog` view modifiers are driven by two disconnected pieces of + /// state: an `isPresented` binding to a boolean that determines if the dialog should be + /// presented, and optional dialog `data` that is used to customize its actions and message. + /// + /// Modeling the domain in this way unfortunately introduces a couple invalid runtime states: + /// + /// * `isPresented` can be `true`, but `data` can be `nil`. + /// * `isPresented` can be `false`, but `data` can be non-`nil`. + /// + /// On top of that, SwiftUI's `confirmationDialog` modifiers take static titles, which means the + /// title cannot be dynamically computed from the dialog data. + /// + /// This overload addresses these shortcomings with a streamlined API. First, it eliminates the + /// invalid runtime states at compile time by driving the dialog's presentation from a single, + /// optional binding. When this binding is non-`nil`, the dialog will be presented. Further, the + /// title can be customized from the dialog data. + /// + /// struct DialogDemo: View { + /// @State var randomMovie: Movie? + /// + /// var body: some View { + /// Button("Pick a random movie", action: self.getRandomMovie) + /// .confirmationDialog(item: self.$randomMovie, titleVisibility: .always) { + /// Text($0.title) + /// } actions: { _ in + /// Button("Pick another", action: self.getRandomMovie) + /// Button("I'm done", action: self.clearRandomMovie) + /// } + /// } + /// + /// func getRandomMovie() { + /// self.randomMovie = Movie.allCases.randomElement() + /// } + /// + /// func clearRandomMovie() { + /// self.randomMovie = nil + /// } + /// } + /// ``` + /// + /// See for more information on how to use this API. + /// + /// - Parameters: + /// - item: A binding to an optional value that determines whether a dialog should be + /// presented. When the binding is updated with non-`nil` value, it is unwrapped and passed + /// to the modifier's closures. You can use this data to populate the fields of a dialog + /// that the system displays to the user. When the user presses or taps one of the dialog's + /// actions, the system sets this value to `nil` and dismisses the dialog. + /// - title: A closure returning the dialog's title given the current dialog state. + /// - titleVisibility: The visibility of the dialog's title. (default: .automatic) + /// - actions: A view builder returning the dialog's actions given the current dialog state. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + item: Binding, + titleVisibility: Visibility = .automatic, + title: (Item) -> Text, + @ViewBuilder actions: (Item) -> A + ) -> some View { + confirmationDialog( + item.wrappedValue.map(title) ?? Text(verbatim: ""), + isPresented: item.isPresent(), + titleVisibility: titleVisibility, + presenting: item.wrappedValue, + actions: actions + ) + } + } +#endif diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index 788a8faff8..857d268063 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -4,8 +4,8 @@ import SwiftUINavigation import XCTest - @MainActor final class ButtonStateTests: XCTestCase { + @MainActor func testAsyncAnimationWarning() async { XCTExpectFailure { $0.compactDescription == """ From a2180625c3689840e0807683f3b7533e99661109 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Fri, 5 Apr 2024 16:15:37 +0000 Subject: [PATCH 028/124] Run swift-format --- Sources/SwiftUINavigation/Alert.swift | 4 +- Sources/SwiftUINavigation/Binding.swift | 2 +- .../Internal/Deprecations.swift | 296 ++++++++++-------- Sources/SwiftUINavigationCore/Binding.swift | 4 +- 4 files changed, 163 insertions(+), 143 deletions(-) diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 45b1a2e689..d3f0e10853 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -1,8 +1,8 @@ #if canImport(SwiftUI) import SwiftUI - + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - extension View { + extension View { /// Presents an alert from a binding to optional alert state. /// diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index fd9012e7f6..bd1547b7fd 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -156,4 +156,4 @@ } } } -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 18b686c553..111d61d4c0 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -15,8 +15,10 @@ ) -> some View { alert(item: value, title: title, actions: actions, message: message) } - - @available(*, deprecated, renamed: "confirmationDialog(item:textVisibility:title:actions:message:)") + + @available( + *, deprecated, renamed: "confirmationDialog(item:textVisibility:title:actions:message:)" + ) public func confirmationDialog( title: (Value) -> Text, titleVisibility: Visibility = .automatic, @@ -118,22 +120,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( title: (Case) -> Text, @@ -153,22 +155,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( unwrapping enum: Binding, @@ -181,22 +183,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func alert( unwrapping enum: Binding, @@ -209,22 +211,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( title: (Case) -> Text, @@ -246,22 +248,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( unwrapping enum: Binding, @@ -277,22 +279,22 @@ @available( iOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 12, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 8, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func confirmationDialog( unwrapping enum: Binding, @@ -308,18 +310,18 @@ @available( iOS, introduced: 14, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available(macOS, unavailable) @available( tvOS, introduced: 14, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 7, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func fullScreenCover( unwrapping enum: Binding, @@ -327,7 +329,7 @@ onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View - where Content: View { + where Content: View { fullScreenCover( unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } @@ -335,22 +337,22 @@ @available( iOS, introduced: 16, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 13, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 16, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 9, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func navigationDestination( unwrapping enum: Binding, @@ -363,12 +365,12 @@ @available( iOS, introduced: 13, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 10.15, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available(tvOS, unavailable) @available(watchOS, unavailable) @@ -390,22 +392,22 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @MainActor public func sheet( @@ -414,7 +416,7 @@ onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content ) -> some View - where Content: View { + where Content: View { sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } } @@ -423,22 +425,22 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public init?(unwrapping enum: Binding, case casePath: AnyCasePath) { guard var `case` = casePath.extract(from: `enum`.wrappedValue) @@ -460,25 +462,25 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func `case`(_ casePath: AnyCasePath) -> Binding - where Value == Enum? { + where Value == Enum? { .init( get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, set: { newValue, transaction in @@ -490,31 +492,31 @@ @available( iOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, deprecated: 9999, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public func isPresent(_ casePath: AnyCasePath) -> Binding - where Value == Enum? { + where Value == Enum? { self.case(casePath).isPresent() } } public struct IfCaseLet: View - where IfContent: View, ElseContent: View { + where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: AnyCasePath public let ifContent: (Binding) -> IfContent @@ -523,22 +525,22 @@ @available( iOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( macOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( tvOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( watchOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) public init( _ enum: Binding, @@ -591,7 +593,7 @@ } public struct IfLet: View - where IfContent: View, ElseContent: View { + where IfContent: View, ElseContent: View { public let value: Binding public let ifContent: (Binding) -> IfContent public let elseContent: ElseContent @@ -599,22 +601,22 @@ @available( iOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( macOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( tvOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) @available( watchOS, deprecated: 9999, message: - "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." + "Use '$enum.case.map { $case in … }' (and 'if !enum.is(\\.case) { … }' if you have an 'else' branch) with a '@CasePathable' enum, instead." ) public init( _ value: Binding, @@ -664,22 +666,22 @@ @available( iOS, introduced: 13, deprecated: 16, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( macOS, introduced: 10.15, deprecated: 13, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( tvOS, introduced: 13, deprecated: 16, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @available( watchOS, introduced: 6, deprecated: 9, message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." + "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) public init( unwrapping enum: Binding, @@ -700,22 +702,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct Switch: View { public let `enum`: Binding @@ -738,25 +740,25 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct CaseLet: View - where Content: View { + where Content: View { @EnvironmentObject private var `enum`: BindingObject public let casePath: AnyCasePath public let content: (Binding) -> Content @@ -777,22 +779,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) public struct Default: View { private let content: Content @@ -809,22 +811,22 @@ @available( iOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( macOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( tvOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) @available( watchOS, deprecated: 9999, message: - "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." + "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) extension Switch { public init( @@ -836,11 +838,12 @@ ) > ) - where + where Content == _ConditionalContent< CaseLet, Default - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -857,11 +860,12 @@ line: UInt = #line, @ViewBuilder content: () -> CaseLet ) - where + where Content == _ConditionalContent< CaseLet, Default<_ExhaustivityCheckView> - > { + > + { self.init(`enum`) { content() Default { _ExhaustivityCheckView(file: file, line: line) } @@ -878,14 +882,15 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -909,14 +914,15 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, CaseLet >, Default<_ExhaustivityCheckView> - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -941,7 +947,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, @@ -951,7 +957,8 @@ CaseLet, Default > - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -978,7 +985,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< CaseLet, @@ -988,7 +995,8 @@ CaseLet, Default<_ExhaustivityCheckView> > - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1016,7 +1024,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1029,7 +1037,8 @@ > >, Default - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1064,7 +1073,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1077,7 +1086,8 @@ > >, Default<_ExhaustivityCheckView> - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1108,7 +1118,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1124,7 +1134,8 @@ CaseLet, Default > - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1163,7 +1174,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1179,7 +1190,8 @@ CaseLet, Default<_ExhaustivityCheckView> > - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1213,7 +1225,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1232,7 +1244,8 @@ >, Default > - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1275,7 +1288,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1294,7 +1307,8 @@ >, Default<_ExhaustivityCheckView> > - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1331,7 +1345,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1353,7 +1367,8 @@ Default > > - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1400,7 +1415,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1422,7 +1437,8 @@ Default<_ExhaustivityCheckView> > > - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1462,7 +1478,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1487,7 +1503,8 @@ > >, Default - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1538,7 +1555,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1563,7 +1580,8 @@ > >, Default<_ExhaustivityCheckView> - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1606,7 +1624,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1634,7 +1652,8 @@ CaseLet, Default > - > { + > + { self.init(enum: `enum`) { let content = content().value if content.0.casePath.extract(from: `enum`.wrappedValue) != nil { @@ -1689,7 +1708,7 @@ ) > ) - where + where Content == _ConditionalContent< _ConditionalContent< _ConditionalContent< @@ -1717,7 +1736,8 @@ CaseLet, Default<_ExhaustivityCheckView> > - > { + > + { let content = content() self.init(`enum`) { content.value.0 @@ -1742,13 +1762,13 @@ public var body: some View { #if DEBUG let message = """ - Warning: Switch.body@\(self.file):\(self.line) + Warning: Switch.body@\(self.file):\(self.line) - "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" + "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ - provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. - """ + Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ + provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. + """ VStack(spacing: 17) { self.exclamation() .font(.largeTitle) @@ -1789,13 +1809,13 @@ let childMirror = Mirror(reflecting: child.value) let associatedValuesMirror = childMirror.displayStyle == .tuple - ? childMirror - : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) + ? childMirror + : Mirror(`enum`, unlabeledChildren: [child.value], displayStyle: .tuple) `case` = """ - \(label)(\ - \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ - ) - """ + \(label)(\ + \(associatedValuesMirror.children.map { "\($0.label ?? "_"):" }.joined())\ + ) + """ } else { `case` = "\(`enum`)" } @@ -1815,7 +1835,7 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func alert( unwrapping value: Binding?>, @@ -1833,7 +1853,7 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func alert( unwrapping enum: Binding, @@ -1852,7 +1872,7 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func confirmationDialog( unwrapping value: Binding?>, @@ -1870,7 +1890,7 @@ *, deprecated, message: - "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." + "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) public func confirmationDialog( unwrapping enum: Binding, @@ -1932,4 +1952,4 @@ ) } } -#endif // canImport(SwiftUI) +#endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigationCore/Binding.swift b/Sources/SwiftUINavigationCore/Binding.swift index 22de85a1ab..c1c4c89f07 100644 --- a/Sources/SwiftUINavigationCore/Binding.swift +++ b/Sources/SwiftUINavigationCore/Binding.swift @@ -9,8 +9,8 @@ /// /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. public func isPresent() -> Binding - where Value == Wrapped? { - self._isPresent + where Value == Wrapped? { + self._isPresent } } From 35526c29f1420a04694f45ca4bf4de09922a73b7 Mon Sep 17 00:00:00 2001 From: x_0o0 Date: Sat, 6 Apr 2024 02:05:58 +0900 Subject: [PATCH 029/124] [VER] Bump up Swift Compiler version to 5.7.1 (#134) Co-authored-by: Stephen Celis --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ff10a97ad2..ee0d514c20 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7.1 import PackageDescription From 2ec6c3a15293efff6083966b38439a4004f25565 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 5 Apr 2024 10:48:55 -0700 Subject: [PATCH 030/124] Add iOS 16-compatible `navigationDestination(item:)` to core (#148) * Add iOS 16-compatible `navigationDestination(item:)` * wip --- .../Documentation.docc/Articles/Bindings.md | 1 - .../SwiftUINavigationCore.md | 18 ++++++++++++- .../NavigationDestination.swift | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftUINavigationCore/NavigationDestination.swift diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index 5dae2c5dfd..07dd305f3c 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -78,7 +78,6 @@ struct SignInView: View { ### Binding transformations -- ``SwiftUI/Binding/isPresent()`` - ``SwiftUI/Binding/removeDuplicates()`` - ``SwiftUI/Binding/removeDuplicates(by:)`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md index 9807e67d2d..ae7be6d215 100644 --- a/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md +++ b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md @@ -1,6 +1,6 @@ # ``SwiftUINavigationCore`` -A few core types included in SwiftUI Navigation. +A few core types and modifiers included in SwiftUI Navigation. ## Topics @@ -10,3 +10,19 @@ A few core types included in SwiftUI Navigation. - ``AlertState`` - ``ConfirmationDialogState`` - ``ButtonState`` + +### Alert and dialog modifiers + +- ``SwiftUI/View/alert(item:title:actions:message:)`` +- ``SwiftUI/View/alert(item:title:actions:)`` +- ``SwiftUI/View/confirmationDialog(item:titleVisibility:title:actions:message:)`` +- ``SwiftUI/View/confirmationDialog(item:titleVisibility:title:actions:)`` + +### Bindings + +- ``SwiftUI/Binding/isPresent()`` +- ``SwiftUI/View/bind(_:to:)`` + +### Navigation + +- ``SwiftUI/View/navigationDestination(item:destination:)`` diff --git a/Sources/SwiftUINavigationCore/NavigationDestination.swift b/Sources/SwiftUINavigationCore/NavigationDestination.swift new file mode 100644 index 0000000000..9b593da463 --- /dev/null +++ b/Sources/SwiftUINavigationCore/NavigationDestination.swift @@ -0,0 +1,25 @@ +#if canImport(SwiftUI) + import SwiftUI + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + extension View { + /// Associates a destination view with a bound value for use within a navigation stack or + /// navigation split view. + /// + /// See `SwiftUI.View.navigationDestination(item:destination:)` for more information. + /// + /// - Parameters: + /// - item: A binding to the data presented, or `nil` if nothing is currently presented. + /// - destination: A view builder that defines a view to display when `item` is not `nil`. + public func navigationDestination( + item: Binding, + @ViewBuilder destination: @escaping (D) -> C + ) -> some View { + navigationDestination(isPresented: item.isPresent()) { + if let item = item.wrappedValue { + destination(item) + } + } + } + } +#endif // canImport(SwiftUI) From 5f77d0ac68e35013bef787590486e634f235d769 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 9 May 2024 14:12:21 -0700 Subject: [PATCH 031/124] Prevent case navigation binding from writing to other cases (#149) * wip * wip * wip --- Sources/SwiftUINavigation/Binding.swift | 16 ++--- .../SwiftUINavigationTests/BindingTests.swift | 59 +++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 Tests/SwiftUINavigationTests/BindingTests.swift diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index bd1547b7fd..c6e0441f81 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -131,7 +131,9 @@ fileprivate subscript( keyPath: KeyPath> ) -> Member? { - get { Self.allCasePaths[keyPath: keyPath].extract(from: self) } + get { + Self.allCasePaths[keyPath: keyPath].extract(from: self) + } set { guard let newValue else { return } self = Self.allCasePaths[keyPath: keyPath].embed(newValue) @@ -144,15 +146,13 @@ keyPath: KeyPath> ) -> Member? { get { - guard let wrapped = self else { return nil } - return Wrapped.allCasePaths[keyPath: keyPath].extract(from: wrapped) + self.flatMap(Wrapped.allCasePaths[keyPath: keyPath].extract(from:)) } set { - guard let newValue else { - self = nil - return - } - self = Wrapped.allCasePaths[keyPath: keyPath].embed(newValue) + let casePath = Wrapped.allCasePaths[keyPath: keyPath] + guard self.flatMap(casePath.extract(from:)) != nil + else { return } + self = newValue.map(casePath.embed) } } } diff --git a/Tests/SwiftUINavigationTests/BindingTests.swift b/Tests/SwiftUINavigationTests/BindingTests.swift new file mode 100644 index 0000000000..7c44bde77d --- /dev/null +++ b/Tests/SwiftUINavigationTests/BindingTests.swift @@ -0,0 +1,59 @@ +#if swift(>=5.9) && canImport(SwiftUI) + import CustomDump + import SwiftUI + import SwiftUINavigation + import XCTest + + final class BindingTests: XCTestCase { + @CasePathable + @dynamicMemberLookup + enum Status: Equatable { + case inStock(quantity: Int) + case outOfStock(isOnBackOrder: Bool) + } + + func testCaseLookup() throws { + @Binding var status: Status + _status = Binding(initialValue: .inStock(quantity: 1)) + + let inStock = try XCTUnwrap($status.inStock) + inStock.wrappedValue += 1 + + XCTAssertEqual(status, .inStock(quantity: 2)) + } + + func testCaseCannotReplaceOtherCase() throws { + @Binding var status: Status + _status = Binding(initialValue: .inStock(quantity: 1)) + + let inStock = try XCTUnwrap($status.inStock) + + status = .outOfStock(isOnBackOrder: true) + + inStock.wrappedValue = 42 + XCTAssertEqual(status, .outOfStock(isOnBackOrder: true)) + } + + func testDestinationCannotReplaceOtherDestination() throws { + @Binding var destination: Status? + _destination = Binding(initialValue: .inStock(quantity: 1)) + + let inStock = try XCTUnwrap($destination.inStock) + + destination = .outOfStock(isOnBackOrder: true) + + inStock.wrappedValue = 42 + XCTAssertEqual(destination, .outOfStock(isOnBackOrder: true)) + } + } + + private extension Binding { + init(initialValue: Value) { + var value = initialValue + self.init( + get: { value }, + set: { value = $0 } + ) + } + } +#endif // canImport(SwiftUI) From 97da14204e10d895591a864902f7a883f901bc7a Mon Sep 17 00:00:00 2001 From: stephencelis Date: Thu, 9 May 2024 21:13:42 +0000 Subject: [PATCH 032/124] Run swift-format --- Tests/SwiftUINavigationTests/BindingTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftUINavigationTests/BindingTests.swift b/Tests/SwiftUINavigationTests/BindingTests.swift index 7c44bde77d..3ee0db10a4 100644 --- a/Tests/SwiftUINavigationTests/BindingTests.swift +++ b/Tests/SwiftUINavigationTests/BindingTests.swift @@ -47,8 +47,8 @@ } } - private extension Binding { - init(initialValue: Value) { + extension Binding { + fileprivate init(initialValue: Value) { var value = initialValue self.init( get: { value }, From ec3b0f9882e004e8f906c7ef43870615abf32a56 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 13 May 2024 10:03:18 -0700 Subject: [PATCH 033/124] Update Package.resolved. --- Package.resolved | 114 +++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5962320b06..1f81df2040 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,61 +1,59 @@ { - "object": { - "pins": [ - { - "package": "swift-case-paths", - "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", - "state": { - "branch": null, - "revision": "e593aba2c6222daad7c4f2732a431eed2c09bb07", - "version": "1.3.0" - } - }, - { - "package": "swift-custom-dump", - "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", - "state": { - "branch": null, - "revision": "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", - "version": "1.1.2" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", - "version": "1.3.0" - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": null, - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": "1.0.0" - } - }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": null, - "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version": "509.0.2" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version": "1.0.2" - } + "pins" : [ + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", + "version" : "1.3.3" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + } + ], + "version" : 2 } From 2b7a69b8031b1f79fb212bae2fbd6de0d47b594b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 20 May 2024 13:24:39 -0700 Subject: [PATCH 034/124] Update examples and fix deprecation warning (#153) * Update examples and fix deprecation warning * wip * wip --- Examples/CaseStudies/01-Alerts.swift | 25 +- .../CaseStudies/02-ConfirmationDialogs.swift | 20 +- .../Internal/Deprecations.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 240 +++++++++--------- .../SwiftUINavigationTests/BindingTests.swift | 16 +- 5 files changed, 151 insertions(+), 152 deletions(-) diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index d72b8f1149..afdc846271 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -21,19 +21,18 @@ struct OptionalAlerts: View { } .disabled(self.model.isLoading) } - .alert( - title: { Text("Fact about \($0.number)") }, - unwrapping: self.$model.fact, - actions: { - Button("Get another fact about \($0.number)") { - Task { await self.model.numberFactButtonTapped() } - } - Button("Close", role: .cancel) { - self.model.fact = nil - } - }, - message: { Text($0.description) } - ) + .alert(item: self.$model.fact) { + Text("Fact about \($0.number)") + } actions: { + Button("Get another fact about \($0.number)") { + Task { await self.model.numberFactButtonTapped() } + } + Button("Close", role: .cancel) { + self.model.fact = nil + } + } message: { + Text($0.description) + } .navigationTitle("Alerts") } } diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index ffa1b26065..a53d62c82c 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -20,17 +20,15 @@ struct OptionalConfirmationDialogs: View { } } .disabled(self.model.isLoading) - .confirmationDialog( - title: { Text("Fact about \($0.number)") }, - titleVisibility: .visible, - unwrapping: self.$model.fact, - actions: { - Button("Get another fact about \($0.number)") { - Task { await self.model.numberFactButtonTapped() } - } - }, - message: { Text($0.description) } - ) + .confirmationDialog(item: self.$model.fact, titleVisibility: .visible) { + Text("Fact about \($0.number)") + } actions: { + Button("Get another fact about \($0.number)") { + Task { await self.model.numberFactButtonTapped() } + } + } message: { + Text($0.description) + } } .navigationTitle("Dialogs") } diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 111d61d4c0..cb417f7950 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -17,7 +17,7 @@ } @available( - *, deprecated, renamed: "confirmationDialog(item:textVisibility:title:actions:message:)" + *, deprecated, renamed: "confirmationDialog(item:titleVisibility:title:actions:message:)" ) public func confirmationDialog( title: (Value) -> Text, diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index a6a4bc1692..114ce490b8 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,124 +1,122 @@ { - "object": { - "pins": [ - { - "package": "combine-schedulers", - "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", - "state": { - "branch": null, - "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version": "1.0.0" - } - }, - { - "package": "swift-case-paths", - "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", - "state": { - "branch": null, - "revision": "8cc3bc05d0cc956f7374c6c208a11f66a7cac3db", - "version": "1.2.2" - } - }, - { - "package": "swift-clocks", - "repositoryURL": "https://github.com/pointfreeco/swift-clocks", - "state": { - "branch": null, - "revision": "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version": "1.0.2" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections", - "state": { - "branch": null, - "revision": "d029d9d39c87bed85b1c50adee7c41795261a192", - "version": "1.0.6" - } - }, - { - "package": "swift-concurrency-extras", - "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", - "state": { - "branch": null, - "revision": "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version": "1.1.0" - } - }, - { - "package": "swift-custom-dump", - "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", - "state": { - "branch": null, - "revision": "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", - "version": "1.1.2" - } - }, - { - "package": "swift-dependencies", - "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", - "state": { - "branch": null, - "revision": "c31b1445c4fae49e6fdb75496b895a3653f6aefc", - "version": "1.1.5" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", - "version": "1.3.0" - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": null, - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": "1.0.0" - } - }, - { - "package": "swift-identified-collections", - "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", - "state": { - "branch": null, - "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version": "1.0.0" - } - }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": null, - "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version": "509.0.2" - } - }, - { - "package": "swift-tagged", - "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", - "state": { - "branch": null, - "revision": "3907a9438f5b57d317001dc99f3f11b46882272b", - "version": "0.10.0" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version": "1.0.2" - } + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "8cc3bc05d0cc956f7374c6c208a11f66a7cac3db", + "version" : "1.2.2" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "http://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "c31b1445c4fae49e6fdb75496b895a3653f6aefc", + "version" : "1.1.5" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections.git", + "state" : { + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged.git", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version" : "1.0.2" + } + } + ], + "version" : 2 } diff --git a/Tests/SwiftUINavigationTests/BindingTests.swift b/Tests/SwiftUINavigationTests/BindingTests.swift index 3ee0db10a4..ba85a092e2 100644 --- a/Tests/SwiftUINavigationTests/BindingTests.swift +++ b/Tests/SwiftUINavigationTests/BindingTests.swift @@ -35,15 +35,19 @@ } func testDestinationCannotReplaceOtherDestination() throws { - @Binding var destination: Status? - _destination = Binding(initialValue: .inStock(quantity: 1)) + #if os(iOS) || os(macOS) + try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) - let inStock = try XCTUnwrap($destination.inStock) + @Binding var destination: Status? + _destination = Binding(initialValue: .inStock(quantity: 1)) - destination = .outOfStock(isOnBackOrder: true) + let inStock = try XCTUnwrap($destination.inStock) - inStock.wrappedValue = 42 - XCTAssertEqual(destination, .outOfStock(isOnBackOrder: true)) + destination = .outOfStock(isOnBackOrder: true) + + inStock.wrappedValue = 42 + XCTAssertEqual(destination, .outOfStock(isOnBackOrder: true)) + #endif } } From 72dbb2a5cac2e877242c553a3b4ad6840fe33da9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 27 May 2024 14:18:18 -0700 Subject: [PATCH 035/124] Add `sheet(item:id:)` (#155) * Add `sheet(item:id:)` `ForEach` has a convenient initializer that takes a key path to some hashable identifier so that the element isn't forced into an identifiable conformance, but `sheet`, `fullScreenCover` and `popover` aren't given the same affordances. Let's close the gap. * fix --- .../SwiftUINavigation/FullScreenCover.swift | 34 +++++++++++++--- .../Internal/Identified.swift | 10 +++++ Sources/SwiftUINavigation/Popover.swift | 39 +++++++++++++++++-- Sources/SwiftUINavigation/Sheet.swift | 22 +++++++++++ 4 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 Sources/SwiftUINavigation/Internal/Identified.swift diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index 317b40e733..ad2d6a8eb5 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -1,7 +1,31 @@ #if canImport(SwiftUI) import SwiftUI + @available(iOS 14, tvOS 14, watchOS 7, *) + @available(macOS, unavailable) extension View { + /// Presents a full-screen cover using a binding as a data source for the sheet's content based + /// on the identity of the underlying item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item` changes, the + /// system dismisses the sheet and replaces it with a new one using the same process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + public func fullScreenCover( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + self.fullScreenCover(item: item[id: id], onDismiss: onDismiss) { _ in + item.wrappedValue.map(content) + } + } + /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// /// SwiftUI comes with a `fullScreenCover(item:)` view modifier that is powered by a binding to @@ -36,15 +60,13 @@ /// /// - Parameters: /// - value: A binding to a source of truth for the sheet. When `value` is non-`nil`, a - /// non-optional binding to the value is passed to the `content` closure. You use this binding - /// to produce content that the system presents to the user in a sheet. Changes made to the - /// sheet's binding will be reflected back in the source of truth. Likewise, changes to - /// `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is + /// non-optional binding to the value is passed to the `content` closure. You use this + /// binding to produce content that the system presents to the user in a sheet. Changes made + /// to the sheet's binding will be reflected back in the source of truth. Likewise, changes + /// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is /// dismissed. /// - onDismiss: The closure to execute when dismissing the sheet. /// - content: A closure returning the content of the sheet. - @available(iOS 14, tvOS 14, watchOS 7, *) - @available(macOS, unavailable) public func fullScreenCover( unwrapping value: Binding, onDismiss: (() -> Void)? = nil, diff --git a/Sources/SwiftUINavigation/Internal/Identified.swift b/Sources/SwiftUINavigation/Internal/Identified.swift new file mode 100644 index 0000000000..f8add1e0a3 --- /dev/null +++ b/Sources/SwiftUINavigation/Internal/Identified.swift @@ -0,0 +1,10 @@ +struct Identified: Identifiable { + let id: ID +} + +extension Optional { + subscript(id keyPath: KeyPath) -> Identified? { + get { (self?[keyPath: keyPath]).map(Identified.init) } + set { if newValue == nil { self = nil } } + } +} diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 43ce6238c9..f2ec1290db 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -1,7 +1,40 @@ #if canImport(SwiftUI) import SwiftUI + @available(tvOS, unavailable) + @available(watchOS, unavailable) extension View { + /// Presents a popover using a binding as a data source for the sheet's content based on the + /// identity of the underlying item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the popover. When `item` is + /// non-`nil`, the system passes the item's content to the modifier's closure. You display + /// this content in a popover that you create that the system displays to the user. If `item` + /// changes, the system dismisses the popover and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the + /// popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow. + /// - content: A closure returning the content of the popover. + public func popover( + item: Binding, + id: KeyPath, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + self.popover( + item: item[id: id], + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge + ) { _ in + item.wrappedValue.map(content) + } + } + /// Presents a popover using a binding as a data source for the popover's content. /// /// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some @@ -46,14 +79,12 @@ /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's /// arrow. /// - content: A closure returning the content of the popover. - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public func popover( + public func popover( unwrapping value: Binding, attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge = .top, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View where Content: View { + ) -> some View { self.popover( isPresented: value.isPresent(), attachmentAnchor: attachmentAnchor, diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index 6823b78e3b..f8c711b4cf 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -8,6 +8,28 @@ #endif extension View { + /// Presents a sheet using a binding as a data source for the sheet's content based on the + /// identity of the underlying item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item` changes, the + /// system dismisses the sheet and replaces it with a new one using the same process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + public func sheet( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + self.sheet(item: item[id: id], onDismiss: onDismiss) { _ in + item.wrappedValue.map(content) + } + } + /// Presents a sheet using a binding as a data source for the sheet's content. /// /// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some From a0ede333b8afd7c2f3c47aaa8f1447ad17db1595 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 28 May 2024 08:30:37 -0700 Subject: [PATCH 036/124] Leverage `alert(item:)` in `alert(_ state:)` (#156) --- Sources/SwiftUINavigation/Alert.swift | 40 +++++++--------- .../ConfirmationDialog.swift | 48 +++++++++---------- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index d3f0e10853..cfe8fced57 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -20,17 +20,15 @@ _ state: Binding?>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.alert( - (state.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: state.isPresent(), - presenting: state.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) + alert(item: state) { + Text($0.title) + } actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + } message: { + $0.message.map(Text.init) + } } /// Presents an alert from a binding to optional alert state. @@ -52,17 +50,15 @@ _ state: Binding?>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.alert( - (state.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: state.isPresent(), - presenting: state.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) + alert(item: state) { + Text($0.title) + } actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + } message: { + $0.message.map(Text.init) + } } } #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 6543e2b9f9..eae1840819 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -19,18 +19,18 @@ _ state: Binding?>, action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { - self.confirmationDialog( - state.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: state.isPresent(), - titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: state.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) + confirmationDialog( + item: state, + titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic + ) { + Text($0.title) + } actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + } message: { + $0.message.map(Text.init) + } } /// Presents a confirmation dialog from a binding to optional confirmation dialog state. @@ -53,18 +53,18 @@ _ state: Binding?>, action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { - self.confirmationDialog( - state.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: state.isPresent(), - titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, - presenting: state.wrappedValue, - actions: { - ForEach($0.buttons) { - Button($0, action: handler) - } - }, - message: { $0.message.map { Text($0) } } - ) + confirmationDialog( + item: state, + titleVisibility: state.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic + ) { + Text($0.title) + } actions: { + ForEach($0.buttons) { + Button($0, action: handler) + } + } message: { + $0.message.map(Text.init) + } } } #endif // canImport(SwiftUI) From 7f163578703446955a53510ee3f4d3ff10919d28 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 28 May 2024 09:33:01 -0700 Subject: [PATCH 037/124] Remove `sheet(unwrapping:)`, etc., helpers for `sheet(item:)` overloads (#157) * Remove `sheet(unwrapping:)`, etc., helpers for `sheet(item:)` overloads * wip * wip --- Examples/CaseStudies/01-Alerts.swift | 20 +- .../CaseStudies/02-ConfirmationDialogs.swift | 18 +- Examples/CaseStudies/03-Sheets.swift | 48 +-- Examples/CaseStudies/04-Popovers.swift | 46 +-- .../CaseStudies/05-FullScreenCovers.swift | 48 +-- .../06-NavigationDestinations.swift | 52 ++-- Examples/CaseStudies/07-NavigationLinks.swift | 46 +-- Examples/CaseStudies/08-Routing.swift | 26 +- .../CaseStudies/09-CustomComponents.swift | 36 +-- .../CaseStudies/10-SynchronizedBindings.swift | 22 +- Examples/CaseStudies/11-IfLet.swift | 14 +- Examples/CaseStudies/12-IfCaseLet.swift | 14 +- Examples/CaseStudies/FactClient.swift | 2 +- Examples/Inventory/Inventory.swift | 42 +-- Examples/Inventory/ItemRow.swift | 44 +-- .../Articles/AlertsDialogs.md | 70 +++-- .../Documentation.docc/Articles/Bindings.md | 4 +- .../Documentation.docc/Articles/Navigation.md | 32 +- .../Articles/SheetsPopoversCovers.md | 30 +- .../Articles/WhatIsNavigation.md | 72 ++--- .../Extensions/Deprecations.md | 6 + .../Documentation.docc/SwiftUINavigation.md | 24 +- .../SwiftUINavigation/FullScreenCover.swift | 110 ++++--- .../Internal/Deprecations.swift | 286 +++++++++--------- .../NavigationDestination.swift | 59 +--- Sources/SwiftUINavigation/Popover.swift | 131 +++++--- Sources/SwiftUINavigation/Sheet.swift | 105 ++++--- 27 files changed, 729 insertions(+), 678 deletions(-) diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index afdc846271..9f91a46aa6 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -7,28 +7,28 @@ struct OptionalAlerts: View { var body: some View { List { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) Button { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } label: { HStack { Text("Get number fact") - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } } } - .disabled(self.model.isLoading) + .disabled(model.isLoading) } - .alert(item: self.$model.fact) { + .alert(item: $model.fact) { Text("Fact about \($0.number)") } actions: { Button("Get another fact about \($0.number)") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } Button("Close", role: .cancel) { - self.model.fact = nil + model.fact = nil } } message: { Text($0.description) @@ -45,9 +45,9 @@ private class FeatureModel { @MainActor func numberFactButtonTapped() async { - self.isLoading = true - self.fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = true + defer { isLoading = false } + fact = await getNumberFact(count) } } diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index a53d62c82c..1c7c8e72d9 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -7,24 +7,24 @@ struct OptionalConfirmationDialogs: View { var body: some View { List { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) Button { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } label: { HStack { Text("Get number fact") - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } } } - .disabled(self.model.isLoading) - .confirmationDialog(item: self.$model.fact, titleVisibility: .visible) { + .disabled(model.isLoading) + .confirmationDialog(item: $model.fact, titleVisibility: .visible) { Text("Fact about \($0.number)") } actions: { Button("Get another fact about \($0.number)") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } } message: { Text($0.description) @@ -42,9 +42,9 @@ private class FeatureModel { @MainActor func numberFactButtonTapped() async { - self.isLoading = true - self.fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = true + defer { isLoading = false } + fact = await getNumberFact(count) } } diff --git a/Examples/CaseStudies/03-Sheets.swift b/Examples/CaseStudies/03-Sheets.swift index 80e2ab5577..f1d29169fc 100644 --- a/Examples/CaseStudies/03-Sheets.swift +++ b/Examples/CaseStudies/03-Sheets.swift @@ -7,14 +7,14 @@ struct OptionalSheets: View { var body: some View { List { Section { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) HStack { Button("Get number fact") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } @@ -24,28 +24,28 @@ struct OptionalSheets: View { } Section { - ForEach(self.model.savedFacts) { fact in + ForEach(model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + .onDelete { model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .sheet(unwrapping: self.$model.fact) { $fact in + .sheet(item: $model.fact) { $fact in NavigationStack { FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) + .disabled(model.isLoading) + .foregroundColor(model.isLoading ? .gray : nil) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.model.cancelButtonTapped() + model.cancelButtonTapped() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.model.saveButtonTapped(fact: fact) + model.saveButtonTapped(fact: fact) } } } @@ -60,7 +60,7 @@ private struct FactEditor: View { var body: some View { VStack { - TextEditor(text: self.$fact) + TextEditor(text: $fact) } .padding() .navigationTitle("Fact editor") @@ -76,41 +76,41 @@ private class FeatureModel { private var task: Task? deinit { - self.task?.cancel() + task?.cancel() } @MainActor func numberFactButtonTapped() async { - self.isLoading = true - self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { + isLoading = true + fact = Fact(description: "\(count) is still loading...", number: count) + task = Task { let fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = false guard !Task.isCancelled else { return } self.fact = fact } - await self.task?.value + await task?.value } @MainActor func cancelButtonTapped() { - self.task?.cancel() - self.task = nil - self.fact = nil + task?.cancel() + task = nil + fact = nil } @MainActor func saveButtonTapped(fact: Fact) { - self.task?.cancel() - self.task = nil - self.savedFacts.append(fact) + task?.cancel() + task = nil + savedFacts.append(fact) self.fact = nil } @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { - self.savedFacts.remove(atOffsets: offsets) + savedFacts.remove(atOffsets: offsets) } } diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift index da6d85de9d..292816baf2 100644 --- a/Examples/CaseStudies/04-Popovers.swift +++ b/Examples/CaseStudies/04-Popovers.swift @@ -7,29 +7,29 @@ struct OptionalPopovers: View { var body: some View { List { Section { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) HStack { Button("Get number fact") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } - .popover(unwrapping: self.$model.fact, arrowEdge: .bottom) { $fact in + .popover(item: $model.fact, arrowEdge: .bottom) { $fact in NavigationStack { FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) + .disabled(model.isLoading) + .foregroundColor(model.isLoading ? .gray : nil) .navigationBarItems( leading: Button("Cancel") { - self.model.cancelButtonTapped() + model.cancelButtonTapped() }, trailing: Button("Save") { - self.model.saveButtonTapped(fact: fact) + model.saveButtonTapped(fact: fact) } ) } } - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } @@ -39,10 +39,10 @@ struct OptionalPopovers: View { } Section { - ForEach(self.model.savedFacts) { fact in + ForEach(model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + .onDelete { model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } @@ -56,7 +56,7 @@ private struct FactEditor: View { var body: some View { VStack { - TextEditor(text: self.$fact) + TextEditor(text: $fact) } .padding() .navigationTitle("Fact editor") @@ -77,36 +77,36 @@ private class FeatureModel { @MainActor func numberFactButtonTapped() async { - self.isLoading = true - self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { + isLoading = true + fact = Fact(description: "\(count) is still loading...", number: count) + task = Task { let fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = false guard !Task.isCancelled else { return } self.fact = fact } - await self.task?.value + await task?.value } @MainActor func cancelButtonTapped() { - self.task?.cancel() - self.task = nil - self.fact = nil + task?.cancel() + task = nil + fact = nil } @MainActor func saveButtonTapped(fact: Fact) { - self.task?.cancel() - self.task = nil - self.savedFacts.append(fact) + task?.cancel() + task = nil + savedFacts.append(fact) self.fact = nil } @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { - self.savedFacts.remove(atOffsets: offsets) + savedFacts.remove(atOffsets: offsets) } } diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift index 2fb26c50d7..cb33d8f61c 100644 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ b/Examples/CaseStudies/05-FullScreenCovers.swift @@ -7,14 +7,14 @@ struct OptionalFullScreenCovers: View { var body: some View { List { Section { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) HStack { Button("Get number fact") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } @@ -24,28 +24,28 @@ struct OptionalFullScreenCovers: View { } Section { - ForEach(self.model.savedFacts) { fact in + ForEach(model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + .onDelete { model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .fullScreenCover(unwrapping: self.$model.fact) { $fact in + .fullScreenCover(item: $model.fact) { $fact in NavigationStack { FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) + .disabled(model.isLoading) + .foregroundColor(model.isLoading ? .gray : nil) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.model.cancelButtonTapped() + model.cancelButtonTapped() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.model.saveButtonTapped(fact: fact) + model.saveButtonTapped(fact: fact) } } } @@ -60,7 +60,7 @@ private struct FactEditor: View { var body: some View { VStack { - TextEditor(text: self.$fact) + TextEditor(text: $fact) } .padding() .navigationTitle("Fact editor") @@ -77,36 +77,36 @@ private class FeatureModel { @MainActor func numberFactButtonTapped() async { - self.isLoading = true - self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { - let fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = true + fact = Fact(description: "\(count) is still loading...", number: count) + task = Task { + let fact = await getNumberFact(count) + isLoading = false guard !Task.isCancelled else { return } self.fact = fact } - await self.task?.value + await task?.value } @MainActor func cancelButtonTapped() { - self.task?.cancel() - self.task = nil - self.fact = nil + task?.cancel() + task = nil + fact = nil } @MainActor func saveButtonTapped(fact: Fact) { - self.task?.cancel() - self.task = nil - self.savedFacts.append(fact) + task?.cancel() + task = nil + savedFacts.append(fact) self.fact = nil } @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { - self.savedFacts.remove(atOffsets: offsets) + savedFacts.remove(atOffsets: offsets) } } diff --git a/Examples/CaseStudies/06-NavigationDestinations.swift b/Examples/CaseStudies/06-NavigationDestinations.swift index 426ae5e065..33dd0a43be 100644 --- a/Examples/CaseStudies/06-NavigationDestinations.swift +++ b/Examples/CaseStudies/06-NavigationDestinations.swift @@ -8,14 +8,14 @@ struct NavigationDestinations: View { var body: some View { List { Section { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) HStack { Button("Get number fact") { - Task { await self.model.numberFactButtonTapped() } + Task { await model.numberFactButtonTapped() } } - if self.model.isLoading { + if model.isLoading { Spacer() ProgressView() } @@ -25,29 +25,29 @@ struct NavigationDestinations: View { } Section { - ForEach(self.model.savedFacts) { fact in + ForEach(model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + .onDelete { model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } .navigationTitle("Destinations") - .navigationDestination(unwrapping: self.$model.fact) { $fact in + .navigationDestination(item: $model.fact) { $fact in FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) + .disabled(model.isLoading) + .foregroundColor(model.isLoading ? .gray : nil) .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - Task { await self.model.cancelButtonTapped() } + Task { await model.cancelButtonTapped() } } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - Task { await self.model.saveButtonTapped(fact: fact) } + Task { await model.saveButtonTapped(fact: fact) } } } } @@ -61,9 +61,9 @@ private struct FactEditor: View { var body: some View { VStack { if #available(iOS 14, *) { - TextEditor(text: self.$fact) + TextEditor(text: $fact) } else { - TextField("Untitled", text: self.$fact) + TextField("Untitled", text: $fact) } } .padding() @@ -80,48 +80,48 @@ private class FeatureModel { private var task: Task? deinit { - self.task?.cancel() + task?.cancel() } @MainActor func setFactNavigation(isActive: Bool) async { if isActive { - self.isLoading = true - self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { + isLoading = true + fact = Fact(description: "\(count) is still loading...", number: count) + task = Task { let fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = false guard !Task.isCancelled else { return } self.fact = fact } - await self.task?.value + await task?.value } else { - self.task?.cancel() - self.task = nil - self.fact = nil + task?.cancel() + task = nil + fact = nil } } @MainActor func numberFactButtonTapped() async { - await self.setFactNavigation(isActive: true) + await setFactNavigation(isActive: true) } @MainActor func cancelButtonTapped() async { - await self.setFactNavigation(isActive: false) + await setFactNavigation(isActive: false) } @MainActor func saveButtonTapped(fact: Fact) async { - self.savedFacts.append(fact) - await self.setFactNavigation(isActive: false) + savedFacts.append(fact) + await setFactNavigation(isActive: false) } @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { - self.savedFacts.remove(atOffsets: offsets) + savedFacts.remove(atOffsets: offsets) } } diff --git a/Examples/CaseStudies/07-NavigationLinks.swift b/Examples/CaseStudies/07-NavigationLinks.swift index 88aa512392..4b87b6b053 100644 --- a/Examples/CaseStudies/07-NavigationLinks.swift +++ b/Examples/CaseStudies/07-NavigationLinks.swift @@ -7,11 +7,11 @@ struct OptionalNavigationLinks: View { var body: some View { List { Section { - Stepper("Number: \(self.model.count)", value: self.$model.count) + Stepper("Number: \(model.count)", value: $model.count) HStack { Button("Get number fact") { - Task { await self.model.setFactNavigation(isActive: true) } + Task { await model.setFactNavigation(isActive: true) } } if self.model.isLoading { @@ -24,28 +24,28 @@ struct OptionalNavigationLinks: View { } Section { - ForEach(self.model.savedFacts) { fact in + ForEach(model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + .onDelete { model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .navigationDestination(unwrapping: self.$model.fact) { $fact in + .navigationDestination(item: $model.fact) { $fact in FactEditor(fact: $fact.description) - .disabled(self.model.isLoading) - .foregroundColor(self.model.isLoading ? .gray : nil) + .disabled(model.isLoading) + .foregroundColor(model.isLoading ? .gray : nil) .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - Task { await self.model.cancelButtonTapped() } + Task { await model.cancelButtonTapped() } } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - Task { await self.model.saveButtonTapped(fact: fact) } + Task { await model.saveButtonTapped(fact: fact) } } } } @@ -59,7 +59,7 @@ private struct FactEditor: View { var body: some View { VStack { - TextEditor(text: self.$fact) + TextEditor(text: $fact) } .padding() .navigationTitle("Fact editor") @@ -75,43 +75,43 @@ private class FeatureModel { private var task: Task? deinit { - self.task?.cancel() + task?.cancel() } @MainActor func setFactNavigation(isActive: Bool) async { if isActive { - self.isLoading = true - self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { + isLoading = true + fact = Fact(description: "\(count) is still loading...", number: count) + task = Task { let fact = await getNumberFact(self.count) - self.isLoading = false + isLoading = false guard !Task.isCancelled else { return } self.fact = fact } - await self.task?.value + await task?.value } else { - self.task?.cancel() - self.task = nil - self.fact = nil + task?.cancel() + task = nil + fact = nil } } @MainActor func cancelButtonTapped() async { - await self.setFactNavigation(isActive: false) + await setFactNavigation(isActive: false) } @MainActor func saveButtonTapped(fact: Fact) async { - self.savedFacts.append(fact) - await self.setFactNavigation(isActive: false) + savedFacts.append(fact) + await setFactNavigation(isActive: false) } @MainActor func removeSavedFacts(atOffsets offsets: IndexSet) { - self.savedFacts.remove(atOffsets: offsets) + savedFacts.remove(atOffsets: offsets) } } diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift index 0b75f9c649..6c34662a68 100644 --- a/Examples/CaseStudies/08-Routing.swift +++ b/Examples/CaseStudies/08-Routing.swift @@ -39,11 +39,11 @@ struct Routing: View { } Section { - Text("Count: \(self.count)") + Text("Count: \(count)") } Button("Alert") { - self.destination = .alert( + destination = .alert( AlertState { TextState("Update count?") } actions: { @@ -58,7 +58,7 @@ struct Routing: View { } Button("Confirmation dialog") { - self.destination = .confirmationDialog( + destination = .confirmationDialog( ConfirmationDialogState(titleVisibility: .visible) { TextState("Update count?") } actions: { @@ -73,41 +73,41 @@ struct Routing: View { } Button("Link") { - self.destination = .link(self.count) + destination = .link(count) } Button("Sheet") { - self.destination = .sheet(self.count) + destination = .sheet(count) } } .navigationTitle("Routing") - .alert(self.$destination.alert) { action in + .alert($destination.alert) { action in switch action { case .randomize?: - self.count = .random(in: 0...1_000) + count = .random(in: 0...1_000) case .reset?: - self.count = 0 + count = 0 case nil: break } } - .confirmationDialog(self.$destination.confirmationDialog) { action in + .confirmationDialog($destination.confirmationDialog) { action in switch action { case .decrement?: - self.count -= 1 + count -= 1 case .increment?: - self.count += 1 + count += 1 case nil: break } } - .navigationDestination(unwrapping: self.$destination.link) { $count in + .navigationDestination(item: $destination.link) { $count in Form { Stepper("Count: \(count)", value: $count) } .navigationTitle("Routing link") } - .sheet(unwrapping: self.$destination.sheet) { $count in + .sheet(item: $destination.sheet, id: \.self) { $count in NavigationStack { Form { Stepper("Count: \(count)", value: $count) diff --git a/Examples/CaseStudies/09-CustomComponents.swift b/Examples/CaseStudies/09-CustomComponents.swift index 329b8e61c0..54d7b36749 100644 --- a/Examples/CaseStudies/09-CustomComponents.swift +++ b/Examples/CaseStudies/09-CustomComponents.swift @@ -25,16 +25,16 @@ struct CustomComponents: View { Button("Show bottom menu") { withAnimation { - self.count = 0 + count = 0 } } - if let count = self.count, count > 0 { + if let count = count, count > 0 { Text("Current count: \(count)") .transition(.opacity) } } - .bottomMenu(unwrapping: self.$count) { $count in + .bottomMenu(item: $count) { $count in Stepper("Number: \(count)", value: $count.animation()) } .navigationTitle("Custom components") @@ -49,13 +49,13 @@ where BottomMenuContent: View { func body(content: Content) -> some View { content.overlay( ZStack(alignment: .bottom) { - if self.isActive { + if isActive { Rectangle() .fill(Color.black.opacity(0.4)) .frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { withAnimation { - self.isActive = false + isActive = false } } .zIndex(1) @@ -83,7 +83,7 @@ extension View { @ViewBuilder content: @escaping () -> Content ) -> some View where Content: View { - self.modifier( + modifier( BottomMenuModifier( isActive: isActive, content: content @@ -91,30 +91,18 @@ extension View { ) } - fileprivate func bottomMenu( - unwrapping value: Binding, - @ViewBuilder content: @escaping (Binding) -> Content + fileprivate func bottomMenu( + item: Binding, + @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { - self.modifier( + modifier( BottomMenuModifier( - isActive: value.isPresent(), - content: { Binding(unwrapping: value).map(content) } + isActive: item.isPresent(), + content: { Binding(unwrapping: item).map(content) } ) ) } - - fileprivate func bottomMenu( - unwrapping value: Binding, - case casePath: AnyCasePath, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.bottomMenu( - unwrapping: value.case(casePath), - content: content - ) - } } #Preview { diff --git a/Examples/CaseStudies/10-SynchronizedBindings.swift b/Examples/CaseStudies/10-SynchronizedBindings.swift index c141de8330..c8abcaf132 100644 --- a/Examples/CaseStudies/10-SynchronizedBindings.swift +++ b/Examples/CaseStudies/10-SynchronizedBindings.swift @@ -19,20 +19,20 @@ struct SynchronizedBindings: View { } Section { - TextField("Username", text: self.$model.username) - .focused(self.$focusedField, equals: .username) + TextField("Username", text: $model.username) + .focused($focusedField, equals: .username) - SecureField("Password", text: self.$model.password) - .focused(self.$focusedField, equals: .password) + SecureField("Password", text: $model.password) + .focused($focusedField, equals: .password) Button("Sign In") { - self.model.signInButtonTapped() + model.signInButtonTapped() } .buttonStyle(.borderedProminent) } .textFieldStyle(.roundedBorder) } - .bind(self.$model.focusedField, to: self.$focusedField) + .bind($model.focusedField, to: $focusedField) .navigationTitle("Synchronized focus") } } @@ -49,12 +49,12 @@ private class FeatureModel { var username: String = "" func signInButtonTapped() { - if self.username.isEmpty { - self.focusedField = .username - } else if self.password.isEmpty { - self.focusedField = .password + if username.isEmpty { + focusedField = .username + } else if password.isEmpty { + focusedField = .password } else { - self.focusedField = nil + focusedField = nil } } } diff --git a/Examples/CaseStudies/11-IfLet.swift b/Examples/CaseStudies/11-IfLet.swift index caa116580e..f6ec2cd4ec 100644 --- a/Examples/CaseStudies/11-IfLet.swift +++ b/Examples/CaseStudies/11-IfLet.swift @@ -17,25 +17,25 @@ struct IfLetCaseStudy: View { Section { Text(readMe) } - Binding(unwrapping: self.$editableString).map { $string in + Binding(unwrapping: $editableString).map { $string in VStack { TextField("Edit string", text: $string) HStack { Button("Discard") { - self.editableString = nil + editableString = nil } Spacer() Button("Save") { - self.string = string - self.editableString = nil + string = string + editableString = nil } } } } - if self.editableString == nil { - Text("\(self.string)") + if editableString == nil { + Text("\(string)") Button("Edit") { - self.editableString = self.string + editableString = string } } } diff --git a/Examples/CaseStudies/12-IfCaseLet.swift b/Examples/CaseStudies/12-IfCaseLet.swift index 47f4df3499..5994f58988 100644 --- a/Examples/CaseStudies/12-IfCaseLet.swift +++ b/Examples/CaseStudies/12-IfCaseLet.swift @@ -24,25 +24,25 @@ struct IfCaseLetCaseStudy: View { Section { Text(readMe) } - self.$editableString.active.map { $string in + $editableString.active.map { $string in VStack { TextField("Edit string", text: $string) HStack { Button("Discard", role: .cancel) { - self.editableString = .inactive + editableString = .inactive } Spacer() Button("Save") { - self.string = string - self.editableString = .inactive + string = string + editableString = .inactive } } } } - if !self.editableString.is(\.active) { - Text("\(self.string)") + if !editableString.is(\.active) { + Text("\(string)") Button("Edit") { - self.editableString = .active(self.string) + editableString = .active(string) } } } diff --git a/Examples/CaseStudies/FactClient.swift b/Examples/CaseStudies/FactClient.swift index d6ae71207c..279b53ff00 100644 --- a/Examples/CaseStudies/FactClient.swift +++ b/Examples/CaseStudies/FactClient.swift @@ -5,7 +5,7 @@ struct Fact: Identifiable { let number: Int var id: AnyHashable { - [self.description as AnyHashable, self.number] + [description as AnyHashable, number] } } diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index 1ec1a260e7..12843b3ece 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -5,7 +5,7 @@ import SwiftUINavigation @Observable class InventoryModel { var inventory: IdentifiedArrayOf { - didSet { self.bind() } + didSet { bind() } } var destination: Destination? @@ -25,46 +25,46 @@ class InventoryModel { } func delete(item: Item) { - _ = self.inventory.remove(id: item.id) + _ = inventory.remove(id: item.id) } func add(item: Item) { withAnimation { - self.inventory.append(ItemRowModel(item: item)) - self.destination = nil + inventory.append(ItemRowModel(item: item)) + destination = nil } } func addButtonTapped() { - self.destination = .add(Item(color: nil, name: "", status: .inStock(quantity: 1))) + destination = .add(Item(color: nil, name: "", status: .inStock(quantity: 1))) } func cancelButtonTapped() { - self.destination = nil + destination = nil } func cancelEditButtonTapped() { - self.destination = nil + destination = nil } func commitEdit(item: Item) { - self.inventory[id: item.id]?.item = item - self.destination = nil + inventory[id: item.id]?.item = item + destination = nil } private func bind() { - for itemRowModel in self.inventory { + for itemRowModel in inventory { itemRowModel.onDelete = { [weak self, weak itemRowModel] in guard let self, let itemRowModel else { return } - self.delete(item: itemRowModel.item) + delete(item: itemRowModel.item) } itemRowModel.onDuplicate = { [weak self] item in guard let self else { return } - self.add(item: item) + add(item: item) } itemRowModel.onTap = { [weak self, weak itemRowModel] in guard let self, let itemRowModel else { return } - self.destination = .edit(itemRowModel.item) + destination = .edit(itemRowModel.item) } } } @@ -75,43 +75,43 @@ struct InventoryView: View { var body: some View { List { - ForEach(self.model.inventory) { + ForEach(model.inventory) { ItemRowView(model: $0) } } .toolbar { ToolbarItem(placement: .primaryAction) { - Button("Add") { self.model.addButtonTapped() } + Button("Add") { model.addButtonTapped() } } } .navigationTitle("Inventory") - .navigationDestination(unwrapping: self.$model.destination.edit) { $item in + .navigationDestination(item: $model.destination.edit) { $item in ItemView(item: $item) .navigationBarTitle("Edit") .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.model.cancelEditButtonTapped() + model.cancelEditButtonTapped() } } ToolbarItem(placement: .primaryAction) { Button("Save") { - self.model.commitEdit(item: item) + model.commitEdit(item: item) } } } } - .sheet(unwrapping: self.$model.destination.add) { $itemToAdd in + .sheet(item: $model.destination.add) { $itemToAdd in NavigationStack { ItemView(item: $itemToAdd) .navigationTitle("Add") .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { self.model.cancelButtonTapped() } + Button("Cancel") { model.cancelButtonTapped() } } ToolbarItem(placement: .primaryAction) { - Button("Save") { self.model.add(item: itemToAdd) } + Button("Save") { model.add(item: itemToAdd) } } } } diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index bc30cf5ca3..93e78f803c 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -21,16 +21,16 @@ class ItemRowModel: Identifiable { var onDuplicate: (Item) -> Void = unimplemented("ItemRowModel.onDuplicate") var onTap: () -> Void = unimplemented("ItemRowModel.onTap") - var id: Item.ID { self.item.id } + var id: Item.ID { item.id } init(item: Item) { self.item = item } func deleteButtonTapped() { - self.destination = .alert( + destination = .alert( AlertState { - TextState(self.item.name) + TextState(item.name) } actions: { ButtonState(role: .destructive, action: .send(.deleteConfirmation, animation: .default)) { TextState("Delete") @@ -44,33 +44,33 @@ class ItemRowModel: Identifiable { func alertButtonTapped(_ action: AlertAction?) { switch action { case .deleteConfirmation?: - self.onDelete() + onDelete() case nil: break } } func cancelButtonTapped() { - self.destination = nil + destination = nil } func duplicateButtonTapped() { - self.destination = .duplicate(self.item.duplicate()) + destination = .duplicate(item.duplicate()) } func duplicate(item: Item) { - self.onDuplicate(item) - self.destination = nil + onDuplicate(item) + destination = nil } func rowTapped() { - self.onTap() + onTap() } } extension Item { func duplicate() -> Self { - Self(color: self.color, name: self.name, status: self.status) + Self(color: color, name: name, status: status) } } @@ -79,14 +79,14 @@ struct ItemRowView: View { var body: some View { Button { - self.model.rowTapped() + model.rowTapped() } label: { HStack { VStack(alignment: .leading) { - Text(self.model.item.name) + Text(model.item.name) .font(.title3) - switch self.model.item.status { + switch model.item.status { case let .inStock(quantity): Text("In stock: \(quantity)") case let .outOfStock(isOnBackOrder): @@ -96,41 +96,41 @@ struct ItemRowView: View { Spacer() - if let color = self.model.item.color { + if let color = model.item.color { Rectangle() .frame(width: 30, height: 30) .foregroundColor(color.swiftUIColor) .border(Color.black, width: 1) } - Button(action: { self.model.duplicateButtonTapped() }) { + Button(action: { model.duplicateButtonTapped() }) { Image(systemName: "square.fill.on.square.fill") } .padding(.leading) - Button(action: { self.model.deleteButtonTapped() }) { + Button(action: { model.deleteButtonTapped() }) { Image(systemName: "trash.fill") } .padding(.leading) } .buttonStyle(.plain) - .foregroundColor(self.model.item.status.is(\.inStock) ? nil : Color.gray) - .alert(self.$model.destination.alert) { - self.model.alertButtonTapped($0) + .foregroundColor(model.item.status.is(\.inStock) ? nil : Color.gray) + .alert($model.destination.alert) { + model.alertButtonTapped($0) } - .popover(unwrapping: self.$model.destination.duplicate) { $item in + .popover(item: $model.destination.duplicate) { $item in NavigationStack { ItemView(item: $item) .navigationBarTitle("Duplicate") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.model.cancelButtonTapped() + model.cancelButtonTapped() } } ToolbarItem(placement: .primaryAction) { Button("Add") { - self.model.duplicate(item: item) + model.duplicate(item: item) } } } diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md index 25d07b3c47..848a88229f 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -19,7 +19,7 @@ your model, as well as an enum that describes every action that can happen in th class FeatureModel { var alert: AlertState? enum AlertAction { - case deletionConfirmed + case confirmDelete } // ... @@ -34,8 +34,12 @@ func deleteButtonTapped() { self.alert = AlertState { TextState("Are you sure?") } actions: { - ButtonState("Delete", action: .send(.delete)) - ButtonState("Nevermind", role: .cancel) + ButtonState(role: .destructive, action: .confirmDelete) { + TextState("Delete") + } + ButtonState(role: .cancel) { + TextState("Nevermind") + } } message: { TextState("Deleting this item cannot be undone.") } @@ -49,11 +53,17 @@ equatability. This makes it possible to write tests against these values. > ```swift > } actions: { > if item.isLocked { -> ButtonState("Unlock and delete", action: .send(.unlockAndDelete)) +> ButtonState(role: .destructive, action: .confirmDelete) { +> TextState("Unlock and delete") +> } > } else { -> ButtonState("Delete", action: .send(.delete)) +> ButtonState(role: .destructive, action: .confirmDelete) { +> TextState("Delete") +> } +> } +> ButtonState(role: .cancel) { +> TextState("Nevermind") > } -> ButtonState("Nevermind", role: .cancel) > } > ``` @@ -62,7 +72,7 @@ Next you can provide an endpoint that will be called when the alert is interacte ```swift func alertButtonTapped(_ action: AlertAction?) { switch action { - case .deletionConfirmed: + case .confirmDelete: // NB: Perform deletion logic here case nil: // NB: Perform cancel button logic here @@ -81,8 +91,8 @@ struct ContentView: View { List { // ... } - .alert(self.$model.alert) { action in - self.model.alertButtonTapped(action) + .alert($model.alert) { action in + model.alertButtonTapped(action) } } } @@ -98,7 +108,7 @@ func testDelete() { model.deleteButtonTapped() XCTAssertEqual(model.alert?.title, TextState("Are you sure?")) - model.alertButtonTapped(.deletionConfirmation) + model.alertButtonTapped(.confirmDelete) // NB: Assert that deletion actually occurred. } ``` @@ -123,7 +133,7 @@ class FeatureModel { } enum AlertAction { - case deletionConfirmed + case confirmDelete } // ... @@ -134,8 +144,8 @@ With this kind of set up you can use an alternative `alert` view modifier that t argument for specifying which case of the enum drives the presentation of the alert: ```swift -.alert(self.$model.destination.alert) { action in - self.model.alertButtonTapped(action) +.alert($model.destination.alert) { action in + model.alertButtonTapped(action) } ``` @@ -155,24 +165,27 @@ For example, the model for a delete confirmation could look like this: class FeatureModel { var dialog: ConfirmationDialogState? enum DialogAction { - case deletionConfirmed + case confirmDelete } func deleteButtonTapped() { - self.dialog = ConfirmationDialogState( - title: TextState("Are you sure?"), - titleVisibility: .visible, - message: TextState("Deleting this item cannot be undone."), - buttons: [ - .destructive(TextState("Delete"), action: .send(.delete)), - .cancel(TextState("Nevermind")), - ] - ) + dialog = ConfirmationDialogState(titleVisibility: .visible) { + TextState("Are you sure?") + } actions: { + ButtonState(role: .destructive, action: .confirmDelete) { + TextState("Delete") + } + ButtonState(role: .cancel) { + TextState("Nevermind") + } + } message: { + TextState("Deleting this item cannot be undone.") + } } func dialogButtonTapped(_ action: DialogAction?) { switch action { - case .deletionConfirmed: + case .confirmDelete: // NB: Perform deletion logic here case nil: // NB: Perform cancel button logic here @@ -191,8 +204,8 @@ struct ContentView: View { List { // ... } - .confirmationDialog(self.$model.dialog) { action in - self.dialogButtonTapped(action) + .confirmationDialog($model.dialog) { action in + dialogButtonTapped(action) } } } @@ -200,11 +213,6 @@ struct ContentView: View { ## Topics -### Alert and dialog modifiers - -- ``SwiftUI/View/alert(title:unwrapping:actions:message:)`` -- ``SwiftUI/View/confirmationDialog(title:titleVisibility:unwrapping:actions:message:)`` - ### Alert state and dialog state - ``SwiftUI/View/alert(_:action:)-sgyk`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index 07dd305f3c..f7e6b9ca36 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -4,7 +4,7 @@ Learn how to manage certain view state, such as `@FocusState` directly in your o ## Overview -SwiftUI comes with many property wrappers that can be used in views to drive view state, such as +SwiftUI comes with many property wrappers that can be used in views to drive view state, such as `@FocusState`. Unfortunately, these property wrappers _must_ be used in views. It's not possible to extract this logic to an `@Observable` class and integrate it with the rest of the model's business @@ -38,7 +38,7 @@ Notice that we store the focus as a regular `var` property in the model rather t This is because `@FocusState` only works when installed directly in a view. It cannot be used in an observable class. -You can implement the view as you would normally, except you must also use `@FocusState` for the +You can implement the view as you would normally, except you must also use `@FocusState` for the focus _and_ use the `bind` helper to make sure that changes to the model's focus are replayed to the view, and vice versa. diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md index 14bf6a481c..2320d5bdd8 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -9,7 +9,7 @@ The library comes with new tools for driving drill-down navigation with optional This includes new initializers on `NavigationLink` and new overloads of the `navigationDestination` view modifier. -Suppose your view or model holds a piece of optional state that represents whether or not a +Suppose your view or model holds a piece of optional state that represents whether or not a drill-down should occur: ```swift @@ -20,14 +20,14 @@ struct ContentView: View { } ``` -Further suppose that the screen being navigated to wants a binding to the integer when it is -non-`nil`. You can construct a `NavigationLink` that will activate when that state becomes +Further suppose that the screen being navigated to wants a binding to the integer when it is +non-`nil`. You can construct a `NavigationLink` that will activate when that state becomes non-`nil`, and will deactivate when the state becomes `nil`: ```swift -NavigationLink(unwrapping: self.$destination) { isActive in - self.destination = isActive ? 42 : nil -} destination: { $number in +NavigationLink(unwrapping: $destination) { isActive in + destination = isActive ? 42 : nil +} destination: { $number in CounterView(number: $number) } label: { Text("Go to counter") @@ -46,13 +46,11 @@ For iOS 16+ you can use the `navigationDestination` overload: ```swift Button { - self.destination = 42 + destination = 42 } label: { Text("Go to counter") } -.navigationDestination( - unwrapping: self.$model.destination -) { $item in +.navigationDestination(item: $model.destination) { $item in CounterView(number: $number) } ``` @@ -60,7 +58,7 @@ Button { Sometimes it is not optimal to model navigation destinations as optionals. In particular, if a feature can navigate to multiple, mutually exclusive screens, then an enum is more appropriate. -Suppose that in addition to be able to drill down to a counter view that one can also open a +Suppose that in addition to be able to drill down to a counter view that one can also open a sheet with some text. We can model those destinations as an enum: ```swift @@ -88,9 +86,9 @@ With this set up you can make use of the which case of the enum you want driving navigation: ```swift -NavigationLink(unwrapping: self.$destination.counter) { isActive in - self.destination = isActive ? .counter(42) : nil -} destination: { $number in +NavigationLink(unwrapping: $destination.counter) { isActive in + destination = isActive ? .counter(42) : nil +} destination: { $number in CounterView(number: $number) } label: { Text("Go to counter") @@ -101,11 +99,11 @@ And similarly for ``SwiftUI/View/navigationDestination(unwrapping:destination:)` ```swift Button { - self.destination = .counter(42) + destination = .counter(42) } label: { Text("Go to counter") } -.navigationDestination(unwrapping: self.$model.destination.counter) { $number in +.navigationDestination(unwrapping: $model.destination.counter) { $number in CounterView(number: $number) } ``` @@ -114,7 +112,7 @@ Button { ### Navigation views and modifiers -- ``SwiftUI/View/navigationDestination(unwrapping:destination:)`` +- ``SwiftUI/View/navigationDestination(item:destination:)`` - ``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` ### Supporting types diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md index 7e03c029b8..5b95ada77d 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md @@ -4,7 +4,7 @@ Learn how to present sheets, popovers and covers in a concise and testable manne ## Overview -The library comes with new tools for driving sheets, popovers and covers from optional and enum +The library comes with new tools for driving sheets, popovers and covers from optional and enum state. * [Sheets](#Sheets) @@ -25,14 +25,14 @@ struct ContentView: View { ``` Further suppose that the screen being presented wants a binding to the integer when it is non-`nil`. -You can use the `sheet(unwrapping:)` view modifier that comes with the library: +You can use the `sheet(item:)` overload that comes with the library: ```swift var body: some View { List { // ... } - .sheet(unwrapping: self.$destination) { $number in + .sheet(item: $destination) { $number in CounterView(number: $number) } } @@ -42,7 +42,7 @@ Notice that the trailing closure is handed a binding to the unwrapped state. Thi handed to the child view, and any changes made by the parent will be reflected in the child, and vice-versa. -Sometimes it is not optimal to model presentation destinations as optionals. In particular, if a +Sometimes it is not optimal to model presentation destinations as optionals. In particular, if a feature can navigate to multiple, mutually exclusive screens, then an enum is more appropriate. There is an additional overload of the `sheet` for this situation. If you model your destinations @@ -65,7 +65,7 @@ var body: some View { List { // ... } - .sheet(unwrapping: self.$destination.counter) { $number in + .sheet(item: $destination.counter) { $number in CounterView(number: $number) } } @@ -84,7 +84,7 @@ struct ContentView: View { List { // ... } - .popover(unwrapping: self.$destination) { $number in + .popover(item: $destination) { $number in CounterView(number: $number) } } @@ -107,7 +107,7 @@ struct ContentView: View { List { // ... } - .popover(unwrapping: self.$destination.counter) { $number in + .popover(item: $destination.counter) { $number in CounterView(number: $number) } } @@ -127,7 +127,7 @@ struct ContentView: View { List { // ... } - .fullscreenCover(unwrapping: self.$destination) { $number in + .fullscreenCover(item: $destination) { $number in CounterView(number: $number) } } @@ -150,7 +150,7 @@ struct ContentView: View { List { // ... } - .fullscreenCover(unwrapping: self.$destination.counter) { $number in + .fullscreenCover(item: $destination.counter) { $number in CounterView(number: $number) } } @@ -161,6 +161,12 @@ struct ContentView: View { ### Presentation modifiers -- ``SwiftUI/View/fullScreenCover(unwrapping:onDismiss:content:)`` -- ``SwiftUI/View/popover(unwrapping:attachmentAnchor:arrowEdge:content:)`` -- ``SwiftUI/View/sheet(unwrapping:onDismiss:content:)`` +- ``SwiftUI/View/fullScreenCover(item:id:onDismiss:content:)-9csbq`` +- ``SwiftUI/View/fullScreenCover(item:onDismiss:content:)`` +- ``SwiftUI/View/fullScreenCover(item:id:onDismiss:content:)-14to1`` +- ``SwiftUI/View/popover(item:id:attachmentAnchor:arrowEdge:content:)-3un96`` +- ``SwiftUI/View/popover(item:attachmentAnchor:arrowEdge:content:)`` +- ``SwiftUI/View/popover(item:id:attachmentAnchor:arrowEdge:content:)-57svy`` +- ``SwiftUI/View/sheet(item:id:onDismiss:content:)-1hi9l`` +- ``SwiftUI/View/sheet(item:onDismiss:content:)`` +- ``SwiftUI/View/sheet(item:id:onDismiss:content:)-6tgux`` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md index 3eea4ab2c0..188202a5b5 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -1,12 +1,12 @@ # What is navigation? -Learn how one can think of navigation as a domain modeling problem, and how that leads to the +Learn how one can think of navigation as a domain modeling problem, and how that leads to the creation of concise and testable APIs for navigation. ## Overview -We will define navigation as a "mode" change in an application. The most prototypical example of -this in SwiftUI are navigation stacks and links. A user taps a button, and a right-to-left +We will define navigation as a "mode" change in an application. The most prototypical example of +this in SwiftUI are navigation stacks and links. A user taps a button, and a right-to-left animation transitions you from the current screen to the next screen. But there are more examples of navigation beyond that one example. Modal sheets can be thought of @@ -15,25 +15,25 @@ new screen. Full screen covers and popovers are also an example of navigation, a similar to sheets except they either take over the full screen (i.e. covers) or only partially take over the screen (i.e. popovers). -Even alerts and confirmation dialogs can be thought of navigation as they take full control over -the interface and force you to make a selection. It's also possible for you to define your own +Even alerts and confirmation dialogs can be thought of navigation as they take full control over +the interface and force you to make a selection. It's also possible for you to define your own notions of navigation, such as bottom sheets, toasts, and more. ## State-driven navigation -All of these seemingly disparate examples of navigation can be unified under a single API. The -presentation and dismissal of a screen can be described with an optional piece of state. When the -state changes from `nil` to non-`nil` the screen will be presented, whether that be via a +All of these seemingly disparate examples of navigation can be unified under a single API. The +presentation and dismissal of a screen can be described with an optional piece of state. When the +state changes from `nil` to non-`nil` the screen will be presented, whether that be via a drill-down, modal, popover, etc. And when the state changes from non-`nil` to `nil` the screen will be dismissed. Driving navigation from state like this can be incredibly powerful: - * It guarantees that your model will always be in sync with the visual representation of the UI. + * It guarantees that your model will always be in sync with the visual representation of the UI. It shouldn't be possible for a piece of state to be non-`nil` and not have the corresponding view present. * It easily enables deep linking capabilities. If all forms of navigation in your application are - driven off of state, then you can instantly open your application into any state imaginable by + driven off of state, then you can instantly open your application into any state imaginable by simply constructing a piece of state, handing it to SwiftUI, and letting it do its thing. * It also allows you to write unit tests for navigation logic without resorting to UI tests, which can be slow, flakey and introduce instability into your test suite. If you write a unit test @@ -69,7 +69,7 @@ sheet for editing the item: class FeatureModel { var editingItem: Item? func tapped(item: Item) { - self.editingItem = item + editingItem = item } // ... } @@ -79,20 +79,20 @@ struct FeatureView: View { var body: some View { List { - ForEach(self.model.items) { item in + ForEach(model.items) { item in Button(item.name) { - self.model.tapped(item: item) + model.tapped(item: item) } } } - .sheet(item: self.$model.editingItem) { item in + .sheet(item: $model.editingItem) { item in EditItemView(item: item) } } } ``` -This works really great. When the button is tapped, the `tapped(item:)` method is called on the +This works really great. When the button is tapped, the `tapped(item:)` method is called on the model causing the `editingItem` state to be hydrated, and then SwiftUI sees that value is no longer `nil` and so causes the sheet to be presented. @@ -124,11 +124,11 @@ sheet view presented is handed a plain, inert value, and if that view wants to m will need to find a way to communicate that back to the parent. However, two-way communication is already a solved problem in SwiftUI with bindings. -So, it might be better if the `sheet(item:content:)` API handed a binding to the unwrapped item so +So, it might be better if the `sheet(item:content:)` API handed a binding to the unwrapped item so that any mutations in the sheet would be instantly observable by the parent: ```swift -.sheet(item: self.$model.editingItem) { $item in +.sheet(item: $model.editingItem) { $item in EditItemView(item: $item) } ``` @@ -139,7 +139,7 @@ The second problem is that while optional state is a great way to drive navigati scale to multiple navigation destinations. For example, suppose that in addition to being able to edit an item, the feature can also add an -item and duplicate an item, and you can navigate to a help screen. That can technically be +item and duplicate an item, and you can navigate to a help screen. That can technically be represented as four optionals: ```swift @@ -153,17 +153,17 @@ class FeatureModel { } ``` -But this is not the most concise way to model this domain. Four optional values means there are +But this is not the most concise way to model this domain. Four optional values means there are `2⁴=16` different states this feature can be in, but only 5 of those states are valid. Either all -can be `nil`, representing we are not navigated anywhere, or at most one can be non-`nil`, +can be `nil`, representing we are not navigated anywhere, or at most one can be non-`nil`, representing navigation to a single screen. But it is not valid to have 2, 3 or 4 non-`nil` values. That would represent multiple screens being simultaneously navigated to, such as two sheets being presented, which is invalid in SwiftUI and can even cause crashes. -This is showing that four optional values is not the best way to represent 4 navigation -destinations. Instead, it is more concise to model the 4 destinations as an enum with a case for +This is showing that four optional values is not the best way to represent 4 navigation +destinations. Instead, it is more concise to model the 4 destinations as an enum with a case for each destination, and then hold onto a single optional value to represent which destination is currently active: @@ -192,8 +192,8 @@ and more from a particular case of that enum. ## SwiftUINavigation's tools -The tools that ship with this library aim to solve the problems discussed above, and more. There are -new APIs for sheets, popovers, covers, alerts, confirmation dialogs _and_ navigation links that +The tools that ship with this library aim to solve the problems discussed above, and more. There are +new APIs for sheets, popovers, covers, alerts, confirmation dialogs _and_ navigation links that allow you to model destinations as an enum and drive navigation by a particular case of the enum. All of the APIs for these seemingly disparate forms of navigation are unified by a single pattern. @@ -203,9 +203,9 @@ content that takes a binding to a non-optional value. For example, the new sheet API now takes a binding to an optional: ```swift -func sheet( - unwrapping: Binding, - content: @escaping (Binding) -> Content +func sheet( + item: Binding, + content: @escaping (Binding) -> Content ) -> some View ``` @@ -214,9 +214,9 @@ optional value, but also from a particular case of an enum. In order to isolate a specific case of an enum we make use of our [CasePaths][case-paths-gh] library. A case path is like a key path, except it is specifically tuned for abstracting over the -shape of enums rather than structs. A key path abstractly bundles up the functionality of getting +shape of enums rather than structs. A key path abstractly bundles up the functionality of getting and setting a property on a struct, whereas a case path bundles up the functionality of "extracting" -a value from an enum and "embedding" a value into an enum. They are an indispensable tool for +a value from an enum and "embedding" a value into an enum. They are an indispensable tool for transforming bindings. Similar APIs are defined for popovers, covers, and more. @@ -246,13 +246,13 @@ shown in a popover, and the `edit` destination in a drill-down. We can do so eas that ship with this library: ```swift -.popover(unwrapping: self.$model.destination.duplicate) { $item in +.popover(item: $model.destination.duplicate) { $item in DuplicateItemView(item: $item) } -.sheet(unwrapping: self.$model.destination.add) { $item in +.sheet(item: $model.destination.add) { $item in AddItemView(item: $item) } -.navigationDestination(unwrapping: self.$model.destination.edit) { $item in +.navigationDestination(item: $model.destination.edit) { $item in EditItemView(item: $item) } ``` @@ -266,9 +266,9 @@ later. If you must support iOS 15 and earlier, you can use the following initial `NavigationLink`, which also has a very similar API to the above: ```swift -NavigationLink(unwrapping: self.$model.destination.edit) { isActive in - self.model.setEditIsActive(isActive) -} destination: { $item in +NavigationLink(unwrapping: $model.destination.edit) { isActive in + model.setEditIsActive(isActive) +} destination: { $item in EditItemView(item: $item) } label: { Text("\(item.name)") @@ -283,7 +283,7 @@ reading the articles below. ### Tools Read the following articles to learn more about the tools that ship with this library for presenting -alerts, dialogs, sheets, popovers, covers, and navigation links all from bindings of enum state. +alerts, dialogs, sheets, popovers, covers, and navigation links all from bindings of enum state. - - diff --git a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md index ed5b569664..c9c45d6276 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md @@ -20,6 +20,7 @@ instead. ### View modifiers +- ``SwiftUI/View/alert(title:unwrapping:actions:message:)`` - ``SwiftUI/View/alert(title:unwrapping:case:actions:message:)`` - ``SwiftUI/View/alert(title:unwrapping:actions:message:)`` - ``SwiftUI/View/alert(unwrapping:action:)-7da26`` @@ -28,6 +29,7 @@ instead. - ``SwiftUI/View/alert(unwrapping:case:action:)-14fwn`` - ``SwiftUI/View/alert(unwrapping:case:action:)-3yw6u`` - ``SwiftUI/View/alert(unwrapping:case:action:)-4w3oq`` +- ``SwiftUI/View/confirmationDialog(title:titleVisibility:unwrapping:actions:message:)`` - ``SwiftUI/View/confirmationDialog(title:titleVisibility:unwrapping:case:actions:message:)`` - ``SwiftUI/View/confirmationDialog(unwrapping:action:)-9465l`` - ``SwiftUI/View/confirmationDialog(unwrapping:action:)-4f8ze`` @@ -35,9 +37,13 @@ instead. - ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-uncl`` - ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-2ddxv`` - ``SwiftUI/View/confirmationDialog(unwrapping:case:action:)-7oi9`` +- ``SwiftUI/View/fullScreenCover(unwrapping:onDismiss:content:)`` - ``SwiftUI/View/fullScreenCover(unwrapping:case:onDismiss:content:)`` +- ``SwiftUI/View/navigationDestination(unwrapping:destination:)`` - ``SwiftUI/View/navigationDestination(unwrapping:case:destination:)`` +- ``SwiftUI/View/popover(unwrapping:attachmentAnchor:arrowEdge:content:)`` - ``SwiftUI/View/popover(unwrapping:case:attachmentAnchor:arrowEdge:content:)`` +- ``SwiftUI/View/sheet(unwrapping:onDismiss:content:)`` - ``SwiftUI/View/sheet(unwrapping:case:onDismiss:content:)`` ### Bindings diff --git a/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md index 77cbfdfcca..2c046a071c 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md @@ -10,27 +10,27 @@ Tools for making SwiftUI navigation simpler, more ergonomic and more precise. ## Overview -SwiftUI comes with many forms of navigation (tabs, alerts, dialogs, modal sheets, popovers, -navigation links, and more), and each comes with a few ways to construct them. These ways roughly +SwiftUI comes with many forms of navigation (tabs, alerts, dialogs, modal sheets, popovers, +navigation links, and more), and each comes with a few ways to construct them. These ways roughly fall in two categories: - * "Fire-and-forget": These are initializers and methods that do not take binding arguments, which - means SwiftUI fully manages navigation state internally. This makes it is easy to get something - on the screen quickly, but you also have no programmatic control over the navigation. Examples - of this are the initializers on [`TabView`][TabView.init] and + * "Fire-and-forget": These are initializers and methods that do not take binding arguments, which + means SwiftUI fully manages navigation state internally. This makes it is easy to get something + on the screen quickly, but you also have no programmatic control over the navigation. Examples + of this are the initializers on [`TabView`][TabView.init] and [`NavigationLink`][NavigationLink.init] that do not take a binding. - * "State-driven": Most other initializers and methods do take a binding, which means you can - mutate state in your domain to tell SwiftUI when it should activate or deactivate navigation. - Using these APIs is more complicated than the "fire-and-forget" style, but doing so instantly - gives you the ability to deep-link into any state of your application by just constructing a + * "State-driven": Most other initializers and methods do take a binding, which means you can + mutate state in your domain to tell SwiftUI when it should activate or deactivate navigation. + Using these APIs is more complicated than the "fire-and-forget" style, but doing so instantly + gives you the ability to deep-link into any state of your application by just constructing a piece of data, handing it to a SwiftUI view, and letting SwiftUI handle the rest. -Navigation that is "state-driven" is the more powerful form of navigation, albeit slightly more +Navigation that is "state-driven" is the more powerful form of navigation, albeit slightly more complicated. To wield it correctly you must be able to model your domain as concisely as possible, and this usually means using enums. -Unfortunately, SwiftUI does not ship with all of the tools necessary to model our domains with +Unfortunately, SwiftUI does not ship with all of the tools necessary to model our domains with enums and make use of navigation APIs. This library bridges that gap by providing APIs that allow you to model your navigation destinations as an enum, and then drive navigation by a binding to that enum. diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index ad2d6a8eb5..92daf52e29 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -4,39 +4,18 @@ @available(iOS 14, tvOS 14, watchOS 7, *) @available(macOS, unavailable) extension View { - /// Presents a full-screen cover using a binding as a data source for the sheet's content based - /// on the identity of the underlying item. - /// - /// - Parameters: - /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, - /// the system passes the item's content to the modifier's closure. You display this content - /// in a sheet that you create that the system displays to the user. If `item` changes, the - /// system dismisses the sheet and replaces it with a new one using the same process. - /// - id: The key path to the provided item's identifier. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - public func fullScreenCover( - item: Binding, - id: KeyPath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Item) -> Content - ) -> some View { - self.fullScreenCover(item: item[id: id], onDismiss: onDismiss) { _ in - item.wrappedValue.map(content) - } - } - /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// /// SwiftUI comes with a `fullScreenCover(item:)` view modifier that is powered by a binding to - /// some hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the - /// content closure. This value, however, is completely static, which prevents the sheet from - /// modifying it. + /// some identifiable state. When this state becomes non-`nil`, it passes an unwrapped value to + /// the content closure. This value, however, is completely static, which prevents the sheet + /// from modifying it. /// /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This /// gives the sheet the ability to write changes back to its source of truth. /// - /// Also unlike `fullScreenCover(item:)`, the binding's value does _not_ need to be hashable. + /// Also unlike `fullScreenCover(item:)`, the binding's value does _not_ need to be + /// identifiable, and can instead specify a key path to the provided data's identifier. /// /// ```swift /// struct TimelineView: View { @@ -46,7 +25,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .fullScreenCover(unwrapping: self.$draft) { $draft in + /// .fullScreenCover(item: $draft, id: \.id) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } @@ -59,25 +38,70 @@ /// ``` /// /// - Parameters: - /// - value: A binding to a source of truth for the sheet. When `value` is non-`nil`, a - /// non-optional binding to the value is passed to the `content` closure. You use this - /// binding to produce content that the system presents to the user in a sheet. Changes made - /// to the sheet's binding will be reflected back in the source of truth. Likewise, changes - /// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is - /// dismissed. + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @_disfavoredOverload + public func fullScreenCover( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + fullScreenCover(item: item[id: id], onDismiss: onDismiss) { _ in + Binding(unwrapping: item).map(content) + } + } + + /// Presents a full-screen cover using a binding as a data source for the sheet's content. + /// + /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes an + /// identifiable item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. /// - onDismiss: The closure to execute when dismissing the sheet. /// - content: A closure returning the content of the sheet. - public func fullScreenCover( - unwrapping value: Binding, + @_disfavoredOverload + public func fullScreenCover( + item: Binding, onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.fullScreenCover( - isPresented: value.isPresent(), - onDismiss: onDismiss - ) { - Binding(unwrapping: value).map(content) + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + fullScreenCover(item: item, id: \.id, onDismiss: onDismiss, content: content) + } + + /// Presents a full-screen cover using a binding as a data source for the sheet's content. + /// + /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that is passed an item + /// and not a binding to an item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + public func fullScreenCover( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + fullScreenCover(item: item, id: id, onDismiss: onDismiss) { + content($0.wrappedValue) } } } diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index cb417f7950..76751085ed 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -2,6 +2,122 @@ import SwiftUI @_spi(RuntimeWarn) import SwiftUINavigationCore + // NB: Deprecated after 1.3.0 + + @available(iOS 14, tvOS 14, watchOS 7, *) + @available(macOS, unavailable) + extension View { + @available( + *, deprecated, + message: + "Use the 'fullScreenCover(item:)' (or 'fullScreenCover(item:id:)') overload that passes a binding" + ) + public func fullScreenCover( + unwrapping value: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.fullScreenCover( + isPresented: value.isPresent(), + onDismiss: onDismiss + ) { + Binding(unwrapping: value).map(content) + } + } + } + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + extension View { + @available( + *, deprecated, + message: "Use the 'navigationDestination(item:)' overload that passes a binding" + ) + @ViewBuilder + public func navigationDestination( + unwrapping value: Binding, + @ViewBuilder destination: (Binding) -> Destination + ) -> some View { + if requiresBindWorkaround { + self.modifier( + _NavigationDestinationBindWorkaround( + isPresented: value.isPresent(), + destination: Binding(unwrapping: value).map(destination) + ) + ) + } else { + self.navigationDestination(isPresented: value.isPresent()) { + Binding(unwrapping: value).map(destination) + } + } + } + } + + // NB: This view modifier works around a bug in SwiftUI's built-in modifier: + // https://gist.github.com/mbrandonw/f8b94957031160336cac6898a919cbb7#file-fb11056434-md + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + private struct _NavigationDestinationBindWorkaround: ViewModifier { + @Binding var isPresented: Bool + let destination: Destination + + @State private var isPresentedState = false + + public func body(content: Content) -> some View { + content + .navigationDestination(isPresented: self.$isPresentedState) { self.destination } + .bind(self.$isPresented, to: self.$isPresentedState) + } + } + + private let requiresBindWorkaround = { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + return true + } + guard #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) + else { return true } + return false + }() + + @available(tvOS, unavailable) + @available(watchOS, unavailable) + extension View { + @available( + *, deprecated, + message: "Use the 'popover(item:)' (or 'popover(item:id:)') overload that passes a binding" + ) + public func popover( + unwrapping value: Binding, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + self.popover( + isPresented: value.isPresent(), + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge + ) { + Binding(unwrapping: value).map(content) + } + } + } + + extension View { + @available( + *, deprecated, + message: "Use the 'sheet(item:)' (or 'sheet(item:id:)') overload that passes a binding" + ) + public func sheet( + unwrapping value: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View + where Content: View { + self.sheet(isPresented: value.isPresent(), onDismiss: onDismiss) { + Binding(unwrapping: value).map(content) + } + } + } + // NB: Deprecated after 1.2.1 @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) @@ -116,24 +232,10 @@ } } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) extension View { @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -153,22 +255,7 @@ } @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -181,22 +268,7 @@ } @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -209,22 +281,7 @@ } @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -246,22 +303,7 @@ } @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -277,22 +319,7 @@ } @available( - iOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 12, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 15, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 8, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -306,20 +333,13 @@ action: handler ) } + } + @available(macOS, unavailable) + @available(iOS 14, tvOS 14, watchOS 8, *) + extension View { @available( - iOS, introduced: 14, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available(macOS, unavailable) - @available( - tvOS, introduced: 14, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 7, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -333,24 +353,12 @@ fullScreenCover( unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } + } + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + extension View { @available( - iOS, introduced: 16, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 13, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, introduced: 16, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, introduced: 9, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @@ -361,19 +369,16 @@ ) -> some View { navigationDestination(unwrapping: `enum`.case(casePath), destination: destination) } + } + @available(tvOS, unavailable) + @available(watchOS, unavailable) + extension View { @available( - iOS, introduced: 13, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, introduced: 10.15, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - @available(tvOS, unavailable) - @available(watchOS, unavailable) public func popover( unwrapping enum: Binding, case casePath: AnyCasePath, @@ -390,22 +395,7 @@ } @available( - iOS, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - macOS, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - tvOS, deprecated: 9999, - message: - "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." - ) - @available( - watchOS, deprecated: 9999, + *, deprecated, message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index 56e534060b..fdf72340c2 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -6,9 +6,8 @@ /// Pushes a view onto a `NavigationStack` using a binding as a data source for the /// destination's content. /// - /// This is a version of SwiftUI's `navigationDestination(isPresented:)` modifier, but powered - /// by a binding to optional state instead of a binding to a boolean. When state becomes - /// non-`nil`, a _binding_ to the unwrapped value is passed to the destination closure. + /// This is a version of SwiftUI's `navigationDestination(item:)` modifier that passes a + /// _binding_ to the unwrapped item to the destination closure. /// /// ```swift /// struct TimelineView: View { @@ -18,7 +17,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .navigationDestination(unwrapping: self.$draft) { $draft in + /// .navigationDestination(unwrapping: $draft) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } @@ -31,55 +30,21 @@ /// ``` /// /// - Parameters: - /// - value: A binding to an optional source of truth for the destination. When `value` is + /// - item: A binding to an optional source of truth for the destination. When `item` is /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. /// You use this binding to produce content that the system pushes to the user in a /// navigation stack. Changes made to the destination's binding will be reflected back in - /// the source of truth. Likewise, changes to `value` are instantly reflected in the - /// destination. If `value` becomes `nil`, the destination is popped. + /// the source of truth. Likewise, changes to `item` are instantly reflected in the + /// destination. If `item` becomes `nil`, the destination is popped. /// - destination: A closure returning the content of the destination. - @ViewBuilder - public func navigationDestination( - unwrapping value: Binding, - @ViewBuilder destination: (Binding) -> Destination + @_disfavoredOverload + public func navigationDestination( + item: Binding, + @ViewBuilder destination: @escaping (Binding) -> C ) -> some View { - if requiresBindWorkaround { - self.modifier( - _NavigationDestinationBindWorkaround( - isPresented: value.isPresent(), - destination: Binding(unwrapping: value).map(destination) - ) - ) - } else { - self.navigationDestination(isPresented: value.isPresent()) { - Binding(unwrapping: value).map(destination) - } + navigationDestination(item: item) { _ in + Binding(unwrapping: item).map(destination) } } } - - // NB: This view modifier works around a bug in SwiftUI's built-in modifier: - // https://gist.github.com/mbrandonw/f8b94957031160336cac6898a919cbb7#file-fb11056434-md - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - private struct _NavigationDestinationBindWorkaround: ViewModifier { - @Binding var isPresented: Bool - let destination: Destination - - @State private var isPresentedState = false - - public func body(content: Content) -> some View { - content - .navigationDestination(isPresented: self.$isPresentedState) { self.destination } - .bind(self.$isPresented, to: self.$isPresentedState) - } - } - - private let requiresBindWorkaround = { - if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { - return true - } - guard #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) - else { return true } - return false - }() #endif // canImport(SwiftUI) diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index f2ec1290db..574acd2f3a 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -1,51 +1,20 @@ #if canImport(SwiftUI) import SwiftUI - @available(tvOS, unavailable) - @available(watchOS, unavailable) + // NB: Moving `@available(tvOS, unavailable)` to the extension causes tvOS builds to fail extension View { - /// Presents a popover using a binding as a data source for the sheet's content based on the - /// identity of the underlying item. - /// - /// - Parameters: - /// - item: A binding to an optional source of truth for the popover. When `item` is - /// non-`nil`, the system passes the item's content to the modifier's closure. You display - /// this content in a popover that you create that the system displays to the user. If `item` - /// changes, the system dismisses the popover and replaces it with a new one using the same - /// process. - /// - id: The key path to the provided item's identifier. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the - /// popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow. - /// - content: A closure returning the content of the popover. - public func popover( - item: Binding, - id: KeyPath, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (Item) -> Content - ) -> some View { - self.popover( - item: item[id: id], - attachmentAnchor: attachmentAnchor, - arrowEdge: arrowEdge - ) { _ in - item.wrappedValue.map(content) - } - } - /// Presents a popover using a binding as a data source for the popover's content. /// /// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the + /// identifiable state. When this state becomes non-`nil`, it passes an unwrapped value to the /// content closure. This value, however, is completely static, which prevents the popover from /// modifying it. /// /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This /// gives the popover the ability to write changes back to its source of truth. /// - /// Also unlike `popover(item:)`, the binding's value does _not_ need to be hashable. + /// Also unlike `popover(item:)`, the binding's value does _not_ need to be identifiable, and + /// can instead specify a key path to the provided data's identifier. /// /// ```swift /// struct TimelineView: View { @@ -55,7 +24,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .popover(unwrapping: self.$draft) { $draft in + /// .popover(unwrapping: $draft) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } @@ -68,29 +37,101 @@ /// ``` /// /// - Parameters: - /// - value: A binding to an optional source of truth for the popover. When `value` is + /// - item: A binding to an optional source of truth for the popover. When `item` is /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You /// use this binding to produce content that the system presents to the user in a popover. /// Changes made to the popover's binding will be reflected back in the source of truth. - /// Likewise, changes to `value` are instantly reflected in the popover. If `value` becomes + /// Likewise, changes to `item` are instantly reflected in the popover. If `item` becomes /// `nil`, the popover is dismissed. + /// - id: The key path to the provided item's identifier. /// - attachmentAnchor: The positioning anchor that defines the attachment point of the /// popover. /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's /// arrow. /// - content: A closure returning the content of the popover. - public func popover( - unwrapping value: Binding, + @_disfavoredOverload + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + item: Binding, + id: KeyPath, attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (Binding) -> Content + @ViewBuilder content: @escaping (Binding) -> Content ) -> some View { - self.popover( - isPresented: value.isPresent(), + popover( + item: item[id: id], attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge - ) { - Binding(unwrapping: value).map(content) + ) { _ in + Binding(unwrapping: item).map(content) + } + } + + /// Presents a full-screen cover using a binding as a data source for the sheet's content. + /// + /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes an + /// identifiable item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the popover. When `item` is + /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You + /// use this binding to produce content that the system presents to the user in a popover. + /// Changes made to the popover's binding will be reflected back in the source of truth. + /// Likewise, changes to `item` are instantly reflected in the popover. If `item` becomes + /// `nil`, the popover is dismissed. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the + /// popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow. + /// - content: A closure returning the content of the popover. + @_disfavoredOverload + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + item: Binding, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + popover( + item: item, + id: \.id, + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge, + content: content + ) + } + + /// Presents a popover using a binding as a data source for the sheet's content based on the + /// identity of the underlying item. + /// + /// A version of ``popover(item:id:attachmentAnchor:arrowEdge:content:)-3un96`` that is passed + /// an item and not a binding to an item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the popover. When `item` is + /// non-`nil`, the system passes the item's content to the modifier's closure. You display + /// this content in a popover that you create that the system displays to the user. If `item` + /// changes, the system dismisses the popover and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the + /// popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow. + /// - content: A closure returning the content of the popover. + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func popover( + item: Binding, + id: KeyPath, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + popover(item: item, id: id, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { + content($0.wrappedValue) } } } diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index f8c711b4cf..22d9f86104 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -8,39 +8,18 @@ #endif extension View { - /// Presents a sheet using a binding as a data source for the sheet's content based on the - /// identity of the underlying item. - /// - /// - Parameters: - /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, - /// the system passes the item's content to the modifier's closure. You display this content - /// in a sheet that you create that the system displays to the user. If `item` changes, the - /// system dismisses the sheet and replaces it with a new one using the same process. - /// - id: The key path to the provided item's identifier. - /// - onDismiss: The closure to execute when dismissing the sheet. - /// - content: A closure returning the content of the sheet. - public func sheet( - item: Binding, - id: KeyPath, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Item) -> Content - ) -> some View { - self.sheet(item: item[id: id], onDismiss: onDismiss) { _ in - item.wrappedValue.map(content) - } - } - /// Presents a sheet using a binding as a data source for the sheet's content. /// /// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some - /// hashable state. When this state becomes non-`nil`, it passes an unwrapped value to the + /// identifiable state. When this state becomes non-`nil`, it passes an unwrapped value to the /// content closure. This value, however, is completely static, which prevents the sheet from /// modifying it. /// - /// This overload differs in that it passes a _binding_ to the content closure, instead. This + /// This overload differs in that it passes a _binding_ to the unwrapped value, instead. This /// gives the sheet the ability to write changes back to its source of truth. /// - /// Also unlike `sheet(item:)`, the binding's value does _not_ need to be hashable. + /// Also unlike `sheet(item:)`, the binding's value does _not_ need to be identifiable, and can + /// instead specify a key path to the provided data's identifier. /// /// ```swift /// struct TimelineView: View { @@ -50,7 +29,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .sheet(unwrapping: self.$draft) { $draft in + /// .sheet(item: $draft, id: \.id) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } @@ -63,23 +42,69 @@ /// ``` /// /// - Parameters: - /// - value: A binding to an optional source of truth for the sheet. When `value` is - /// non-`nil`, a non-optional binding to the value is passed to the `content` closure. You - /// use this binding to produce content that the system presents to the user in a sheet. - /// Changes made to the sheet's binding will be reflected back in the source of truth. - /// Likewise, changes to `value` are instantly reflected in the sheet. If `value` becomes - /// `nil`, the sheet is dismissed. + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + @_disfavoredOverload + public func sheet( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + sheet(item: item[id: id], onDismiss: onDismiss) { _ in + Binding(unwrapping: item).map(content) + } + } + + /// Presents a sheet using a binding as a data source for the sheet's content. + /// + /// A version of ``sheet(item:id:onDismiss:content:)-1hi9l`` that takes an identifiable item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. /// - onDismiss: The closure to execute when dismissing the sheet. /// - content: A closure returning the content of the sheet. - @MainActor - public func sheet( - unwrapping value: Binding, + @_disfavoredOverload + public func sheet( + item: Binding, onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - self.sheet(isPresented: value.isPresent(), onDismiss: onDismiss) { - Binding(unwrapping: value).map(content) + @ViewBuilder content: @escaping (Binding) -> Content + ) -> some View { + sheet(item: item, id: \.id, onDismiss: onDismiss, content: content) + } + + /// Presents a sheet using a binding as a data source for the sheet's content. + /// + /// A version of ``sheet(item:id:onDismiss:content:)-1hi9l`` that is passed an item and not a + /// binding to an item. + /// + /// - Parameters: + /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, + /// the system passes the item's content to the modifier's closure. You display this content + /// in a sheet that you create that the system displays to the user. If `item`'s identity + /// changes, the system dismisses the sheet and replaces it with a new one using the same + /// process. + /// - id: The key path to the provided item's identifier. + /// - onDismiss: The closure to execute when dismissing the sheet. + /// - content: A closure returning the content of the sheet. + public func sheet( + item: Binding, + id: KeyPath, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Item) -> Content + ) -> some View { + sheet(item: item, id: id, onDismiss: onDismiss) { + content($0.wrappedValue) } } } From 7ab04c6e2e6a73d34d5a762970ef88bf0aedb084 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 28 May 2024 10:16:33 -0700 Subject: [PATCH 038/124] Fix DocC and update `NavigationLink(unwrapping:)` (#158) * Fix DocC and update `NavigationLink(unwrapping:)` * wip * wip --- .../Documentation.docc/Articles/Navigation.md | 12 +++++----- .../Articles/WhatIsNavigation.md | 2 +- .../Extensions/Deprecations.md | 1 + .../SwiftUINavigation/FullScreenCover.swift | 8 +++---- .../Internal/Deprecations.swift | 22 ++++++++++++++++- .../NavigationDestination.swift | 2 +- .../SwiftUINavigation/NavigationLink.swift | 24 +++++++++---------- Sources/SwiftUINavigation/Popover.swift | 10 ++++---- Sources/SwiftUINavigation/Sheet.swift | 7 +++--- .../ConfirmationDialogState.swift | 4 ++-- 10 files changed, 57 insertions(+), 35 deletions(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md index 2320d5bdd8..93310cd8c8 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -25,7 +25,7 @@ non-`nil`. You can construct a `NavigationLink` that will activate when that sta non-`nil`, and will deactivate when the state becomes `nil`: ```swift -NavigationLink(unwrapping: $destination) { isActive in +NavigationLink(item: $destination) { isActive in destination = isActive ? 42 : nil } destination: { $number in CounterView(number: $number) @@ -81,12 +81,12 @@ one of these destinations: ``` With this set up you can make use of the -``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` initializer on +``SwiftUI/NavigationLink/init(item:onNavigate:destination:label:)`` initializer on `NavigationLink` in order to specify a binding to the optional destination, and further specify which case of the enum you want driving navigation: ```swift -NavigationLink(unwrapping: $destination.counter) { isActive in +NavigationLink(item: $destination.counter) { isActive in destination = isActive ? .counter(42) : nil } destination: { $number in CounterView(number: $number) @@ -95,7 +95,7 @@ NavigationLink(unwrapping: $destination.counter) { isActive in } ``` -And similarly for ``SwiftUI/View/navigationDestination(unwrapping:destination:)``: +And similarly for ``SwiftUI/View/navigationDestination(item:destination:)``: ```swift Button { @@ -103,7 +103,7 @@ Button { } label: { Text("Go to counter") } -.navigationDestination(unwrapping: $model.destination.counter) { $number in +.navigationDestination(item: $model.destination.counter) { $number in CounterView(number: $number) } ``` @@ -113,7 +113,7 @@ Button { ### Navigation views and modifiers - ``SwiftUI/View/navigationDestination(item:destination:)`` -- ``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` +- ``SwiftUI/NavigationLink/init(item:onNavigate:destination:label:)`` ### Supporting types diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md index 188202a5b5..2ef7ab4853 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -266,7 +266,7 @@ later. If you must support iOS 15 and earlier, you can use the following initial `NavigationLink`, which also has a very similar API to the above: ```swift -NavigationLink(unwrapping: $model.destination.edit) { isActive in +NavigationLink(item: $model.destination.edit) { isActive in model.setEditIsActive(isActive) } destination: { $item in EditItemView(item: $item) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md index c9c45d6276..d220b913af 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md @@ -13,6 +13,7 @@ instead. - ``IfLet`` - ``IfCaseLet`` +- ``SwiftUI/NavigationLink/init(unwrapping:onNavigate:destination:label:)`` - ``SwiftUI/NavigationLink/init(unwrapping:case:onNavigate:destination:label:)`` - ``SwiftUI/NavigationLink/init(unwrapping:destination:onNavigate:label:)`` - ``SwiftUI/NavigationLink/init(unwrapping:case:destination:onNavigate:label:)`` diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index 92daf52e29..75cb77af23 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -60,8 +60,8 @@ /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// - /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes an - /// identifiable item. + /// A version of ``SwiftUI/View/fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes + /// an identifiable item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, @@ -82,8 +82,8 @@ /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// - /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that is passed an item - /// and not a binding to an item. + /// A version of ``SwiftUI/View/fullScreenCover(item:id:onDismiss:content:)-14to1`` that is + /// passed an item and not a binding to an item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 76751085ed..4afc9722fe 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -118,6 +118,26 @@ } } + @available(iOS, introduced: 13, deprecated: 16) + @available(macOS, introduced: 10.15, deprecated: 13) + @available(tvOS, introduced: 13, deprecated: 16) + @available(watchOS, introduced: 6, deprecated: 9) + extension NavigationLink { + @available(*, deprecated, renamed: "init(item:onNavigate:destination:label:)") + public init( + unwrapping value: Binding, + onNavigate: @escaping (_ isActive: Bool) -> Void, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label + ) where Destination == WrappedDestination? { + self.init( + destination: Binding(unwrapping: value).map(destination), + isActive: value.isPresent().didSet(onNavigate), + label: label + ) + } + } + // NB: Deprecated after 1.2.1 @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) @@ -681,7 +701,7 @@ @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( - unwrapping: `enum`.case(casePath), + item: `enum`.case(casePath), onNavigate: onNavigate, destination: destination, label: label diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index fdf72340c2..fbd3ba95b8 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -17,7 +17,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .navigationDestination(unwrapping: $draft) { $draft in + /// .navigationDestination(item: $draft) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index b604a7919e..0dd89b713c 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -7,7 +7,7 @@ /// /// > Note: This interface is deprecated to match the availability of the corresponding SwiftUI /// > API. If you are targeting iOS 16 or later, use - /// > ``SwiftUI/View/navigationDestination(unwrapping:destination:)``, instead. + /// > ``SwiftUI/View/navigationDestination(item:destination:)``, instead. /// /// This allows you to drive navigation to a destination from an optional value. When the /// optional value becomes non-`nil` a binding to an honest value is derived and passed to the @@ -21,8 +21,8 @@ /// /// var body: some View { /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$postToEdit) { isActive in - /// self.postToEdit = isActive ? post : nil + /// NavigationLink(item: $postToEdit) { isActive in + /// postToEdit = isActive ? post : nil /// } destination: { $draft in /// EditPostView(post: $draft) /// } label: { @@ -39,30 +39,30 @@ /// ``` /// /// - Parameters: - /// - value: A binding to an optional source of truth for the destination. When `value` is + /// - item: A binding to an optional source of truth for the destination. When `item` is /// non-`nil`, a non-optional binding to the value is passed to the `destination` closure. /// The destination can use this binding to produce its content and write changes back to - /// the source of truth. Upstream changes to `value` will also be instantly reflected in the - /// destination. If `value` becomes `nil`, the destination is dismissed. + /// the source of truth. Upstream changes to `item` will also be instantly reflected in the + /// destination. If `item` becomes `nil`, the destination is dismissed. /// - onNavigate: A closure that executes when the link becomes active or inactive with a /// boolean that describes if the link was activated or not. Use this closure to populate /// the source of truth when it is passed a value of `true`. When passed `false`, the system - /// will automatically write `nil` to `value`. + /// will automatically write `nil` to `item`. /// - destination: A view for the navigation link to present. /// - label: A view builder to produce a label describing the `destination` to present. @available(iOS, introduced: 13, deprecated: 16) @available(macOS, introduced: 10.15, deprecated: 13) @available(tvOS, introduced: 13, deprecated: 16) @available(watchOS, introduced: 6, deprecated: 9) - public init( - unwrapping value: Binding, + public init( + item: Binding, onNavigate: @escaping (_ isActive: Bool) -> Void, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( - destination: Binding(unwrapping: value).map(destination), - isActive: value.isPresent().didSet(onNavigate), + destination: Binding(unwrapping: item).map(destination), + isActive: item.isPresent().didSet(onNavigate), label: label ) } diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 574acd2f3a..ee17ae2327 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -24,7 +24,7 @@ /// Button("Compose") { /// self.draft = Post() /// } - /// .popover(unwrapping: $draft) { $draft in + /// .popover(item: $draft) { $draft in /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } @@ -70,8 +70,8 @@ /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// - /// A version of ``fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes an - /// identifiable item. + /// A version of ``SwiftUI/View/fullScreenCover(item:id:onDismiss:content:)-14to1`` that takes + /// an identifiable item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the popover. When `item` is @@ -106,8 +106,8 @@ /// Presents a popover using a binding as a data source for the sheet's content based on the /// identity of the underlying item. /// - /// A version of ``popover(item:id:attachmentAnchor:arrowEdge:content:)-3un96`` that is passed - /// an item and not a binding to an item. + /// A version of ``SwiftUI/View/popover(item:id:attachmentAnchor:arrowEdge:content:)-3un96`` + /// that is passed an item and not a binding to an item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the popover. When `item` is diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index 22d9f86104..21ea1621d3 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -64,7 +64,8 @@ /// Presents a sheet using a binding as a data source for the sheet's content. /// - /// A version of ``sheet(item:id:onDismiss:content:)-1hi9l`` that takes an identifiable item. + /// A version of ``SwiftUI/View/sheet(item:id:onDismiss:content:)-1hi9l`` that takes an + /// identifiable item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, @@ -85,8 +86,8 @@ /// Presents a sheet using a binding as a data source for the sheet's content. /// - /// A version of ``sheet(item:id:onDismiss:content:)-1hi9l`` that is passed an item and not a - /// binding to an item. + /// A version of ``SwiftUI/View/sheet(item:id:onDismiss:content:)-1hi9l`` that is passed an item + /// and not a binding to an item. /// /// - Parameters: /// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`, diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index ef9f86a10d..0fa20e7e7b 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -70,7 +70,7 @@ /// } /// ``` /// - /// And in your view you can use the `.confirmationDialog(unwrapping:action:)` view modifier to + /// And in your view you can use the `.confirmationDialog(_:action:)` view modifier to /// present the dialog: /// /// ```swift @@ -83,7 +83,7 @@ /// self.model.infoButtonTapped() /// } /// } - /// .confirmationDialog(unwrapping: self.$model.dialog) { action in + /// .confirmationDialog($model.dialog) { action in /// self.model.dialogButtonTapped(action) /// } /// } From dacc3997c0c6e582898f393aabbc8eaa6ee3de4b Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Tue, 28 May 2024 10:42:20 -0700 Subject: [PATCH 039/124] Fix a few sendability warnings in core. (#159) --- Package@swift-5.9.swift | 63 +++++++++++++++++++ Sources/SwiftUINavigation/Alert.swift | 4 +- .../ConfirmationDialog.swift | 4 +- .../SwiftUINavigationCore/AlertState.swift | 5 +- .../SwiftUINavigationCore/ButtonState.swift | 10 ++- 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 Package@swift-5.9.swift diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000000..23ac97bb79 --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,63 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let package = Package( + name: "swiftui-navigation", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "SwiftUINavigation", + targets: ["SwiftUINavigation"] + ), + .library( + name: "SwiftUINavigationCore", + targets: ["SwiftUINavigationCore"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), + ], + targets: [ + .target( + name: "SwiftUINavigation", + dependencies: [ + "SwiftUINavigationCore", + .product(name: "CasePaths", package: "swift-case-paths"), + ] + ), + .testTarget( + name: "SwiftUINavigationTests", + dependencies: [ + "SwiftUINavigation" + ] + ), + .target( + name: "SwiftUINavigationCore", + dependencies: [ + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ] + ), + ] +) + +for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings!.append(contentsOf: [ + .enableExperimentalFeature("StrictConcurrency") + ]) + // target.swiftSettings?.append( + // .unsafeFlags([ + // "-enable-library-evolution", + // ]) + // ) +} diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index cfe8fced57..6cd7524cb6 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -46,9 +46,9 @@ /// dismisses the alert, and the action is fed to the `action` closure. /// - handler: A closure that is called with an action from a particular alert button when /// tapped. - public func alert( + public func alert( _ state: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { alert(item: state) { Text($0.title) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index eae1840819..89d5b1a14d 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -49,9 +49,9 @@ /// - handler: A closure that is called with an action from a particular dialog button when /// tapped. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public func confirmationDialog( + public func confirmationDialog( _ state: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { confirmationDialog( item: state, diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index aff3f6ed53..30506c023a 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -249,7 +249,10 @@ /// - state: Alert state used to populate the alert. /// - action: An action handler, called when a button with an action is tapped, by passing the /// action to the closure. - public init(_ state: AlertState, action: @escaping (Action?) async -> Void) { + public init( + _ state: AlertState, + action: @escaping @Sendable (Action?) async -> Void + ) { if state.buttons.count == 2 { self.init( title: Text(state.title), diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index 91f062a7ea..efe8acbcf2 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -260,7 +260,10 @@ /// - Parameters: /// - button: Button state. /// - action: An action closure that is invoked when the button is tapped. - public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { + public init( + _ button: ButtonState, + action: @escaping @Sendable (Action?) async -> Void + ) { let action = { _ = Task { await button.withAction(action) } } switch button.role { case .cancel: @@ -310,7 +313,10 @@ /// - button: Button state. /// - action: An action closure that is invoked when the button is tapped. @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) - public init(_ button: ButtonState, action: @escaping (Action?) async -> Void) { + public init( + _ button: ButtonState, + action: @escaping @Sendable (Action?) async -> Void + ) { self.init( role: button.role.map(ButtonRole.init), action: { Task { await button.withAction(action) } } From 393e8ff960b1b2fd4ff1436b0bb6a41fbe7585a1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 4 Jun 2024 14:54:38 -0700 Subject: [PATCH 040/124] Introduce `Binding.init(_: Binding)` (#160) * Introduce `Binding.init(_: Binding)` We currently have an `isPresent()` method for transforming bindings, but it seems to be an outlier compared to most binding transformations that come with SwiftUI, which use initializers. While updating docs I noticed the core module wasn't really organized at all, so took a quick pass. * wip * wip --- .../CaseStudies/09-CustomComponents.swift | 2 +- .../Internal/Deprecations.swift | 60 +++++++++---------- .../SwiftUINavigation/NavigationLink.swift | 2 +- Sources/SwiftUINavigationCore/Alert.swift | 4 +- .../SwiftUINavigationCore/AlertState.swift | 12 ++++ Sources/SwiftUINavigationCore/Binding.swift | 11 ++-- .../ConfirmationDialog.swift | 4 +- .../Extensions/AlertState.md | 22 +++++++ .../Extensions/AlertStateDeprecations.md | 22 +++++++ .../Extensions/ButtonState.md | 36 +++++++++++ .../Extensions/ButtonStateDeprecations.md | 24 ++++++++ .../Extensions/ConfirmationDialogState.md | 26 ++++++++ .../ConfirmationDialogStateDeprecations.md | 21 +++++++ .../Extensions/Deprecations.md | 19 ++++++ .../Extensions/TextState.md | 14 +++++ .../SwiftUINavigationCore.md | 6 +- .../Internal/Deprecations.swift | 13 ++++ .../NavigationDestination.swift | 2 +- 18 files changed, 255 insertions(+), 45 deletions(-) create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertState.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertStateDeprecations.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonState.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonStateDeprecations.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogState.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogStateDeprecations.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/Deprecations.md create mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/TextState.md diff --git a/Examples/CaseStudies/09-CustomComponents.swift b/Examples/CaseStudies/09-CustomComponents.swift index 54d7b36749..0533c22223 100644 --- a/Examples/CaseStudies/09-CustomComponents.swift +++ b/Examples/CaseStudies/09-CustomComponents.swift @@ -98,7 +98,7 @@ extension View { where Content: View { modifier( BottomMenuModifier( - isActive: item.isPresent(), + isActive: Binding(item), content: { Binding(unwrapping: item).map(content) } ) ) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 4afc9722fe..1f28c27b4a 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -19,7 +19,7 @@ ) -> some View where Content: View { self.fullScreenCover( - isPresented: value.isPresent(), + isPresented: Binding(value), onDismiss: onDismiss ) { Binding(unwrapping: value).map(content) @@ -41,12 +41,12 @@ if requiresBindWorkaround { self.modifier( _NavigationDestinationBindWorkaround( - isPresented: value.isPresent(), + isPresented: Binding(value), destination: Binding(unwrapping: value).map(destination) ) ) } else { - self.navigationDestination(isPresented: value.isPresent()) { + self.navigationDestination(isPresented: Binding(value)) { Binding(unwrapping: value).map(destination) } } @@ -92,7 +92,7 @@ @ViewBuilder content: @escaping (Binding) -> Content ) -> some View { self.popover( - isPresented: value.isPresent(), + isPresented: Binding(value), attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge ) { @@ -112,7 +112,7 @@ @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { - self.sheet(isPresented: value.isPresent(), onDismiss: onDismiss) { + self.sheet(isPresented: Binding(value), onDismiss: onDismiss) { Binding(unwrapping: value).map(content) } } @@ -132,7 +132,7 @@ ) where Destination == WrappedDestination? { self.init( destination: Binding(unwrapping: value).map(destination), - isActive: value.isPresent().didSet(onNavigate), + isActive: Binding(value).didSet(onNavigate), label: label ) } @@ -164,7 +164,7 @@ ) -> some View { self.confirmationDialog( value.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: value.isPresent(), + isPresented: Binding(value), titleVisibility: titleVisibility, presenting: value.wrappedValue, actions: actions, @@ -184,7 +184,7 @@ ) -> some View { alert( (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), + isPresented: Binding(value), presenting: value.wrappedValue, actions: { ForEach($0.buttons) { @@ -196,13 +196,13 @@ } @available(*, deprecated, renamed: "alert(_:action:)") - public func alert( + public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { alert( (value.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: value.isPresent(), + isPresented: Binding(value), presenting: value.wrappedValue, actions: { ForEach($0.buttons) { @@ -220,7 +220,7 @@ ) -> some View { confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), + isPresented: Binding(value), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, actions: { @@ -233,13 +233,13 @@ } @available(*, deprecated, renamed: "confirmationDialog(_:action:)") - public func confirmationDialog( + public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(verbatim: ""), - isPresented: value.isPresent(), + isPresented: Binding(value), titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, actions: { @@ -292,10 +292,10 @@ message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - public func alert( + public func alert( unwrapping enum: Binding, case casePath: AnyCasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { alert(`enum`.case(casePath), action: handler) } @@ -343,10 +343,10 @@ message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - public func confirmationDialog( + public func confirmationDialog( unwrapping enum: Binding, case casePath: AnyCasePath>, - action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } + action handler: @escaping @Sendable (Value?) async -> Void = { (_: Never?) async in } ) -> some View { confirmationDialog( `enum`.case(casePath), @@ -521,7 +521,7 @@ ) public func isPresent(_ casePath: AnyCasePath) -> Binding where Value == Enum? { - self.case(casePath).isPresent() + .init(self.case(casePath)) } } @@ -767,7 +767,7 @@ message: "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) - public struct CaseLet: View + public struct CaseLet: Sendable, View where Content: View { @EnvironmentObject private var `enum`: BindingObject public let casePath: AnyCasePath @@ -1847,9 +1847,9 @@ message: "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) - public func alert( + public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { alert(value) { (value: Value?) in if let value = value { @@ -1865,10 +1865,10 @@ message: "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) - public func alert( + public func alert( unwrapping enum: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in if let value = value { @@ -1884,9 +1884,9 @@ message: "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) - public func confirmationDialog( + public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { confirmationDialog(unwrapping: value) { (value: Value?) in if let value = value { @@ -1902,10 +1902,10 @@ message: "'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals." ) - public func confirmationDialog( + public func confirmationDialog( unwrapping enum: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in if let value = value { @@ -1941,7 +1941,7 @@ ) where Destination == WrappedDestination? { self.init( destination: Binding(unwrapping: value).map(destination), - isActive: value.isPresent().didSet(onNavigate), + isActive: Binding(value).didSet(onNavigate), label: label ) } diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index 0dd89b713c..5e05277b89 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -62,7 +62,7 @@ ) where Destination == WrappedDestination? { self.init( destination: Binding(unwrapping: item).map(destination), - isActive: item.isPresent().didSet(onNavigate), + isActive: Binding(item).didSet(onNavigate), label: label ) } diff --git a/Sources/SwiftUINavigationCore/Alert.swift b/Sources/SwiftUINavigationCore/Alert.swift index 448cda9f86..ba3479a5b0 100644 --- a/Sources/SwiftUINavigationCore/Alert.swift +++ b/Sources/SwiftUINavigationCore/Alert.swift @@ -67,7 +67,7 @@ ) -> some View { alert( item.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: item.isPresent(), + isPresented: Binding(item), presenting: item.wrappedValue, actions: actions, message: message @@ -132,7 +132,7 @@ ) -> some View { alert( item.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: item.isPresent(), + isPresented: Binding(item), presenting: item.wrappedValue, actions: actions ) diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index 30506c023a..a8e4fdb5c8 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -219,6 +219,18 @@ // MARK: - SwiftUI bridging + @available( + iOS, introduced: 13, deprecated: 100000, message: "use 'View.alert(_:action:)' instead." + ) + @available( + macOS, introduced: 10.15, deprecated: 100000, message: "use 'View.alert(_:action:)' instead." + ) + @available( + tvOS, introduced: 13, deprecated: 100000, message: "use 'View.alert(_:action:)' instead." + ) + @available( + watchOS, introduced: 6, deprecated: 100000, message: "use 'View.alert(_:action:)' instead." + ) extension Alert { /// Creates an alert from alert state. /// diff --git a/Sources/SwiftUINavigationCore/Binding.swift b/Sources/SwiftUINavigationCore/Binding.swift index c1c4c89f07..e20b55095e 100644 --- a/Sources/SwiftUINavigationCore/Binding.swift +++ b/Sources/SwiftUINavigationCore/Binding.swift @@ -2,15 +2,13 @@ import SwiftUI extension Binding { - /// Creates a binding by projecting the current optional value to a boolean describing if it's - /// non-`nil`. + /// Creates a binding by projecting the base optional value to a Boolean value. /// /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing. /// - /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`. - public func isPresent() -> Binding - where Value == Wrapped? { - self._isPresent + /// - Parameter base: A value to project to a Boolean value. + public init(_ base: Binding) where Value == Bool { + self = base._isPresent } } @@ -23,5 +21,4 @@ } } } - #endif diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialog.swift b/Sources/SwiftUINavigationCore/ConfirmationDialog.swift index f84f139280..0336ea5d68 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialog.swift @@ -70,7 +70,7 @@ ) -> some View { confirmationDialog( item.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: item.isPresent(), + isPresented: Binding(item), titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions, @@ -140,7 +140,7 @@ ) -> some View { confirmationDialog( item.wrappedValue.map(title) ?? Text(verbatim: ""), - isPresented: item.isPresent(), + isPresented: Binding(item), titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertState.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertState.md new file mode 100644 index 0000000000..f8866bd317 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertState.md @@ -0,0 +1,22 @@ +# ``SwiftUINavigationCore/AlertState`` + +## Topics + +### Creating alerts + +- ``init(title:actions:message:)`` + +### Reading alert data + +- ``id`` +- ``title`` +- ``message`` +- ``buttons`` + +### Transforming alerts + +- ``map(_:)`` + +### Deprecations + +- diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertStateDeprecations.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertStateDeprecations.md new file mode 100644 index 0000000000..6a0db22b86 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertStateDeprecations.md @@ -0,0 +1,22 @@ +# Deprecations + +Review unsupported SwiftUI Navigation APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Creating alerts + +- ``AlertState/init(title:message:primaryButton:secondaryButton:)`` +- ``AlertState/init(title:message:dismissButton:)`` +- ``AlertState/init(title:message:buttons:)`` + +### Supporting types + +- ``AlertState/Button`` +- ``AlertState/ButtonAction`` +- ``AlertState/ButtonRole`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonState.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonState.md new file mode 100644 index 0000000000..bb12492039 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonState.md @@ -0,0 +1,36 @@ +# ``SwiftUINavigationCore/ButtonState`` + +## Topics + +### Creating buttons + +- ``init(role:action:label:)-99wi3`` +- ``init(role:action:label:)-2ixoi`` +- ``ButtonStateRole`` +- ``ButtonStateAction`` + +### Composing buttons + +- ``ButtonStateBuilder`` + +### Reading button data + +- ``id`` +- ``role-swift.property`` +- ``action`` +- ``label`` + +### Performing actions + +- ``withAction(_:)-56ifj`` +- ``withAction(_:)-71nj4`` + +### Transforming buttons + +- ``SwiftUI/Button`` +- ``SwiftUI/ButtonRole`` +- ``map(_:)`` + +### Deprecations + +- diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonStateDeprecations.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonStateDeprecations.md new file mode 100644 index 0000000000..94ef02d78b --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonStateDeprecations.md @@ -0,0 +1,24 @@ +# Deprecations + +Review unsupported SwiftUI Navigation APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Creating buttons + +- ``ButtonState/cancel(_:action:)`` +- ``ButtonState/default(_:action:)`` +- ``ButtonState/destructive(_:action:)`` + +### Readin + +### Supporting types + +- ``ButtonState/ButtonAction`` +- ``ButtonState/Handler`` +- ``ButtonState/Role-swift.typealias`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogState.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogState.md new file mode 100644 index 0000000000..9badcf10a4 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogState.md @@ -0,0 +1,26 @@ +# ``SwiftUINavigationCore/ConfirmationDialogState`` + +## Topics + +### Creating dialogs + +- ``init(title:actions:message:)`` +- ``init(titleVisibility:title:actions:message:)`` +- ``ConfirmationDialogStateTitleVisibility`` + +### Reading dialog data + +- ``id`` +- ``title`` +- ``titleVisibility`` +- ``message`` +- ``buttons`` + +### Transforming dialogs + +- ``map(_:)`` +- ``SwiftUI/Visibility`` + +### Deprecations + +- diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogStateDeprecations.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogStateDeprecations.md new file mode 100644 index 0000000000..6dcb7d73ec --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogStateDeprecations.md @@ -0,0 +1,21 @@ +# Deprecations + +Review unsupported SwiftUI Navigation APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Creating dialogs + +- ``ActionSheetState`` +- ``ConfirmationDialogState/init(title:message:buttons:)`` +- ``ConfirmationDialogState/init(title:titleVisibility:message:buttons:)`` + +### Supporting types + +- ``ConfirmationDialogState/Button`` +- ``ConfirmationDialogState/Visibility`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/Deprecations.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/Deprecations.md new file mode 100644 index 0000000000..970202ea47 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/Deprecations.md @@ -0,0 +1,19 @@ +# Deprecations + +Review unsupported SwiftUI Navigation APIs and their replacements. + +## Overview + +Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use +instead. + +## Topics + +### Bindings + +- ``SwiftUI/Binding/isPresent()`` + +### Alerts and dialogs + +- ``SwiftUI/ActionSheet/init(_:action:)`` +- ``SwiftUI/Alert`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/TextState.md b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/TextState.md new file mode 100644 index 0000000000..b2b63acd42 --- /dev/null +++ b/Sources/SwiftUINavigationCore/Documentation.docc/Extensions/TextState.md @@ -0,0 +1,14 @@ +# ``SwiftUINavigationCore/TextState`` + +## Topics + +### Creating text state + +- ``init(_:)`` +- ``init(_:tableName:bundle:comment:)`` +- ``init(verbatim:)`` + +### Text state transformations + +- ``SwiftUI/Text`` +- ``Swift/String`` diff --git a/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md index ae7be6d215..c51d6dd995 100644 --- a/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md +++ b/Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md @@ -20,9 +20,13 @@ A few core types and modifiers included in SwiftUI Navigation. ### Bindings -- ``SwiftUI/Binding/isPresent()`` +- ``SwiftUI/Binding/init(_:)`` - ``SwiftUI/View/bind(_:to:)`` ### Navigation - ``SwiftUI/View/navigationDestination(item:destination:)`` + +### Deprecations + +- diff --git a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift index b473de04b7..1b9c55a848 100644 --- a/Sources/SwiftUINavigationCore/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigationCore/Internal/Deprecations.swift @@ -1,6 +1,19 @@ #if canImport(SwiftUI) import SwiftUI + // NB: Deprecated after 1.4.0 + + extension Binding { + @available( + *, deprecated, + message: "Use 'Binding.init(_:)' to project an optional binding to a Boolean, instead." + ) + public func isPresent() -> Binding + where Value == Wrapped? { + Binding(self) + } + } + // NB: Deprecated after 0.5.0 extension ButtonState { diff --git a/Sources/SwiftUINavigationCore/NavigationDestination.swift b/Sources/SwiftUINavigationCore/NavigationDestination.swift index 9b593da463..5ff9915aa0 100644 --- a/Sources/SwiftUINavigationCore/NavigationDestination.swift +++ b/Sources/SwiftUINavigationCore/NavigationDestination.swift @@ -15,7 +15,7 @@ item: Binding, @ViewBuilder destination: @escaping (D) -> C ) -> some View { - navigationDestination(isPresented: item.isPresent()) { + navigationDestination(isPresented: Binding(item)) { if let item = item.wrappedValue { destination(item) } From b7c9a79f6f6b1fefb87d3e5a83a9c2fe7cdc9720 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 13 Jun 2024 09:00:18 -0700 Subject: [PATCH 041/124] Pass `CI` environment variable to `xcodebuild` (#162) Apparently Xcode 15.3+ only makes environment variables prefixed with `TEST_RUNNER_` available to tests: https://forums.developer.apple.com/forums/thread/749185 --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index f0a2a7715b..83e13b1026 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ PLATFORM_MACOS = macOS PLATFORM_TVOS = tvOS Simulator,name=Apple TV PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 7 (45mm) +TEST_RUNNER_CI = $(CI) + default: test test: From 7b6c34a0f87e8a991b36ff59c5b6390a2ce16355 Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:49:28 -0400 Subject: [PATCH 042/124] Fix some docs. (#163) --- .../Articles/SheetsPopoversCovers.md | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md index 5b95ada77d..e093f9fa05 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md @@ -42,6 +42,22 @@ Notice that the trailing closure is handed a binding to the unwrapped state. Thi handed to the child view, and any changes made by the parent will be reflected in the child, and vice-versa. +However, this does not compile just yet because `sheet(item:)` requires that the item being +presented conform to `Identifable`, and `Int` does not conform. This library comes with an overload +of `sheet`, called ``SwiftUI/View/sheet(item:id:onDismiss:content:)-1hi9l``, that allows you to +specify the ID of the item being presented: + +```swift +var body: some View { + List { + // ... + } + .sheet(item: $destination, id: \.self) { $number in + CounterView(number: $number) + } +} +``` + Sometimes it is not optimal to model presentation destinations as optionals. In particular, if a feature can navigate to multiple, mutually exclusive screens, then an enum is more appropriate. @@ -65,7 +81,7 @@ var body: some View { List { // ... } - .sheet(item: $destination.counter) { $number in + .sheet(item: $destination.counter, id: \.self) { $number in CounterView(number: $number) } } @@ -73,7 +89,7 @@ var body: some View { ### Popovers -Popovers work similarly to covers. If the popover's state is represented as an optional you can do +Popovers work similarly to sheets. If the popover's state is represented as an optional you can do the following: ```swift @@ -84,7 +100,7 @@ struct ContentView: View { List { // ... } - .popover(item: $destination) { $number in + .popover(item: $destination, id: \.self) { $number in CounterView(number: $number) } } @@ -107,7 +123,7 @@ struct ContentView: View { List { // ... } - .popover(item: $destination.counter) { $number in + .popover(item: $destination.counter, id: \.self) { $number in CounterView(number: $number) } } @@ -116,7 +132,7 @@ struct ContentView: View { ### Covers -Full screen covers work similarly to covers and sheets. If the cover's state is represented as an +Full screen covers work similarly to sheets and popovers. If the cover's state is represented as an optional you can do the following: ```swift @@ -127,7 +143,7 @@ struct ContentView: View { List { // ... } - .fullscreenCover(item: $destination) { $number in + .fullscreenCover(item: $destination, id: \.self) { $number in CounterView(number: $number) } } @@ -150,7 +166,7 @@ struct ContentView: View { List { // ... } - .fullscreenCover(item: $destination.counter) { $number in + .fullscreenCover(item: $destination.counter, id: \.self) { $number in CounterView(number: $number) } } From b9cfa9ab2bce9429e11f2502555a0fad5d09c8f5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Jun 2024 08:39:35 -0700 Subject: [PATCH 043/124] Update .spi.yml --- .spi.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.spi.yml b/.spi.yml index 980ef4497e..eeeb1f4d06 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,4 +2,3 @@ version: 1 builder: configs: - documentation_targets: [SwiftUINavigation, SwiftUINavigationCore] - swift_version: 5.9 From 0a0deb234b481760cb4bb6a421d6f28670b00146 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Jun 2024 13:32:14 -0700 Subject: [PATCH 044/124] Swift 6 Language Mode (#165) * Address Xcode 16 warnings * Swift 6 Language Mode * Update Package@swift-6.0.swift --- Package@swift-6.0.swift | 52 +++++ Sources/SwiftUINavigation/Binding.swift | 6 +- .../Internal/Binding+Internal.swift | 2 +- .../Internal/Deprecations.swift | 201 +++++++++--------- .../Internal/LockIsolated.swift | 18 ++ .../SwiftUINavigation/NavigationLink.swift | 2 +- .../Internal/RuntimeWarnings.swift | 16 +- 7 files changed, 191 insertions(+), 106 deletions(-) create mode 100644 Package@swift-6.0.swift create mode 100644 Sources/SwiftUINavigation/Internal/LockIsolated.swift diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000000..b3a8b94dc8 --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,52 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "swiftui-navigation", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "SwiftUINavigation", + targets: ["SwiftUINavigation"] + ), + .library( + name: "SwiftUINavigationCore", + targets: ["SwiftUINavigationCore"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), + ], + targets: [ + .target( + name: "SwiftUINavigation", + dependencies: [ + "SwiftUINavigationCore", + .product(name: "CasePaths", package: "swift-case-paths"), + ] + ), + .testTarget( + name: "SwiftUINavigationTests", + dependencies: [ + "SwiftUINavigation" + ] + ), + .target( + name: "SwiftUINavigationCore", + dependencies: [ + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ] + ), + ], + swiftLanguageVersions: [.v6] +) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index c6e0441f81..8ed49d61da 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -61,7 +61,9 @@ /// - Parameter isDuplicate: A closure to evaluate whether two elements are equivalent, for /// purposes of filtering writes. Return `true` from this closure to indicate that the second /// element is a duplicate of the first. - public func removeDuplicates(by isDuplicate: @escaping (Value, Value) -> Bool) -> Self { + public func removeDuplicates( + by isDuplicate: @Sendable @escaping (Value, Value) -> Bool + ) -> Self { .init( get: { self.wrappedValue }, set: { newValue, transaction in @@ -81,7 +83,7 @@ /// /// [FB9404926]: https://gist.github.com/mbrandonw/70df235e42d505b3b1b9b7d0d006b049 public func removeDuplicates() -> Self { - self.removeDuplicates(by: ==) + self.removeDuplicates(by: { $0 == $1 }) } } diff --git a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift index 670a5b1336..b313740b50 100644 --- a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift +++ b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift @@ -2,7 +2,7 @@ import SwiftUI extension Binding { - func didSet(_ perform: @escaping (Value) -> Void) -> Self { + func didSet(_ perform: @escaping @Sendable (Value) -> Void) -> Self { .init( get: { self.wrappedValue }, set: { newValue, transaction in diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 1f28c27b4a..c47c53be2c 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -126,7 +126,7 @@ @available(*, deprecated, renamed: "init(item:onNavigate:destination:label:)") public init( unwrapping value: Binding, - onNavigate: @escaping (_ isActive: Bool) -> Void, + onNavigate: @escaping @Sendable (_ isActive: Bool) -> Void, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { @@ -452,19 +452,22 @@ message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - public init?(unwrapping enum: Binding, case casePath: AnyCasePath) { - guard var `case` = casePath.extract(from: `enum`.wrappedValue) + public init?(unwrapping enum: Binding, case casePath: AnyCasePath) + where Value: Sendable { + guard let `case` = casePath.extract(from: `enum`.wrappedValue).map({ LockIsolated($0) }) else { return nil } self.init( get: { - `case` = casePath.extract(from: `enum`.wrappedValue) ?? `case` - return `case` + `case`.withLock { + $0 = casePath.extract(from: `enum`.wrappedValue) ?? $0 + return $0 + } }, - set: { + set: { newValue, transaction in guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } - `case` = $0 - `enum`.transaction($1).wrappedValue = casePath.embed($0) + `case`.withLock { $0 = newValue } + `enum`.transaction(transaction).wrappedValue = casePath.embed(newValue) } ) } @@ -525,7 +528,7 @@ } } - public struct IfCaseLet: View + public struct IfCaseLet: View where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: AnyCasePath @@ -696,7 +699,7 @@ public init( unwrapping enum: Binding, case casePath: AnyCasePath, - onNavigate: @escaping (Bool) -> Void, + onNavigate: @escaping @Sendable (Bool) -> Void, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { @@ -767,7 +770,7 @@ message: "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) - public struct CaseLet: Sendable, View + public struct CaseLet: Sendable, View where Content: View { @EnvironmentObject private var `enum`: BindingObject public let casePath: AnyCasePath @@ -839,7 +842,7 @@ "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) extension Switch { - public init( + public init( _ enum: Binding, @ViewBuilder content: () -> TupleView< ( @@ -864,7 +867,7 @@ } } - public init( + public init( _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, @@ -882,7 +885,7 @@ } } - public init( + public init( _ enum: Binding, @ViewBuilder content: () -> TupleView< ( @@ -913,7 +916,7 @@ } } - public init( + public init( _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, @@ -942,9 +945,9 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, DefaultContent >( _ enum: Binding, @@ -983,7 +986,7 @@ } } - public init( + public init( _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, @@ -1017,10 +1020,10 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, DefaultContent >( _ enum: Binding, @@ -1066,10 +1069,10 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4 >( _ enum: Binding, file: StaticString = #fileID, @@ -1109,11 +1112,11 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, DefaultContent >( _ enum: Binding, @@ -1165,11 +1168,11 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5 >( _ enum: Binding, file: StaticString = #fileID, @@ -1214,12 +1217,12 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, DefaultContent >( _ enum: Binding, @@ -1277,12 +1280,12 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6 >( _ enum: Binding, file: StaticString = #fileID, @@ -1332,13 +1335,13 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7, DefaultContent >( _ enum: Binding, @@ -1402,13 +1405,13 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7 >( _ enum: Binding, file: StaticString = #fileID, @@ -1463,14 +1466,14 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7, + Case8: Sendable, Content8, DefaultContent >( _ enum: Binding, @@ -1540,14 +1543,14 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7, + Case8: Sendable, Content8 >( _ enum: Binding, file: StaticString = #fileID, @@ -1607,15 +1610,15 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9, + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7, + Case8: Sendable, Content8, + Case9: Sendable, Content9, DefaultContent >( _ enum: Binding, @@ -1691,15 +1694,15 @@ } public init< - Case1, Content1, - Case2, Content2, - Case3, Content3, - Case4, Content4, - Case5, Content5, - Case6, Content6, - Case7, Content7, - Case8, Content8, - Case9, Content9 + Case1: Sendable, Content1, + Case2: Sendable, Content2, + Case3: Sendable, Content3, + Case4: Sendable, Content4, + Case5: Sendable, Content5, + Case6: Sendable, Content6, + Case7: Sendable, Content7, + Case8: Sendable, Content8, + Case9: Sendable, Content9 >( _ enum: Binding, file: StaticString = #fileID, @@ -1936,7 +1939,7 @@ public init( unwrapping value: Binding, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - onNavigate: @escaping (_ isActive: Bool) -> Void, + onNavigate: @escaping @Sendable (_ isActive: Bool) -> Void, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( @@ -1951,7 +1954,7 @@ unwrapping enum: Binding, case casePath: CasePath, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, - onNavigate: @escaping (Bool) -> Void, + onNavigate: @escaping @Sendable (Bool) -> Void, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( diff --git a/Sources/SwiftUINavigation/Internal/LockIsolated.swift b/Sources/SwiftUINavigation/Internal/LockIsolated.swift new file mode 100644 index 0000000000..40a89ad82e --- /dev/null +++ b/Sources/SwiftUINavigation/Internal/LockIsolated.swift @@ -0,0 +1,18 @@ +import Foundation + +final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSRecursiveLock() + init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { + self._value = try value() + } + func withLock( + _ operation: @Sendable (inout Value) throws -> T + ) rethrows -> T { + lock.lock() + defer { lock.unlock() } + var value = _value + defer { _value = value } + return try operation(&value) + } +} diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index 5e05277b89..53d53cefff 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -56,7 +56,7 @@ @available(watchOS, introduced: 6, deprecated: 9) public init( item: Binding, - onNavigate: @escaping (_ isActive: Bool) -> Void, + onNavigate: @escaping @Sendable (_ isActive: Bool) -> Void, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index 5d188c1ebb..7d968a2fe0 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -21,7 +21,7 @@ #if canImport(os) os_log( .fault, - dso: dso, + dso: dso.wrappedValue, log: OSLog(subsystem: "com.apple.runtime-issues", category: category), "%@", message @@ -46,7 +46,7 @@ // // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc @usableFromInline - let dso = { () -> UnsafeMutableRawPointer in + let dso = UncheckedSendable({ let count = _dyld_image_count() for i in 0..: @unchecked Sendable { + @usableFromInline + var wrappedValue: Value + init(_ value: Value) { + self.wrappedValue = value + } + public var projectedValue: Self { self } + } #else import Foundation From a09c56e4442ee06b246b5adf6ce86b3167b5691b Mon Sep 17 00:00:00 2001 From: stephencelis Date: Fri, 14 Jun 2024 20:34:22 +0000 Subject: [PATCH 045/124] Run swift-format --- .../Internal/RuntimeWarnings.swift | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index 7d968a2fe0..ee1435997d 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -46,20 +46,21 @@ // // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc @usableFromInline - let dso = UncheckedSendable({ - let count = _dyld_image_count() - for i in 0..( + { + let count = _dyld_image_count() + for i in 0..: @unchecked Sendable { From 3a5fcc15cdfefc4d63b9c783b01baae796a8685b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Jun 2024 13:54:41 -0700 Subject: [PATCH 046/124] Fix --- Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index ee1435997d..25c01c169f 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -69,7 +69,6 @@ init(_ value: Value) { self.wrappedValue = value } - public var projectedValue: Self { self } } #else import Foundation From 47404dea1dffe1bf5c826724b627d1196ccde4ae Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 15 Jun 2024 10:42:23 -0700 Subject: [PATCH 047/124] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8c1eeecd1..887da116a5 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ dependencies: [ ## Documentation The latest documentation for the SwiftUI Navigation APIs is available -[here](http://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/). +[here](https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation). ## License From 6f16652f103047cfc46089376cafb77dfa72a3d1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 15 Jun 2024 10:43:47 -0700 Subject: [PATCH 048/124] Delete .github/workflows/documentation.yml --- .github/workflows/documentation.yml | 74 ----------------------------- 1 file changed, 74 deletions(-) delete mode 100644 .github/workflows/documentation.yml diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index ac5eef39b2..0000000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Build and deploy DocC to GitHub pages. Based off of @karwa's work here: -# https://github.com/karwa/swift-url/blob/main/.github/workflows/docs.yml -name: Documentation - -on: - release: - types: - - published - push: - branches: - - main - workflow_dispatch: - -concurrency: - group: docs-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: macos-12 - steps: - - name: Select Xcode 14.1 - run: sudo xcode-select -s /Applications/Xcode_14.1.app - - - name: Checkout Package - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Checkout gh-pages Branch - uses: actions/checkout@v2 - with: - ref: gh-pages - path: docs-out - - - name: Build documentation - run: > - rm -rf docs-out/.git; - rm -rf docs-out/main; - git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.0" | tail -n +6 | xargs -I {} rm -rf {}; - - for tag in $(echo "main"; git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.0" | head -6); - do - if [ -d "docs-out/$tag/data/documentation/swiftuinavigation" ] - then - echo "✅ Documentation for "$tag" already exists."; - else - echo "⏳ Generating documentation for SwiftUINavigation @ "$tag" release."; - rm -rf "docs-out/$tag"; - - git checkout .; - git checkout "$tag"; - - swift package \ - --allow-writing-to-directory docs-out/"$tag" \ - generate-documentation \ - --target SwiftUINavigation \ - --output-path docs-out/"$tag" \ - --transform-for-static-hosting \ - --hosting-base-path /swiftui-navigation/"$tag" \ - && echo "✅ Documentation generated for SwiftUINavigation @ "$tag" release." \ - || echo "⚠️ Documentation skipped for SwiftUINavigation @ "$tag"."; - fi; - done - - - name: Fix permissions - run: 'sudo chown -R $USER docs-out' - - - name: Publish documentation to GitHub Pages - uses: JamesIves/github-pages-deploy-action@4.1.7 - with: - branch: gh-pages - folder: docs-out - single-commit: true From 94987c35f7e906e15f9d344b860c7b749d6764f6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 17 Jun 2024 09:45:28 -0700 Subject: [PATCH 049/124] Swift Language Support: Drop <5.9 (#166) * Swift Language Support: Drop <5.9 * wip --- .github/workflows/ci.yml | 9 ++- Package.resolved | 4 +- Package.swift | 9 ++- Package@swift-5.9.swift | 63 ------------------- .../Internal/Deprecations.swift | 41 +++++------- .../SwiftUINavigationCore/AlertState.swift | 2 +- .../ConfirmationDialogState.swift | 2 +- .../Internal/RuntimeWarnings.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- 9 files changed, 36 insertions(+), 100 deletions(-) delete mode 100644 Package@swift-5.9.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6600c27c1..12d9053adb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,11 @@ concurrency: jobs: library: - runs-on: macos-13 + runs-on: macos-14 strategy: matrix: xcode: - - '15.0' - - '14.3.1' + - '15.4' steps: - uses: actions/checkout@v4 @@ -42,8 +41,8 @@ jobs: steps: - uses: compnerd/gha-setup-swift@main with: - branch: swift-5.9.1-release - tag: 5.9.1-RELEASE + branch: swift-5.10-release + tag: 5.10-RELEASE - uses: actions/checkout@v4 - name: Build run: swift build -c ${{ matrix.config }} diff --git a/Package.resolved b/Package.resolved index 1f81df2040..dc5a3d88df 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", - "version" : "1.3.3" + "revision" : "b871e5ed11a23e52c2896a92ce2c829982ff8619", + "version" : "1.4.2" } }, { diff --git a/Package.swift b/Package.swift index ee0d514c20..1af8379830 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7.1 +// swift-tools-version: 5.9 import PackageDescription @@ -49,3 +49,10 @@ let package = Package( ), ] ) + +for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings!.append(contentsOf: [ + .enableExperimentalFeature("StrictConcurrency") + ]) +} diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift deleted file mode 100644 index 23ac97bb79..0000000000 --- a/Package@swift-5.9.swift +++ /dev/null @@ -1,63 +0,0 @@ -// swift-tools-version:5.9 - -import PackageDescription - -let package = Package( - name: "swiftui-navigation", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v6), - ], - products: [ - .library( - name: "SwiftUINavigation", - targets: ["SwiftUINavigation"] - ), - .library( - name: "SwiftUINavigationCore", - targets: ["SwiftUINavigationCore"] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), - ], - targets: [ - .target( - name: "SwiftUINavigation", - dependencies: [ - "SwiftUINavigationCore", - .product(name: "CasePaths", package: "swift-case-paths"), - ] - ), - .testTarget( - name: "SwiftUINavigationTests", - dependencies: [ - "SwiftUINavigation" - ] - ), - .target( - name: "SwiftUINavigationCore", - dependencies: [ - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ] - ), - ] -) - -for target in package.targets { - target.swiftSettings = target.swiftSettings ?? [] - target.swiftSettings!.append(contentsOf: [ - .enableExperimentalFeature("StrictConcurrency") - ]) - // target.swiftSettings?.append( - // .unsafeFlags([ - // "-enable-library-evolution", - // ]) - // ) -} diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index c47c53be2c..641171d0a0 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -12,12 +12,11 @@ message: "Use the 'fullScreenCover(item:)' (or 'fullScreenCover(item:id:)') overload that passes a binding" ) - public func fullScreenCover( + public func fullScreenCover( unwrapping value: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { + ) -> some View { self.fullScreenCover( isPresented: Binding(value), onDismiss: onDismiss @@ -106,12 +105,11 @@ *, deprecated, message: "Use the 'sheet(item:)' (or 'sheet(item:id:)') overload that passes a binding" ) - public func sheet( + public func sheet( unwrapping value: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { + ) -> some View { self.sheet(isPresented: Binding(value), onDismiss: onDismiss) { Binding(unwrapping: value).map(content) } @@ -363,13 +361,12 @@ message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - public func fullScreenCover( + public func fullScreenCover( unwrapping enum: Binding, case casePath: AnyCasePath, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { + ) -> some View { fullScreenCover( unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } @@ -399,13 +396,13 @@ message: "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) - public func popover( + public func popover( unwrapping enum: Binding, case casePath: AnyCasePath, attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge = .top, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View where Content: View { + ) -> some View { popover( unwrapping: `enum`.case(casePath), attachmentAnchor: attachmentAnchor, @@ -420,13 +417,12 @@ "Chain a '@CasePathable' enum binding into a case directly instead of specifying a case path." ) @MainActor - public func sheet( + public func sheet( unwrapping enum: Binding, case casePath: AnyCasePath, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { + ) -> some View { sheet(unwrapping: `enum`.case(casePath), onDismiss: onDismiss, content: content) } } @@ -528,8 +524,7 @@ } } - public struct IfCaseLet: View - where IfContent: View, ElseContent: View { + public struct IfCaseLet: View { public let `enum`: Binding public let casePath: AnyCasePath public let ifContent: (Binding) -> IfContent @@ -605,8 +600,7 @@ } } - public struct IfLet: View - where IfContent: View, ElseContent: View { + public struct IfLet: View { public let value: Binding public let ifContent: (Binding) -> IfContent public let elseContent: ElseContent @@ -770,8 +764,7 @@ message: "Switch over a '@CasePathable' enum and derive bindings from each case using '$enum.case.map { $case in … }', instead." ) - public struct CaseLet: Sendable, View - where Content: View { + public struct CaseLet: Sendable, View { @EnvironmentObject private var `enum`: BindingObject public let casePath: AnyCasePath public let content: (Binding) -> Content @@ -1855,7 +1848,7 @@ action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { alert(value) { (value: Value?) in - if let value = value { + if let value { await handler(value) } } @@ -1874,7 +1867,7 @@ action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { alert(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { + if let value { await handler(value) } } @@ -1892,7 +1885,7 @@ action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { confirmationDialog(unwrapping: value) { (value: Value?) in - if let value = value { + if let value { await handler(value) } } @@ -1911,7 +1904,7 @@ action handler: @escaping @Sendable (Value) async -> Void = { (_: Void) async in } ) -> some View { confirmationDialog(unwrapping: `enum`, case: casePath) { (value: Value?) async in - if let value = value { + if let value { await handler(value) } } diff --git a/Sources/SwiftUINavigationCore/AlertState.swift b/Sources/SwiftUINavigationCore/AlertState.swift index a8e4fdb5c8..2d27e8cd1e 100644 --- a/Sources/SwiftUINavigationCore/AlertState.swift +++ b/Sources/SwiftUINavigationCore/AlertState.swift @@ -188,7 +188,7 @@ if !self.buttons.isEmpty { children.append(("actions", self.buttons)) } - if let message = self.message { + if let message { children.append(("message", message)) } return Mirror( diff --git a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift index 0fa20e7e7b..a2247803fa 100644 --- a/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift +++ b/Sources/SwiftUINavigationCore/ConfirmationDialogState.swift @@ -235,7 +235,7 @@ if !self.buttons.isEmpty { children.append(("actions", self.buttons)) } - if let message = self.message { + if let message { children.append(("message", message)) } return Mirror( diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift index 25c01c169f..6212de6f29 100644 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift @@ -12,7 +12,7 @@ let message = message() let category = category ?? "Runtime Warning" if _XCTIsTesting { - if let file = file, let line = line { + if let file, let line { XCTFail(message, file: file, line: line) } else { XCTFail(message) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 114ce490b8..7416d722b7 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "8cc3bc05d0cc956f7374c6c208a11f66a7cac3db", - "version" : "1.2.2" + "revision" : "b871e5ed11a23e52c2896a92ce2c829982ff8619", + "version" : "1.4.2" } }, { From 2a9882cc5492545ff1a86579314de105f57e3f29 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 30 Jun 2024 11:21:27 -0700 Subject: [PATCH 050/124] Update README.md Fix #178. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 887da116a5..bba0b2d03d 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ dependencies: [ ## Documentation The latest documentation for the SwiftUI Navigation APIs is available -[here](https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation). +[here](https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation). ## License From 434f58630607708a6e2fdcb1840999889a1d1052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Thu, 4 Jul 2024 03:02:58 +0200 Subject: [PATCH 051/124] Add `.editorconfig` for consistent code formatting (#179) --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..410ff6c9ed --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true From 1a781f0a37ee42ab372bb7d032c2e33636636c7e Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Fri, 19 Jul 2024 07:10:47 -0500 Subject: [PATCH 052/124] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 83e13b1026..d4081517e9 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ test: -workspace SwiftUINavigation.xcworkspace \ -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_TVOS)" - xcodebuild \ + xcodebuild test \ -workspace SwiftUINavigation.xcworkspace \ -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_WATCHOS)" From bc0e37936a6a09a47b34cf655d0059f1fd4dd38d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jul 2024 16:28:30 -0700 Subject: [PATCH 053/124] Swift Testing support for runtime warnings (#181) * Swift Testing support for runtime warnings * wip * bump * wip * wip * wip * wip * wip --------- Co-authored-by: Brandon Williams --- Examples/Examples.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 222 ++++++++++-------- Package.resolved | 24 +- Package.swift | 9 +- Package@swift-6.0.swift | 9 +- .../Internal/Deprecations.swift | 113 +++++++-- .../SwiftUINavigationCore/ButtonState.swift | 3 +- .../Internal/RuntimeWarnings.swift | 84 ------- .../xcshareddata/swiftpm/Package.resolved | 39 +-- .../SwiftUINavigationTests/BindingTests.swift | 4 + .../ButtonStateTests.swift | 3 +- .../SwiftUINavigationTests.swift | 2 + 12 files changed, 264 insertions(+), 250 deletions(-) delete mode 100644 Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 3eac9bf078..22c0a4c45e 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -561,7 +561,7 @@ repositoryURL = "http://github.com/pointfreeco/swift-dependencies"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.3.4; }; }; DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */ = { diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f33c147a90..dec6c15a32 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,106 +1,122 @@ { - "object": { - "pins": [ - { - "package": "combine-schedulers", - "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", - "state": { - "branch": null, - "revision": "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version": "1.0.0" - } - }, - { - "package": "swift-case-paths", - "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", - "state": { - "branch": null, - "revision": "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version": "1.0.0" - } - }, - { - "package": "swift-clocks", - "repositoryURL": "https://github.com/pointfreeco/swift-clocks", - "state": { - "branch": null, - "revision": "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version": "1.0.0" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections", - "state": { - "branch": null, - "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version": "1.0.4" - } - }, - { - "package": "swift-concurrency-extras", - "repositoryURL": "https://github.com/pointfreeco/swift-concurrency-extras", - "state": { - "branch": null, - "revision": "ea631ce892687f5432a833312292b80db238186a", - "version": "1.0.0" - } - }, - { - "package": "swift-custom-dump", - "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", - "state": { - "branch": null, - "revision": "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version": "1.0.0" - } - }, - { - "package": "swift-dependencies", - "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", - "state": { - "branch": null, - "revision": "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version": "1.0.0" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6", - "version": "1.0.0" - } - }, - { - "package": "swift-identified-collections", - "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", - "state": { - "branch": null, - "revision": "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version": "1.0.0" - } - }, - { - "package": "swift-tagged", - "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", - "state": { - "branch": null, - "revision": "af06825aaa6adffd636c10a2570b2010c7c07e6a", - "version": "0.9.0" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version": "1.0.2" - } + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "487a4d151e795a5e076a7e7aedcd13c2ebff6c31", + "version" : "1.0.1" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "031704ba0634b45e02fe875b8ddddc7f30a07f49", + "version" : "1.5.3" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "eb64eacfed55635a771e3410f9c91de46cf5c6a0", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "http://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "52018827ce21e482a36e3795bea2666b3898164c", + "version" : "1.3.4" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections.git", + "state" : { + "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-issue-reporting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-issue-reporting", + "state" : { + "branch" : "1.2.0", + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged.git", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + } + ], + "version" : 2 } diff --git a/Package.resolved b/Package.resolved index dc5a3d88df..9a54b0f913 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "b871e5ed11a23e52c2896a92ce2c829982ff8619", - "version" : "1.4.2" + "revision" : "031704ba0634b45e02fe875b8ddddc7f30a07f49", + "version" : "1.5.3" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" + "revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e", + "version" : "1.3.1" } }, { @@ -37,21 +37,21 @@ } }, { - "identity" : "swift-syntax", + "identity" : "swift-issue-reporting", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/pointfreeco/swift-issue-reporting", "state" : { - "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", - "version" : "510.0.2" + "branch" : "1.2.0", + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f" } }, { - "identity" : "xctest-dynamic-overlay", + "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version" : "1.1.2" + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" } } ], diff --git a/Package.swift b/Package.swift index 1af8379830..63f72c632f 100644 --- a/Package.swift +++ b/Package.swift @@ -22,9 +22,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.3"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.1"), + .package(url: "https://github.com/pointfreeco/swift-issue-reporting", branch: "1.2.0"), ], targets: [ .target( @@ -32,6 +32,7 @@ let package = Package( dependencies: [ "SwiftUINavigationCore", .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "IssueReporting", package: "swift-issue-reporting"), ] ), .testTarget( @@ -44,7 +45,7 @@ let package = Package( name: "SwiftUINavigationCore", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "IssueReporting", package: "swift-issue-reporting"), ] ), ] diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index b3a8b94dc8..b565f5ed65 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -22,9 +22,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.3"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.1"), + .package(url: "https://github.com/pointfreeco/swift-issue-reporting", from: "1.2.0"), ], targets: [ .target( @@ -32,6 +32,7 @@ let package = Package( dependencies: [ "SwiftUINavigationCore", .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "IssueReporting", package: "swift-issue-reporting"), ] ), .testTarget( @@ -44,7 +45,7 @@ let package = Package( name: "SwiftUINavigationCore", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "IssueReporting", package: "swift-issue-reporting"), ] ), ], diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 641171d0a0..c9ddd73d30 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,6 +1,6 @@ #if canImport(SwiftUI) + import IssueReporting import SwiftUI - @_spi(RuntimeWarn) import SwiftUINavigationCore // NB: Deprecated after 1.3.0 @@ -862,8 +862,10 @@ public init( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> CaseLet ) where @@ -874,7 +876,11 @@ { self.init(`enum`) { content() - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -911,8 +917,10 @@ public init( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -933,7 +941,11 @@ self.init(`enum`) { content.value.0 content.value.1 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -981,8 +993,10 @@ public init( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1008,7 +1022,11 @@ content.value.0 content.value.1 content.value.2 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1068,8 +1086,10 @@ Case4: Sendable, Content4 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1100,7 +1120,11 @@ content.value.1 content.value.2 content.value.3 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1168,8 +1192,10 @@ Case5: Sendable, Content5 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1205,7 +1231,11 @@ content.value.2 content.value.3 content.value.4 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1281,8 +1311,10 @@ Case6: Sendable, Content6 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1323,7 +1355,11 @@ content.value.3 content.value.4 content.value.5 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1407,8 +1443,10 @@ Case7: Sendable, Content7 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1454,7 +1492,11 @@ content.value.4 content.value.5 content.value.6 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1546,8 +1588,10 @@ Case8: Sendable, Content8 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1598,7 +1642,11 @@ content.value.5 content.value.6 content.value.7 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } @@ -1698,8 +1746,10 @@ Case9: Sendable, Content9 >( _ enum: Binding, - file: StaticString = #fileID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, line: UInt = #line, + column: UInt = #column, @ViewBuilder content: () -> TupleView< ( CaseLet, @@ -1755,25 +1805,32 @@ content.value.6 content.value.7 content.value.8 - Default { _ExhaustivityCheckView(file: file, line: line) } + Default { + _ExhaustivityCheckView( + fileID: fileID, filePath: filePath, line: line, column: column + ) + } } } } public struct _ExhaustivityCheckView: View { @EnvironmentObject private var `enum`: BindingObject - let file: StaticString + let fileID: StaticString + let filePath: StaticString let line: UInt + let column: UInt public var body: some View { #if DEBUG let message = """ - Warning: Switch.body@\(self.file):\(self.line) + Warning: Switch.body@\(fileID):\(line) "Switch" did not handle "\(describeCase(self.enum.wrappedValue.wrappedValue))" - Make sure that you exhaustively provide a "CaseLet" view for each case in "\(Enum.self)", \ - provide a "Default" view at the end of the "Switch", or use an "IfCaseLet" view instead. + Make sure that you exhaustively provide a "CaseLet" view for each case in \ + "\(Enum.self)", provide a "Default" view at the end of the "Switch", or use an \ + "IfCaseLet" view instead. """ VStack(spacing: 17) { self.exclamation() @@ -1785,7 +1842,15 @@ .foregroundColor(.white) .padding() .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { runtimeWarn(message, file: self.file, line: self.line) } + .onAppear { + reportIssue( + message, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } #else EmptyView() #endif diff --git a/Sources/SwiftUINavigationCore/ButtonState.swift b/Sources/SwiftUINavigationCore/ButtonState.swift index efe8acbcf2..35494883aa 100644 --- a/Sources/SwiftUINavigationCore/ButtonState.swift +++ b/Sources/SwiftUINavigationCore/ButtonState.swift @@ -1,5 +1,6 @@ #if canImport(SwiftUI) import CustomDump + import IssueReporting import SwiftUI public struct ButtonState: Identifiable { @@ -79,7 +80,7 @@ case let .animatedSend(action, _): var output = "" customDump(self.action, to: &output, indent: 4) - runtimeWarn( + reportIssue( """ An animated action was performed asynchronously: … diff --git a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift deleted file mode 100644 index 6212de6f29..0000000000 --- a/Sources/SwiftUINavigationCore/Internal/RuntimeWarnings.swift +++ /dev/null @@ -1,84 +0,0 @@ -#if canImport(SwiftUI) - @_spi(RuntimeWarn) - @_transparent - @inline(__always) - public func runtimeWarn( - _ message: @autoclosure () -> String, - category: String? = "SwiftUINavigation", - file: StaticString? = nil, - line: UInt? = nil - ) { - #if DEBUG - let message = message() - let category = category ?? "Runtime Warning" - if _XCTIsTesting { - if let file, let line { - XCTFail(message, file: file, line: line) - } else { - XCTFail(message) - } - } else { - #if canImport(os) - os_log( - .fault, - dso: dso.wrappedValue, - log: OSLog(subsystem: "com.apple.runtime-issues", category: category), - "%@", - message - ) - #else - fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr) - #endif - } - #endif - } - - #if DEBUG - import XCTestDynamicOverlay - - #if canImport(os) - import os - import Foundation - - // NB: Xcode runtime warnings offer a much better experience than traditional assertions and - // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. - // To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead. - // - // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc - @usableFromInline - let dso = UncheckedSendable( - { - let count = _dyld_image_count() - for i in 0..: @unchecked Sendable { - @usableFromInline - var wrappedValue: Value - init(_ value: Value) { - self.wrappedValue = value - } - } - #else - import Foundation - - @usableFromInline - let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:MM:SS.sssZ" - return formatter - }() - #endif - #endif -#endif // canImport(SwiftUI) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7416d722b7..d4778940bf 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "b871e5ed11a23e52c2896a92ce2c829982ff8619", - "version" : "1.4.2" + "revision" : "12bc5b9191b62ee62cafecbfed953fbb1e1554cd", + "version" : "1.5.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", - "version" : "1.1.2" + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "http://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "c31b1445c4fae49e6fdb75496b895a3653f6aefc", - "version" : "1.1.5" + "revision" : "d80613633e76d1ef86f41926e72fbef6a2f77d9c", + "version" : "1.3.3" } }, { @@ -86,17 +86,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections.git", "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" + "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-issue-reporting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-issue-reporting", + "state" : { + "branch" : "1.2.0", + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" } }, { @@ -113,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", + "version" : "1.2.0" } } ], diff --git a/Tests/SwiftUINavigationTests/BindingTests.swift b/Tests/SwiftUINavigationTests/BindingTests.swift index ba85a092e2..8d45a3888c 100644 --- a/Tests/SwiftUINavigationTests/BindingTests.swift +++ b/Tests/SwiftUINavigationTests/BindingTests.swift @@ -12,6 +12,7 @@ case outOfStock(isOnBackOrder: Bool) } + @MainActor func testCaseLookup() throws { @Binding var status: Status _status = Binding(initialValue: .inStock(quantity: 1)) @@ -22,6 +23,7 @@ XCTAssertEqual(status, .inStock(quantity: 2)) } + @MainActor func testCaseCannotReplaceOtherCase() throws { @Binding var status: Status _status = Binding(initialValue: .inStock(quantity: 1)) @@ -34,6 +36,7 @@ XCTAssertEqual(status, .outOfStock(isOnBackOrder: true)) } + @MainActor func testDestinationCannotReplaceOtherDestination() throws { #if os(iOS) || os(macOS) try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) @@ -52,6 +55,7 @@ } extension Binding { + @MainActor fileprivate init(initialValue: Value) { var value = initialValue self.init( diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index 857d268063..9cf8c2f247 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -5,11 +5,10 @@ import XCTest final class ButtonStateTests: XCTestCase { - @MainActor func testAsyncAnimationWarning() async { XCTExpectFailure { $0.compactDescription == """ - An animated action was performed asynchronously: … + failed - An animated action was performed asynchronously: … Action: ButtonStateAction.send( diff --git a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift index abd3bda8ea..57718e2cad 100644 --- a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift +++ b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -5,6 +5,7 @@ @testable import SwiftUINavigation final class SwiftUINavigationTests: XCTestCase { + @MainActor func testBindingUnwrap() throws { var value: Int? let binding = Binding(get: { value }, set: { value = $0 }) @@ -29,6 +30,7 @@ XCTAssertEqual(unwrapped.wrappedValue, 1729) } + @MainActor func testBindingCase() throws { struct MyError: Error, Equatable {} var value: Result? = nil From f1d9d6a2e817cbed681c54b9975d78bf8d66ba38 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Mon, 22 Jul 2024 23:30:03 +0000 Subject: [PATCH 054/124] Run swift-format --- .../xcshareddata/swiftpm/Package.resolved | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index d4778940bf..dec6c15a32 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "revision" : "487a4d151e795a5e076a7e7aedcd13c2ebff6c31", + "version" : "1.0.1" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "12bc5b9191b62ee62cafecbfed953fbb1e1554cd", - "version" : "1.5.2" + "revision" : "031704ba0634b45e02fe875b8ddddc7f30a07f49", + "version" : "1.5.3" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" + "revision" : "eb64eacfed55635a771e3410f9c91de46cf5c6a0", + "version" : "1.0.3" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" + "revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e", + "version" : "1.3.1" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "http://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "d80613633e76d1ef86f41926e72fbef6a2f77d9c", - "version" : "1.3.3" + "revision" : "52018827ce21e482a36e3795bea2666b3898164c", + "version" : "1.3.4" } }, { @@ -116,15 +116,6 @@ "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", "version" : "0.10.0" } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", - "version" : "1.2.0" - } } ], "version" : 2 From 97f854044356ac082e7e698f39264cc035544d77 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jul 2024 16:37:42 -0700 Subject: [PATCH 055/124] wip --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 9a54b0f913..7c00374097 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-issue-reporting", "state" : { - "branch" : "1.2.0", - "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f" + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", + "version" : "1.2.0" } }, { diff --git a/Package.swift b/Package.swift index 63f72c632f..e68111d395 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.3"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.1"), - .package(url: "https://github.com/pointfreeco/swift-issue-reporting", branch: "1.2.0"), + .package(url: "https://github.com/pointfreeco/swift-issue-reporting", from: "1.2.0"), ], targets: [ .target( From e4f6000387262e51d599f310468a4bef4e637651 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Mon, 22 Jul 2024 23:39:39 +0000 Subject: [PATCH 056/124] Run swift-format --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index dec6c15a32..a7fbf94317 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-issue-reporting", "state" : { - "branch" : "1.2.0", - "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f" + "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", + "version" : "1.2.0" } }, { From 0adf7ccdd5c7906502d29076b6a6973439a18028 Mon Sep 17 00:00:00 2001 From: David Furman Date: Tue, 23 Jul 2024 12:14:21 -0700 Subject: [PATCH 057/124] Fix article links (#180) * Fix article links Fulfills https://github.com/pointfreeco/swiftui-navigation/issues/178 * Update references instead * revert brackets to parentheses --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bba0b2d03d..871eed17cd 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,9 @@ This library is released under the MIT license. See [LICENSE](LICENSE) for detai [NavigationLink.init]: https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s [TabView.init]: https://developer.apple.com/documentation/swiftui/tabview/init(content:) [case-paths-gh]: https://github.com/pointfreeco/swift-case-paths -[what-is-article]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/whatisnavigation -[nav-links-dests-article]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/navigation -[sheets-popovers-covers-article]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/sheetspopoverscovers -[alerts-dialogs-article]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/alertsdialogs -[bindings]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/bindings -[docs]: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/ +[what-is-article]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation/whatisnavigation +[nav-links-dests-article]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation/navigation +[sheets-popovers-covers-article]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation/sheetspopoverscovers +[alerts-dialogs-article]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation/alertsdialogs +[bindings]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation/bindings +[docs]: https://swiftpackageindex.com/pointfreeco/swiftui-navigation/main/documentation/swiftuinavigation From b05be6ee6b26eec3c030148c444ae43358131b4d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 24 Jul 2024 12:02:20 -0700 Subject: [PATCH 058/124] Point Issue Reporting to xctest-dynamic-overlay repo (#185) * Point Issue Reporting to xctest-dynamic-overlay repo Swift Package Manager honors redirects, but it appears to associate the path suffix with the package name, and this conflicts with package resolution in certain (but not all) cases. I think we have no choice but to roll back everything to point to the original xctest-dynamic-overlay URL and extract Issue Reporting to a dedicated repo. * wip --- Examples/Examples.xcodeproj/project.pbxproj | 2 +- Package.resolved | 24 +++++------ Package.swift | 10 ++--- .../xcshareddata/swiftpm/Package.resolved | 40 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 22c0a4c45e..acc6de1430 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -561,7 +561,7 @@ repositoryURL = "http://github.com/pointfreeco/swift-dependencies"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.4; + minimumVersion = 1.3.5; }; }; DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */ = { diff --git a/Package.resolved b/Package.resolved index 7c00374097..9b70b8a699 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "031704ba0634b45e02fe875b8ddddc7f30a07f49", - "version" : "1.5.3" + "branch" : "xct-name", + "revision" : "593151ec13a564a79dc930cf57c82a67355b76c2" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e", - "version" : "1.3.1" + "branch" : "xct-name", + "revision" : "3af72bba805d9d91aecdc5c0df15d5f8b89de8de" } }, { @@ -37,21 +37,21 @@ } }, { - "identity" : "swift-issue-reporting", + "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-issue-reporting", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", - "version" : "1.2.0" + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" } }, { - "identity" : "swift-syntax", + "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", - "version" : "600.0.0-prerelease-2024-06-12" + "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495", + "version" : "1.2.2" } } ], diff --git a/Package.swift b/Package.swift index e68111d395..63d94d4ae8 100644 --- a/Package.swift +++ b/Package.swift @@ -22,9 +22,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.3"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.1"), - .package(url: "https://github.com/pointfreeco/swift-issue-reporting", from: "1.2.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", branch: "xct-name"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", branch: "xct-name"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), ], targets: [ .target( @@ -32,7 +32,7 @@ let package = Package( dependencies: [ "SwiftUINavigationCore", .product(name: "CasePaths", package: "swift-case-paths"), - .product(name: "IssueReporting", package: "swift-issue-reporting"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), ] ), .testTarget( @@ -45,7 +45,7 @@ let package = Package( name: "SwiftUINavigationCore", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "IssueReporting", package: "swift-issue-reporting"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), ] ), ] diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index a7fbf94317..115ca181e5 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "487a4d151e795a5e076a7e7aedcd13c2ebff6c31", - "version" : "1.0.1" + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "031704ba0634b45e02fe875b8ddddc7f30a07f49", - "version" : "1.5.3" + "branch" : "xct-name", + "revision" : "593151ec13a564a79dc930cf57c82a67355b76c2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "eb64eacfed55635a771e3410f9c91de46cf5c6a0", - "version" : "1.0.3" + "revision" : "3581e280bf0d90c3fb9236fb23e75a5d8c46b533", + "version" : "1.0.4" } }, { @@ -39,7 +39,7 @@ { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras.git", "state" : { "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", "version" : "1.1.0" @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e", - "version" : "1.3.1" + "branch" : "xct-name", + "revision" : "3af72bba805d9d91aecdc5c0df15d5f8b89de8de" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "http://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "52018827ce21e482a36e3795bea2666b3898164c", - "version" : "1.3.4" + "revision" : "cc26d06125dbc913c6d9e8a905a5db0b994509e0", + "version" : "1.3.5" } }, { @@ -90,15 +90,6 @@ "version" : "1.1.0" } }, - { - "identity" : "swift-issue-reporting", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-issue-reporting", - "state" : { - "revision" : "926f43898706eaa127db79ac42138e1ad7e85a3f", - "version" : "1.2.0" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -116,6 +107,15 @@ "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", "version" : "0.10.0" } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495", + "version" : "1.2.2" + } } ], "version" : 2 From 1b18b77a2e32250b45d3429f9928222981983826 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 24 Jul 2024 12:07:05 -0700 Subject: [PATCH 059/124] Bump versions --- Package.resolved | 8 ++++---- Package.swift | 4 ++-- Package@swift-6.0.swift | 8 ++++---- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Package.resolved b/Package.resolved index 9b70b8a699..031478ea7b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "branch" : "xct-name", - "revision" : "593151ec13a564a79dc930cf57c82a67355b76c2" + "revision" : "71344dd930fde41e8f3adafe260adcbb2fc2a3dc", + "version" : "1.5.4" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "branch" : "xct-name", - "revision" : "3af72bba805d9d91aecdc5c0df15d5f8b89de8de" + "revision" : "aec6a73f5c1dc1f1be4f61888094b95cf995d973", + "version" : "1.3.2" } }, { diff --git a/Package.swift b/Package.swift index 63d94d4ae8..6d602663b5 100644 --- a/Package.swift +++ b/Package.swift @@ -22,8 +22,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", branch: "xct-name"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", branch: "xct-name"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.4"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), ], targets: [ diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index b565f5ed65..c05de82e27 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -22,9 +22,9 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.3"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.1"), - .package(url: "https://github.com/pointfreeco/swift-issue-reporting", from: "1.2.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.4"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), ], targets: [ .target( @@ -32,7 +32,7 @@ let package = Package( dependencies: [ "SwiftUINavigationCore", .product(name: "CasePaths", package: "swift-case-paths"), - .product(name: "IssueReporting", package: "swift-issue-reporting"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), ] ), .testTarget( diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 115ca181e5..ba1a2dd163 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "branch" : "xct-name", - "revision" : "593151ec13a564a79dc930cf57c82a67355b76c2" + "revision" : "71344dd930fde41e8f3adafe260adcbb2fc2a3dc", + "version" : "1.5.4" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "branch" : "xct-name", - "revision" : "3af72bba805d9d91aecdc5c0df15d5f8b89de8de" + "revision" : "aec6a73f5c1dc1f1be4f61888094b95cf995d973", + "version" : "1.3.2" } }, { From fc91d591ebba1f90d65028ccb65c861e5979e898 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 24 Jul 2024 15:02:22 -0700 Subject: [PATCH 060/124] Update Package@swift-6.0.swift (#186) --- Package@swift-6.0.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index c05de82e27..80252b013c 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -45,7 +45,7 @@ let package = Package( name: "SwiftUINavigationCore", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "IssueReporting", package: "swift-issue-reporting"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), ] ), ], From ef8a5223764d93f8d72481c290d1570edc2bca60 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 1 Aug 2024 14:02:06 -0700 Subject: [PATCH 061/124] UIKitNavigation (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Run swift-format * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * wip * wip * wip * wip * modernize wifi settings demo * wip * Run swift-format * wip * wip * added some tests * more tests * more assertions * Run swift-format * wip * uwip * Run swift-format * wip * Run swift-format * test clean up * Run swift-format * clean up tests * Run swift-format * added some tests that dont pass but should * Run swift-format * fix tests * clean up tests * Run swift-format * wip * fix some tests * wip * wip * wip * fix * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * wip * wip * wip * wip * wip * wip * wip * wip * push trait * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * moved wifi demo into case studies * wip * wip * wip * wip * wip * case studies * wip * wip * wip * fixes * update test withUITransaction * wip * fixed some tests * fix more tests * wip * wip * wip * wip * wip * wip * remove UIKitCaseStudies target we bundle them directly into the CaseStudies target now * tvos * wip * runtime warning tests * wrote some tests, fixed some tests * fix for testDecodePath and some other tests * Update UIBindable.swift * tests for rebind and unbind * clean up and tests * wip * wip * wip * animation case study * wip * more tests and beginnings of focus case study * finish focus case study * Add accessibilityLabel property to ButtonState (#171) * Add accessibilityLabel property to ButtonState * Revert "Add accessibilityLabel property to ButtonState" This reverts commit de7a369f002793d945527447b56c4e75f7f88e39. * Add accessibilityLabel support to UIAlertController à la Stephen Celis * Make UIAlertAction convenience initializers public * Apply suggestions from code review --------- Co-authored-by: Stephen Celis * docs * wip * finish focus case study * Dismiss correct view controller when doing presentation. (#176) * Dismiss correct view controller when doing presentation. * wip * new test * Fix * wip * wip * fix project * wip * wip * wip * Fix UIKit navigation issues (#183) * Fix dismissal in `present` method * Remove unneeded params from closures * Fix push behavior when item is updated * Revert "Fix push behavior when item is updated" This reverts commit c3021c42b5e0ea0944773069fd47c352a572e219. * Add test --------- Co-authored-by: Stephen Celis * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * lots of docs * playing around with isolation * swift 6 * wip * wip * fix memory leak * let alert state compile on other platforms * wip * wip * temp * wip * Revert "temp" This reverts commit 036c93305096191f0f55ddc0d118e464fae62c1a. * wip * fixes * wip * wip * wip * fix * wip * wip * wip * fix tests * small fixes * wip; --------- Co-authored-by: stephencelis Co-authored-by: Brandon Williams Co-authored-by: mbrandonw Co-authored-by: Cosmic Flamingo <67525430+acosmicflamingo@users.noreply.github.com> Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Co-authored-by: Oskar Ek --- .spi.yml | 5 +- Examples/CaseStudies/01-Alerts.swift | 56 - .../CaseStudies/02-ConfirmationDialogs.swift | 53 - Examples/CaseStudies/03-Sheets.swift | 119 - Examples/CaseStudies/04-Popovers.swift | 115 - .../CaseStudies/05-FullScreenCovers.swift | 115 - .../06-NavigationDestinations.swift | 132 -- Examples/CaseStudies/07-NavigationLinks.swift | 122 - Examples/CaseStudies/08-Routing.swift | 125 - .../CaseStudies/09-CustomComponents.swift | 110 - .../CaseStudies/10-SynchronizedBindings.swift | 64 - Examples/CaseStudies/11-IfLet.swift | 48 - Examples/CaseStudies/12-IfCaseLet.swift | 55 - Examples/CaseStudies/Internal/CaseStudy.swift | 222 ++ .../CaseStudies/Internal/DetentsHelper.swift | 12 + .../{ => Internal}/FactClient.swift | 4 +- .../CaseStudies/Internal/Text+Template.swift | 59 + Examples/CaseStudies/RootView.swift | 60 +- .../SwiftUI/AlertDialogState.swift | 85 + .../CaseStudies/SwiftUI/EnumControls.swift | 61 + .../CaseStudies/SwiftUI/EnumNavigation.swift | 135 ++ .../SwiftUI/OptionalNavigation.swift | 122 + .../SwiftUI/SwiftUICaseStudies.swift | 24 + .../SwiftUI/SynchronizedBindings.swift | 62 + .../UIKit/AnimationsViewController.swift | 89 + .../BasicsNavigationViewController.swift | 115 + .../ConciseEnumNavigationViewController.swift | 131 ++ .../UIKit/EnumControlsViewController.swift | 109 + .../ErasedNavigationStackController.swift | 234 ++ .../UIKit/FocusViewController.swift | 122 + .../MinimalObservationViewController.swift | 76 + .../StaticNavigationStackController.swift | 157 ++ .../UIControlBindingsViewController.swift | 130 ++ .../CaseStudies/UIKit/UIKitCaseStudies.swift | 40 + .../WiFiFeature/ConnectToNetworkFeature.swift | 111 + .../UIKit/WiFiFeature/Network.swift | 53 + .../WiFiFeature/NetworkDetailFeature.swift | 99 + .../WiFiFeature/WiFiSettingsFeature.swift | 311 +++ .../CaseStudiesTests/CaseStudies.xctestplan | 42 + .../Internal/AssertEventually.swift | 160 ++ .../CaseStudiesTests/Internal/SetUp.swift | 18 + .../CaseStudiesTests/Internal/XCTTODO.swift | 11 + .../NavigationPathTests.swift | 646 ++++++ .../NavigationStackTests.swift | 318 +++ .../CaseStudiesTests/PresentationTests.swift | 482 ++++ .../RuntimeWarningTests.swift | 144 ++ Examples/Examples.xcodeproj/project.pbxproj | 414 +++- .../xcshareddata/swiftpm/Package.resolved | 42 +- .../xcschemes/CaseStudies.xcscheme | 96 + Examples/Inventory/Item.swift | 2 +- Makefile | 31 +- Package.resolved | 25 +- Package.swift | 50 +- Package@swift-6.0.swift | 45 +- README.md | 335 ++- Sources/SwiftNavigation/AlertState.swift | 217 ++ .../Bind.swift | 0 .../Binding.swift | 0 Sources/SwiftNavigation/ButtonState.swift | 402 ++++ .../SwiftNavigation/ButtonStateBuilder.swift | 35 + .../ConfirmationDialogState.swift | 258 +++ .../Articles/CrossPlatform.md | 34 + .../Articles/WhatIsNavigation.md | 54 + .../Extensions/AlertState.md | 6 +- .../Extensions/ButtonState.md | 14 +- .../Extensions/ConfirmationDialogState.md | 6 +- .../Extensions/TextState.md | 2 +- .../Extensions/UIBindable.md | 17 + .../Extensions/UIBinding.md | 27 + .../Documentation.docc/SwiftNavigation.md | 64 + .../HashableObject.swift | 0 .../SwiftNavigation/Internal/Exports.swift | 2 + Sources/SwiftNavigation/Observe.swift | 188 ++ .../TextState.swift | 280 ++- Sources/SwiftNavigation/UIBindable.swift | 191 ++ Sources/SwiftNavigation/UIBinding.swift | 739 ++++++ .../SwiftNavigation/UINavigationPath.swift | 205 ++ Sources/SwiftNavigation/UITransaction.swift | 94 + Sources/SwiftUINavigation/Alert.swift | 196 ++ Sources/SwiftUINavigation/Binding.swift | 57 +- .../ConfirmationDialog.swift | 158 +- .../Articles/AlertsDialogs.md | 10 +- .../Documentation.docc/Articles/Bindings.md | 3 +- .../Documentation.docc/Articles/Navigation.md | 5 +- .../Articles/SwiftUINavigationTools.md | 5 + .../Articles/WhatIsNavigation.md | 293 --- .../Extensions/Deprecations.md | 54 - .../Documentation.docc/Extensions/Switch.md | 8 - .../Documentation.docc/SwiftUINavigation.md | 105 +- .../SwiftUINavigation/FullScreenCover.swift | 4 +- .../Internal/Deprecations.swift | 2026 ----------------- .../SwiftUINavigation/Internal/Exports.swift | 3 +- .../Internal/Identified.swift | 7 +- .../Internal/LockIsolated.swift | 18 - .../NavigationDestination.swift | 19 + Sources/SwiftUINavigation/Popover.swift | 4 +- Sources/SwiftUINavigation/Sheet.swift | 4 +- Sources/SwiftUINavigation/WithState.swift | 5 + Sources/SwiftUINavigationCore/Alert.swift | 141 -- .../SwiftUINavigationCore/AlertState.swift | 284 --- .../SwiftUINavigationCore/ButtonState.swift | 377 --- .../ButtonStateBuilder.swift | 36 - .../ConfirmationDialog.swift | 150 -- .../ConfirmationDialogState.swift | 294 --- .../Extensions/AlertStateDeprecations.md | 22 - .../Extensions/ButtonStateDeprecations.md | 24 - .../ConfirmationDialogStateDeprecations.md | 21 - .../Extensions/Deprecations.md | 19 - .../SwiftUINavigationCore.md | 32 - .../Internal/Deprecations.swift | 331 --- .../NavigationDestination.swift | 25 - .../Bindings/UIColorWell.swift | 30 + .../UIKitNavigation/Bindings/UIControl.swift | 105 + .../Bindings/UIDatePicker.swift | 30 + .../Bindings/UIPageControl.swift | 28 + .../Bindings/UISegmentedControl.swift | 44 + .../UIKitNavigation/Bindings/UISlider.swift | 29 + .../UIKitNavigation/Bindings/UIStepper.swift | 29 + .../UIKitNavigation/Bindings/UISwitch.swift | 32 + .../Bindings/UITabBarController.swift | 34 + .../Bindings/UITextField.swift | 310 +++ .../Extensions/UIColorWell.md | 8 + .../Extensions/UIControlProtocol.md | 8 + .../Extensions/UIDatePicker.md | 8 + .../Extensions/UIKitAnimation.md | 35 + .../Extensions/UIPageControl.md | 8 + .../Documentation.docc/Extensions/UISlider.md | 8 + .../Extensions/UIStepper.md | 8 + .../Documentation.docc/Extensions/UISwitch.md | 8 + .../Extensions/UITextField.md | 22 + .../Extensions/UIViewController.md | 26 + .../Documentation.docc/Extensions/observe.md | 7 + .../Documentation.docc/UIKitNavigation.md | 165 ++ .../Internal/ErrorMechanism.swift | 20 + .../UIKitNavigation/Internal/Exports.swift | 3 + .../Internal/PopFromViewController.swift | 13 + .../Internal/ToOptionalUnit.swift | 12 + .../UIKitNavigation/Navigation/Dismiss.swift | 45 + .../NavigationStackController.swift | 443 ++++ .../Navigation/Presentation.swift | 452 ++++ Sources/UIKitNavigation/Navigation/Push.swift | 43 + .../Navigation/UIAlertController.swift | 87 + Sources/UIKitNavigation/Observe.swift | 177 ++ .../SwiftUI/Representable.swift | 24 + Sources/UIKitNavigation/UIBinding.swift | 13 + Sources/UIKitNavigation/UIKitAnimation.swift | 408 ++++ Sources/UIKitNavigation/UITransaction.swift | 51 + Sources/UIKitNavigationShim/include/shim.h | 19 + Sources/UIKitNavigationShim/shim.m | 88 + .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcschemes/SwiftNavigation.xcscheme | 78 + .../xcschemes/SwiftUINavigation.xcscheme | 25 +- .../xcschemes/SwiftUINavigationCore.xcscheme | 67 + .../xcschemes/UIKitNavigation.xcscheme | 72 + .../xcshareddata/swiftpm/Package.resolved | 122 - Tests/SwiftNavigation.xctestplan | 24 + .../SwiftNavigationTests/IsolationTests.swift | 50 + .../SwiftNavigationTests/LifetimeTests.swift | 33 + Tests/SwiftUINavigation.xctestplan | 24 + Tests/SwiftUINavigationTests/AlertTests.swift | 123 +- .../SwiftUINavigationTests/BindingTests.swift | 3 +- .../ButtonStateTests.swift | 33 +- .../SwiftUINavigationTests.swift | 16 +- .../TextStateTests.swift | 2 +- Tests/UIKitNavigation.xctestplan | 24 + .../Internal/XCTTODO.swift | 11 + .../MemoryManagementTests.swift | 69 + .../UIBindableTests.swift | 49 + .../UIKitNavigationTests/UIBindingTests.swift | 187 ++ .../UIKitNavigationTests/UIControlTests.swift | 176 ++ .../UINavigationPathTests.swift | 32 + 172 files changed, 12345 insertions(+), 5933 deletions(-) delete mode 100644 Examples/CaseStudies/01-Alerts.swift delete mode 100644 Examples/CaseStudies/02-ConfirmationDialogs.swift delete mode 100644 Examples/CaseStudies/03-Sheets.swift delete mode 100644 Examples/CaseStudies/04-Popovers.swift delete mode 100644 Examples/CaseStudies/05-FullScreenCovers.swift delete mode 100644 Examples/CaseStudies/06-NavigationDestinations.swift delete mode 100644 Examples/CaseStudies/07-NavigationLinks.swift delete mode 100644 Examples/CaseStudies/08-Routing.swift delete mode 100644 Examples/CaseStudies/09-CustomComponents.swift delete mode 100644 Examples/CaseStudies/10-SynchronizedBindings.swift delete mode 100644 Examples/CaseStudies/11-IfLet.swift delete mode 100644 Examples/CaseStudies/12-IfCaseLet.swift create mode 100644 Examples/CaseStudies/Internal/CaseStudy.swift create mode 100644 Examples/CaseStudies/Internal/DetentsHelper.swift rename Examples/CaseStudies/{ => Internal}/FactClient.swift (90%) create mode 100644 Examples/CaseStudies/Internal/Text+Template.swift create mode 100644 Examples/CaseStudies/SwiftUI/AlertDialogState.swift create mode 100644 Examples/CaseStudies/SwiftUI/EnumControls.swift create mode 100644 Examples/CaseStudies/SwiftUI/EnumNavigation.swift create mode 100644 Examples/CaseStudies/SwiftUI/OptionalNavigation.swift create mode 100644 Examples/CaseStudies/SwiftUI/SwiftUICaseStudies.swift create mode 100644 Examples/CaseStudies/SwiftUI/SynchronizedBindings.swift create mode 100644 Examples/CaseStudies/UIKit/AnimationsViewController.swift create mode 100644 Examples/CaseStudies/UIKit/BasicsNavigationViewController.swift create mode 100644 Examples/CaseStudies/UIKit/ConciseEnumNavigationViewController.swift create mode 100644 Examples/CaseStudies/UIKit/EnumControlsViewController.swift create mode 100644 Examples/CaseStudies/UIKit/ErasedNavigationStackController.swift create mode 100644 Examples/CaseStudies/UIKit/FocusViewController.swift create mode 100644 Examples/CaseStudies/UIKit/MinimalObservationViewController.swift create mode 100644 Examples/CaseStudies/UIKit/StaticNavigationStackController.swift create mode 100644 Examples/CaseStudies/UIKit/UIControlBindingsViewController.swift create mode 100644 Examples/CaseStudies/UIKit/UIKitCaseStudies.swift create mode 100644 Examples/CaseStudies/UIKit/WiFiFeature/ConnectToNetworkFeature.swift create mode 100644 Examples/CaseStudies/UIKit/WiFiFeature/Network.swift create mode 100644 Examples/CaseStudies/UIKit/WiFiFeature/NetworkDetailFeature.swift create mode 100644 Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift create mode 100644 Examples/CaseStudiesTests/CaseStudies.xctestplan create mode 100644 Examples/CaseStudiesTests/Internal/AssertEventually.swift create mode 100644 Examples/CaseStudiesTests/Internal/SetUp.swift create mode 100644 Examples/CaseStudiesTests/Internal/XCTTODO.swift create mode 100644 Examples/CaseStudiesTests/NavigationPathTests.swift create mode 100644 Examples/CaseStudiesTests/NavigationStackTests.swift create mode 100644 Examples/CaseStudiesTests/PresentationTests.swift create mode 100644 Examples/CaseStudiesTests/RuntimeWarningTests.swift create mode 100644 Examples/Examples.xcodeproj/xcshareddata/xcschemes/CaseStudies.xcscheme create mode 100644 Sources/SwiftNavigation/AlertState.swift rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Bind.swift (100%) rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Binding.swift (100%) create mode 100644 Sources/SwiftNavigation/ButtonState.swift create mode 100644 Sources/SwiftNavigation/ButtonStateBuilder.swift create mode 100644 Sources/SwiftNavigation/ConfirmationDialogState.swift create mode 100644 Sources/SwiftNavigation/Documentation.docc/Articles/CrossPlatform.md create mode 100644 Sources/SwiftNavigation/Documentation.docc/Articles/WhatIsNavigation.md rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Documentation.docc/Extensions/AlertState.md (67%) rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Documentation.docc/Extensions/ButtonState.md (59%) rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Documentation.docc/Extensions/ConfirmationDialogState.md (74%) rename Sources/{SwiftUINavigationCore => SwiftNavigation}/Documentation.docc/Extensions/TextState.md (82%) create mode 100644 Sources/SwiftNavigation/Documentation.docc/Extensions/UIBindable.md create mode 100644 Sources/SwiftNavigation/Documentation.docc/Extensions/UIBinding.md create mode 100644 Sources/SwiftNavigation/Documentation.docc/SwiftNavigation.md rename Sources/{SwiftUINavigation => SwiftNavigation}/HashableObject.swift (100%) create mode 100644 Sources/SwiftNavigation/Internal/Exports.swift create mode 100644 Sources/SwiftNavigation/Observe.swift rename Sources/{SwiftUINavigationCore => SwiftNavigation}/TextState.swift (80%) create mode 100644 Sources/SwiftNavigation/UIBindable.swift create mode 100644 Sources/SwiftNavigation/UIBinding.swift create mode 100644 Sources/SwiftNavigation/UINavigationPath.swift create mode 100644 Sources/SwiftNavigation/UITransaction.swift create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/SwiftUINavigationTools.md delete mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md delete mode 100644 Sources/SwiftUINavigation/Documentation.docc/Extensions/Deprecations.md delete mode 100644 Sources/SwiftUINavigation/Documentation.docc/Extensions/Switch.md delete mode 100644 Sources/SwiftUINavigation/Internal/Deprecations.swift delete mode 100644 Sources/SwiftUINavigation/Internal/LockIsolated.swift delete mode 100644 Sources/SwiftUINavigationCore/Alert.swift delete mode 100644 Sources/SwiftUINavigationCore/AlertState.swift delete mode 100644 Sources/SwiftUINavigationCore/ButtonState.swift delete mode 100644 Sources/SwiftUINavigationCore/ButtonStateBuilder.swift delete mode 100644 Sources/SwiftUINavigationCore/ConfirmationDialog.swift delete mode 100644 Sources/SwiftUINavigationCore/ConfirmationDialogState.swift delete mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/AlertStateDeprecations.md delete mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ButtonStateDeprecations.md delete mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/ConfirmationDialogStateDeprecations.md delete mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/Extensions/Deprecations.md delete mode 100644 Sources/SwiftUINavigationCore/Documentation.docc/SwiftUINavigationCore.md delete mode 100644 Sources/SwiftUINavigationCore/Internal/Deprecations.swift delete mode 100644 Sources/SwiftUINavigationCore/NavigationDestination.swift create mode 100644 Sources/UIKitNavigation/Bindings/UIColorWell.swift create mode 100644 Sources/UIKitNavigation/Bindings/UIControl.swift create mode 100644 Sources/UIKitNavigation/Bindings/UIDatePicker.swift create mode 100644 Sources/UIKitNavigation/Bindings/UIPageControl.swift create mode 100644 Sources/UIKitNavigation/Bindings/UISegmentedControl.swift create mode 100644 Sources/UIKitNavigation/Bindings/UISlider.swift create mode 100644 Sources/UIKitNavigation/Bindings/UIStepper.swift create mode 100644 Sources/UIKitNavigation/Bindings/UISwitch.swift create mode 100644 Sources/UIKitNavigation/Bindings/UITabBarController.swift create mode 100644 Sources/UIKitNavigation/Bindings/UITextField.swift create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIColorWell.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIControlProtocol.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIDatePicker.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIKitAnimation.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIPageControl.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UISlider.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIStepper.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UISwitch.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UITextField.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/UIViewController.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/Extensions/observe.md create mode 100644 Sources/UIKitNavigation/Documentation.docc/UIKitNavigation.md create mode 100644 Sources/UIKitNavigation/Internal/ErrorMechanism.swift create mode 100644 Sources/UIKitNavigation/Internal/Exports.swift create mode 100644 Sources/UIKitNavigation/Internal/PopFromViewController.swift create mode 100644 Sources/UIKitNavigation/Internal/ToOptionalUnit.swift create mode 100644 Sources/UIKitNavigation/Navigation/Dismiss.swift create mode 100644 Sources/UIKitNavigation/Navigation/NavigationStackController.swift create mode 100644 Sources/UIKitNavigation/Navigation/Presentation.swift create mode 100644 Sources/UIKitNavigation/Navigation/Push.swift create mode 100644 Sources/UIKitNavigation/Navigation/UIAlertController.swift create mode 100644 Sources/UIKitNavigation/Observe.swift create mode 100644 Sources/UIKitNavigation/SwiftUI/Representable.swift create mode 100644 Sources/UIKitNavigation/UIBinding.swift create mode 100644 Sources/UIKitNavigation/UIKitAnimation.swift create mode 100644 Sources/UIKitNavigation/UITransaction.swift create mode 100644 Sources/UIKitNavigationShim/include/shim.h create mode 100644 Sources/UIKitNavigationShim/shim.m rename {SwiftUINavigation.xcworkspace => SwiftNavigation.xcworkspace}/contents.xcworkspacedata (100%) rename {SwiftUINavigation.xcworkspace => SwiftNavigation.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 SwiftNavigation.xcworkspace/xcshareddata/xcschemes/SwiftNavigation.xcscheme rename {SwiftUINavigation.xcworkspace => SwiftNavigation.xcworkspace}/xcshareddata/xcschemes/SwiftUINavigation.xcscheme (80%) create mode 100644 SwiftNavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigationCore.xcscheme create mode 100644 SwiftNavigation.xcworkspace/xcshareddata/xcschemes/UIKitNavigation.xcscheme delete mode 100644 SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Tests/SwiftNavigation.xctestplan create mode 100644 Tests/SwiftNavigationTests/IsolationTests.swift create mode 100644 Tests/SwiftNavigationTests/LifetimeTests.swift create mode 100644 Tests/SwiftUINavigation.xctestplan create mode 100644 Tests/UIKitNavigation.xctestplan create mode 100644 Tests/UIKitNavigationTests/Internal/XCTTODO.swift create mode 100644 Tests/UIKitNavigationTests/MemoryManagementTests.swift create mode 100644 Tests/UIKitNavigationTests/UIBindableTests.swift create mode 100644 Tests/UIKitNavigationTests/UIBindingTests.swift create mode 100644 Tests/UIKitNavigationTests/UIControlTests.swift create mode 100644 Tests/UIKitNavigationTests/UINavigationPathTests.swift diff --git a/.spi.yml b/.spi.yml index eeeb1f4d06..11f61019ab 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,7 @@ version: 1 builder: configs: - - documentation_targets: [SwiftUINavigation, SwiftUINavigationCore] + - documentation_targets: + - SwiftNavigation + - SwiftUINavigation + - UIKitNavigation diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift deleted file mode 100644 index 9f91a46aa6..0000000000 --- a/Examples/CaseStudies/01-Alerts.swift +++ /dev/null @@ -1,56 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -struct OptionalAlerts: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Stepper("Number: \(model.count)", value: $model.count) - Button { - Task { await model.numberFactButtonTapped() } - } label: { - HStack { - Text("Get number fact") - if model.isLoading { - Spacer() - ProgressView() - } - } - } - .disabled(model.isLoading) - } - .alert(item: $model.fact) { - Text("Fact about \($0.number)") - } actions: { - Button("Get another fact about \($0.number)") { - Task { await model.numberFactButtonTapped() } - } - Button("Close", role: .cancel) { - model.fact = nil - } - } message: { - Text($0.description) - } - .navigationTitle("Alerts") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var isLoading = false - var fact: Fact? - - @MainActor - func numberFactButtonTapped() async { - isLoading = true - defer { isLoading = false } - fact = await getNumberFact(count) - } -} - -#Preview { - OptionalAlerts() -} diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift deleted file mode 100644 index 1c7c8e72d9..0000000000 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ /dev/null @@ -1,53 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -struct OptionalConfirmationDialogs: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Stepper("Number: \(model.count)", value: $model.count) - Button { - Task { await model.numberFactButtonTapped() } - } label: { - HStack { - Text("Get number fact") - if model.isLoading { - Spacer() - ProgressView() - } - } - } - .disabled(model.isLoading) - .confirmationDialog(item: $model.fact, titleVisibility: .visible) { - Text("Fact about \($0.number)") - } actions: { - Button("Get another fact about \($0.number)") { - Task { await model.numberFactButtonTapped() } - } - } message: { - Text($0.description) - } - } - .navigationTitle("Dialogs") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var isLoading = false - var fact: Fact? - - @MainActor - func numberFactButtonTapped() async { - isLoading = true - defer { isLoading = false } - fact = await getNumberFact(count) - } -} - -#Preview { - OptionalConfirmationDialogs() -} diff --git a/Examples/CaseStudies/03-Sheets.swift b/Examples/CaseStudies/03-Sheets.swift deleted file mode 100644 index f1d29169fc..0000000000 --- a/Examples/CaseStudies/03-Sheets.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -struct OptionalSheets: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Section { - Stepper("Number: \(model.count)", value: $model.count) - - HStack { - Button("Get number fact") { - Task { await model.numberFactButtonTapped() } - } - - if model.isLoading { - Spacer() - ProgressView() - } - } - } header: { - Text("Fact Finder") - } - - Section { - ForEach(model.savedFacts) { fact in - Text(fact.description) - } - .onDelete { model.removeSavedFacts(atOffsets: $0) } - } header: { - Text("Saved Facts") - } - } - .sheet(item: $model.fact) { $fact in - NavigationStack { - FactEditor(fact: $fact.description) - .disabled(model.isLoading) - .foregroundColor(model.isLoading ? .gray : nil) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - model.cancelButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - model.saveButtonTapped(fact: fact) - } - } - } - } - } - .navigationTitle("Sheets") - } -} - -private struct FactEditor: View { - @Binding var fact: String - - var body: some View { - VStack { - TextEditor(text: $fact) - } - .padding() - .navigationTitle("Fact editor") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var fact: Fact? - var isLoading = false - var savedFacts: [Fact] = [] - private var task: Task? - - deinit { - task?.cancel() - } - - @MainActor - func numberFactButtonTapped() async { - isLoading = true - fact = Fact(description: "\(count) is still loading...", number: count) - task = Task { - let fact = await getNumberFact(self.count) - isLoading = false - guard !Task.isCancelled - else { return } - self.fact = fact - } - await task?.value - } - - @MainActor - func cancelButtonTapped() { - task?.cancel() - task = nil - fact = nil - } - - @MainActor - func saveButtonTapped(fact: Fact) { - task?.cancel() - task = nil - savedFacts.append(fact) - self.fact = nil - } - - @MainActor - func removeSavedFacts(atOffsets offsets: IndexSet) { - savedFacts.remove(atOffsets: offsets) - } -} - -#Preview { - OptionalSheets() -} diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift deleted file mode 100644 index 292816baf2..0000000000 --- a/Examples/CaseStudies/04-Popovers.swift +++ /dev/null @@ -1,115 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -struct OptionalPopovers: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Section { - Stepper("Number: \(model.count)", value: $model.count) - - HStack { - Button("Get number fact") { - Task { await model.numberFactButtonTapped() } - } - .popover(item: $model.fact, arrowEdge: .bottom) { $fact in - NavigationStack { - FactEditor(fact: $fact.description) - .disabled(model.isLoading) - .foregroundColor(model.isLoading ? .gray : nil) - .navigationBarItems( - leading: Button("Cancel") { - model.cancelButtonTapped() - }, - trailing: Button("Save") { - model.saveButtonTapped(fact: fact) - } - ) - } - } - - if model.isLoading { - Spacer() - ProgressView() - } - } - } header: { - Text("Fact Finder") - } - - Section { - ForEach(model.savedFacts) { fact in - Text(fact.description) - } - .onDelete { model.removeSavedFacts(atOffsets: $0) } - } header: { - Text("Saved Facts") - } - } - .navigationTitle("Popovers") - } -} - -private struct FactEditor: View { - @Binding var fact: String - - var body: some View { - VStack { - TextEditor(text: $fact) - } - .padding() - .navigationTitle("Fact editor") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var fact: Fact? - var isLoading = false - var savedFacts: [Fact] = [] - private var task: Task? - - deinit { - self.task?.cancel() - } - - @MainActor - func numberFactButtonTapped() async { - isLoading = true - fact = Fact(description: "\(count) is still loading...", number: count) - task = Task { - let fact = await getNumberFact(self.count) - isLoading = false - guard !Task.isCancelled - else { return } - self.fact = fact - } - await task?.value - } - - @MainActor - func cancelButtonTapped() { - task?.cancel() - task = nil - fact = nil - } - - @MainActor - func saveButtonTapped(fact: Fact) { - task?.cancel() - task = nil - savedFacts.append(fact) - self.fact = nil - } - - @MainActor - func removeSavedFacts(atOffsets offsets: IndexSet) { - savedFacts.remove(atOffsets: offsets) - } -} - -#Preview { - OptionalPopovers() -} diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift deleted file mode 100644 index cb33d8f61c..0000000000 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ /dev/null @@ -1,115 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -struct OptionalFullScreenCovers: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Section { - Stepper("Number: \(model.count)", value: $model.count) - - HStack { - Button("Get number fact") { - Task { await model.numberFactButtonTapped() } - } - - if model.isLoading { - Spacer() - ProgressView() - } - } - } header: { - Text("Fact Finder") - } - - Section { - ForEach(model.savedFacts) { fact in - Text(fact.description) - } - .onDelete { model.removeSavedFacts(atOffsets: $0) } - } header: { - Text("Saved Facts") - } - } - .fullScreenCover(item: $model.fact) { $fact in - NavigationStack { - FactEditor(fact: $fact.description) - .disabled(model.isLoading) - .foregroundColor(model.isLoading ? .gray : nil) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - model.cancelButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - model.saveButtonTapped(fact: fact) - } - } - } - } - } - .navigationTitle("Full-screen covers") - } -} - -private struct FactEditor: View { - @Binding var fact: String - - var body: some View { - VStack { - TextEditor(text: $fact) - } - .padding() - .navigationTitle("Fact editor") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var fact: Fact? - var isLoading = false - var savedFacts: [Fact] = [] - private var task: Task? - - @MainActor - func numberFactButtonTapped() async { - isLoading = true - fact = Fact(description: "\(count) is still loading...", number: count) - task = Task { - let fact = await getNumberFact(count) - isLoading = false - guard !Task.isCancelled - else { return } - self.fact = fact - } - await task?.value - } - - @MainActor - func cancelButtonTapped() { - task?.cancel() - task = nil - fact = nil - } - - @MainActor - func saveButtonTapped(fact: Fact) { - task?.cancel() - task = nil - savedFacts.append(fact) - self.fact = nil - } - - @MainActor - func removeSavedFacts(atOffsets offsets: IndexSet) { - savedFacts.remove(atOffsets: offsets) - } -} - -#Preview { - OptionalFullScreenCovers() -} diff --git a/Examples/CaseStudies/06-NavigationDestinations.swift b/Examples/CaseStudies/06-NavigationDestinations.swift deleted file mode 100644 index 33dd0a43be..0000000000 --- a/Examples/CaseStudies/06-NavigationDestinations.swift +++ /dev/null @@ -1,132 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -@available(iOS 16, *) -struct NavigationDestinations: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Section { - Stepper("Number: \(model.count)", value: $model.count) - - HStack { - Button("Get number fact") { - Task { await model.numberFactButtonTapped() } - } - - if model.isLoading { - Spacer() - ProgressView() - } - } - } header: { - Text("Fact Finder") - } - - Section { - ForEach(model.savedFacts) { fact in - Text(fact.description) - } - .onDelete { model.removeSavedFacts(atOffsets: $0) } - } header: { - Text("Saved Facts") - } - } - .navigationTitle("Destinations") - .navigationDestination(item: $model.fact) { $fact in - FactEditor(fact: $fact.description) - .disabled(model.isLoading) - .foregroundColor(model.isLoading ? .gray : nil) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - Task { await model.cancelButtonTapped() } - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - Task { await model.saveButtonTapped(fact: fact) } - } - } - } - } - } -} - -private struct FactEditor: View { - @Binding var fact: String - - var body: some View { - VStack { - if #available(iOS 14, *) { - TextEditor(text: $fact) - } else { - TextField("Untitled", text: $fact) - } - } - .padding() - .navigationBarTitle("Fact Editor") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var fact: Fact? - var isLoading = false - var savedFacts: [Fact] = [] - private var task: Task? - - deinit { - task?.cancel() - } - - @MainActor - func setFactNavigation(isActive: Bool) async { - if isActive { - isLoading = true - fact = Fact(description: "\(count) is still loading...", number: count) - task = Task { - let fact = await getNumberFact(self.count) - isLoading = false - guard !Task.isCancelled - else { return } - self.fact = fact - } - await task?.value - } else { - task?.cancel() - task = nil - fact = nil - } - } - - @MainActor - func numberFactButtonTapped() async { - await setFactNavigation(isActive: true) - } - - @MainActor - func cancelButtonTapped() async { - await setFactNavigation(isActive: false) - } - - @MainActor - func saveButtonTapped(fact: Fact) async { - savedFacts.append(fact) - await setFactNavigation(isActive: false) - } - - @MainActor - func removeSavedFacts(atOffsets offsets: IndexSet) { - savedFacts.remove(atOffsets: offsets) - } -} - -#Preview { - NavigationStack { - NavigationDestinations() - } -} diff --git a/Examples/CaseStudies/07-NavigationLinks.swift b/Examples/CaseStudies/07-NavigationLinks.swift deleted file mode 100644 index 4b87b6b053..0000000000 --- a/Examples/CaseStudies/07-NavigationLinks.swift +++ /dev/null @@ -1,122 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -struct OptionalNavigationLinks: View { - @State private var model = FeatureModel() - - var body: some View { - List { - Section { - Stepper("Number: \(model.count)", value: $model.count) - - HStack { - Button("Get number fact") { - Task { await model.setFactNavigation(isActive: true) } - } - - if self.model.isLoading { - Spacer() - ProgressView() - } - } - } header: { - Text("Fact Finder") - } - - Section { - ForEach(model.savedFacts) { fact in - Text(fact.description) - } - .onDelete { model.removeSavedFacts(atOffsets: $0) } - } header: { - Text("Saved Facts") - } - } - .navigationDestination(item: $model.fact) { $fact in - FactEditor(fact: $fact.description) - .disabled(model.isLoading) - .foregroundColor(model.isLoading ? .gray : nil) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - Task { await model.cancelButtonTapped() } - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - Task { await model.saveButtonTapped(fact: fact) } - } - } - } - } - .navigationTitle("Links") - } -} - -private struct FactEditor: View { - @Binding var fact: String - - var body: some View { - VStack { - TextEditor(text: $fact) - } - .padding() - .navigationTitle("Fact editor") - } -} - -@Observable -private class FeatureModel { - var count = 0 - var fact: Fact? - var isLoading = false - var savedFacts: [Fact] = [] - private var task: Task? - - deinit { - task?.cancel() - } - - @MainActor - func setFactNavigation(isActive: Bool) async { - if isActive { - isLoading = true - fact = Fact(description: "\(count) is still loading...", number: count) - task = Task { - let fact = await getNumberFact(self.count) - isLoading = false - guard !Task.isCancelled - else { return } - self.fact = fact - } - await task?.value - } else { - task?.cancel() - task = nil - fact = nil - } - } - - @MainActor - func cancelButtonTapped() async { - await setFactNavigation(isActive: false) - } - - @MainActor - func saveButtonTapped(fact: Fact) async { - savedFacts.append(fact) - await setFactNavigation(isActive: false) - } - - @MainActor - func removeSavedFacts(atOffsets offsets: IndexSet) { - savedFacts.remove(atOffsets: offsets) - } -} - -#Preview { - NavigationStack { - OptionalNavigationLinks() - } -} diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift deleted file mode 100644 index 6c34662a68..0000000000 --- a/Examples/CaseStudies/08-Routing.swift +++ /dev/null @@ -1,125 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This case study demonstrates how to power multiple forms of navigation from a single destination \ - enum that describes all of the possible destinations one can travel to from this screen. - - The screen has four navigation destinations: an alert, a confirmation dialog, a navigation link \ - to a count stepper, and a modal sheet to a count stepper. The state for each of these \ - destinations is held as associated data of an enum, and bindings to the cases of that enum are \ - derived using the tools in this library. - """ - -@CasePathable -enum Destination { - case alert(AlertState) - case confirmationDialog(ConfirmationDialogState) - case link(Int) - case sheet(Int) - - enum AlertAction { - case randomize - case reset - } - enum DialogAction { - case decrement - case increment - } -} - -struct Routing: View { - @State var count = 0 - @State var destination: Destination? - - var body: some View { - Form { - Section { - Text(readMe) - } - - Section { - Text("Count: \(count)") - } - - Button("Alert") { - destination = .alert( - AlertState { - TextState("Update count?") - } actions: { - ButtonState(action: .send(.randomize)) { - TextState("Randomize") - } - ButtonState(role: .destructive, action: .send(.reset)) { - TextState("Reset") - } - } - ) - } - - Button("Confirmation dialog") { - destination = .confirmationDialog( - ConfirmationDialogState(titleVisibility: .visible) { - TextState("Update count?") - } actions: { - ButtonState(action: .send(.increment)) { - TextState("Increment") - } - ButtonState(action: .send(.decrement)) { - TextState("Decrement") - } - } - ) - } - - Button("Link") { - destination = .link(count) - } - - Button("Sheet") { - destination = .sheet(count) - } - } - .navigationTitle("Routing") - .alert($destination.alert) { action in - switch action { - case .randomize?: - count = .random(in: 0...1_000) - case .reset?: - count = 0 - case nil: - break - } - } - .confirmationDialog($destination.confirmationDialog) { action in - switch action { - case .decrement?: - count -= 1 - case .increment?: - count += 1 - case nil: - break - } - } - .navigationDestination(item: $destination.link) { $count in - Form { - Stepper("Count: \(count)", value: $count) - } - .navigationTitle("Routing link") - } - .sheet(item: $destination.sheet, id: \.self) { $count in - NavigationStack { - Form { - Stepper("Count: \(count)", value: $count) - } - .navigationTitle("Routing sheet") - } - } - } -} - -#Preview { - NavigationStack { - Routing() - } -} diff --git a/Examples/CaseStudies/09-CustomComponents.swift b/Examples/CaseStudies/09-CustomComponents.swift deleted file mode 100644 index 0533c22223..0000000000 --- a/Examples/CaseStudies/09-CustomComponents.swift +++ /dev/null @@ -1,110 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This case study demonstrates how to enhance an existing SwiftUI component so that it can be \ - driven off of optional and enum state. - - The BottomMenuModifier component in this is file is primarily powered by a simple boolean \ - binding, which means its content cannot be dynamic based off of the source of truth that drives \ - its presentation, and it cannot make mutations to the source of truth. - - However, by leveraging the binding transformations that come with this library we can extend the \ - bottom menu component with additional APIs that allow presentation and dismissal to be powered \ - by optionals and enums. - """ - -struct CustomComponents: View { - @State var count: Int? - - var body: some View { - Form { - Section { - Text(readMe) - } - - Button("Show bottom menu") { - withAnimation { - count = 0 - } - } - - if let count = count, count > 0 { - Text("Current count: \(count)") - .transition(.opacity) - } - } - .bottomMenu(item: $count) { $count in - Stepper("Number: \(count)", value: $count.animation()) - } - .navigationTitle("Custom components") - } -} - -private struct BottomMenuModifier: ViewModifier -where BottomMenuContent: View { - @Binding var isActive: Bool - let content: () -> BottomMenuContent - - func body(content: Content) -> some View { - content.overlay( - ZStack(alignment: .bottom) { - if isActive { - Rectangle() - .fill(Color.black.opacity(0.4)) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .onTapGesture { - withAnimation { - isActive = false - } - } - .zIndex(1) - .transition(.opacity) - - self.content() - .padding() - .background(Color.white) - .cornerRadius(10) - .frame(maxWidth: .infinity) - .padding(24) - .padding(.bottom) - .zIndex(2) - .transition(.move(edge: .bottom)) - } - } - .ignoresSafeArea() - ) - } -} - -extension View { - fileprivate func bottomMenu( - isActive: Binding, - @ViewBuilder content: @escaping () -> Content - ) -> some View - where Content: View { - modifier( - BottomMenuModifier( - isActive: isActive, - content: content - ) - ) - } - - fileprivate func bottomMenu( - item: Binding, - @ViewBuilder content: @escaping (Binding) -> Content - ) -> some View - where Content: View { - modifier( - BottomMenuModifier( - isActive: Binding(item), - content: { Binding(unwrapping: item).map(content) } - ) - ) - } -} - -#Preview { - CustomComponents() -} diff --git a/Examples/CaseStudies/10-SynchronizedBindings.swift b/Examples/CaseStudies/10-SynchronizedBindings.swift deleted file mode 100644 index c8abcaf132..0000000000 --- a/Examples/CaseStudies/10-SynchronizedBindings.swift +++ /dev/null @@ -1,64 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This demonstrates how to synchronize model state with view state using the "bind" view modifier. \ - The model starts focused on the "Username" field, which is immediately focused when the form \ - first appears. When you tap the "Sign in" button, the focus will change to the first non-empty \ - field. - """ - -struct SynchronizedBindings: View { - @FocusState private var focusedField: FeatureModel.Field? - @State private var model = FeatureModel() - - var body: some View { - Form { - Section { - Text(readMe) - } - - Section { - TextField("Username", text: $model.username) - .focused($focusedField, equals: .username) - - SecureField("Password", text: $model.password) - .focused($focusedField, equals: .password) - - Button("Sign In") { - model.signInButtonTapped() - } - .buttonStyle(.borderedProminent) - } - .textFieldStyle(.roundedBorder) - } - .bind($model.focusedField, to: $focusedField) - .navigationTitle("Synchronized focus") - } -} - -@Observable -private class FeatureModel { - enum Field: String { - case username - case password - } - - var focusedField: Field? = .username - var password: String = "" - var username: String = "" - - func signInButtonTapped() { - if username.isEmpty { - focusedField = .username - } else if password.isEmpty { - focusedField = .password - } else { - focusedField = nil - } - } -} - -#Preview { - SynchronizedBindings() -} diff --git a/Examples/CaseStudies/11-IfLet.swift b/Examples/CaseStudies/11-IfLet.swift deleted file mode 100644 index f6ec2cd4ec..0000000000 --- a/Examples/CaseStudies/11-IfLet.swift +++ /dev/null @@ -1,48 +0,0 @@ -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This demonstrates how to unwrap a binding of an optional into a binding of an honest value. - - Tap the "Edit" button to put the form into edit mode. Then you can make changes to the message \ - and either commit the changes by tapping "Save", or discard the changes by tapping "Discard". - """ - -struct IfLetCaseStudy: View { - @State var string: String = "Hello" - @State var editableString: String? - - var body: some View { - Form { - Section { - Text(readMe) - } - Binding(unwrapping: $editableString).map { $string in - VStack { - TextField("Edit string", text: $string) - HStack { - Button("Discard") { - editableString = nil - } - Spacer() - Button("Save") { - string = string - editableString = nil - } - } - } - } - if editableString == nil { - Text("\(string)") - Button("Edit") { - editableString = string - } - } - } - .buttonStyle(.borderless) - } -} - -#Preview { - IfLetCaseStudy() -} diff --git a/Examples/CaseStudies/12-IfCaseLet.swift b/Examples/CaseStudies/12-IfCaseLet.swift deleted file mode 100644 index 5994f58988..0000000000 --- a/Examples/CaseStudies/12-IfCaseLet.swift +++ /dev/null @@ -1,55 +0,0 @@ -import CasePaths -import SwiftUI -import SwiftUINavigation - -private let readMe = """ - This demonstrates how to destructure a binding of an enum into a binding of one of its cases. - - Tap the "Edit" button to put the form into edit mode. Then you can make changes to the message \ - and either commit the changes by tapping "Save", or discard the changes by tapping "Discard". - """ - -struct IfCaseLetCaseStudy: View { - @State var string: String = "Hello" - @State var editableString: EditableString = .inactive - - @CasePathable - enum EditableString { - case active(String) - case inactive - } - - var body: some View { - Form { - Section { - Text(readMe) - } - $editableString.active.map { $string in - VStack { - TextField("Edit string", text: $string) - HStack { - Button("Discard", role: .cancel) { - editableString = .inactive - } - Spacer() - Button("Save") { - string = string - editableString = .inactive - } - } - } - } - if !editableString.is(\.active) { - Text("\(string)") - Button("Edit") { - editableString = .active(string) - } - } - } - .buttonStyle(.borderless) - } -} - -#Preview { - IfCaseLetCaseStudy() -} diff --git a/Examples/CaseStudies/Internal/CaseStudy.swift b/Examples/CaseStudies/Internal/CaseStudy.swift new file mode 100644 index 0000000000..9012fb85ea --- /dev/null +++ b/Examples/CaseStudies/Internal/CaseStudy.swift @@ -0,0 +1,222 @@ +import SwiftUI +import UIKitNavigation + +protocol CaseStudy { + var readMe: String { get } + var caseStudyTitle: String { get } + var caseStudyNavigationTitle: String { get } + var usesOwnLayout: Bool { get } + var isPresentedInSheet: Bool { get } +} +protocol SwiftUICaseStudy: CaseStudy, View {} +protocol UIKitCaseStudy: CaseStudy, UIViewController {} + +extension CaseStudy { + var caseStudyNavigationTitle: String { caseStudyTitle } + var isPresentedInSheet: Bool { false } +} +extension SwiftUICaseStudy { + var usesOwnLayout: Bool { false } +} +extension UIKitCaseStudy { + var usesOwnLayout: Bool { true } +} + +@resultBuilder +@MainActor +enum CaseStudyViewBuilder { + @ViewBuilder + static func buildBlock() -> some View {} + @ViewBuilder + static func buildExpression(_ caseStudy: some SwiftUICaseStudy) -> some View { + SwiftUICaseStudyButton(caseStudy: caseStudy) + } + @ViewBuilder + static func buildExpression(_ caseStudy: some UIKitCaseStudy) -> some View { + UIKitCaseStudyButton(caseStudy: caseStudy) + } + static func buildPartialBlock(first: some View) -> some View { + first + } + @ViewBuilder + static func buildPartialBlock(accumulated: some View, next: some View) -> some View { + accumulated + next + } +} + +struct SwiftUICaseStudyButton: View { + let caseStudy: C + @State var isPresented = false + var body: some View { + if caseStudy.isPresentedInSheet { + Button(caseStudy.caseStudyTitle) { + isPresented = true + } + .sheet(isPresented: $isPresented) { + CaseStudyView { + caseStudy + } + .modifier(CaseStudyModifier(caseStudy: caseStudy)) + } + } else { + NavigationLink(caseStudy.caseStudyTitle) { + CaseStudyView { + caseStudy + } + .modifier(CaseStudyModifier(caseStudy: caseStudy)) + } + } + } +} + +struct UIKitCaseStudyButton: View { + let caseStudy: C + @State var isPresented = false + var body: some View { + if caseStudy.isPresentedInSheet { + Button(caseStudy.caseStudyTitle) { + isPresented = true + } + .sheet(isPresented: $isPresented) { + UIViewControllerRepresenting { + ( + (caseStudy as? UINavigationController) + ?? UINavigationController(rootViewController: caseStudy) + ) + .setUp(caseStudy: caseStudy) + } + .modifier(CaseStudyModifier(caseStudy: caseStudy)) + } + } else { + NavigationLink(caseStudy.caseStudyTitle) { + UIViewControllerRepresenting { + caseStudy + } + .modifier(CaseStudyModifier(caseStudy: caseStudy)) + } + } + } +} + +extension UINavigationController { + func setUp(caseStudy: some CaseStudy) -> Self { + self.viewControllers[0].title = caseStudy.caseStudyNavigationTitle + self.viewControllers[0].navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "About", + primaryAction: UIAction { [weak self] _ in + self?.present( + UIHostingController( + rootView: Form { + Text(template: caseStudy.readMe) + } + .presentationDetents([.medium]) + ), + animated: true + ) + }) + return self + } +} + +struct CaseStudyModifier: ViewModifier { + let caseStudy: C + @State var isAboutPresented = false + func body(content: Content) -> some View { + content + .navigationTitle(caseStudy.caseStudyNavigationTitle) + .toolbar { + ToolbarItem { + Button("About") { isAboutPresented = true } + } + } + .sheet(isPresented: $isAboutPresented) { + Form { + Text(template: caseStudy.readMe) + } + .presentationDetents([.medium]) + } + } +} + +struct CaseStudyView: View { + @ViewBuilder let caseStudy: C + @State var isAboutPresented = false + var body: some View { + if caseStudy.usesOwnLayout { + VStack { + caseStudy + } + } else { + Form { + caseStudy + } + } + } +} + +struct CaseStudyGroupView: View { + @CaseStudyViewBuilder let content: Content + @ViewBuilder let title: Title + + var body: some View { + Section { + content + } header: { + title + } + } +} + +extension CaseStudyGroupView where Title == Text { + init(_ title: String, @CaseStudyViewBuilder content: () -> Content) { + self.init(content: content) { Text(title) } + } +} + +extension SwiftUICaseStudy { + fileprivate func navigationLink() -> some View { + NavigationLink(caseStudyTitle) { + self + } + } +} + +#Preview("SwiftUI case study") { + NavigationStack { + CaseStudyView { + DemoCaseStudy() + } + } +} + +#Preview("SwiftUI case study group") { + NavigationStack { + Form { + CaseStudyGroupView("Group") { + DemoCaseStudy() + } + } + } +} + +private struct DemoCaseStudy: SwiftUICaseStudy { + let caseStudyTitle = "Demo Case Study" + let readMe = """ + Hello! This is a demo case study. + + Enjoy! + """ + var body: some View { + Text("Hello!") + } +} + +private class DemoCaseStudyController: UIViewController, UIKitCaseStudy { + let caseStudyTitle = "Demo Case Study" + let readMe = """ + Hello! This is a demo case study. + + Enjoy! + """ +} diff --git a/Examples/CaseStudies/Internal/DetentsHelper.swift b/Examples/CaseStudies/Internal/DetentsHelper.swift new file mode 100644 index 0000000000..6f9ed41449 --- /dev/null +++ b/Examples/CaseStudies/Internal/DetentsHelper.swift @@ -0,0 +1,12 @@ +import UIKit + +extension UIViewController { + func mediumDetents() { + if let sheet = sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersScrollingExpandsWhenScrolledToEdge = false + sheet.prefersEdgeAttachedInCompactHeight = true + sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true + } + } +} diff --git a/Examples/CaseStudies/FactClient.swift b/Examples/CaseStudies/Internal/FactClient.swift similarity index 90% rename from Examples/CaseStudies/FactClient.swift rename to Examples/CaseStudies/Internal/FactClient.swift index 279b53ff00..7538b6d430 100644 --- a/Examples/CaseStudies/FactClient.swift +++ b/Examples/CaseStudies/Internal/FactClient.swift @@ -4,8 +4,8 @@ struct Fact: Identifiable { var description: String let number: Int - var id: AnyHashable { - [description as AnyHashable, number] + var id: Int { + number } } diff --git a/Examples/CaseStudies/Internal/Text+Template.swift b/Examples/CaseStudies/Internal/Text+Template.swift new file mode 100644 index 0000000000..435e8744d0 --- /dev/null +++ b/Examples/CaseStudies/Internal/Text+Template.swift @@ -0,0 +1,59 @@ +import SwiftUI + +extension Text { + init(template: String, _ style: Font.TextStyle = .body) { + enum Style: Hashable { + case code + case emphasis + case strong + } + + var segments: [Text] = [] + var currentValue = "" + var currentStyles: Set