From f615959e718595854d850db30a22fa0ac18402c4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Jan 2022 11:25:57 -0500 Subject: [PATCH 001/181] Add `Binding.removeDuplicates()` (#11) * Add `Binding.removeDuplicates()` Because this library makes it easy to add logic around navigation, it's probably also a good idea to ship with helpers that work around some surprising bugs/behaviors that currently exist in SwiftUI. For example, as noticed by #10, `NavigationLink` writes `nil` to its binding twice on dismissal. It's probably not appropriate for us to automatically filter duplicate writes, but we can at least ship a `Binding.removeDuplicates()` that makes it easy to achieve this behavior. * wip --- Sources/SwiftUINavigation/Binding.swift | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index 546aee4870..ef31a456c2 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -133,4 +133,38 @@ extension 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 + } + ) + } +} + +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: ==) + } } From 75ad98cc91008acfc7903f09c89eabf2f05f2aa5 Mon Sep 17 00:00:00 2001 From: June Bash Date: Tue, 14 Jun 2022 17:36:50 -0700 Subject: [PATCH 002/181] Update IfCaseLet.swift (#22) * Update IfCaseLet.swift * Conform ElseContent to View --- Sources/SwiftUINavigation/IfCaseLet.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index ec0ab0604a..60344e83ac 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -36,7 +36,7 @@ import SwiftUI /// /// 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 { +public struct IfCaseLet: View where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: CasePath public let ifContent: (Binding) -> IfContent @@ -69,7 +69,11 @@ public struct IfCaseLet: View where IfConten } public var body: some View { - Binding(unwrapping: self.enum, case: self.casePath).map(self.ifContent) + if let caseBinding = Binding(unwrapping: self.enum, case: self.casePath) { + ifContent(caseBinding) + } else { + elseContent() + } } } From 7191be709528b0d0040105c7071d35fbb923d462 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Wed, 15 Jun 2022 00:43:51 +0000 Subject: [PATCH 003/181] Run swift-format --- Sources/SwiftUINavigation/IfCaseLet.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index 60344e83ac..bdb7dc5426 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -36,7 +36,8 @@ import SwiftUI /// /// 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 struct IfCaseLet: View +where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: CasePath public let ifContent: (Binding) -> IfContent From c3aff0f9485ca1e60b80b33635007ca8a1e8d0cd Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 13 Sep 2022 00:29:32 -0400 Subject: [PATCH 004/181] Git-ignore .swiftpm/ --- .gitignore | 1 + .../xcschemes/SwiftUINavigation.xcscheme | 77 ---------------- .../SwiftUINavigation_watchOS.xcscheme | 67 -------------- .../xcschemes/swiftui-navigation.xcscheme | 91 ------------------- 4 files changed, 1 insertion(+), 235 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation_watchOS.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/swiftui-navigation.xcscheme diff --git a/.gitignore b/.gitignore index bb460e7be9..f2c6e1ebaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store /.build +/.swiftpm /Packages /*.xcodeproj xcuserdata/ diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation.xcscheme deleted file mode 100644 index 8821c19d50..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation_watchOS.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation_watchOS.xcscheme deleted file mode 100644 index 30f7bd1431..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftUINavigation_watchOS.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-navigation.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-navigation.xcscheme deleted file mode 100644 index 967110966f..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-navigation.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From b1370133893b784db12cea4465dd541dc284f25d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 13 Sep 2022 00:53:54 -0400 Subject: [PATCH 005/181] Fix CI (#24) --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a9fc27c126..05da743610 100644 --- a/Makefile +++ b/Makefile @@ -7,16 +7,16 @@ default: test test: xcodebuild test \ - -scheme SwiftUINavigation \ + -scheme swiftui-navigation \ -destination platform="$(PLATFORM_IOS)" xcodebuild test \ - -scheme SwiftUINavigation \ + -scheme swiftui-navigation \ -destination platform="$(PLATFORM_MACOS)" xcodebuild test \ - -scheme SwiftUINavigation \ + -scheme swiftui-navigation \ -destination platform="$(PLATFORM_TVOS)" xcodebuild \ - -scheme SwiftUINavigation_watchOS \ + -scheme swiftui-navigation \ -destination platform="$(PLATFORM_WATCHOS)" DOC_WARNINGS := $(shell xcodebuild clean docbuild \ From 51c7b145a721af4f4a336b96ce02b309038dd83c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 13 Sep 2022 01:33:01 -0400 Subject: [PATCH 006/181] Fix CI (#25) * Fix CI * wip --- Makefile | 12 ++- .../contents.xcworkspacedata | 7 ++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ .../xcshareddata/swiftpm/Package.resolved | 16 ++++ .../xcschemes/SwiftUINavigation.xcscheme | 77 +++++++++++++++++++ 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 SwiftUINavigation.xcworkspace/contents.xcworkspacedata create mode 100644 SwiftUINavigation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme diff --git a/Makefile b/Makefile index 05da743610..34b66926e6 100644 --- a/Makefile +++ b/Makefile @@ -7,16 +7,20 @@ default: test test: xcodebuild test \ - -scheme swiftui-navigation \ + -workspace SwiftUINavigation.xcworkspace \ + -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_IOS)" xcodebuild test \ - -scheme swiftui-navigation \ + -workspace SwiftUINavigation.xcworkspace \ + -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_MACOS)" xcodebuild test \ - -scheme swiftui-navigation \ + -workspace SwiftUINavigation.xcworkspace \ + -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_TVOS)" xcodebuild \ - -scheme swiftui-navigation \ + -workspace SwiftUINavigation.xcworkspace \ + -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_WATCHOS)" DOC_WARNINGS := $(shell xcodebuild clean docbuild \ diff --git a/SwiftUINavigation.xcworkspace/contents.xcworkspacedata b/SwiftUINavigation.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..ca3329e1a1 --- /dev/null +++ b/SwiftUINavigation.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftUINavigation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/SwiftUINavigation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..a3f5bbb849 --- /dev/null +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-case-paths", + "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", + "state": { + "branch": null, + "revision": "7346701ea29da0a85d4403cf3d7a589a58ae3dee", + "version": "0.9.2" + } + } + ] + }, + "version": 1 +} diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme b/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme new file mode 100644 index 0000000000..43ae5ec349 --- /dev/null +++ b/SwiftUINavigation.xcworkspace/xcshareddata/xcschemes/SwiftUINavigation.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 128f264790775b976d16cfebc9a32f4aeb0461ac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Oct 2022 17:16:18 -0700 Subject: [PATCH 007/181] Runtime warn instead of breakpoint in `Switch` (#26) * Runtime warn instead of breakpoint in `Switch` * Bump swift tools version * update CI * wip --- .github/workflows/ci.yml | 13 ++-- Makefile | 6 +- Package.resolved | 13 +++- Package.swift | 8 ++- .../Internal/Breakpoint.swift | 30 -------- .../Internal/RuntimeWarnings.swift | 71 +++++++++++++++++++ Sources/SwiftUINavigation/Switch.swift | 10 +-- .../contents.xcworkspacedata | 3 + .../xcshareddata/swiftpm/Package.resolved | 31 +++++++- 9 files changed, 127 insertions(+), 58 deletions(-) delete mode 100644 Sources/SwiftUINavigation/Internal/Breakpoint.swift create mode 100644 Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78cbb08ed8..69edd975cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,19 +10,14 @@ on: jobs: library: - runs-on: macos-11.0 + runs-on: macos-12 strategy: matrix: - xcode: - - '12.4' - - '12.5.1' - - '13.1' + xcode: ['14.0.1'] + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run tests run: make test - - name: Compile documentation - if: ${{ matrix.xcode == '13.1' }} - run: make test-docs diff --git a/Makefile b/Makefile index 34b66926e6..71f712fd5e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -PLATFORM_IOS = iOS Simulator,name=iPhone 11 Pro Max +PLATFORM_IOS = iOS Simulator,name=iPhone 13 Pro Max PLATFORM_MACOS = macOS -PLATFORM_TVOS = tvOS Simulator,name=Apple TV 4K (at 1080p) -PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 4 - 44mm +PLATFORM_TVOS = tvOS Simulator,name=Apple TV +PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 7 (45mm) default: test diff --git a/Package.resolved b/Package.resolved index 3660a4030f..3c14d1d575 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,17 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", - "version": "0.7.0" + "revision": "15bba50ebf3a2065388c8d12210debe4f6ada202", + "version": "0.10.0" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version": "0.5.0" } } ] diff --git a/Package.swift b/Package.swift index 077ae02e7c..d7d7eb86c5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 import PackageDescription @@ -17,13 +17,15 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.7.0") + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.10.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.5.0"), ], targets: [ .target( name: "SwiftUINavigation", dependencies: [ - .product(name: "CasePaths", package: "swift-case-paths") + .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), .testTarget( diff --git a/Sources/SwiftUINavigation/Internal/Breakpoint.swift b/Sources/SwiftUINavigation/Internal/Breakpoint.swift deleted file mode 100644 index b21a677fc0..0000000000 --- a/Sources/SwiftUINavigation/Internal/Breakpoint.swift +++ /dev/null @@ -1,30 +0,0 @@ -/// Raises a debug breakpoint if a debugger is attached. -@inline(__always) func breakpoint(_ message: @autoclosure () -> String = "") { - #if DEBUG - // https://github.com/bitstadium/HockeySDK-iOS/blob/c6e8d1e940299bec0c0585b1f7b86baf3b17fc82/Classes/BITHockeyHelper.m#L346-L370 - var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] - var info: kinfo_proc = kinfo_proc() - var info_size = MemoryLayout.size - - let isDebuggerAttached = name.withUnsafeMutableBytes { - $0.bindMemory(to: Int32.self).baseAddress - .map { - sysctl($0, 4, &info, &info_size, nil, 0) != -1 && info.kp_proc.p_flag & P_TRACED != 0 - } - ?? false - } - - if isDebuggerAttached { - fputs( - """ - \(message()) - - Caught debug breakpoint. Type "continue" ("c") to resume execution. - - """, - stderr - ) - raise(SIGTRAP) - } - #endif -} diff --git a/Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift b/Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift new file mode 100644 index 0000000000..d0747c8a94 --- /dev/null +++ b/Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift @@ -0,0 +1,71 @@ +@_transparent +@inline(__always) +@usableFromInline +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 { + #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 +} + +#if DEBUG + import XCTestDynamicOverlay + + #if canImport(os) + import os + + // 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..: View { .foregroundColor(.white) .padding() .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { - breakpoint( - """ - --- - \(message) - --- - """ - ) - } + .onAppear { runtimeWarn(message, file: self.file, line: self.line) } #else EmptyView() #endif diff --git a/SwiftUINavigation.xcworkspace/contents.xcworkspacedata b/SwiftUINavigation.xcworkspace/contents.xcworkspacedata index ca3329e1a1..b50680ee8c 100644 --- a/SwiftUINavigation.xcworkspace/contents.xcworkspacedata +++ b/SwiftUINavigation.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index a3f5bbb849..ac6b7a1b6b 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,35 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "7346701ea29da0a85d4403cf3d7a589a58ae3dee", - "version": "0.9.2" + "revision": "15bba50ebf3a2065388c8d12210debe4f6ada202", + "version": "0.10.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", + "state": { + "branch": null, + "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36", + "version": "0.4.1" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version": "0.5.0" } } ] From 12769915973543f46fbcb3828cdadc56e44dc62d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Oct 2022 17:16:24 -0700 Subject: [PATCH 008/181] NavigationLink updates; less escaping (#27) --- Examples/CaseStudies/06-NavigationLinks.swift | 36 ++++++------ .../CaseStudies/07-NavigationLinkList.swift | 6 +- Examples/CaseStudies/08-Routing.swift | 6 +- Examples/Inventory/ItemRow.swift | 6 +- Sources/SwiftUINavigation/Alert.swift | 8 +-- .../ConfirmationDialog.swift | 8 +-- Sources/SwiftUINavigation/IfCaseLet.swift | 14 ++--- Sources/SwiftUINavigation/IfLet.swift | 8 +-- .../Internal/Deprecations.swift | 33 +++++++++++ .../SwiftUINavigation/NavigationLink.swift | 36 +++++++----- Sources/SwiftUINavigation/Switch.swift | 56 +++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 18 ++++++ 12 files changed, 146 insertions(+), 89 deletions(-) create mode 100644 Sources/SwiftUINavigation/Internal/Deprecations.swift diff --git a/Examples/CaseStudies/06-NavigationLinks.swift b/Examples/CaseStudies/06-NavigationLinks.swift index e2c8916537..99dde3c084 100644 --- a/Examples/CaseStudies/06-NavigationLinks.swift +++ b/Examples/CaseStudies/06-NavigationLinks.swift @@ -10,28 +10,26 @@ struct OptionalNavigationLinks: View { Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) HStack { - NavigationLink( - unwrapping: self.$viewModel.fact, - destination: { $fact in - FactEditor(fact: $fact.description) - .disabled(self.viewModel.isLoading) - .foregroundColor(self.viewModel.isLoading ? .gray : nil) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - self.viewModel.cancelButtonTapped() - } + NavigationLink(unwrapping: self.$viewModel.fact) { + self.viewModel.setFactNavigation(isActive: $0) + } destination: { $fact in + FactEditor(fact: $fact.description) + .disabled(self.viewModel.isLoading) + .foregroundColor(self.viewModel.isLoading ? .gray : nil) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.viewModel.cancelButtonTapped() } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - self.viewModel.saveButtonTapped(fact: fact) - } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + self.viewModel.saveButtonTapped(fact: fact) } } - }, - onNavigate: { self.viewModel.setFactNavigation(isActive: $0) } - ) { + } + } label: { Text("Get number fact") } diff --git a/Examples/CaseStudies/07-NavigationLinkList.swift b/Examples/CaseStudies/07-NavigationLinkList.swift index 4a5d0f68d0..4932faf152 100644 --- a/Examples/CaseStudies/07-NavigationLinkList.swift +++ b/Examples/CaseStudies/07-NavigationLinkList.swift @@ -61,7 +61,9 @@ private struct RowView: View { NavigationLink( unwrapping: self.$viewModel.route, case: /ListOfNavigationLinksRowViewModel.Route.edit - ) { $counter in + ) { + self.viewModel.setEditNavigation(isActive: $0) + } destination: { $counter in EditView(counter: $counter) .navigationBarBackButtonHidden(true) .toolbar { @@ -72,8 +74,6 @@ private struct RowView: View { Button("Cancel") { self.viewModel.cancelButtonTapped() } } } - } onNavigate: { - self.viewModel.setEditNavigation(isActive: $0) } label: { Text("\(self.viewModel.counter)") } diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift index 2505ee5806..a6b6ed178a 100644 --- a/Examples/CaseStudies/08-Routing.swift +++ b/Examples/CaseStudies/08-Routing.swift @@ -47,12 +47,12 @@ struct Routing: View { } ) - NavigationLink(unwrapping: self.$route, case: /Route.link) { $count in + NavigationLink(unwrapping: self.$route, case: /Route.link) { + self.route = $0 ? .link(0) : nil + } destination: { $count in Form { Stepper("Number: \(count)", value: $count) } - } onNavigate: { - self.route = $0 ? .link(0) : nil } label: { Text("Link") } diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index e3ca89f40e..1a3f2f87a9 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -62,7 +62,9 @@ struct ItemRowView: View { @ObservedObject var viewModel: ItemRowViewModel var body: some View { - NavigationLink(unwrapping: self.$viewModel.route, case: /ItemRowViewModel.Route.edit) { $item in + NavigationLink(unwrapping: self.$viewModel.route, case: /ItemRowViewModel.Route.edit) { + self.viewModel.setEditNavigation(isActive: $0) + } destination: { $item in ItemView(item: $item) .navigationBarTitle("Edit") .navigationBarBackButtonHidden(true) @@ -78,8 +80,6 @@ struct ItemRowView: View { } } } - } onNavigate: { - self.viewModel.setEditNavigation(isActive: $0) } label: { HStack { VStack(alignment: .leading) { diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 19a490f9c7..d47b95de78 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -55,8 +55,8 @@ public func alert( title: (Value) -> Text, unwrapping value: Binding, - @ViewBuilder actions: @escaping (Value) -> A, - @ViewBuilder message: @escaping (Value) -> M + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M ) -> some View { self.alert( value.wrappedValue.map(title) ?? Text(""), @@ -88,8 +88,8 @@ title: (Case) -> Text, unwrapping enum: Binding, case casePath: CasePath, - @ViewBuilder actions: @escaping (Case) -> A, - @ViewBuilder message: @escaping (Case) -> M + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M ) -> some View { self.alert( title: title, diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 605327f8e2..dded8a5f36 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -58,8 +58,8 @@ title: (Value) -> Text, titleVisibility: Visibility = .automatic, unwrapping value: Binding, - @ViewBuilder actions: @escaping (Value) -> A, - @ViewBuilder message: @escaping (Value) -> M + @ViewBuilder actions: (Value) -> A, + @ViewBuilder message: (Value) -> M ) -> some View { self.confirmationDialog( value.wrappedValue.map(title) ?? Text(""), @@ -94,8 +94,8 @@ titleVisibility: Visibility = .automatic, unwrapping enum: Binding, case casePath: CasePath, - @ViewBuilder actions: @escaping (Case) -> A, - @ViewBuilder message: @escaping (Case) -> M + @ViewBuilder actions: (Case) -> A, + @ViewBuilder message: (Case) -> M ) -> some View { self.confirmationDialog( title: title, diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index bdb7dc5426..7b82c9e7f7 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -41,7 +41,7 @@ where IfContent: View, ElseContent: View { public let `enum`: Binding public let casePath: CasePath public let ifContent: (Binding) -> IfContent - public let elseContent: () -> ElseContent + 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. @@ -61,19 +61,19 @@ where IfContent: View, ElseContent: View { _ `enum`: Binding, pattern casePath: CasePath, @ViewBuilder ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder elseContent: @escaping () -> ElseContent + @ViewBuilder elseContent: () -> ElseContent ) { self.casePath = casePath - self.elseContent = elseContent + self.elseContent = elseContent() self.enum = `enum` self.ifContent = ifContent } public var body: some View { - if let caseBinding = Binding(unwrapping: self.enum, case: self.casePath) { - ifContent(caseBinding) + if let $case = Binding(unwrapping: self.enum, case: self.casePath) { + self.ifContent($case) } else { - elseContent() + self.elseContent } } } @@ -85,7 +85,7 @@ extension IfCaseLet where ElseContent == EmptyView { @ViewBuilder ifContent: @escaping (Binding) -> IfContent ) { self.casePath = casePath - self.elseContent = { EmptyView() } + self.elseContent = EmptyView() self.enum = `enum` self.ifContent = ifContent } diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift index fd57d1f42a..b4ab9adb24 100644 --- a/Sources/SwiftUINavigation/IfLet.swift +++ b/Sources/SwiftUINavigation/IfLet.swift @@ -31,7 +31,7 @@ public struct IfLet: View where IfContent: View, ElseContent: View { public let value: Binding public let ifContent: (Binding) -> IfContent - public let elseContent: () -> ElseContent + public let elseContent: ElseContent /// Computes content by unwrapping a binding to an optional and passing a non-optional binding to /// its content closure. @@ -48,18 +48,18 @@ public struct IfLet: View where IfContent: View, public init( _ value: Binding, @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder else elseContent: @escaping () -> ElseContent + @ViewBuilder else elseContent: () -> ElseContent ) { self.value = value self.ifContent = ifContent - self.elseContent = elseContent + self.elseContent = elseContent() } public var body: some View { if let $value = Binding(unwrapping: self.value) { self.ifContent($value) } else { - self.elseContent() + self.elseContent } } } diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift new file mode 100644 index 0000000000..8d3321661d --- /dev/null +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -0,0 +1,33 @@ +// 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 + ) + } + + @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 + ) + } +} diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index efef35f7aa..a056bc572d 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -13,11 +13,11 @@ extension NavigationLink { /// /// var body: some View { /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$postToEdit) { $draft in - /// EditPostView(post: $draft) - /// } onNavigate: { isActive in + /// NavigationLink(unwrapping: self.$postToEdit) { isActive in /// self.postToEdit = isActive ? post : nil - /// } label: { + /// } destination: { $draft in + /// EditPostView(post: $draft) + /// } onNavigate: label: { /// Text(post.title) /// } /// } @@ -36,17 +36,21 @@ extension NavigationLink { /// 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. - /// - destination: A view for the navigation link to present. /// - 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, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, onNavigate: @escaping (_ isActive: Bool) -> Void, - @ViewBuilder label: @escaping () -> Label + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( destination: Binding(unwrapping: value).map(destination), @@ -75,10 +79,10 @@ extension NavigationLink { /// /// var body: some View { /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$route, case: /Route.edit) { $draft in - /// EditPostView(post: $draft) - /// } onNavigate: { isActive in + /// NavigationLink(unwrapping: self.$route, case: /Route.edit) { isActive in /// self.route = isActive ? .edit(post) : nil + /// } destination: { $draft in + /// EditPostView(post: $draft) /// } label: { /// Text(post.title) /// } @@ -102,23 +106,27 @@ extension NavigationLink { /// 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. - /// - destination: A view for the navigation link to present. /// - 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, - @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, onNavigate: @escaping (Bool) -> Void, - @ViewBuilder label: @escaping () -> Label + @ViewBuilder destination: @escaping (Binding) -> WrappedDestination, + @ViewBuilder label: () -> Label ) where Destination == WrappedDestination? { self.init( unwrapping: `enum`.case(casePath), - destination: destination, onNavigate: onNavigate, + destination: destination, label: label ) } diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index 1d9c17caee..301f691e17 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -40,20 +40,20 @@ /// > 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 where Content: View { +public struct Switch: View { public let `enum`: Binding - public let content: () -> Content + public let content: Content private init( enum: Binding, - @ViewBuilder content: @escaping () -> Content + @ViewBuilder content: () -> Content ) { self.enum = `enum` - self.content = content + self.content = content() } public var body: some View { - self.content() + self.content .environmentObject(BindingObject(binding: self.enum)) } } @@ -89,27 +89,27 @@ where Content: View { /// 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 where Content: View { - private let content: () -> Content +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: @escaping () -> Content) { - self.content = content + public init(@ViewBuilder content: () -> Content) { + self.content = content() } public var body: some View { - self.content() + self.content } } extension Switch { public init( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, Default @@ -136,7 +136,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> CaseLet + @ViewBuilder content: () -> CaseLet ) where Content == _ConditionalContent< @@ -152,7 +152,7 @@ extension Switch { public init( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -186,7 +186,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet @@ -217,7 +217,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -257,7 +257,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -294,7 +294,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -345,7 +345,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -388,7 +388,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -446,7 +446,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -495,7 +495,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -560,7 +560,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -615,7 +615,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -687,7 +687,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -748,7 +748,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -827,7 +827,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -894,7 +894,7 @@ extension Switch { DefaultContent >( _ enum: Binding, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, @@ -980,7 +980,7 @@ extension Switch { _ enum: Binding, file: StaticString = #fileID, line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< + @ViewBuilder content: () -> TupleView< ( CaseLet, CaseLet, diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index ac6b7a1b6b..2365cd366e 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -36,6 +36,24 @@ "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", "version": "0.5.0" } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", + "state": { + "branch": null, + "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36", + "version": "0.4.1" + } } ] }, From a974c35e3d3617f47e2de79e563b78c38bc6a9f1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Oct 2022 17:18:13 -0700 Subject: [PATCH 009/181] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c496f2f2f..280d4dfeca 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,10 @@ struct ContentView { var body: some View { ForEach(self.posts) { post in - NavigationLink(unwrapping: self.$route, case: /Route.edit) { $post in - EditPostView(post: $post) - } onNavigate: { isActive in + NavigationLink(unwrapping: self.$route, case: /Route.edit) { isActive in self.route = isActive ? .edit(post) : nil + } destination: { $post in + EditPostView(post: $post) } label: { Text(post.title) } From 8d9ac80c12b8223081ae6cb398fa3da48bb1f6c1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 18 Oct 2022 17:22:43 -0700 Subject: [PATCH 010/181] Update Package.resolved --- .../xcshareddata/swiftpm/Package.resolved | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2365cd366e..8191f41552 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,24 +37,6 @@ "version": "0.5.0" } }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - }, - { - "package": "swift-identified-collections", - "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", - "state": { - "branch": null, - "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36", - "version": "0.4.1" - } - } ] }, "version": 1 From 8d1d9227825c5cf25ef19be8224ae44eac166230 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Wed, 19 Oct 2022 00:31:24 +0000 Subject: [PATCH 011/181] Run swift-format --- .../xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8191f41552..ac6b7a1b6b 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -36,7 +36,7 @@ "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", "version": "0.5.0" } - }, + } ] }, "version": 1 From 5bf9dadd086d2d6beef5ea1fe9a2070ad4eab2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Fri, 18 Nov 2022 22:23:54 +0900 Subject: [PATCH 012/181] Fix typos in README and CaseStudies (#30) * Fix typos in README * Fix mismatched navigation titles in CaseStudies --- Examples/CaseStudies/04-Popovers.swift | 2 +- Examples/CaseStudies/05-FullScreenCovers.swift | 2 +- README.md | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift index 32703dd4ad..d62f238d41 100644 --- a/Examples/CaseStudies/04-Popovers.swift +++ b/Examples/CaseStudies/04-Popovers.swift @@ -51,7 +51,7 @@ struct OptionalPopovers: View { } } } - .navigationTitle("Sheets") + .navigationTitle("Popovers") } } diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift index a980bea75f..4028dd2f4a 100644 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ b/Examples/CaseStudies/05-FullScreenCovers.swift @@ -51,7 +51,7 @@ struct OptionalFullScreenCovers: View { } } } - .navigationTitle("Sheets") + .navigationTitle("Full-Screen Covers") } } diff --git a/README.md b/README.md index 280d4dfeca..8e9e6b2e3c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Tools for making SwiftUI navigation simpler, more ergonomic and more precise. 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 [`NavigationLink`][NavigationLink.init] that do not take a binding. + * "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 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. [NavigationLink.init]: https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s [TabView.init]: https://developer.apple.com/documentation/swiftui/tabview/init(content:) @@ -78,7 +78,7 @@ struct ContentView: View { } ``` -This forces us to hold 3 optional values in state, which has 2^3=8 different states, 4 of which are invalid. The only valid states is for all values to be `nil` or exactly one be non-`nil`. It makes no sense if two or more values are non-`nil`, for that would representing wanting to show two modal sheets at the same time. +This forces us to hold 3 optional values in state, which has 2^3=8 different states, 4 of which are invalid. The only valid state is for all values to be `nil` or exactly one be non-`nil`. It makes no sense if two or more values are non-`nil`, for that would represent wanting to show two modal sheets at the same time. Ideally we'd like to represent these navigation destinations as 3 mutually exclusive states so that we could guarantee at compile time that only one can be active at a time. Luckily for us Swift’s enums are perfect for this: @@ -96,7 +96,7 @@ And then we could hold an optional `Route` in state to represent that we are eit @State var route: Route? ``` -This would be the most optimal way to model our navigation domain, but unfortunately SwiftUI's tools do not make easy for us to drive navigation off of enums. +This would be the most optimal way to model our navigation domain, but unfortunately SwiftUI's tools do not make it easy for us to drive navigation off of enums. This library comes with a number of `Binding` transformations and navigation API overloads that allow you to model your domain as concisely as possible, using enums, while still allowing you to use SwiftUI's navigation tools. @@ -130,7 +130,7 @@ struct ContentView { The forward-slash syntax you see above represents a [case path](https://github.com/pointfreeco/swift-case-paths) to a particular case of an enum. Case paths are our imagining of what key paths could look like for enums, and every concept for key paths has an analogous concept for case paths: - * Each property of an struct is naturally endowed with a key path, and so each case of an enum is endowed with a case path. + * Each property of a struct is naturally endowed with a key path, and so each case of an enum is endowed with a case path. * Key paths are constructed using a back slash, name of the type and name of the property (_e.g._, `\User.name`), and case paths are constructed similarly, but with a forward slash (_e.g._, `/Route.draft`). * Key paths describe how to get and set a value in some root structure, whereas case paths describe how to extract and embed a value into a root structure. @@ -331,7 +331,7 @@ If you want to use SwiftUI Navigation in a [SwiftPM](https://swift.org/package-m ``` swift dependencies: [ - .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.1.0") + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.3.0") ] ``` From 102ab45e10986a27ef2cfacac00f03410461436b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 21 Nov 2022 10:35:24 -0800 Subject: [PATCH 013/181] Support navigationDestination, alerts, confirmation dialogs, and more! (#28) * wip * wip * Update APIs * wip * wip * docs * wip * readme tweaks * TextState tests * alert tests * updates * dont export swiftui * wip * wip * update ci * docc * doc updates * docs * docs * wip * Update documentation.yml * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip; * wip * modernize inventory demo * more modernization * wip * wip * wip * wip * wip * wip * wip * bring back navigationDestination hack; * wip * wip * wip * wip * wip; * wip; * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Fix typos in README and CaseStudies (#30) * Fix typos in README * Fix mismatched navigation titles in CaseStudies * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * some <5.7 fixes * another <5.7 fix Co-authored-by: Brandon Williams --- .github/workflows/ci.yml | 7 +- .github/workflows/documentation.yml | 79 +- Examples/CaseStudies/01-Alerts.swift | 23 +- .../CaseStudies/02-ConfirmationDialogs.swift | 22 +- Examples/CaseStudies/03-Sheets.swift | 30 +- Examples/CaseStudies/04-Popovers.swift | 54 +- .../CaseStudies/05-FullScreenCovers.swift | 28 +- .../06-NavigationDestinations.swift | 118 +++ ...onLinks.swift => 07-NavigationLinks.swift} | 30 +- ...List.swift => 08-NavigationLinkList.swift} | 56 +- Examples/CaseStudies/08-Routing.swift | 82 -- Examples/CaseStudies/09-Routing.swift | 128 +++ ...onents.swift => 10-CustomComponents.swift} | 15 +- .../CaseStudies/11-SynchronizedBindings.swift | 65 ++ Examples/CaseStudies/12-IfLet.swift | 47 ++ Examples/CaseStudies/13-IfCaseLet.swift | 53 ++ Examples/CaseStudies/RootView.swift | 36 +- Examples/Examples.xcodeproj/project.pbxproj | 52 +- .../xcshareddata/swiftpm/Package.resolved | 31 +- .../xcshareddata/xcschemes/Inventory.xcscheme | 78 ++ Examples/Inventory/App.swift | 83 +- Examples/Inventory/Inventory.swift | 161 ++-- Examples/Inventory/Item.swift | 9 +- Examples/Inventory/ItemRow.swift | 123 ++- Package.resolved | 18 + Package.swift | 11 + README.md | 359 ++------- Sources/SwiftUINavigation/Alert.swift | 272 +++++-- Sources/SwiftUINavigation/Bind.swift | 84 ++ Sources/SwiftUINavigation/Binding.swift | 44 +- .../ConfirmationDialog.swift | 286 +++++-- .../Articles/AlertsDialogs.md | 194 +++++ .../Documentation.docc/Articles/Bindings.md | 63 ++ .../Articles/DestructuringViews.md | 164 ++++ .../Documentation.docc/Articles/Navigation.md | 115 +++ .../Articles/SheetsPopoversCovers.md | 162 ++++ .../Articles/WhatIsNavigation.md | 300 +++++++ .../Documentation.docc/SwiftUINavigation.md | 60 ++ .../SwiftUINavigation/FullScreenCover.swift | 13 +- Sources/SwiftUINavigation/IfCaseLet.swift | 4 +- Sources/SwiftUINavigation/IfLet.swift | 2 + .../Internal/Binding+Internal.swift | 2 + .../Internal/Deprecations.swift | 16 + .../SwiftUINavigation/Internal/Exports.swift | 2 +- .../NavigationDestination.swift | 94 +++ .../SwiftUINavigation/NavigationLink.swift | 12 +- Sources/SwiftUINavigation/Popover.swift | 10 +- Sources/SwiftUINavigation/Sheet.swift | 16 +- Sources/SwiftUINavigation/Switch.swift | 2 + Sources/SwiftUINavigation/WithState.swift | 47 ++ .../_SwiftUINavigationState/AlertState.swift | 311 ++++++++ .../_SwiftUINavigationState/ButtonState.swift | 253 ++++++ .../ButtonStateBuilder.swift | 32 + .../ConfirmationDialogState.swift | 361 +++++++++ .../_SwiftUINavigationState/TextState.swift | 741 ++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 18 + Tests/SwiftUINavigationTests/AlertTests.swift | 81 ++ .../SwiftUINavigationTests.swift | 1 + .../TextStateTests.swift | 74 ++ 59 files changed, 4722 insertions(+), 912 deletions(-) create mode 100644 Examples/CaseStudies/06-NavigationDestinations.swift rename Examples/CaseStudies/{06-NavigationLinks.swift => 07-NavigationLinks.swift} (73%) rename Examples/CaseStudies/{07-NavigationLinkList.swift => 08-NavigationLinkList.swift} (60%) delete mode 100644 Examples/CaseStudies/08-Routing.swift create mode 100644 Examples/CaseStudies/09-Routing.swift rename Examples/CaseStudies/{09-CustomComponents.swift => 10-CustomComponents.swift} (89%) create mode 100644 Examples/CaseStudies/11-SynchronizedBindings.swift create mode 100644 Examples/CaseStudies/12-IfLet.swift create mode 100644 Examples/CaseStudies/13-IfCaseLet.swift create mode 100644 Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme create mode 100644 Sources/SwiftUINavigation/Bind.swift create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md create mode 100644 Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md create mode 100644 Sources/SwiftUINavigation/NavigationDestination.swift create mode 100644 Sources/SwiftUINavigation/WithState.swift create mode 100644 Sources/_SwiftUINavigationState/AlertState.swift create mode 100644 Sources/_SwiftUINavigationState/ButtonState.swift create mode 100644 Sources/_SwiftUINavigationState/ButtonStateBuilder.swift create mode 100644 Sources/_SwiftUINavigationState/ConfirmationDialogState.swift create mode 100644 Sources/_SwiftUINavigationState/TextState.swift create mode 100644 Tests/SwiftUINavigationTests/AlertTests.swift create mode 100644 Tests/SwiftUINavigationTests/TextStateTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69edd975cb..7007984a26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,18 @@ on: pull_request: branches: - '*' + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: library: runs-on: macos-12 strategy: matrix: - xcode: ['14.0.1'] + xcode: ['14.1'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 3e980a292d..ac5eef39b2 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,29 +1,74 @@ +# 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: ubuntu-latest - + runs-on: macos-12 steps: - - uses: actions/checkout@v2 - - name: Generate Documentation - uses: SwiftDocOrg/swift-doc@master + - 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: - base-url: /swiftui-navigation/ - format: html - inputs: Sources/SwiftUINavigation - module-name: SwiftUINavigation - output: Documentation - - name: Update Permissions - run: 'sudo chown --recursive $USER Documentation' - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@releases/v3 + 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: - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - BRANCH: gh-pages - FOLDER: Documentation + branch: gh-pages + folder: docs-out + single-commit: true diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index 60a0010990..23c142e7ac 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -1,32 +1,33 @@ +import SwiftUI import SwiftUINavigation @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) struct OptionalAlerts: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) - Button(action: { self.viewModel.numberFactButtonTapped() }) { + Stepper("Number: \(self.model.count)", value: self.$model.count) + Button(action: { self.model.numberFactButtonTapped() }) { HStack { Text("Get number fact") - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } } } - .disabled(self.viewModel.isLoading) + .disabled(self.model.isLoading) } .alert( title: { Text("Fact about \($0.number)") }, - unwrapping: self.$viewModel.fact, + unwrapping: self.$model.fact, actions: { Button("Get another fact about \($0.number)") { - self.viewModel.numberFactButtonTapped() + self.model.numberFactButtonTapped() } Button("Cancel", role: .cancel) { - self.viewModel.fact = nil + self.model.fact = nil } }, message: { Text($0.description) } @@ -35,16 +36,16 @@ struct OptionalAlerts: View { } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var isLoading = false @Published var fact: Fact? func numberFactButtonTapped() { - self.isLoading = true Task { @MainActor in + self.isLoading = true + defer { self.isLoading = false } self.fact = await getNumberFact(self.count) - self.isLoading = false } } } diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index 58e760bf56..400c2456ba 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -3,47 +3,47 @@ import SwiftUINavigation @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) struct OptionalConfirmationDialogs: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) - Button(action: { self.viewModel.numberFactButtonTapped() }) { + Stepper("Number: \(self.model.count)", value: self.$model.count) + Button(action: { self.model.numberFactButtonTapped() }) { HStack { Text("Get number fact") - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } } } - .disabled(self.viewModel.isLoading) + .disabled(self.model.isLoading) } .confirmationDialog( title: { Text("Fact about \($0.number)") }, titleVisibility: .visible, - unwrapping: self.$viewModel.fact, + unwrapping: self.$model.fact, actions: { Button("Get another fact about \($0.number)") { - self.viewModel.numberFactButtonTapped() + self.model.numberFactButtonTapped() } }, message: { Text($0.description) } ) - .navigationTitle("Confirmation dialogs") + .navigationTitle("Dialogs") } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var isLoading = false @Published var fact: Fact? func numberFactButtonTapped() { - self.isLoading = true Task { @MainActor in + self.isLoading = true + defer { self.isLoading = false } self.fact = await getNumberFact(self.count) - self.isLoading = false } } } diff --git a/Examples/CaseStudies/03-Sheets.swift b/Examples/CaseStudies/03-Sheets.swift index ddd99dacb6..87fcea317d 100644 --- a/Examples/CaseStudies/03-Sheets.swift +++ b/Examples/CaseStudies/03-Sheets.swift @@ -2,19 +2,19 @@ import SwiftUI import SwiftUINavigation struct OptionalSheets: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { Section { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) + Stepper("Number: \(self.model.count)", value: self.$model.count) HStack { Button("Get number fact") { - self.viewModel.numberFactButtonTapped() + self.model.numberFactButtonTapped() } - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } @@ -24,28 +24,28 @@ struct OptionalSheets: View { } Section { - ForEach(self.viewModel.savedFacts) { fact in + ForEach(self.model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.viewModel.removeSavedFacts(atOffsets: $0) } + .onDelete { self.model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .sheet(unwrapping: self.$viewModel.fact) { $fact in + .sheet(unwrapping: self.$model.fact) { $fact in NavigationView { FactEditor(fact: $fact.description) - .disabled(self.viewModel.isLoading) - .foregroundColor(self.viewModel.isLoading ? .gray : nil) + .disabled(self.model.isLoading) + .foregroundColor(self.model.isLoading ? .gray : nil) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.viewModel.cancelButtonTapped() + self.model.cancelButtonTapped() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.viewModel.saveButtonTapped(fact: fact) + self.model.saveButtonTapped(fact: fact) } } } @@ -63,17 +63,21 @@ private struct FactEditor: View { TextEditor(text: self.$fact) } .padding() - .navigationTitle("Fact Editor") + .navigationTitle("Fact editor") } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @Published var isLoading = false @Published var savedFacts: [Fact] = [] private var task: Task? + deinit { + self.task?.cancel() + } + func numberFactButtonTapped() { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift index d62f238d41..68497ab4ee 100644 --- a/Examples/CaseStudies/04-Popovers.swift +++ b/Examples/CaseStudies/04-Popovers.swift @@ -2,19 +2,34 @@ import SwiftUI import SwiftUINavigation struct OptionalPopovers: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { Section { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) + Stepper("Number: \(self.model.count)", value: self.$model.count) HStack { Button("Get number fact") { - self.viewModel.numberFactButtonTapped() + self.model.numberFactButtonTapped() + } + .popover(unwrapping: self.$model.fact, arrowEdge: .bottom) { $fact in + NavigationView { + FactEditor(fact: $fact.description) + .disabled(self.model.isLoading) + .foregroundColor(self.model.isLoading ? .gray : nil) + .navigationBarItems( + leading: Button("Cancel") { + self.model.cancelButtonTapped() + }, + trailing: Button("Save") { + self.model.saveButtonTapped(fact: fact) + } + ) + } } - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } @@ -24,33 +39,14 @@ struct OptionalPopovers: View { } Section { - ForEach(self.viewModel.savedFacts) { fact in + ForEach(self.model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.viewModel.removeSavedFacts(atOffsets: $0) } + .onDelete { self.model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .popover(unwrapping: self.$viewModel.fact) { $fact in - NavigationView { - FactEditor(fact: $fact.description) - .disabled(self.viewModel.isLoading) - .foregroundColor(self.viewModel.isLoading ? .gray : nil) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - self.viewModel.cancelButtonTapped() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - self.viewModel.saveButtonTapped(fact: fact) - } - } - } - } - } .navigationTitle("Popovers") } } @@ -63,17 +59,21 @@ private struct FactEditor: View { TextEditor(text: self.$fact) } .padding() - .navigationTitle("Fact Editor") + .navigationTitle("Fact editor") } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @Published var isLoading = false @Published var savedFacts: [Fact] = [] private var task: Task? + deinit { + self.task?.cancel() + } + func numberFactButtonTapped() { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift index 4028dd2f4a..e11025792c 100644 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ b/Examples/CaseStudies/05-FullScreenCovers.swift @@ -2,19 +2,19 @@ import SwiftUI import SwiftUINavigation struct OptionalFullScreenCovers: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { Section { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) + Stepper("Number: \(self.model.count)", value: self.$model.count) HStack { Button("Get number fact") { - self.viewModel.numberFactButtonTapped() + self.model.numberFactButtonTapped() } - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } @@ -24,34 +24,34 @@ struct OptionalFullScreenCovers: View { } Section { - ForEach(self.viewModel.savedFacts) { fact in + ForEach(self.model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.viewModel.removeSavedFacts(atOffsets: $0) } + .onDelete { self.model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } } - .fullScreenCover(unwrapping: self.$viewModel.fact) { $fact in + .fullScreenCover(unwrapping: self.$model.fact) { $fact in NavigationView { FactEditor(fact: $fact.description) - .disabled(self.viewModel.isLoading) - .foregroundColor(self.viewModel.isLoading ? .gray : nil) + .disabled(self.model.isLoading) + .foregroundColor(self.model.isLoading ? .gray : nil) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.viewModel.cancelButtonTapped() + self.model.cancelButtonTapped() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.viewModel.saveButtonTapped(fact: fact) + self.model.saveButtonTapped(fact: fact) } } } } } - .navigationTitle("Full-Screen Covers") + .navigationTitle("Full-screen covers") } } @@ -63,11 +63,11 @@ private struct FactEditor: View { TextEditor(text: self.$fact) } .padding() - .navigationTitle("Fact Editor") + .navigationTitle("Fact editor") } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @Published var isLoading = false diff --git a/Examples/CaseStudies/06-NavigationDestinations.swift b/Examples/CaseStudies/06-NavigationDestinations.swift new file mode 100644 index 0000000000..3582c5e9a9 --- /dev/null +++ b/Examples/CaseStudies/06-NavigationDestinations.swift @@ -0,0 +1,118 @@ +import SwiftUI +import SwiftUINavigation + +@available(iOS 16, *) +struct NavigationDestinations: View { + @ObservedObject private var model = FeatureModel() + + var body: some View { + List { + Section { + Stepper("Number: \(self.model.count)", value: self.$model.count) + + HStack { + Button("Get number fact") { + self.model.numberFactButtonTapped() + } + + if self.model.isLoading { + Spacer() + ProgressView() + } + } + } header: { + Text("Fact Finder") + } + + Section { + ForEach(self.model.savedFacts) { fact in + Text(fact.description) + } + .onDelete { self.model.removeSavedFacts(atOffsets: $0) } + } header: { + Text("Saved Facts") + } + } + .navigationTitle("Destinations") + .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") { + self.model.cancelButtonTapped() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + self.model.saveButtonTapped(fact: fact) + } + } + } + } + } +} + +private struct FactEditor: View { + @Binding var fact: String + + var body: some View { + VStack { + if #available(iOS 14, *) { + TextEditor(text: self.$fact) + } else { + TextField("Untitled", text: self.$fact) + } + } + .padding() + .navigationBarTitle("Fact Editor") + } +} + +private class FeatureModel: ObservableObject { + @Published var count = 0 + @Published var fact: Fact? + @Published var isLoading = false + @Published var savedFacts: [Fact] = [] + private var task: Task? + + deinit { + self.task?.cancel() + } + + func setFactNavigation(isActive: Bool) { + if isActive { + self.isLoading = true + self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) + self.task = Task { @MainActor in + let fact = await getNumberFact(self.count) + self.isLoading = false + try Task.checkCancellation() + self.fact = fact + } + } else { + self.task?.cancel() + self.task = nil + self.fact = nil + } + } + + func numberFactButtonTapped() { + self.setFactNavigation(isActive: true) + } + + func cancelButtonTapped() { + self.setFactNavigation(isActive: false) + } + + func saveButtonTapped(fact: Fact) { + self.savedFacts.append(fact) + self.setFactNavigation(isActive: false) + } + + func removeSavedFacts(atOffsets offsets: IndexSet) { + self.savedFacts.remove(atOffsets: offsets) + } +} diff --git a/Examples/CaseStudies/06-NavigationLinks.swift b/Examples/CaseStudies/07-NavigationLinks.swift similarity index 73% rename from Examples/CaseStudies/06-NavigationLinks.swift rename to Examples/CaseStudies/07-NavigationLinks.swift index 99dde3c084..2e1c769053 100644 --- a/Examples/CaseStudies/06-NavigationLinks.swift +++ b/Examples/CaseStudies/07-NavigationLinks.swift @@ -2,30 +2,30 @@ import SwiftUI import SwiftUINavigation struct OptionalNavigationLinks: View { - @ObservedObject private var viewModel = ViewModel() + @ObservedObject private var model = FeatureModel() var body: some View { List { Section { - Stepper("Number: \(self.viewModel.count)", value: self.$viewModel.count) + Stepper("Number: \(self.model.count)", value: self.$model.count) HStack { - NavigationLink(unwrapping: self.$viewModel.fact) { - self.viewModel.setFactNavigation(isActive: $0) + NavigationLink(unwrapping: self.$model.fact) { + self.model.setFactNavigation(isActive: $0) } destination: { $fact in FactEditor(fact: $fact.description) - .disabled(self.viewModel.isLoading) - .foregroundColor(self.viewModel.isLoading ? .gray : nil) + .disabled(self.model.isLoading) + .foregroundColor(self.model.isLoading ? .gray : nil) .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.viewModel.cancelButtonTapped() + self.model.cancelButtonTapped() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { - self.viewModel.saveButtonTapped(fact: fact) + self.model.saveButtonTapped(fact: fact) } } } @@ -33,7 +33,7 @@ struct OptionalNavigationLinks: View { Text("Get number fact") } - if self.viewModel.isLoading { + if self.model.isLoading { Spacer() ProgressView() } @@ -43,10 +43,10 @@ struct OptionalNavigationLinks: View { } Section { - ForEach(self.viewModel.savedFacts) { fact in + ForEach(self.model.savedFacts) { fact in Text(fact.description) } - .onDelete { self.viewModel.removeSavedFacts(atOffsets: $0) } + .onDelete { self.model.removeSavedFacts(atOffsets: $0) } } header: { Text("Saved Facts") } @@ -63,17 +63,21 @@ private struct FactEditor: View { TextEditor(text: self.$fact) } .padding() - .navigationTitle("Fact Editor") + .navigationTitle("Fact editor") } } -private class ViewModel: ObservableObject { +private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @Published var isLoading = false @Published var savedFacts: [Fact] = [] private var task: Task? + deinit { + self.task?.cancel() + } + func setFactNavigation(isActive: Bool) { if isActive { self.isLoading = true diff --git a/Examples/CaseStudies/07-NavigationLinkList.swift b/Examples/CaseStudies/08-NavigationLinkList.swift similarity index 60% rename from Examples/CaseStudies/07-NavigationLinkList.swift rename to Examples/CaseStudies/08-NavigationLinkList.swift index 4932faf152..170b1fe2b3 100644 --- a/Examples/CaseStudies/07-NavigationLinkList.swift +++ b/Examples/CaseStudies/08-NavigationLinkList.swift @@ -5,12 +5,12 @@ 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 Route enum, and it uses the \ - library's NavigationLink initializer to drive navigation from the route enum. + 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 viewModel: ListOfNavigationLinksViewModel + @ObservedObject var model: ListOfNavigationLinksModel var body: some View { Form { @@ -19,27 +19,27 @@ struct ListOfNavigationLinks: View { } List { - ForEach(self.viewModel.rows) { rowViewModel in - RowView(viewModel: rowViewModel) + ForEach(self.model.rows) { rowModel in + RowView(model: rowModel) } - .onDelete(perform: self.viewModel.deleteButtonTapped(indexSet:)) + .onDelete(perform: self.model.deleteButtonTapped(indexSet:)) } } - .navigationTitle("List of Links") + .navigationTitle("List of links") .toolbar { ToolbarItem { Button("Add") { - self.viewModel.addButtonTapped() + self.model.addButtonTapped() } } } } } -class ListOfNavigationLinksViewModel: ObservableObject { - @Published var rows: [ListOfNavigationLinksRowViewModel] +class ListOfNavigationLinksModel: ObservableObject { + @Published var rows: [ListOfNavigationLinksRowModel] - init(rows: [ListOfNavigationLinksRowViewModel] = []) { + init(rows: [ListOfNavigationLinksRowModel] = []) { self.rows = rows } @@ -55,59 +55,59 @@ class ListOfNavigationLinksViewModel: ObservableObject { } private struct RowView: View { - @ObservedObject var viewModel: ListOfNavigationLinksRowViewModel + @ObservedObject var model: ListOfNavigationLinksRowModel var body: some View { NavigationLink( - unwrapping: self.$viewModel.route, - case: /ListOfNavigationLinksRowViewModel.Route.edit - ) { - self.viewModel.setEditNavigation(isActive: $0) + 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.viewModel.saveButtonTapped(counter: counter) } + Button("Save") { self.model.saveButtonTapped(counter: counter) } } ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { self.viewModel.cancelButtonTapped() } + Button("Cancel") { self.model.cancelButtonTapped() } } } } label: { - Text("\(self.viewModel.counter)") + Text("\(self.model.counter)") } } } -class ListOfNavigationLinksRowViewModel: Identifiable, ObservableObject { +class ListOfNavigationLinksRowModel: Identifiable, ObservableObject { let id = UUID() @Published var counter: Int - @Published var route: Route? + @Published var destination: Destination? - enum Route { + enum Destination { case edit(Int) } init( counter: Int = 0, - route: Route? = nil + destination: Destination? = nil ) { self.counter = counter - self.route = route + self.destination = destination } func setEditNavigation(isActive: Bool) { - self.route = isActive ? .edit(self.counter) : nil + self.destination = isActive ? .edit(self.counter) : nil } func saveButtonTapped(counter: Int) { self.counter = counter - self.route = nil + self.destination = nil } func cancelButtonTapped() { - self.route = nil + self.destination = nil } } @@ -131,7 +131,7 @@ struct ListOfNavigationLinks_Previews: PreviewProvider { static var previews: some View { NavigationView { ListOfNavigationLinks( - viewModel: .init( + model: .init( rows: [ .init(counter: 0), .init(counter: 0), diff --git a/Examples/CaseStudies/08-Routing.swift b/Examples/CaseStudies/08-Routing.swift deleted file mode 100644 index a6b6ed178a..0000000000 --- a/Examples/CaseStudies/08-Routing.swift +++ /dev/null @@ -1,82 +0,0 @@ -import SwiftUINavigation - -private let readMe = """ - This case study demonstrates how to power multiple forms of navigation from a single route enum \ - that describes all of the possible destinations one can travel to from this screen. - - The screen has three navigation destinations: an alert, 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. - """ - -enum Route { - case alert(String) - case link(Int) - case sheet(Int) -} - -struct Routing: View { - @State var route: Route? - - var body: some View { - Form { - Section { - Text(readMe) - } - - Button("Alert") { - self.route = .alert("Hello world!") - } - .alert( - title: { Text($0) }, - unwrapping: self.$route, - case: /Route.alert, - actions: { _ in - Button("Activate link") { - self.route = .link(0) - } - Button("Activate sheet") { - self.route = .sheet(0) - } - Button("Cancel", role: .cancel) { - } - }, - message: { _ in - - } - ) - - NavigationLink(unwrapping: self.$route, case: /Route.link) { - self.route = $0 ? .link(0) : nil - } destination: { $count in - Form { - Stepper("Number: \(count)", value: $count) - } - } label: { - Text("Link") - } - - Button("Sheet") { - self.route = .sheet(0) - } - .sheet( - unwrapping: self.$route, - case: /Route.sheet - ) { $count in - Form { - Stepper("Number: \(count)", value: $count) - } - } - } - .navigationTitle("Routing") - } -} - -struct Routing_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - Routing() - } - } -} diff --git a/Examples/CaseStudies/09-Routing.swift b/Examples/CaseStudies/09-Routing.swift new file mode 100644 index 0000000000..4140c1b1cb --- /dev/null +++ b/Examples/CaseStudies/09-Routing.swift @@ -0,0 +1,128 @@ +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 three navigation destinations: an alert, 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. + """ + +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: \(self.count)") + } + + Button("Alert") { + self.destination = .alert( + AlertState { + TextState("Update count?") + } actions: { + ButtonState(action: .send(.randomize)) { + TextState("Randomize") + } + ButtonState(role: .destructive, action: .send(.reset)) { + TextState("Reset") + } + } + ) + } + + Button("Confirmation dialog") { + self.destination = .confirmationDialog( + ConfirmationDialogState(titleVisibility: .visible) { + TextState("Update count?") + } actions: { + ButtonState(action: .send(.increment)) { + TextState("Increment") + } + ButtonState(action: .send(.decrement)) { + TextState("Decrement") + } + } + ) + } + + NavigationLink(unwrapping: self.$destination, case: /Destination.link) { isActive in + if isActive { + self.destination = .link(self.count) + } + } destination: { $count in + Form { + Stepper("Number: \(count)", value: $count) + } + .navigationTitle("Routing link") + } label: { + Text("Link") + } + + Button("Sheet") { + self.destination = .sheet(self.count) + } + } + .navigationTitle("Routing") + .alert(unwrapping: self.$destination, case: /Destination.alert) { action in + switch action { + case .randomize: + self.count = .random(in: 0...1_000) + case .reset: + self.count = 0 + } + } + .confirmationDialog( + unwrapping: self.$destination, + case: /Destination.confirmationDialog + ) { action in + switch action { + case .decrement: + self.count -= 1 + case .increment: + self.count += 1 + } + } + .sheet(unwrapping: self.$destination, case: /Destination.sheet) { $count in + NavigationView { + Form { + Stepper("Number: \(count)", value: $count) + } + .navigationTitle("Routing sheet") + } + } + } +} + +struct Routing_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + Routing() + } + } +} diff --git a/Examples/CaseStudies/09-CustomComponents.swift b/Examples/CaseStudies/10-CustomComponents.swift similarity index 89% rename from Examples/CaseStudies/09-CustomComponents.swift rename to Examples/CaseStudies/10-CustomComponents.swift index 029806d322..f537cf4f39 100644 --- a/Examples/CaseStudies/09-CustomComponents.swift +++ b/Examples/CaseStudies/10-CustomComponents.swift @@ -1,16 +1,17 @@ +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. + 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. + 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. + bottom menu component with additional APIs that allow presentation and dismissal to be powered \ + by optionals and enums. """ struct CustomComponents: View { diff --git a/Examples/CaseStudies/11-SynchronizedBindings.swift b/Examples/CaseStudies/11-SynchronizedBindings.swift new file mode 100644 index 0000000000..80f718c5e5 --- /dev/null +++ b/Examples/CaseStudies/11-SynchronizedBindings.swift @@ -0,0 +1,65 @@ +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? + @ObservedObject private var model = FeatureModel() + + var body: some View { + Form { + Section { + Text(readMe) + } + + Section { + TextField("Username", text: self.$model.username) + .focused(self.$focusedField, equals: .username) + + SecureField("Password", text: self.$model.password) + .focused(self.$focusedField, equals: .password) + + Button("Sign In") { + self.model.signInButtonTapped() + } + .buttonStyle(.borderedProminent) + } + .textFieldStyle(.roundedBorder) + } + .bind(self.$model.focusedField, to: self.$focusedField) + .navigationTitle("Synchronized focus") + } +} + +private class FeatureModel: ObservableObject { + enum Field: String { + case username + case password + } + + @Published var focusedField: Field? = .username + @Published var password: String = "" + @Published var username: String = "" + + func signInButtonTapped() { + if self.username.isEmpty { + self.focusedField = .username + } else if self.password.isEmpty { + self.focusedField = .password + } else { + self.focusedField = nil + } + } +} + +struct SynchronizedBindings_Previews: PreviewProvider { + static var previews: some View { + SynchronizedBindings() + } +} diff --git a/Examples/CaseStudies/12-IfLet.swift b/Examples/CaseStudies/12-IfLet.swift new file mode 100644 index 0000000000..0798ecd17b --- /dev/null +++ b/Examples/CaseStudies/12-IfLet.swift @@ -0,0 +1,47 @@ +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. + + 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) + } + IfLet(self.$editableString) { $string in + TextField("Edit string", text: $string) + HStack { + Button("Discard") { + self.editableString = nil + } + Button("Save") { + self.string = string + self.editableString = nil + } + } + } else: { + Text("\(self.string)") + Button("Edit") { + self.editableString = self.string + } + } + .buttonStyle(.borderless) + } + } +} + +struct IfLetCaseStudy_EditStringView_Previews: PreviewProvider { + static var previews: some View { + IfLetCaseStudy() + } +} diff --git a/Examples/CaseStudies/13-IfCaseLet.swift b/Examples/CaseStudies/13-IfCaseLet.swift new file mode 100644 index 0000000000..84a0ef6150 --- /dev/null +++ b/Examples/CaseStudies/13-IfCaseLet.swift @@ -0,0 +1,53 @@ +import CasePaths +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. + + 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 + + enum EditableString { + case active(String) + case inactive + } + + var body: some View { + Form { + 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 + } + } + } else: { + Text("\(self.string)") + Button("Edit") { + self.editableString = .active(self.string) + } + } + .buttonStyle(.borderless) + } + } +} + +struct IfCaseLetCaseStudy_EditStringView_Previews: PreviewProvider { + static var previews: some View { + IfCaseLetCaseStudy() + } +} diff --git a/Examples/CaseStudies/RootView.swift b/Examples/CaseStudies/RootView.swift index 4e1d1c8158..5dec7adea1 100644 --- a/Examples/CaseStudies/RootView.swift +++ b/Examples/CaseStudies/RootView.swift @@ -1,20 +1,19 @@ +import SwiftUI import SwiftUINavigation struct RootView: View { var body: some View { NavigationView { List { - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - Section { - NavigationLink("Optional-driven alerts") { - OptionalAlerts() - } - NavigationLink("Optional confirmation dialogs") { - OptionalConfirmationDialogs() - } - } header: { - Text("Alerts and confirmation dialogs") + Section { + NavigationLink("Optional-driven alerts") { + OptionalAlerts() } + NavigationLink("Optional confirmation dialogs") { + OptionalConfirmationDialogs() + } + } header: { + Text("Alerts and confirmation dialogs") } Section { @@ -32,11 +31,17 @@ struct RootView: View { } Section { + NavigationLink("Optional destinations") { + NavigationStack { + NavigationDestinations() + } + .navigationTitle("Navigation stack") + } NavigationLink("Optional navigation links") { OptionalNavigationLinks() } NavigationLink("List of navigation links") { - ListOfNavigationLinks(viewModel: .init()) + ListOfNavigationLinks(model: ListOfNavigationLinksModel()) } } header: { Text("Navigation links") @@ -49,6 +54,15 @@ struct RootView: View { NavigationLink("Custom components") { CustomComponents() } + NavigationLink("Synchronized bindings") { + SynchronizedBindings() + } + NavigationLink("IfLet view") { + IfLetCaseStudy() + } + NavigationLink("IfCaseLet view") { + IfCaseLetCaseStudy() + } } header: { Text("Advanced") } diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 6bce4ed8d7..66257644b8 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -22,12 +22,16 @@ 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 /* 09-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */; }; - CA70FED7274B1907005A0D53 /* 07-NavigationLinkList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */; }; - CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */; }; + CA47383E272F0F9B0012CAC3 /* 10-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 10-CustomComponents.swift */; }; + 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 */; }; + 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 */; }; + 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 */; }; 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 /* 06-NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */; }; + DCD4E68B274180F500CDF3BD /* 07-NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E68A274180F500CDF3BD /* 07-NavigationLinks.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -47,12 +51,16 @@ 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 /* 09-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "09-CustomComponents.swift"; sourceTree = ""; }; - CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "07-NavigationLinkList.swift"; sourceTree = ""; }; - CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-Routing.swift"; sourceTree = ""; }; + CA47383D272F0F9B0012CAC3 /* 10-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "10-CustomComponents.swift"; 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 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 /* 06-NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "06-NavigationLinks.swift"; sourceTree = ""; }; + DCD4E68A274180F500CDF3BD /* 07-NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "07-NavigationLinks.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -124,10 +132,14 @@ CA473832272F0D860012CAC3 /* 03-Sheets.swift */, DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */, DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */, - DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */, - CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */, - CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */, - CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.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 */, CA473830272F0D860012CAC3 /* CaseStudiesApp.swift */, CA473831272F0D860012CAC3 /* FactClient.swift */, CA47382E272F0D860012CAC3 /* RootView.swift */, @@ -257,17 +269,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */, + CABE9FC1272F2C0000AFC150 /* 09-Routing.swift in Sources */, + DC6A8411291F227400B3F6C9 /* 11-SynchronizedBindings.swift in Sources */, CA473837272F0D860012CAC3 /* FactClient.swift in Sources */, CA473835272F0D860012CAC3 /* 02-ConfirmationDialogs.swift in Sources */, - CA47383E272F0F9B0012CAC3 /* 09-CustomComponents.swift in Sources */, - CA70FED7274B1907005A0D53 /* 07-NavigationLinkList.swift in Sources */, + CA47383E272F0F9B0012CAC3 /* 10-CustomComponents.swift in Sources */, + CA70FED7274B1907005A0D53 /* 08-NavigationLinkList.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 /* 06-NavigationLinks.swift in Sources */, + DCD4E68B274180F500CDF3BD /* 07-NavigationLinks.swift in Sources */, + CAAC0072292BDE660083F2FF /* 12-IfLet.swift in Sources */, + DC609AD6291D76150052647F /* 06-NavigationDestinations.swift in Sources */, + CA93236B292BE733004B1130 /* 13-IfCaseLet.swift in Sources */, CA473838272F0D860012CAC3 /* 03-Sheets.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -406,6 +422,7 @@ 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", @@ -435,6 +452,7 @@ 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", @@ -464,6 +482,7 @@ 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", @@ -494,6 +513,7 @@ 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", diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 70ae16beb2..84d7697dcc 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/swift-case-paths", "state": { "branch": null, - "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", - "version": "0.7.0" + "revision": "bb436421f57269fbcfe7360735985321585a86e5", + "version": "0.10.1" } }, { @@ -19,6 +19,24 @@ "version": "1.0.1" } }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "819d9d370cd721c9d87671e29d947279292e4541", + "version": "0.6.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", @@ -27,6 +45,15 @@ "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", "version": "0.3.2" } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version": "0.5.0" + } } ] }, diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme new file mode 100644 index 0000000000..4053f97674 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Inventory.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Inventory/App.swift b/Examples/Inventory/App.swift index bfa93e06d3..6efbbb2e99 100644 --- a/Examples/Inventory/App.swift +++ b/Examples/Inventory/App.swift @@ -1,47 +1,72 @@ import SwiftUI -class AppViewModel: ObservableObject { - @Published var inventoryViewModel: InventoryViewModel +@main +struct InventoryApp: App { + let model = AppModel( + inventoryModel: InventoryModel( + inventory: [ + ItemRowModel( + item: Item(color: .red, name: "Keyboard", status: .inStock(quantity: 100)) + ), + ItemRowModel( + item: Item(color: .blue, name: "Mouse", status: .inStock(quantity: 200)) + ), + ItemRowModel( + item: Item(color: .green, name: "Monitor", status: .inStock(quantity: 20)) + ), + ItemRowModel( + item: Item(color: .yellow, name: "Chair", status: .outOfStock(isOnBackOrder: true)) + ), + ] + ) + ) + + var body: some Scene { + WindowGroup { + AppView(model: self.model) + } + } +} + +class AppModel: ObservableObject { + @Published var inventoryModel: InventoryModel @Published var selectedTab: Tab init( - inventoryViewModel: InventoryViewModel = .init(), - selectedTab: Tab = .inventory + inventoryModel: InventoryModel, + selectedTab: Tab = .first ) { - self.inventoryViewModel = inventoryViewModel + self.inventoryModel = inventoryModel self.selectedTab = selectedTab } enum Tab { + case first case inventory } } -@main -struct InventoryApp: App { - @ObservedObject var viewModel = AppViewModel( - inventoryViewModel: InventoryViewModel( - inventory: [], - route: .add( - .init( - name: "Keyboard", - color: .blue, - status: .outOfStock(isOnBackOrder: true) - ) - ) - ) - ) +struct AppView: View { + @ObservedObject var model: AppModel - var body: some Scene { - WindowGroup { - TabView(selection: self.$viewModel.selectedTab) { - NavigationView { - InventoryView(viewModel: self.viewModel.inventoryViewModel) - .tag(AppViewModel.Tab.inventory) - .tabItem { - Label("Inventory", systemImage: "building.2") - } - } + var body: some View { + TabView(selection: self.$model.selectedTab) { + Button { + self.model.selectedTab = .inventory + } label: { + Text("Go to inventory tab") + } + .tag(AppModel.Tab.first) + .tabItem { + Label("First", systemImage: "arrow.forward") + } + + NavigationStack { + InventoryView(model: self.model.inventoryModel) + } + .tag(AppModel.Tab.inventory) + .tabItem { + Label("Inventory", systemImage: "list.clipboard.fill") } } } diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index c24f2a929c..4c121ef142 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -1,25 +1,25 @@ import IdentifiedCollections +import SwiftUI import SwiftUINavigation -class InventoryViewModel: ObservableObject { - @Published var inventory: IdentifiedArrayOf - @Published var route: Route? +class InventoryModel: ObservableObject { + @Published var inventory: IdentifiedArrayOf { + didSet { self.bind() } + } + @Published var destination: Destination? - enum Route: Equatable { + enum Destination: Equatable { case add(Item) - case row(id: ItemRowViewModel.ID, route: ItemRowViewModel.Route) + case edit(Item) } init( - inventory: IdentifiedArrayOf = [], - route: Route? = nil + inventory: IdentifiedArrayOf = [], + destination: Destination? = nil ) { - self.inventory = [] - self.route = route - - for itemRowViewModel in inventory { - self.bind(itemRowViewModel: itemRowViewModel) - } + self.inventory = inventory + self.destination = destination + self.bind() } func delete(item: Item) { @@ -30,88 +30,99 @@ class InventoryViewModel: ObservableObject { func add(item: Item) { withAnimation { - self.bind(itemRowViewModel: .init(item: item)) - self.route = nil + self.inventory.append(ItemRowModel(item: item)) + self.destination = nil } } func addButtonTapped() { - self.route = .add(.init(name: "", color: nil, status: .inStock(quantity: 1))) - - Task { @MainActor in - try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) - try (/Route.add).modify(&self.route) { - $0.name = "Bluetooth Keyboard" - } - } + self.destination = .add(Item(color: nil, name: "", status: .inStock(quantity: 1))) } func cancelButtonTapped() { - self.route = nil + self.destination = nil } - private func bind(itemRowViewModel: ItemRowViewModel) { - itemRowViewModel.onDelete = { [weak self, item = itemRowViewModel.item] in - withAnimation { - self?.delete(item: item) - } - } + func cancelEditButtonTapped() { + self.destination = nil + } - itemRowViewModel.onDuplicate = { [weak self] item in - withAnimation { - self?.add(item: item) - } - } + func commitEdit(item: Item) { + self.inventory[id: item.id]?.item = item + self.destination = nil + } - itemRowViewModel.$route - .map { [id = itemRowViewModel.id] route in - route.map { Route.row(id: id, route: $0) } + private func bind() { + for itemRowModel in self.inventory { + itemRowModel.onDelete = { [weak self, weak itemRowModel] in + guard let self, let itemRowModel else { return } + withAnimation { + self.delete(item: itemRowModel.item) + } } - .removeDuplicates() - .dropFirst() - .assign(to: &self.$route) - - self.$route - .map { [id = itemRowViewModel.id] route in - guard - case let .row(id: routeRowId, route: route) = route, - routeRowId == id - else { return nil } - return route + itemRowModel.onDuplicate = { [weak self] item in + guard let self else { return } + withAnimation { + self.add(item: item) + } } - .removeDuplicates() - .assign(to: &itemRowViewModel.$route) - - self.inventory.append(itemRowViewModel) + itemRowModel.onTap = { [weak self, weak itemRowModel] in + guard let self, let itemRowModel else { return } + self.destination = .edit(itemRowModel.item) + } + } } } struct InventoryView: View { - @ObservedObject var viewModel: InventoryViewModel + @ObservedObject var model: InventoryModel var body: some View { List { ForEach( - self.viewModel.inventory, - content: ItemRowView.init(viewModel:) + self.model.inventory, + content: ItemRowView.init(model:) ) } .toolbar { ToolbarItem(placement: .primaryAction) { - Button("Add") { self.viewModel.addButtonTapped() } + Button("Add") { self.model.addButtonTapped() } } } .navigationTitle("Inventory") - .sheet(unwrapping: self.$viewModel.route, case: /InventoryViewModel.Route.add) { $itemToAdd in - NavigationView { + .navigationDestination( + unwrapping: self.$model.destination, + case: /InventoryModel.Destination.edit + ) { $item in + ItemView(item: $item) + .navigationBarTitle("Edit") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.model.cancelEditButtonTapped() + } + } + ToolbarItem(placement: .primaryAction) { + Button("Save") { + self.model.commitEdit(item: item) + } + } + } + } + .sheet( + unwrapping: self.$model.destination, + case: /InventoryModel.Destination.add + ) { $itemToAdd in + NavigationStack { ItemView(item: $itemToAdd) .navigationTitle("Add") .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { self.viewModel.cancelButtonTapped() } + Button("Cancel") { self.model.cancelButtonTapped() } } ToolbarItem(placement: .primaryAction) { - Button("Save") { self.viewModel.add(item: itemToAdd) } + Button("Save") { self.model.add(item: itemToAdd) } } } } @@ -121,21 +132,27 @@ struct InventoryView: View { struct InventoryView_Previews: PreviewProvider { static var previews: some View { - let keyboard = Item(name: "Keyboard", color: .blue, status: .inStock(quantity: 100)) + let keyboard = Item(color: .blue, name: "Keyboard", status: .inStock(quantity: 100)) - NavigationView { + NavigationStack { InventoryView( - viewModel: .init( + model: InventoryModel( inventory: [ - .init(item: keyboard), - .init(item: Item(name: "Charger", color: .yellow, status: .inStock(quantity: 20))), - .init( - item: Item(name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true))), - .init( + 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( - name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false))), - ], - route: nil + color: .green, name: "Headphones", status: .outOfStock(isOnBackOrder: false) + ) + ), + ] ) ) } diff --git a/Examples/Inventory/Item.swift b/Examples/Inventory/Item.swift index 301ea260fc..640e9ec8b0 100644 --- a/Examples/Inventory/Item.swift +++ b/Examples/Inventory/Item.swift @@ -1,9 +1,10 @@ +import SwiftUI import SwiftUINavigation struct Item: Equatable, Identifiable { let id = UUID() - var name: String var color: Color? + var name: String var status: Status enum Status: Equatable { @@ -39,7 +40,7 @@ struct Item: Equatable, Identifiable { static let white = Self(name: "White", red: 1, green: 1, blue: 1) var swiftUIColor: SwiftUI.Color { - .init(red: self.red, green: self.green, blue: self.blue) + SwiftUI.Color(red: self.red, green: self.green, blue: self.blue) } } } @@ -90,10 +91,10 @@ struct ItemView: View { } struct ItemView_Previews: PreviewProvider, View { - @State var item = Item(name: "", color: nil, status: .inStock(quantity: 1)) + @State var item = Item(color: nil, name: "", status: .inStock(quantity: 1)) static var previews: some View { - NavigationView { + NavigationStack { ItemView_Previews() } } diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index 1a3f2f87a9..fdbde13c60 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -1,91 +1,88 @@ +import SwiftUI import SwiftUINavigation +import XCTestDynamicOverlay -class ItemRowViewModel: Identifiable, ObservableObject { +class ItemRowModel: Identifiable, ObservableObject { @Published var item: Item - @Published var route: Route? + @Published var destination: Destination? - enum Route: Equatable { - case deleteAlert + enum Destination: Equatable { + case alert(AlertState) case duplicate(Item) - case edit(Item) } - var onDelete: () -> Void = {} - var onDuplicate: (Item) -> Void = { _ in } + enum AlertAction { + case deleteConfirmation + } + + var onDelete: () -> Void = unimplemented("ItemRowModel.onDelete") + var onDuplicate: (Item) -> Void = unimplemented("ItemRowModel.onDuplicate") + var onTap: () -> Void = unimplemented("ItemRowModel.onTap") var id: Item.ID { self.item.id } - init( - item: Item - ) { + init(item: Item) { self.item = item } func deleteButtonTapped() { - self.route = .deleteAlert - } - - func deleteConfirmationButtonTapped() { - self.onDelete() - } - - func setEditNavigation(isActive: Bool) { - self.route = isActive ? .edit(self.item) : nil + self.destination = .alert( + AlertState { + TextState(self.item.name) + } actions: { + ButtonState(role: .destructive, action: .send(.deleteConfirmation, animation: .default)) { + TextState("Delete") + } + } message: { + TextState("Are you sure you want to delete this item?") + } + ) } - func edit(item: Item) { - self.item = item - self.route = nil + func alertButtonTapped(_ action: AlertAction) { + switch action { + case .deleteConfirmation: + self.onDelete() + } } func cancelButtonTapped() { - self.route = nil + self.destination = nil } func duplicateButtonTapped() { - self.route = .duplicate(self.item.duplicate()) + self.destination = .duplicate(self.item.duplicate()) } func duplicate(item: Item) { self.onDuplicate(item) - self.route = nil + self.destination = nil + } + + func rowTapped() { + self.onTap() } } extension Item { func duplicate() -> Self { - .init(name: self.name, color: self.color, status: self.status) + Self(color: self.color, name: self.name, status: self.status) } } struct ItemRowView: View { - @ObservedObject var viewModel: ItemRowViewModel + @ObservedObject var model: ItemRowModel var body: some View { - NavigationLink(unwrapping: self.$viewModel.route, case: /ItemRowViewModel.Route.edit) { - self.viewModel.setEditNavigation(isActive: $0) - } destination: { $item in - ItemView(item: $item) - .navigationBarTitle("Edit") - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - self.viewModel.cancelButtonTapped() - } - } - ToolbarItem(placement: .primaryAction) { - Button("Save") { - self.viewModel.edit(item: item) - } - } - } + Button { + self.model.rowTapped() } label: { HStack { VStack(alignment: .leading) { - Text(self.viewModel.item.name) + Text(self.model.item.name) + .font(.title3) - switch self.viewModel.item.status { + switch self.model.item.status { case let .inStock(quantity): Text("In stock: \(quantity)") case let .outOfStock(isOnBackOrder): @@ -95,54 +92,46 @@ struct ItemRowView: View { Spacer() - if let color = self.viewModel.item.color { + if let color = self.model.item.color { Rectangle() .frame(width: 30, height: 30) .foregroundColor(color.swiftUIColor) .border(Color.black, width: 1) } - Button(action: { self.viewModel.duplicateButtonTapped() }) { + Button(action: { self.model.duplicateButtonTapped() }) { Image(systemName: "square.fill.on.square.fill") } .padding(.leading) - Button(action: { self.viewModel.deleteButtonTapped() }) { + Button(action: { self.model.deleteButtonTapped() }) { Image(systemName: "trash.fill") } .padding(.leading) } .buttonStyle(.plain) - .foregroundColor(self.viewModel.item.status.isInStock ? nil : Color.gray) + .foregroundColor(self.model.item.status.isInStock ? nil : Color.gray) .alert( - title: { Text(self.viewModel.item.name) }, - unwrapping: self.$viewModel.route, - case: /ItemRowViewModel.Route.deleteAlert, - actions: { - Button("Delete", role: .destructive) { - self.viewModel.deleteConfirmationButtonTapped() - } - }, - message: { - Text("Are you sure you want to delete this item?") - } + unwrapping: self.$model.destination, + case: /ItemRowModel.Destination.alert, + action: self.model.alertButtonTapped ) .popover( - unwrapping: self.$viewModel.route, - case: /ItemRowViewModel.Route.duplicate + unwrapping: self.$model.destination, + case: /ItemRowModel.Destination.duplicate ) { $item in - NavigationView { + NavigationStack { ItemView(item: $item) .navigationBarTitle("Duplicate") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - self.viewModel.cancelButtonTapped() + self.model.cancelButtonTapped() } } ToolbarItem(placement: .primaryAction) { Button("Add") { - self.viewModel.duplicate(item: item) + self.model.duplicate(item: item) } } } diff --git a/Package.resolved b/Package.resolved index 3c14d1d575..1849640f68 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,24 @@ "version": "0.10.0" } }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "819d9d370cd721c9d87671e29d947279292e4541", + "version": "0.6.0" + } + }, + { + "package": "SwiftDocCPlugin", + "repositoryURL": "https://github.com/apple/swift-docc-plugin", + "state": { + "branch": null, + "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "version": "1.0.0" + } + }, { "package": "xctest-dynamic-overlay", "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", diff --git a/Package.swift b/Package.swift index d7d7eb86c5..5f570461a0 100644 --- a/Package.swift +++ b/Package.swift @@ -17,14 +17,25 @@ 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.10.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.5.0"), ], targets: [ + .target( + name: "_SwiftUINavigationState", + dependencies: [ + .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ] + ), .target( name: "SwiftUINavigation", dependencies: [ + "_SwiftUINavigationState", .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), diff --git a/README.md b/README.md index 8e9e6b2e3c..5d60eea1a6 100644 --- a/README.md +++ b/README.md @@ -6,302 +6,64 @@ Tools for making SwiftUI navigation simpler, more ergonomic and more precise. - * [Motivation](#motivation) - * [Tools](#tools) - * [Navigation overloads](#navigation-api-overloads) - * [Navigation views](#navigation-views) - * [Binding transformations](#binding-transformations) + * [Overview](#overview) * [Examples](#examples) * [Learn more](#learn-more) * [Installation](#installation) * [Documentation](#documentation) * [License](#license) -## Motivation - -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 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. - - [NavigationLink.init]: https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s - [TabView.init]: https://developer.apple.com/documentation/swiftui/tabview/init(content:) - - * "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 complicated, but unfortunately SwiftUI does not ship with all the tools necessary to model our domains as concisely as possible and use these navigation APIs. - -For example, to show a modal sheet in SwiftUI you can provide a binding of some optional state so that when the state flips to non-`nil` the modal is presented. However, the content closure of the sheet is handed a plain value, not a binding: - -```swift -struct ContentView: View { - @State var draft: Post? - - var body: some View { - Button("Edit") { - self.draft = Post() - } - .sheet(item: self.$draft) { (draft: Post) in - EditPostView(post: draft) - } - } -} - -struct EditPostView: View { - let post: Post - var body: some View { ... } -} -``` - -This means that the `Post` handed to the `EditPostView` is fully disconnected from the source of truth `draft` that powers the presentation of the modal. Ideally we should be able to derive a `Binding` for the draft so that any mutations `EditPostView` makes will be instantly visible in `ContentView`. - -Another problem arises when trying to model multiple navigation destinations as multiple optional values. For example, suppose there are 3 different sheets that can be shown in a screen: - -```swift -struct ContentView: View { - @State var draft: Post? - @State var settings: Settings? - @State var userProfile: UserProfile? - - var body: some View { - /* Main view omitted */ - - .sheet(item: self.$draft) { (draft: Post) in - EditPostView(post: draft) - } - .sheet(item: self.$settings) { (settings: Settings) in - SettingsView(settings: settings) - } - .sheet(item: self.$userProfile) { (userProfile: Profile) in - UserProfile(profile: userProfile) - } - } -} -``` - -This forces us to hold 3 optional values in state, which has 2^3=8 different states, 4 of which are invalid. The only valid state is for all values to be `nil` or exactly one be non-`nil`. It makes no sense if two or more values are non-`nil`, for that would represent wanting to show two modal sheets at the same time. - -Ideally we'd like to represent these navigation destinations as 3 mutually exclusive states so that we could guarantee at compile time that only one can be active at a time. Luckily for us Swift’s enums are perfect for this: - -```swift -enum Route { - case draft(Post) - case settings(Settings) - case userProfile(Profile) -} -``` - -And then we could hold an optional `Route` in state to represent that we are either navigating to a specific destination or we are not navigating anywhere: - -```swift -@State var route: Route? -``` - -This would be the most optimal way to model our navigation domain, but unfortunately SwiftUI's tools do not make it easy for us to drive navigation off of enums. - -This library comes with a number of `Binding` transformations and navigation API overloads that allow you to model your domain as concisely as possible, using enums, while still allowing you to use SwiftUI's navigation tools. - -For example, powering multiple modal sheets off a single `Route` enum looks like this with the tools in this library: - -```swift -struct ContentView { - @State var route: Route? - - enum Route { - case draft(Post) - case settings(Settings) - case userProfile(Profile) - } - - var body: some View { - /* Main view omitted */ - - .sheet(unwrapping: self.$route, case: /Route.draft) { $draft in - EditPostView(post: $draft) - } - .sheet(unwrapping: self.$route, case: /Route.settings) { $settings in - SettingsView(settings: $settings) - } - .sheet(unwrapping: self.$route, case: /Route.userProfile) { $userProfile in - UserProfile(profile: $userProfile) - } - } -} -``` - -The forward-slash syntax you see above represents a [case path](https://github.com/pointfreeco/swift-case-paths) to a particular case of an enum. Case paths are our imagining of what key paths could look like for enums, and every concept for key paths has an analogous concept for case paths: - - * Each property of a struct is naturally endowed with a key path, and so each case of an enum is endowed with a case path. - * Key paths are constructed using a back slash, name of the type and name of the property (_e.g._, `\User.name`), and case paths are constructed similarly, but with a forward slash (_e.g._, `/Route.draft`). - * Key paths describe how to get and set a value in some root structure, whereas case paths describe how to extract and embed a value into a root structure. - -Case paths are crucial for allowing us to build the tools to drive navigation off of enum state. - -## Tools - -This library comes with many tools that allow you to model your domain as concisely as possible, using enums, while still allowing you to use SwiftUI's navigation APIs. - -### Navigation API overloads - -This library provides additional overloads for all of SwiftUI's "state-driven" navigation APIs that allow you to activate navigation based on a particular case of an enum. Further, all overloads unify presentation in a single, consistent API: - - * `NavigationLink.init(unwrapping:case:)` - * `View.alert(unwrapping:case:)` - * `View.confirmationDialog(unwrapping:case:)` - * `View.fullScreenCover(unwrapping:case:)` - * `View.popover(unwrapping:case:)` - * `View.sheet(unwrapping:case:)` - -For example, here is how a navigation link, a modal sheet and an alert can all be driven off a single enum with 3 cases: - -```swift -enum Route { - case add(Post) - case alert(Alert) - case edit(Post) -} - -struct ContentView { - @State var posts: [Post] - @State var route: Route? - - var body: some View { - ForEach(self.posts) { post in - NavigationLink(unwrapping: self.$route, case: /Route.edit) { isActive in - self.route = isActive ? .edit(post) : nil - } destination: { $post in - EditPostView(post: $post) - } label: { - Text(post.title) - } - } - .sheet(unwrapping: self.$route, case: /Route.add) { $post in - EditPostView(post: $post) - } - .alert( - title: { Text("Delete \($0.title)?") }, - unwrapping: self.$route, - case: /Route.alert - actions: { post in - Button("Delete") { self.posts.remove(post) } - }, - message: { Text($0.summary) } - ) - } -} - -struct EditPostView: View { - @Binding var post: Post - var body: some View { ... } -} -``` - -### Navigation views - -This library comes with additional SwiftUI views that transform and destructure bindings, allowing you to better handle optional and enum state: - - * `IfLet` - * `IfCaseLet` - * `Switch`/`CaseLet` - -For example, suppose you were working on an inventory application that modeled in-stock and out-of-stock as an enum: - -```swift -enum ItemStatus { - case inStock(quantity: Int) - case outOfStock(isOnBackorder: Bool) -} -``` - -If you want to conditionally show a stepper view for the quantity when in-stock and a toggle for the backorder when out-of-stock, you're out of luck when it comes to using SwiftUI's standard tools. However, the `Switch` view that comes with this library allows you to destructure a `Binding` into bindings of each case so that you can present different views: - -```swift -struct InventoryItemView { - @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) } - } - } - } -} -``` - -### Binding transformations - -This library comes with tools that transform and destructure bindings of optional and enum state, which allows you to build your own navigation views similar to the ones that ship in this library. - - * `Binding.init(unwrapping:)` - * `Binding.case(_:)` - * `Binding.isPresent()` and `Binding.isPresent(_:)` - -For example, suppose you have built a `BottomSheet` view for presenting a modal-like view that only takes up the bottom half of the screen. You can build the entire view using the most simplistic domain modeling where navigation is driven off a single boolean binding: - -```swift -struct BottomSheet: View where Content: View { - @Binding var isActive: Bool - let content: () -> Content - - var body: some View { - ... - } -} -``` - -Then, additional convenience initializers can be introduced that allow the bottom sheet to be created with a more concisely modeled domain. - -For example, an initializer that allows the bottom sheet to be presented and dismissed with optional state, and further the content closure is provided a binding of the non-optional state. We can accomplish this using the `isPresent()` method and `Binding.init(unwrapping:)`: - -```swift -extension BottomSheet { - init( - unwrapping value: Binding, - @ViewBuilder content: @escaping (Binding) -> WrappedContent - ) - where Content == WrappedContent? - { - self.init( - isActive: value.isPresent(), - content: { Binding(unwrapping: value).map(content) } - ) - } -} -``` - -An even more robust initializer can be provided by providing a binding to an optional enum _and_ a case path to specify which case of the enum triggers navigation. This can be accomplished using the `case(_:)` method on binding: - -```swift -extension BottomSheet { - init( - unwrapping enum: Binding, - case casePath: CasePath, - @ViewBuilder content: @escaping (Binding) -> WrappedContent - ) - where Content == WrappedContent? - { - self.init( - unwrapping: `enum`.case(casePath), - content: content - ) - } -} -``` - -Both of these more powerful initializers are just conveniences. If the user of `BottomSheet` does not want to worry about concise domain modeling they are free to continue using the `isActive` boolean binding. But the day they need the more powerful APIs they will be available. - +## 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 +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 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 + 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, but unfortunately SwiftUI does not ship with all the tools necessary to model our +domains as concisely as possible and use these navigation APIs. + +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: + +* **[What is navigation?][what-is-article]**: + 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. + +* **[Navigation links and destinations][nav-links-dests-article]**: + Learn how to drive navigation in NavigationView and NavigationStack in a concise and testable + manner. + +* **[Sheets, popovers, and covers][sheets-popovers-covers-article]**: + Learn how to present sheets, popovers and covers in a concise and testable manner. + +* **[Alerts and dialogs][alerts-dialogs-article]**: + 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. + ## Examples -This repo comes with lots of examples to demonstrate how to solve common and complex navigation problems with the library. Check out [this](./Examples) directory to see them all, including: +This repo comes with lots of examples to demonstrate how to solve common and complex navigation +problems with the library. Check out [this](./Examples) directory to see them all, including: * [Case Studies](./Examples/CaseStudies) * Alerts & Confirmation Dialogs @@ -309,11 +71,13 @@ This repo comes with lots of examples to demonstrate how to solve common and com * Navigation Links * Routing * Custom Components -* [Inventory](./Examples/Inventory): A multi-screen application with lists, sheets, popovers and alerts, all driven by state and deep-linkable. +* [Inventory](./Examples/Inventory): A multi-screen application with lists, sheets, popovers and +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). @@ -327,7 +91,8 @@ You can add SwiftUI Navigation to an Xcode project by adding it as a package dep > https://github.com/pointfreeco/swiftui-navigation -If you want to use SwiftUI Navigation in a [SwiftPM](https://swift.org/package-manager/) project, it's as simple as adding it to a `dependencies` clause in your `Package.swift`: +If you want to use SwiftUI Navigation in a [SwiftPM](https://swift.org/package-manager/) project, +it's as simple as adding it to a `dependencies` clause in your `Package.swift`: ``` swift dependencies: [ @@ -337,8 +102,18 @@ dependencies: [ ## Documentation -The latest documentation for the SwiftUI Navigation APIs is available [here](https://pointfreeco.github.io/swiftui-navigation/). +The latest documentation for the SwiftUI Navigation APIs is available [here](http://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/). ## License This library is released under the MIT license. See [LICENSE](LICENSE) for details. + +[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/ diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index d47b95de78..278b5ce3c5 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -1,102 +1,216 @@ -#if compiler(>=5.5) - extension View { - /// Presents an alert from a binding to optional alert state. - /// - /// 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) } - /// ) - /// } +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``. /// - /// func getRandomMovie() { - /// self.randomMovie = Movie.allCases.randomElement() - /// } - /// } - /// ``` + /// See for more information on how to use this API. /// /// - 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. + /// 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. + /// - action: 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: (Value) -> Text, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M + public func alert( + unwrapping value: Binding?>, + action: @escaping (Value) -> Void = { (_: Never) in fatalError() } ) -> some View { self.alert( - value.wrappedValue.map(title) ?? Text(""), + (value.wrappedValue?.title).map(Text.init) ?? Text(""), isPresented: value.isPresent(), presenting: value.wrappedValue, - actions: actions, - message: message + actions: { + ForEach($0.buttons) { + Button($0, action: action) + } + }, + message: { $0.message.map { Text($0) } } ) } - /// Presents an alert from a binding to an optional enum, and a case path to a specific case. + /// 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. + /// 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: - /// - 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. + /// 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. - /// - 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. + /// - action: 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: @escaping (Value) -> Void = { (_: Never) in fatalError() } + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: action) + } + #else + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping value: Binding?>, + action: @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: action) + } + }, + message: { $0.message.map { Text($0) } } + ) + } + @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( + unwrapping value: Binding?> ) -> some View { self.alert( - title: title, - unwrapping: `enum`.case(casePath), - actions: actions, - message: message + (value.wrappedValue?.title).map(Text.init) ?? Text(""), + isPresented: value.isPresent(), + presenting: value.wrappedValue, + actions: { + ForEach($0.buttons) { + Button($0, action: { (_: Never) in fatalError() }) + } + }, + message: { $0.message.map { Text($0) } } ) } - } -#endif + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func alert( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action: @escaping (Value) -> Void + ) -> some View { + self.alert(unwrapping: `enum`.case(casePath), action: action) + } + + @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), action: { (_: Never) in fatalError() }) + } + #endif + + // TODO: support iOS <15? +} diff --git a/Sources/SwiftUINavigation/Bind.swift b/Sources/SwiftUINavigation/Bind.swift new file mode 100644 index 0000000000..8362a2a4ac --- /dev/null +++ b/Sources/SwiftUINavigation/Bind.swift @@ -0,0 +1,84 @@ +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)) + } +} + +@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 + + 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: DynamicProperty { + 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.Binding: _Bindable {} + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +extension AppStorage: _Bindable {} + +extension Binding: _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.Binding: _Bindable {} + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +extension SceneStorage: _Bindable {} + +extension State: _Bindable {} diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index ef31a456c2..710d50f45d 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -1,3 +1,5 @@ +import SwiftUI + extension Binding { /// Creates a binding by projecting the base value to an unwrapped value. /// @@ -37,6 +39,7 @@ extension Binding { return `case` }, set: { + guard casePath.extract(from: `enum`.wrappedValue) != nil else { return } `case` = $0 `enum`.transaction($1).wrappedValue = casePath.embed($0) } @@ -56,6 +59,7 @@ extension Binding { .init( get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, set: { newValue, transaction in + guard self.wrappedValue != nil else { return } self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) } ) @@ -88,35 +92,35 @@ extension Binding { /// 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 route 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 route enum into a boolean binding using `isPresent`, so that it - /// can be passed to various presentation APIs. + /// 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 Route { + /// enum Destination { /// case deleteAlert /// ... /// } /// /// struct ProductView: View { - /// @State var route: Route? + /// @State var destination: Destination? /// @State var product: Product /// /// var body: some View { /// Button("Delete") { - /// self.viewModel.route = .deleteAlert + /// self.model.destination = .deleteAlert /// } /// // SwiftUI's vanilla alert modifier /// .alert( /// self.product.name - /// isPresented: self.$viewModel.route.isPresent(/Route.deleteAlert), + /// isPresented: self.$model.destination.isPresent(/Destination.deleteAlert), /// actions: { /// Button("Delete", role: .destructive) { - /// self.viewModel.deleteConfirmationButtonTapped() + /// self.model.deleteConfirmationButtonTapped() /// } /// }, /// message: { @@ -168,3 +172,19 @@ extension Binding where Value: Equatable { 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 + } + ) + } +} diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index dded8a5f36..6d301fdb2b 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -1,109 +1,223 @@ -#if compiler(>=5.5) - extension View { - /// Presents a confirmation dialog from a binding to optional dialog state. - /// - /// 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) } - /// ) - /// } +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``. /// - /// 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. + /// - 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. + /// - action: 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: (Value) -> Text, - titleVisibility: Visibility = .automatic, - unwrapping value: Binding, - @ViewBuilder actions: (Value) -> A, - @ViewBuilder message: (Value) -> M + public func confirmationDialog( + unwrapping value: Binding?>, + action: @escaping (Value) -> Void = { (_: Never) in fatalError() } ) -> some View { self.confirmationDialog( - value.wrappedValue.map(title) ?? Text(""), + value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), isPresented: value.isPresent(), - titleVisibility: titleVisibility, + titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic, presenting: value.wrappedValue, - actions: actions, - message: message + actions: { + ForEach($0.buttons) { + Button($0, action: action) + } + }, + 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. + /// specific case of ``ConfirmationDialogState``. /// - /// A version of `confirmationDialog(unwrapping:)` that works with enum state. + /// 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. + /// 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. + /// - action: 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( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action: @escaping (Value) -> Void = { (_: Never) in fatalError() } ) -> some View { self.confirmationDialog( - title: title, - titleVisibility: titleVisibility, unwrapping: `enum`.case(casePath), - actions: actions, - message: message + action: action ) } - } -#endif + #else + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping value: Binding?>, + action: @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: action) + } + }, + 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, + action: { (_: Never) in fatalError() } + ) + } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public func confirmationDialog( + unwrapping `enum`: Binding, + case casePath: CasePath>, + action: @escaping (Value) -> Void + ) -> some View { + self.confirmationDialog( + unwrapping: `enum`.case(casePath), + action: action + ) + } + @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), + action: { (_: Never) in fatalError() } + ) + } + #endif + + // TODO: support iOS <15? +} diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md new file mode 100644 index 0000000000..ebaae9296a --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -0,0 +1,194 @@ +# Alerts and dialogs + +Learn how to present alerts and confirmation dialogs in a concise and testable manner. + +## Overview + +The library comes with new tools for driving alerts and confirmation dialogs from optional and enum +state, and makes them more testable. + +### Alerts + +Suppose you have a feature for deleting something in your application and you want to show an alert +for the user to confirm the deletion. You can do this by holding onto an optional `AlertState` in +your model, as well as an enum that describes every action that can happen in the alert: + + +```swift +class FeatureModel: ObservableObject { + var alert: AlertState? + enum AlertAction { + case deletionConfirmed + } + + // ... +} +``` + +Then, when you need to show an alert you can hydate the alert state with a title, message and +buttons: + +```swift +func deleteButtonTapped() { + self.alert = AlertState { + TextState("Are you sure?") + } actions: { + ButtonState("Delete", action: .send(.delete)) + ButtonState("Nevermind", role: .cancel) + } message: { + TextState("Deleting this item cannot be undone.") + } +} +``` + +The type `TextState` is closely related to `Text` from SwiftUI, but plays more nicely with +equatability. This makes it possible to write tests against these values. + +> Tip: The `actions` closure is a result builder, which allows you to insert small bits of logic: +> ```swift +> } actions: { +> if item.isLocked { +> ButtonState("Unlock and delete", action: .send(.unlockAndDelete)) +> } else { +> ButtonState("Delete", action: .send(.delete)) +> } +> ButtonState("Nevermind", role: .cancel) +> } +> ``` + +Next you can provide an endpoint that will be called when the alert is interacted with: + +```swift +func alertButtonTapped(_ action: AlertAction) { + switch action { + case .deletionConfirmed: + // NB: Perform deletion logic here + } +} +``` + +Finally, you can use a new, overloaded `.alert` view modifier for showing the alert when this state +becomes non-`nil`: + +```swift +struct ContentView: View { + @ObservedObject var model: FeatureModel + + var body: some View { + List { + // ... + } + .alert( + unwrapping: self.$model.alert, + action: self.alertButtonTapped + ) + } +} +``` + +By having all of the alert's state in your feature's model, you instantly unlock the ability to test +it: + +```swift +func testDelete() { + let model = FeatureModel(…) + + model.deleteButtonTapped() + XCTAssertEqual(model.alert?.title, TextState("Are you sure?")) + + model.alertButtonTapped(.deletionConfirmation) + // NB: Assert that deletion actually occurred. +} +``` + +This works because all of the types for describing an alert are `Equatable`, including `AlertState`, +`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. + +In such a case + + +```swift +class FeatureModel: ObservableObject { + var destination: Destination? + enum Destination { + case alert(AlertState) + // NB: Other destinations + } + enum AlertAction { + case deletionConfirmed + } + + // ... +} +``` + +With this kind of set up you can use an alternative `alert` view modifier that takes an additional +argument for specifying which case of the enum drives the presentation of the alert: + +```swift +.alert( + unwrapping: self.$model.destination, + case: /Destination.alert, + action: self.alertButtonTapped +) +``` + +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. + +### Confirmation dialogs + +The APIs for driving confirmation dialogs from optional and enum state look nearly identical to that +of alerts. + +For example, the model for a delete confirmation could look like this: + +```swift +class FeatureModel: ObservableObject { + var dialog: ConfirmationDialogState? + enum DialogAction { + case deletionConfirmed + } + + 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")), + ] + ) + } + + func dialogButtonTapped(_ action: DialogAction) { + switch action { + case .deletionConfirmed: + // NB: Perform deletion logic here + } + } +} +``` + +And then the view would look like this: + +```swift +struct ContentView: View { + @ObservedObject var model: FeatureModel + + var body: some View { + List { + // ... + } + .confirmationDialog( + unwrapping: self.$model.dialog, + action: self.dialogButtonTapped + ) + } +} +``` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md new file mode 100644 index 0000000000..9a2e64ac42 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -0,0 +1,63 @@ +# Bindings + +Learn how to manage certain view state, such as `@FocusState` directly in your observable object. + +## 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 +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 implement like so: + +```swift +class SignInModel: ObservableObject { + @Published var email: String + @Published var password: String + @Published var focus: Field? + enum Field { case email, password } + + func signInButtonTapped() async { + do { + try await self.apiClient.signIn(self.email, self.password) + } catch { + self.focus = .email + } + } +} +``` + +Notice that we store the focus as a `@Published` 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. + +You can implement the view as you would normally, except you must also us `@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. + +```swift +struct SignInView: View { + @FocusState var focus: SignInModel.Field? + @ObservedObject var model: SignInModel + + var body: some View { + Form { + TextField("Email", text: self.$model.email) + TextField("Password", text: self.$model.password) + Button("Sign in") { + Task { + await self.model.signInButtonTapped() + } + } + } + // ⬇️ Replays changes of `model.focus` to `focus` and vice-versa. + .bind(self.$model.focus, to: self.$focus) + } +} +``` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md new file mode 100644 index 0000000000..9cf898bae7 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md @@ -0,0 +1,164 @@ +# 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 deriving 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 = nil + } + Button("Save") { + self.string = string + self.editableString = nil + } + } + } else: { + Text("\(self.string)") + Button("Edit") { + self.editableString = 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 indispensible 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 { + @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 new file mode 100644 index 0000000000..f5787d7bbd --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -0,0 +1,115 @@ +# Navigation links and destinations + +Learn how to drive navigation in `NavigationView` and `NavigationStack` in a concise and testable +manner. + +## Overview + +The library comes with new tools for driving drill-down navigation with optional and enum state. +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 +drill-down should occur: + +```swift +struct ContentView: View { + @State var destination: Int? + + // ... +} +``` + +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 + CounterView(number: $number) +} label: { + Text("Go to counter") +} +``` + +The first trailing closure is the "action" of the navigation link. It is invoked with `true` when +the user taps on the link, and it is invoked with `false` when the user taps the back button or +swipes on the left edge of the screen. It is your job to hydrate the state in the action closure. + +The second trailing closure, labeled `destination`, takes an argument that is the binding of the +unwrapped state. This binding can be handed to the child view, and any changes made by the parent +will be reflected in the child, and vice-versa. + +For iOS 16+ you can use the `navigationDestination` overload: + +```swift +Button { + self.destination = 42 +} label: { + Text("Go to counter") +} +.navigationDestination( + unwrapping: self.$model.destination +) { $item in + CounterView(number: $number) +} +``` + +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 +sheet with some text. We can model those destinations as an enum: + +```swift +enum Destination { + case counter(Int) + case text(String) +} +``` + +And we can hold an optional destination in state to represent whether or not we are navigated to +one of these destinations: + +```swift +@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: + +```swift +NavigationLink( + unwrapping: self.$destination, + case: /Destination.counter +) { isActive in + self.destination = isActive ? .counter(42) : nil +} destination: { $number in + CounterView(number: $number) +} label: { + Text("Go to counter") +} +``` + +And similarly for `navigationDestination`: + +```swift +Button { + self.destination = .counter(42) +} label: { + Text("Go to counter") +} +.navigationDestination( + unwrapping: self.$model.destination, + case: /Destination.counter +) { $item 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. diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md new file mode 100644 index 0000000000..c60a0ba950 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/SheetsPopoversCovers.md @@ -0,0 +1,162 @@ +# Sheets, popovers, and covers + +Learn how to present sheets, popovers and covers in a concise and testable manner. + +## Overview + +The library comes with new tools for driving sheets, popovers and covers from optional and enum +state. + +* [Sheets](#Sheets) +* [Popovers](#Popovers) +* [Covers](#Covers) + +### Sheets + +Suppose your view or model holds a piece of optional state that represents whether or not a modal +sheet is presented: + +```swift +struct ContentView: View { + @State var destination: Int? + + // ... +} +``` + +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: + +```swift +var body: some View { + List { + // ... + } + .sheet(unwrapping: self.$destination) { $number in + CounterView(number: $number) + } +} +``` + +Notice that the trailing closure is handed a binding to the unwrapped state. This binding can be +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 +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: + +```swift +@State var destination: Destination? + +enum Destination { + var counter(Int) + // More destinations +} +``` + +Then you can show a sheet from the `counter` case with the following: + +```swift +var body: some View { + List { + // ... + } + .sheet( + unwrapping: self.$destination, + case: /Destination.counter + ) { $number in + CounterView(number: $number) + } +} +``` + +### Popovers + +Popovers work similarly to covers. If the popover's state is represented as an optional you can do +the following: + +```swift +struct ContentView: View { + @State var destination: Int? + + var body: some View { + List { + // ... + } + .popover(unwrapping: self.$destination) { $number in + CounterView(number: $number) + } + } +} +``` + +And if the popover state is represented as an enum, then you can do the following: + +```swift +struct ContentView: View { + @State var destination: Destination? + enum Destination { + case counter(Int) + // More destinations + } + + var body: some View { + List { + // ... + } + .popover( + unwrapping: self.$destination, + case: /Destination.counter + ) { $number in + CounterView(number: $number) + } + } +} +``` + +### Covers + +Full screen covers work similarly to covers and sheets. If the cover's state is represented as an +optional you can do the following: + +```swift +struct ContentView: View { + @State var destination: Int? + + var body: some View { + List { + // ... + } + .fullscreenCover(unwrapping: self.$destination) { $number in + CounterView(number: $number) + } + } +} +``` + +And if the cover's' state is represented as an enum, then you can do the following: + +```swift +struct ContentView: View { + @State var destination: Destination? + enum Destination { + case counter(Int) + // More destinations + } + + var body: some View { + List { + // ... + } + .fullscreenCover( + unwrapping: self.$destination, + case: /Destination.counter + ) { $number in + CounterView(number: $number) + } + } +} +``` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md new file mode 100644 index 0000000000..f9955d5657 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -0,0 +1,300 @@ +# What is navigation? + +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 +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 +as navigation too. They slide from bottom-to-top and transition you from the current screen to a +new screen. Full screen covers and popovers are also an example of navigation, as they are very +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 +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 +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 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? + +## SwiftUI's tools for navigation + +Many of SwiftUI's navigation tools are driven off of optional state, but sadly not all. + +The simplest example is modal sheets. A simple API is provided that takes a binding of an optional +item, and when that item flips to a non-`nil` value it is handed to a content closure to produce +a view, and that view is what is animated from bottom-to-top: + +```swift +func sheet( + item: Binding, + onDismiss: (() -> Void)? = nil, + content: @escaping (Item) -> Content +) -> some View +``` + +When SwiftUI detects the binding flips back to `nil`, the sheet will automatically be dismissed. + +For example, suppose you have a list of items, and when one is tapped you want to bring up a modal +sheet for editing the item: + +```swift +class FeatureModel: ObservableObject { + @Published var editingItem: Item? + func tapped(item: Item) { + self.editingItem = item + } + // ... +} + +struct FeatureView: View { + @ObservedObject var model: FeatureModel + + var body: some View { + List { + ForEach(self.model.items) { item in + Button(item.name) { + self.model.tapped(item: item) + } + } + } + .sheet(item: self.$model.editingItem) { item in + EditItemView(item: item) + } + } +} +``` + +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. + +A lot of SwiftUI's navigation APIs follow this pattern. For example, here's the signatures for +showing popovers and full screen covers: + +```swift +func popover( + item: Binding, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + content: @escaping (Item) -> Content +) -> some View where Item : Identifiable, Content : View + +func fullScreenCover( + item: Binding, + onDismiss: (() -> Void)? = nil, + content: @escaping (Item) -> Content +) -> some View where Item : Identifiable, Content : View +``` + +Both take a binding of an optional and a content closure for transforming the non-`nil` state into +a view that is presented in the popover or cover. + +There are, however, two potential problems with these APIs. + +First, the argument passed to the `content` closure is the plain, non-`nil` value. This means the +sheet view presented is handed a plain, inert value, and if that view wants to make mutations it +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 +that any mutations in the sheet would be instantly observable by the parent: + +```swift +.sheet(item: self.$model.editingItem) { $item in + EditItemView(item: $item) +} +``` + +However, this is not the API exposed to us from SwiftUI. + +The second problem is that while optional state is a great way to drive navigation, it doesn't +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 +represented as four optionals: + +```swift +class FeatureModel: ObservableObject { + @Published var addItem: Item? + @Published var duplicateItem: Item? + @Published var editingItem: Item? + @Published var help: Help? + // ... +} +``` + +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`, +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 +each destination, and then hold onto a single optional value to represent which destination +is currently active: + +```swift +class FeatureModel: ObservableObject { + @Published var destination: Destination? + // ... + + enum Destination { + case add(Item) + case duplicate(Item) + case edit(Item) + case help(Help) + } +} +``` + +This allows you to prove that at most one destination can be active at a time. It is impossible +to have both an "add" and "duplicate" screen presented at the same time. + +But sadly SwiftUI does not come with the tools necessary to drive navigation off of an optional +enum. This is what motivated the creation of this library. It should be possible to represent +all of the screens a feature can navigate to as an enum, and then drive sheets, popovers, covers +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 +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. + +For example, the new sheet API now takes a binding to an optional enum, and something known as a +[`CasePath`][case-paths-gh]: + +```swift +func sheet( + unwrapping: Binding, + case: CasePath, + content: @escaping (Binding) -> Content +) -> some View where Content : View +``` + +This allows you to drive the presentation and dismiss of a sheet 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] +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" +a value from an enum and "embedding" a value into an enum. They are an indispensible tool for +transforming bindings. + +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? + // ... + + enum Destination { + case add(Item) + case duplicate(Item) + case edit(Item) + } +} +``` + +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 + DuplicateItemView(item: $item) +} +.sheet( + unwrapping: self.$model.destination, + case: /FeatureModel.Destination.add +) { $item in + AddItemView(item: $item) +} +.navigationDestination( + unwrapping: self.$model.destination, + case: /FeatureModel.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. + +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: + +```swift +NavigationLink( + unwrapping: self.$model.destination, + case: /FeatureModel.Destination.edit +) { isActive in + self.model.setEditIsActive(isActive) +} destination: { $item in + EditItemView(item: $item) +} label: { + Text("\(item.name)") +} +``` + +That is the basics of using this library's APIs for driving navigation off of state. Learn more +by reading the articles below. + +## Topics + +### 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. + +- +- +- +- +- + +[case-paths-gh]: http://github.com/pointfreeco/swift-case-paths diff --git a/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md new file mode 100644 index 0000000000..53dc495143 --- /dev/null +++ b/Sources/SwiftUINavigation/Documentation.docc/SwiftUINavigation.md @@ -0,0 +1,60 @@ +# ``SwiftUINavigation`` + +Tools for making SwiftUI navigation simpler, more ergonomic and more precise. + +## Additional Resources + +- [GitHub Repo](https://github.com/pointfreeco/swiftui-navigation) +- [Discussions](https://github.com/pointfreeco/swiftui-navigation/discussions) +- [Point-Free Videos](https://www.pointfree.co/collections/swiftui/navigation) + +## 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 +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 + [`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 + 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. 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 +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. + +## Topics + +### Essentials + +- + +### Tools + +- +- +- +- +- + +## See Also + +The collection of videos from [Point-Free](https://www.pointfree.co) that dive deep into the +development of the library. + +* [Point-Free Videos](https://www.pointfree.co/collections/swiftui/navigation) + +[NavigationLink.init]: https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s +[TabView.init]: https://developer.apple.com/documentation/swiftui/tabview/init(content:) diff --git a/Sources/SwiftUINavigation/FullScreenCover.swift b/Sources/SwiftUINavigation/FullScreenCover.swift index 59def3a991..bcb5ed2784 100644 --- a/Sources/SwiftUINavigation/FullScreenCover.swift +++ b/Sources/SwiftUINavigation/FullScreenCover.swift @@ -1,3 +1,5 @@ +import SwiftUI + extension View { /// Presents a full-screen cover using a binding as a data source for the sheet's content. /// @@ -20,7 +22,7 @@ extension View { /// self.draft = Post() /// } /// .fullScreenCover(unwrapping: self.$draft) { $draft in - /// ComposeView(post: $draft, onSubmit: { ... }) + /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } /// } @@ -35,7 +37,7 @@ extension View { /// - 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 or truth. Likewise, changes to + /// 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. @@ -48,7 +50,10 @@ extension View { @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { - self.fullScreenCover(isPresented: value.isPresent(), onDismiss: onDismiss) { + self.fullScreenCover( + isPresented: value.isPresent(), + onDismiss: onDismiss + ) { Binding(unwrapping: value).map(content) } } @@ -63,7 +68,7 @@ extension View { /// 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, change to `enum` + /// 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 diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index 7b82c9e7f7..95f108942a 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -60,8 +60,8 @@ where IfContent: View, ElseContent: View { public init( _ `enum`: Binding, pattern casePath: CasePath, - @ViewBuilder ifContent: @escaping (Binding) -> IfContent, - @ViewBuilder elseContent: () -> ElseContent + @ViewBuilder then ifContent: @escaping (Binding) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent ) { self.casePath = casePath self.elseContent = elseContent() diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift index b4ab9adb24..33e6952588 100644 --- a/Sources/SwiftUINavigation/IfLet.swift +++ b/Sources/SwiftUINavigation/IfLet.swift @@ -1,3 +1,5 @@ +import SwiftUI + /// A view that computes content by unwrapping a binding to an optional and passing a non-optional /// binding to its content closure. /// diff --git a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift index fac929fc9a..7716ed670d 100644 --- a/Sources/SwiftUINavigation/Internal/Binding+Internal.swift +++ b/Sources/SwiftUINavigation/Internal/Binding+Internal.swift @@ -1,3 +1,5 @@ +import SwiftUI + extension Binding { func didSet(_ perform: @escaping (Value) -> Void) -> Self { .init( diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 8d3321661d..f0038c1994 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,3 +1,19 @@ +import SwiftUI + +// 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) + } +} + // NB: Deprecated after 0.2.0 extension NavigationLink { diff --git a/Sources/SwiftUINavigation/Internal/Exports.swift b/Sources/SwiftUINavigation/Internal/Exports.swift index 3886010dcc..944ac1756c 100644 --- a/Sources/SwiftUINavigation/Internal/Exports.swift +++ b/Sources/SwiftUINavigation/Internal/Exports.swift @@ -1,2 +1,2 @@ @_exported import CasePaths -@_exported import SwiftUI +@_exported import _SwiftUINavigationState diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift new file mode 100644 index 0000000000..7b12247b97 --- /dev/null +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -0,0 +1,94 @@ +#if swift(>=5.7) + import SwiftUI + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + extension View { + /// 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. + /// + /// ```swift + /// struct TimelineView: View { + /// @State var detail: Post? + /// + /// var body: Body { + /// Button("Compose") { + /// self.draft = Post() + /// } + /// .navigationDestination(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 destination. When `value` 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. + /// - destination: A closure returning the content of the destination. + public func navigationDestination( + unwrapping value: Binding, + @ViewBuilder destination: (Binding) -> Destination + ) -> some View { + self.modifier( + _NavigationDestination( + isPresented: value.isPresent(), + destination: Binding(unwrapping: value).map(destination) + ) + ) + } + + /// 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: + // https://gist.github.com/mbrandonw/f8b94957031160336cac6898a919cbb7#file-fb11056434-md + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + private struct _NavigationDestination: 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) + } + } +#endif diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index a056bc572d..b49f1f5b5a 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -1,3 +1,5 @@ +import SwiftUI + extension NavigationLink { /// Creates a navigation link that presents the destination view when a bound value is non-`nil`. /// @@ -69,18 +71,18 @@ extension NavigationLink { /// /// ```swift /// struct ContentView: View { - /// @State var route: Route? + /// @State var destination: Destination? /// @State var posts: [Post] /// - /// enum Route { + /// enum Destination { /// case edit(Post) - /// /* other routes */ + /// /* other destinations */ /// } /// /// var body: some View { /// ForEach(self.posts) { post in - /// NavigationLink(unwrapping: self.$route, case: /Route.edit) { isActive in - /// self.route = isActive ? .edit(post) : nil + /// NavigationLink(unwrapping: self.$destination, case: /Destination.edit) { isActive in + /// self.destination = isActive ? .edit(post) : nil /// } destination: { $draft in /// EditPostView(post: $draft) /// } label: { diff --git a/Sources/SwiftUINavigation/Popover.swift b/Sources/SwiftUINavigation/Popover.swift index 4c6a9afa39..86bd1096ac 100644 --- a/Sources/SwiftUINavigation/Popover.swift +++ b/Sources/SwiftUINavigation/Popover.swift @@ -1,3 +1,5 @@ +import SwiftUI + extension View { /// Presents a popover using a binding as a data source for the popover's content. /// @@ -35,7 +37,7 @@ extension View { /// - 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 or truth. Likewise, + /// 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. @@ -51,7 +53,9 @@ extension View { @ViewBuilder content: @escaping (Binding) -> Content ) -> some View where Content: View { self.popover( - isPresented: value.isPresent(), attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge + isPresented: value.isPresent(), + attachmentAnchor: attachmentAnchor, + arrowEdge: arrowEdge ) { Binding(unwrapping: value).map(content) } @@ -66,7 +70,7 @@ extension View { /// 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, change to + /// 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 diff --git a/Sources/SwiftUINavigation/Sheet.swift b/Sources/SwiftUINavigation/Sheet.swift index e6c985201d..c1c4acdbad 100644 --- a/Sources/SwiftUINavigation/Sheet.swift +++ b/Sources/SwiftUINavigation/Sheet.swift @@ -1,3 +1,11 @@ +import SwiftUI + +#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. /// @@ -20,7 +28,7 @@ extension View { /// self.draft = Post() /// } /// .sheet(unwrapping: self.$draft) { $draft in - /// ComposeView(post: $draft, onSubmit: { ... }) + /// ComposeView(post: $draft, onSubmit: { ... }) /// } /// } /// } @@ -35,11 +43,12 @@ extension View { /// - 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 or truth. Likewise, changes + /// 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, @@ -60,13 +69,14 @@ extension View { /// 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, change to `enum` + /// 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, diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index 301f691e17..d4d702da83 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -1,3 +1,5 @@ +import SwiftUI + /// 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 diff --git a/Sources/SwiftUINavigation/WithState.swift b/Sources/SwiftUINavigation/WithState.swift new file mode 100644 index 0000000000..746928ed40 --- /dev/null +++ b/Sources/SwiftUINavigation/WithState.swift @@ -0,0 +1,47 @@ +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 + + 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) + } +} diff --git a/Sources/_SwiftUINavigationState/AlertState.swift b/Sources/_SwiftUINavigationState/AlertState.swift new file mode 100644 index 0000000000..20eff155fe --- /dev/null +++ b/Sources/_SwiftUINavigationState/AlertState.swift @@ -0,0 +1,311 @@ +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: +/// // ... +/// } +/// } +/// } +/// ``` +/// +/// 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 + + /// 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.title = title() + self.message = message?() + self.buttons = actions() + } +} + +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 + ) + } +} + +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) + } +} + +// 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) } + ) + } + } +} + +// MARK: - Deprecations + +extension AlertState { + @available(*, deprecated, message: "Use 'ButtonState' instead.") + public typealias Button = ButtonState + + @available(*, deprecated, message: "Use 'ButtonState.ButtonAction' instead.") + public typealias ButtonAction = ButtonState.ButtonAction + + @available(*, deprecated, message: "Use 'ButtonState.Role' instead.") + public typealias ButtonRole = ButtonState.Role + + @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.title = title + self.message = message + self.buttons = buttons + } + + @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.title = title + self.message = message + self.buttons = dismissButton.map { [$0] } ?? [] + } + + @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.title = title + self.message = message + self.buttons = [primaryButton, secondaryButton] + } +} diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift new file mode 100644 index 0000000000..82797cde5f --- /dev/null +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -0,0 +1,253 @@ +import CustomDump +import SwiftUI + +public struct ButtonState: Identifiable { + /// A type that wraps an action with additional context, _e.g._ for animation. + public struct Handler { + public let type: _ActionType + + 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 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 Role { + /// 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 + } + + public let id = UUID() + public let action: Handler? + public let label: TextState + public let 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: Role? = nil, + action: Handler? = nil, + label: () -> TextState + ) { + self.role = role + self.action = action + self.label = label() + } + + /// 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: Role? = nil, + action: Action? = nil, + label: () -> TextState + ) { + self.role = role + self.action = action.map(Handler.send) + self.label = label() + } + + /// 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: animation): + withAnimation(animation) { + perform(action) + } + case .none: + return + } + } +} + +extension ButtonState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + var children: [(label: String?, value: Any)] = [] + if let role = self.role { + children.append(("role", role)) + } + if let action = self.action { + children.append(("action", action)) + } + children.append(("label", self.label)) + return Mirror( + self, + children: children, + displayStyle: .struct + ) + } +} + +extension ButtonState.Handler: CustomDumpReflectable { + public var customDumpMirror: Mirror { + switch self.type { + case let .send(action): + return Mirror( + self, + children: [ + "send": action + ], + displayStyle: .enum + ) + case let .animatedSend(action, animation): + return Mirror( + self, + children: [ + "send": (action, animation: animation) + ], + displayStyle: .enum + ) + } + } +} + +extension ButtonState.Handler: Equatable where Action: Equatable {} +extension ButtonState.Handler._ActionType: Equatable where Action: Equatable {} +extension ButtonState.Role: Equatable {} +extension ButtonState: Equatable where Action: Equatable {} + +extension ButtonState.Handler: Hashable where Action: Hashable {} +extension ButtonState.Handler._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 ButtonState.Role: 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) + } +} + +// MARK: - SwiftUI bridging + +extension Alert.Button { + public init(_ button: ButtonState, action: @escaping (Action) -> Void) { + let action = button.action.map { _ in { 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) + } + } +} + +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension ButtonRole { + public init(_ role: ButtonState.Role) { + switch role { + case .cancel: + self = .cancel + case .destructive: + self = .destructive + } + } +} + +extension Button where Label == Text { + @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) + } + } +} + +// MARK: - Deprecations + +extension ButtonState { + @available(*, deprecated, renamed: "Handler") + public typealias ButtonAction = Handler +} + +extension ButtonState.Handler { + @available(*, deprecated, message: "Use 'ButtonState.withAction' instead.") + public typealias ActionType = _ActionType +} + +@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: Handler? = nil) -> Self { + Self(role: .cancel, action: action) { + label + } + } + + public static func `default`(_ label: TextState, action: Handler? = nil) -> Self { + Self(action: action) { + label + } + } + + public static func destructive(_ label: TextState, action: Handler? = nil) -> Self { + Self(role: .destructive, action: action) { + label + } + } +} diff --git a/Sources/_SwiftUINavigationState/ButtonStateBuilder.swift b/Sources/_SwiftUINavigationState/ButtonStateBuilder.swift new file mode 100644 index 0000000000..70957312fb --- /dev/null +++ b/Sources/_SwiftUINavigationState/ButtonStateBuilder.swift @@ -0,0 +1,32 @@ +@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 buildLimitedAvailability( + _ 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 buildExpression(_ expression: ButtonState) -> [ButtonState] { + [expression] + } + + public static func buildOptional(_ component: [ButtonState]?) -> [ButtonState] { + component ?? [] + } +} diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift new file mode 100644 index 0000000000..c50cd27047 --- /dev/null +++ b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift @@ -0,0 +1,361 @@ +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: Visibility + + /// 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: Visibility, + title: () -> TextState, + @ButtonStateBuilder actions: () -> [ButtonState], + message: (() -> TextState)? = nil + ) { + self.buttons = actions() + self.message = message?() + self.title = title() + self.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.buttons = actions() + self.message = message?() + self.title = title() + self.titleVisibility = .automatic + } + + public enum Visibility { + case automatic + case hidden + 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: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons + } +} + +@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) + } +} + +// MARK: - SwiftUI bridging + +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension Visibility { + public init(_ visibility: ConfirmationDialogState.Visibility) { + switch visibility { + case .automatic: + self = .automatic + case .hidden: + self = .hidden + case .visible: + self = .visible + } + } +} + +// MARK: - Deprecations + +@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( + 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: Visibility, + message: TextState? = nil, + buttons: [ButtonState] = [] + ) { + self.buttons = buttons + self.message = message + self.title = title + self.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.buttons = buttons + self.message = message + self.title = title + self.titleVisibility = .automatic + } +} + +@available(iOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") +@available(macOS, 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, + 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) } + ) + } +} diff --git a/Sources/_SwiftUINavigationState/TextState.swift b/Sources/_SwiftUINavigationState/TextState.swift new file mode 100644 index 0000000000..15c98083db --- /dev/null +++ b/Sources/_SwiftUINavigationState/TextState.swift @@ -0,0 +1,741 @@ +import CustomDump +import SwiftUI + +/// An equatable description of SwiftUI `Text`. Useful for storing rich text in feature models +/// that can still be tested for equality. +/// +/// Although `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` are value types that conform to +/// `Equatable`, their `==` do not return `true` when used with seemingly equal values. If we were +/// to naively store these values in state, our tests may begin to fail. +/// +/// ``TextState`` solves this problem by providing an interface similar to `SwiftUI.Text` that can +/// be held in state and asserted against. +/// +/// Let's say you wanted to hold some dynamic, styled text content in your app state. You could use +/// ``TextState``: +/// +/// ```swift +/// class Model: Equatable { +/// @Published var label = TextState("") +/// } +/// ``` +/// +/// Your model can then assign a value to this state using an API similar to that of `SwiftUI.Text`. +/// +/// ```swift +/// self.label = TextState("Hello, ") + TextState(name).bold() + TextState("!") +/// ``` +/// +/// And your view can render it by passing it to a `SwiftUI.Text` initializer: +/// +/// ```swift +/// var body: some View { +/// Text(self.model.label) +/// } +/// ``` +/// +/// SwiftUI Navigation comes with a few convenience APIs for alerts and dialogs that wrap +/// ``TextState`` under the hood. See ``AlertState`` and ``ConfirmationDialogState`` accordingly. +/// +/// In the future, should `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` reliably conform to +/// `Equatable`, ``TextState`` may be deprecated. +/// +/// - 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 { + fileprivate var modifiers: [Modifier] = [] + fileprivate let storage: Storage + + fileprivate enum Modifier: Equatable, Hashable { + case accessibilityHeading(AccessibilityHeadingLevel) + case accessibilityLabel(TextState) + case accessibilityTextContentType(AccessibilityTextContentType) + case baselineOffset(CGFloat) + case bold(isActive: Bool) + case font(Font?) + case fontDesign(Font.Design?) + case fontWeight(Font.Weight?) + case fontWidth(FontWidth?) + case foregroundColor(Color?) + case italic(isActive: Bool) + case kerning(CGFloat) + case monospacedDigit + case speechAdjustedPitch(Double) + case speechAlwaysIncludesPunctuation(Bool) + case speechAnnouncementsQueued(Bool) + case speechSpellsOutCharacters(Bool) + case strikethrough(isActive: Bool, pattern: LineStylePattern?, color: Color?) + case tracking(CGFloat) + case underline(isActive: Bool, pattern: LineStylePattern?, color: Color?) + } + + public enum FontWidth: String, Equatable, Hashable { + 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 { + 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 + } + } + } + + fileprivate enum Storage: Equatable, Hashable { + 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 + } + } + + func hash(into hasher: inout Hasher) { + enum Key { + case concatenated + case localized + case verbatim + } + + 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 + +extension TextState { + public init(verbatim content: String) { + self.storage = .verbatim(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 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 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` + } + + 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 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` + } + + 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` + } + + @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` + } + + @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` + } + + @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 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` + } +} + +// MARK: Accessibility + +extension TextState { + public enum AccessibilityTextContentType: String, Equatable, Hashable { + 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 + } + } + #endif + } + + public enum AccessibilityHeadingLevel: String, Equatable, Hashable { + 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 + } + } + #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` + } + + public func accessibilityLabel(_ label: Self) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(label)) + return `self` + } + + public func accessibilityLabel(_ string: String) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(.init(string))) + return `self` + } + + public func accessibilityLabel(_ string: S) -> 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 accessibilityTextContentType(_ type: AccessibilityTextContentType) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityTextContentType(type)) + return `self` + } + + public func speechAdjustedPitch(_ value: Double) -> Self { + var `self` = self + `self`.modifiers.append(.speechAdjustedPitch(value)) + return `self` + } + + public func speechAlwaysIncludesPunctuation(_ value: Bool = true) -> Self { + var `self` = self + `self`.modifiers.append(.speechAlwaysIncludesPunctuation(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 { + 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 contcatenated `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 + #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 + _ = 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) + } 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.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 { + 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 + } + } + } +} + +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 .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 + } + 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) + } +} + +// 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)" + } + } + 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 + } + } + return output + } + + return dumpHelp(self) + } +} diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index ac6b7a1b6b..55a6c453e4 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,24 @@ "version": "1.0.3" } }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "ead7d30cc224c3642c150b546f4f1080d1c411a8", + "version": "0.6.1" + } + }, + { + "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", diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift new file mode 100644 index 0000000000..ecc0bd6ee8 --- /dev/null +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -0,0 +1,81 @@ +import CustomDump +import SwiftUINavigation +import XCTest + +final class AlertTests: XCTestCase { + func testAlertState() { + var dump = "" + customDump( + AlertState( + title: .init("Alert!"), + message: .init("Something went wrong..."), + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), + secondaryButton: .cancel(.init("Cancel"), action: .send(false)) + ), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + AlertState( + title: "Alert!", + actions: [ + [0]: ButtonState( + role: ButtonState.Role.destructive, + action: ButtonState.Handler.send( + true, + animation: Animation.easeInOut + ), + label: "Destroy" + ), + [1]: ButtonState( + role: ButtonState.Role.cancel, + action: ButtonState.Handler.send(false), + label: "Cancel" + ) + ], + message: "Something went wrong..." + ) + """ + ) + + 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: .default)), + .cancel(.init("Cancel"), action: .send(false)), + ] + ), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + ConfirmationDialogState( + title: "Alert!", + actions: [ + [0]: ButtonState( + role: ButtonState.Role.destructive, + action: ButtonState.Handler.send( + true, + animation: Animation.easeInOut + ), + label: "Destroy" + ), + [1]: ButtonState( + role: ButtonState.Role.cancel, + action: ButtonState.Handler.send(false), + label: "Cancel" + ) + ], + message: "Something went wrong..." + ) + """ + ) + } + } +} diff --git a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift index e496142a7d..92d6107259 100644 --- a/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift +++ b/Tests/SwiftUINavigationTests/SwiftUINavigationTests.swift @@ -1,3 +1,4 @@ +import SwiftUI import XCTest @testable import SwiftUINavigation diff --git a/Tests/SwiftUINavigationTests/TextStateTests.swift b/Tests/SwiftUINavigationTests/TextStateTests.swift new file mode 100644 index 0000000000..cee7a47e9c --- /dev/null +++ b/Tests/SwiftUINavigationTests/TextStateTests.swift @@ -0,0 +1,74 @@ +import CustomDump +import SwiftUINavigation +import XCTest + +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("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 + """ + """# + ) + } +} From 37eb93f853c6e1a354ba5ffcdc5c425a6e8f62e5 Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Mon, 21 Nov 2022 18:42:28 +0000 Subject: [PATCH 014/181] Run swift-format --- .../ConfirmationDialogState.swift | 4 +- .../_SwiftUINavigationState/TextState.swift | 34 ++++---- .../TextStateTests.swift | 86 +++++++++---------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift index c50cd27047..893e073651 100644 --- a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift +++ b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift @@ -201,8 +201,8 @@ extension ConfirmationDialogState: CustomDumpReflectable { 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 + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons } } diff --git a/Sources/_SwiftUINavigationState/TextState.swift b/Sources/_SwiftUINavigationState/TextState.swift index 15c98083db..55843a7bc8 100644 --- a/Sources/_SwiftUINavigationState/TextState.swift +++ b/Sources/_SwiftUINavigationState/TextState.swift @@ -120,7 +120,7 @@ public struct TextState: Equatable, Hashable { 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) + == rk.formatted(tableName: rt, bundle: rb, comment: rc) case let (.verbatim(lhs), .verbatim(rhs)): return lhs == rhs @@ -129,7 +129,7 @@ public struct TextState: Equatable, Hashable { 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. + // NB: We do not attempt to equate concatenated cases. default: return false } @@ -455,8 +455,8 @@ extension Text { } #else case .accessibilityHeading, - .accessibilityLabel, - .accessibilityTextContentType: + .accessibilityLabel, + .accessibilityTextContentType: return text #endif case let .baselineOffset(baselineOffset): @@ -611,7 +611,7 @@ extension LocalizedStringKey { // `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?) + Array(Mirror(reflecting: children[0].value).children)[0].value as! (Any, Formatter?) } else { value = children[0].value formatter = children[1].value as? Formatter @@ -718,18 +718,18 @@ extension TextState: CustomDumpRepresentable { 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: _): + .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 } } diff --git a/Tests/SwiftUINavigationTests/TextStateTests.swift b/Tests/SwiftUINavigationTests/TextStateTests.swift index cee7a47e9c..8a01843aa7 100644 --- a/Tests/SwiftUINavigationTests/TextStateTests.swift +++ b/Tests/SwiftUINavigationTests/TextStateTests.swift @@ -8,67 +8,67 @@ final class TextStateTests: XCTestCase { customDump(TextState("Hello, world!"), to: &dump) XCTAssertEqual( dump, - """ - "Hello, world!" - """ + """ + "Hello, world!" + """ ) dump = "" customDump( TextState("Hello, ") - + TextState("world").bold().italic() - + TextState("!"), + + TextState("world").bold().italic() + + TextState("!"), to: &dump ) XCTAssertEqual( dump, - """ - "Hello, _**world**_!" - """ + """ + "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), + + 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 - """ - """# + #""" + """ + 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 + """ + """# ) } } From 8db42e81f62e131df71a8427c000b0ffbff6c1c2 Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Mon, 21 Nov 2022 15:10:57 -0500 Subject: [PATCH 015/181] Some small fixes (#31) * Some small fixes * wip --- .github/workflows/ci.yml | 2 +- Package.swift | 4 ++ .../NavigationDestination.swift | 40 +++++++++---------- .../ConfirmationDialogState.swift | 3 +- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7007984a26..cfdab77877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: macos-12 strategy: matrix: - xcode: ['14.1'] + xcode: ['13.4.1', '14.1'] steps: - uses: actions/checkout@v3 diff --git a/Package.swift b/Package.swift index 5f570461a0..d012af77ef 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,10 @@ let package = Package( .library( name: "SwiftUINavigation", targets: ["SwiftUINavigation"] + ), + .library( + name: "_SwiftUINavigationState", + targets: ["_SwiftUINavigationState"] ) ], dependencies: [ diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index 7b12247b97..e9af2b68f2 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -3,12 +3,12 @@ @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) extension View { - /// Pushes a view onto a `NavigationStack` using a binding as a data source for the destination's - /// content. + /// 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(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. /// /// ```swift /// struct TimelineView: View { @@ -32,11 +32,11 @@ /// /// - 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. 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. + /// 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. /// - destination: A closure returning the content of the destination. public func navigationDestination( unwrapping value: Binding, @@ -50,20 +50,20 @@ ) } - /// Pushes a view onto a `NavigationStack` using a binding and case path as a data source for the - /// destination's content. + /// 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. + /// - 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. diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift index 893e073651..addfa24170 100644 --- a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift +++ b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift @@ -317,7 +317,7 @@ extension ConfirmationDialogState { } @available(iOS, introduced: 13, deprecated: 100000, renamed: "ConfirmationDialogState") -@available(macOS, unavailable) +@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 @@ -331,6 +331,7 @@ public typealias ActionSheetState = ConfirmationDialogState ) @available( macOS, + introduced: 12, unavailable ) @available( From ee78c0e69f605f461acb4150df7ae016aa57a1de Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Mon, 21 Nov 2022 20:21:46 +0000 Subject: [PATCH 016/181] Run swift-format --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index d012af77ef..3e008a781a 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( .library( name: "_SwiftUINavigationState", targets: ["_SwiftUINavigationState"] - ) + ), ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), From 4b666bcc59ba1711a7543ecb37e1d181963b180c Mon Sep 17 00:00:00 2001 From: konomae Date: Tue, 22 Nov 2022 22:16:41 +0900 Subject: [PATCH 017/181] Exclude `id` from `ButtonState`'s equatable conformance (#33) --- .../_SwiftUINavigationState/ButtonState.swift | 8 +++++++- Tests/SwiftUINavigationTests/AlertTests.swift | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift index 82797cde5f..c66149fe07 100644 --- a/Sources/_SwiftUINavigationState/ButtonState.swift +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -137,7 +137,13 @@ extension ButtonState.Handler: CustomDumpReflectable { extension ButtonState.Handler: Equatable where Action: Equatable {} extension ButtonState.Handler._ActionType: Equatable where Action: Equatable {} extension ButtonState.Role: Equatable {} -extension ButtonState: Equatable where Action: 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 ButtonState.Handler: Hashable where Action: Hashable {} extension ButtonState.Handler._ActionType: Hashable where Action: Hashable { diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index ecc0bd6ee8..6c0e372468 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -4,16 +4,24 @@ import XCTest final class AlertTests: XCTestCase { func testAlertState() { - var dump = "" - customDump( + let alert = AlertState( + title: .init("Alert!"), + message: .init("Something went wrong..."), + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), + secondaryButton: .cancel(.init("Cancel"), action: .send(false)) + ) + XCTAssertNoDifference( + alert, AlertState( title: .init("Alert!"), message: .init("Something went wrong..."), primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), secondaryButton: .cancel(.init("Cancel"), action: .send(false)) - ), - to: &dump + ) ) + + var dump = "" + customDump(alert, to: &dump) XCTAssertNoDifference( dump, """ From 2a16b53abc73865b9d9650881b2e24fb7beb496b Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Tue, 22 Nov 2022 13:24:44 +0000 Subject: [PATCH 018/181] Run swift-format --- Tests/SwiftUINavigationTests/AlertTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 6c0e372468..b0152c4f13 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -19,7 +19,7 @@ final class AlertTests: XCTestCase { secondaryButton: .cancel(.init("Cancel"), action: .send(false)) ) ) - + var dump = "" customDump(alert, to: &dump) XCTAssertNoDifference( From 15644a37587b26af3764551d0a86556e2e6ce6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Wed, 23 Nov 2022 20:59:41 +0900 Subject: [PATCH 019/181] Change where confirmationDialog modifier applied (#38) --- .../CaseStudies/02-ConfirmationDialogs.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index 400c2456ba..48ee1b61b2 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -18,18 +18,18 @@ 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)") { + self.model.numberFactButtonTapped() + } + }, + message: { Text($0.description) } + ) } - .confirmationDialog( - title: { Text("Fact about \($0.number)") }, - titleVisibility: .visible, - unwrapping: self.$model.fact, - actions: { - Button("Get another fact about \($0.number)") { - self.model.numberFactButtonTapped() - } - }, - message: { Text($0.description) } - ) .navigationTitle("Dialogs") } } From 753ae58ba44fe72aca3b6474a62e26e9e0aa9c1d Mon Sep 17 00:00:00 2001 From: Quico Moya Date: Wed, 23 Nov 2022 13:00:04 +0100 Subject: [PATCH 020/181] Update dependency version (#34) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d60eea1a6..c37024b77e 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,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.3.0") + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.4.1") ] ``` From 46641ea333abc3418333eca192f2acb78da81e7e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 24 Nov 2022 09:30:16 -0600 Subject: [PATCH 021/181] Update poster image to free recap --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c37024b77e..1bd8d45601 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Swift language, hosted by [Brandon Williams](https://twitter.com/mbrandonw) and You can watch all of the episodes [here](https://www.pointfree.co/collections/swiftui/navigation). - video poster image + video poster image ## Installation From 15b0477eaa3a5b14ae3a0c90cb9b603a38ab1686 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 24 Nov 2022 09:39:24 -0600 Subject: [PATCH 022/181] Remove .spi.yml to restore watchOS builds --- .spi.yml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml deleted file mode 100644 index 8bfa4337f0..0000000000 --- a/.spi.yml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -builder: - configs: - - platform: watchos - scheme: SwiftUINavigation_watchOS From 2dbe77c11fcb0d330c94e079a8773c5e9eda354b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Tue, 29 Nov 2022 00:56:29 +0900 Subject: [PATCH 023/181] Gardening some examples (#42) --- Examples/Inventory/Inventory.swift | 7 +++---- Sources/SwiftUINavigation/NavigationDestination.swift | 2 +- Sources/SwiftUINavigation/NavigationLink.swift | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index 4c121ef142..be21b3574f 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -79,10 +79,9 @@ struct InventoryView: View { var body: some View { List { - ForEach( - self.model.inventory, - content: ItemRowView.init(model:) - ) + ForEach(self.model.inventory) { + ItemRowView(model: $0) + } } .toolbar { ToolbarItem(placement: .primaryAction) { diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index e9af2b68f2..cc1fa3a589 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -12,7 +12,7 @@ /// /// ```swift /// struct TimelineView: View { - /// @State var detail: Post? + /// @State var draft: Post? /// /// var body: Body { /// Button("Compose") { diff --git a/Sources/SwiftUINavigation/NavigationLink.swift b/Sources/SwiftUINavigation/NavigationLink.swift index b49f1f5b5a..69b2094608 100644 --- a/Sources/SwiftUINavigation/NavigationLink.swift +++ b/Sources/SwiftUINavigation/NavigationLink.swift @@ -19,7 +19,7 @@ extension NavigationLink { /// self.postToEdit = isActive ? post : nil /// } destination: { $draft in /// EditPostView(post: $draft) - /// } onNavigate: label: { + /// } label: { /// Text(post.title) /// } /// } From c374e21e195b70b5a8b0c738a3f07d1bc7c73284 Mon Sep 17 00:00:00 2001 From: Shinolr Date: Mon, 5 Dec 2022 00:37:29 +0800 Subject: [PATCH 024/181] Fix typo (#43) --- .../SwiftUINavigation/Documentation.docc/Articles/Bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index 9a2e64ac42..f6a6c97a72 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -37,7 +37,7 @@ Notice that we store the focus as a `@Published` property in the model rather th This is because `@FocusState` only works when installed directly in a view. It cannot be used in an observable object. -You can implement the view as you would normally, except you must also us `@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. From a4a84e387c83a735e56d03bf9736d49a07b3c3ae Mon Sep 17 00:00:00 2001 From: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> Date: Mon, 5 Dec 2022 17:56:55 -0300 Subject: [PATCH 025/181] Remove `Bindable` `DynamicProperty` inheritance. (#46) --- Sources/SwiftUINavigation/Bind.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Bind.swift b/Sources/SwiftUINavigation/Bind.swift index 8362a2a4ac..3c1236b463 100644 --- a/Sources/SwiftUINavigation/Bind.swift +++ b/Sources/SwiftUINavigation/Bind.swift @@ -53,7 +53,7 @@ where ModelValue.Value == ViewValue.Value, ModelValue.Value: Equatable { } } -public protocol _Bindable: DynamicProperty { +public protocol _Bindable { associatedtype Value var wrappedValue: Value { get nonmutating set } } From 26e4de100f9acaa07bb65a5c74299158e2ad42a8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 7 Dec 2022 13:12:45 -0500 Subject: [PATCH 026/181] Update Bindings.md --- .../SwiftUINavigation/Documentation.docc/Articles/Bindings.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index f6a6c97a72..63f6a0f729 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -49,7 +49,9 @@ struct SignInView: View { var body: some View { Form { TextField("Email", text: self.$model.email) + .focused(self.$focus, equals: .email) TextField("Password", text: self.$model.password) + .focused(self.$focus, equals: .password) Button("Sign in") { Task { await self.model.signInButtonTapped() From 54e000156ab5569d0dc2e156310dab31102d9227 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 5 Dec 2022 15:29:23 -0500 Subject: [PATCH 027/181] Typo fix --- Sources/_SwiftUINavigationState/TextState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_SwiftUINavigationState/TextState.swift b/Sources/_SwiftUINavigationState/TextState.swift index 55843a7bc8..d3bc899a3b 100644 --- a/Sources/_SwiftUINavigationState/TextState.swift +++ b/Sources/_SwiftUINavigationState/TextState.swift @@ -441,7 +441,7 @@ extension Text { return text.accessibilityLabel( Text(key, tableName: tableName, bundle: bundle, comment: comment)) case .concatenated(_, _): - assertionFailure("`.accessibilityLabel` does not support contcatenated `TextState`") + assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") return text } } else { From b36d135f5e8238bc83548e37105aad2ad2d117c5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 12 Dec 2022 16:42:07 -0500 Subject: [PATCH 028/181] Fix `ButtonState.init` ambiguity (#49) Right now we have two overloads that specify optional `action`s, which means omitting the `action` parameter (or passing `nil`) is ambiguous. --- Sources/_SwiftUINavigationState/ButtonState.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift index c66149fe07..493ab00c0b 100644 --- a/Sources/_SwiftUINavigationState/ButtonState.swift +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -66,11 +66,11 @@ public struct ButtonState: Identifiable { /// - label: A view that describes the purpose of the button's `action`. public init( role: Role? = nil, - action: Action? = nil, + action: Action, label: () -> TextState ) { self.role = role - self.action = action.map(Handler.send) + self.action = .send(action) self.label = label() } From 949a90619d43a57a2a239f8e740be39c0d2cf972 Mon Sep 17 00:00:00 2001 From: Yunosuke Sakai <37182704+ski-u@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:50:43 +0900 Subject: [PATCH 029/181] Fix `Alert.Button.init` action (#50) --- Sources/_SwiftUINavigationState/ButtonState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift index 493ab00c0b..a0bb5c283f 100644 --- a/Sources/_SwiftUINavigationState/ButtonState.swift +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -167,7 +167,7 @@ extension ButtonState: Hashable where Action: Hashable { extension Alert.Button { public init(_ button: ButtonState, action: @escaping (Action) -> Void) { - let action = button.action.map { _ in { button.withAction(action) } } + let action = { button.withAction(action) } switch button.role { case .cancel: self = .cancel(Text(button.label), action: action) From 46acf5ecc1cabdb28d7fe03289f6c8b13a023f52 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 14 Dec 2022 16:26:08 -0500 Subject: [PATCH 030/181] Allow `Binding` to write into `nil` (#54) Fixes #53. --- Sources/SwiftUINavigation/Binding.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Binding.swift b/Sources/SwiftUINavigation/Binding.swift index 710d50f45d..58adb782f3 100644 --- a/Sources/SwiftUINavigation/Binding.swift +++ b/Sources/SwiftUINavigation/Binding.swift @@ -59,7 +59,6 @@ extension Binding { .init( get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, set: { newValue, transaction in - guard self.wrappedValue != nil else { return } self.transaction(transaction).wrappedValue = newValue.map(casePath.embed) } ) From c7891deeac2aeac7c5cb7dba0b5ba1a8d8308551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Sun, 25 Dec 2022 23:07:07 +0900 Subject: [PATCH 031/181] Fix typos in Articles (#60) * Fix typos in Articles * Update Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md Co-authored-by: Stephen Celis --- README.md | 2 +- .../Documentation.docc/Articles/AlertsDialogs.md | 4 ++-- .../Documentation.docc/Articles/DestructuringViews.md | 2 +- .../Documentation.docc/Articles/WhatIsNavigation.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1bd8d45601..b75873f245 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,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.4.1") + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.4.5") ] ``` diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md index ebaae9296a..ec5f4aba11 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -25,7 +25,7 @@ class FeatureModel: ObservableObject { } ``` -Then, when you need to show an alert you can hydate the alert state with a title, message and +Then, when you need to show an alert you can update the alert state with a title, message and buttons: ```swift @@ -107,7 +107,7 @@ This works because all of the types for describing an alert are `Equatable`, inc 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. -In such a case +In such a case: ```swift diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md index 9cf898bae7..fa69513cf7 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md @@ -117,7 +117,7 @@ The "pattern" for the ``IfCaseLet`` is expressed by what is known as a "[case pa 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 indispensible tool for +a value from an enum and "embedding" a value into an enum. They are an indispensable tool for transforming bindings. ### Switch and CaseLet diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md index f9955d5657..1e3996e2d4 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/WhatIsNavigation.md @@ -214,7 +214,7 @@ In order to isolate a specific case of an enum we must make use of our [CasePath 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" -a value from an enum and "embedding" a value into an enum. They are an indispensible 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. From 14bb76a918ac4c11be24110820ae854a90c247c3 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 31 Dec 2022 00:27:27 -0500 Subject: [PATCH 032/181] Update WithState.swift --- Sources/SwiftUINavigation/WithState.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftUINavigation/WithState.swift b/Sources/SwiftUINavigation/WithState.swift index 746928ed40..b4e40e330b 100644 --- a/Sources/SwiftUINavigation/WithState.swift +++ b/Sources/SwiftUINavigation/WithState.swift @@ -19,7 +19,6 @@ import SwiftUI /// /// So, instead you can use ``WithState``: /// -/// /// ```swift /// struct TextField_Previews: PreviewProvider { /// static var previews: some View { From 9e41fd55f9b70c395930a575e8cba35862bbd34e Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Mon, 9 Jan 2023 09:08:54 -0800 Subject: [PATCH 033/181] New case study app showing off swift-dependencies (#61) * wip * wip * wip * wip * wip * wip * wip * lots of helpers * added a ui test * clean up * wip * wip * clean up * wip * wip * wip * wip * clean up * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * xcode 13 fix * clean up * wip * wip * wip * wip * wip * wip * wip * wip Co-authored-by: Stephen Celis --- Examples/Examples.xcodeproj/project.pbxproj | 551 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 43 +- .../xcshareddata/xcschemes/Standups.xcscheme | 99 ++++ Examples/Standups/Readme.md | 77 +++ .../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 + .../Standups/Dependencies/SpeechClient.swift | 193 ++++++ Examples/Standups/Standups/EditStandup.swift | 136 +++++ Examples/Standups/Standups/Helpers.swift | 47 ++ Examples/Standups/Standups/Models.swift | 117 ++++ .../Preview Assets.xcassets/Contents.json | 6 + .../Standups/Standups/RecordMeeting.swift | 394 +++++++++++++ .../Standups/Standups/StandupDetail.swift | 409 +++++++++++++ Examples/Standups/Standups/StandupsApp.swift | 29 + Examples/Standups/Standups/StandupsList.swift | 351 +++++++++++ .../StandupsTests/EditStandupTests.swift | 137 +++++ .../StandupsTests/RecordMeetingTests.swift | 323 ++++++++++ .../StandupsTests/StandupDetailTests.swift | 163 ++++++ .../StandupsTests/StandupsListTests.swift | 215 +++++++ .../StandupsUITests/StandupsListUITests.swift | 49 ++ Makefile | 5 + Package.resolved | 8 +- Package.swift | 4 +- Sources/SwiftUINavigation/Alert.swift | 20 +- .../xcshareddata/swiftpm/Package.resolved | 61 +- Tests/SwiftUINavigationTests/AlertTests.swift | 29 + 46 files changed, 4147 insertions(+), 41 deletions(-) create mode 100644 Examples/Examples.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme create mode 100644 Examples/Standups/Readme.md create mode 100644 Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json create mode 100644 Examples/Standups/Standups/Dependencies/DataManager.swift create mode 100644 Examples/Standups/Standups/Dependencies/OpenSettings.swift create mode 100644 Examples/Standups/Standups/Dependencies/SpeechClient.swift create mode 100644 Examples/Standups/Standups/EditStandup.swift create mode 100644 Examples/Standups/Standups/Helpers.swift create mode 100644 Examples/Standups/Standups/Models.swift create mode 100644 Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Examples/Standups/Standups/RecordMeeting.swift create mode 100644 Examples/Standups/Standups/StandupDetail.swift create mode 100644 Examples/Standups/Standups/StandupsApp.swift create mode 100644 Examples/Standups/Standups/StandupsList.swift create mode 100644 Examples/Standups/StandupsTests/EditStandupTests.swift create mode 100644 Examples/Standups/StandupsTests/RecordMeetingTests.swift create mode 100644 Examples/Standups/StandupsTests/StandupDetailTests.swift create mode 100644 Examples/Standups/StandupsTests/StandupsListTests.swift create mode 100644 Examples/Standups/StandupsUITests/StandupsListUITests.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 66257644b8..b747ae438f 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,13 +7,13 @@ 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 */; }; CA4737FA272F09D00012CAC3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4737F6272F09D00012CAC3 /* Item.swift */; }; CA4737FB272F09D00012CAC3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4737F7272F09D00012CAC3 /* App.swift */; }; CA4737FC272F09D00012CAC3 /* Inventory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4737F8272F09D00012CAC3 /* Inventory.swift */; }; - CA4737FF272F09F20012CAC3 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = CA4737FE272F09F20012CAC3 /* IdentifiedCollections */; }; CA47380B272F0D340012CAC3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA47380A272F0D340012CAC3 /* Assets.xcassets */; }; CA473834272F0D860012CAC3 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47382E272F0D860012CAC3 /* RootView.swift */; }; CA473835272F0D860012CAC3 /* 02-ConfirmationDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47382F272F0D860012CAC3 /* 02-ConfirmationDialogs.swift */; }; @@ -23,18 +23,58 @@ 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 */; }; + CA53F80C295F8BE600DE68FE /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */; }; + 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 */; }; + 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 /* EditStandup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07A42947CFA000293F45 /* EditStandup.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 */; }; 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 */; }; 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 = ""; }; @@ -52,15 +92,35 @@ 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 = ""; }; + 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 /* EditStandup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStandup.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 = ""; }; 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 = ""; }; 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 */ @@ -69,7 +129,7 @@ buildActionMask = 2147483647; files = ( CA4737F4272F09780012CAC3 /* SwiftUINavigation in Frameworks */, - CA4737FF272F09F20012CAC3 /* IdentifiedCollections in Frameworks */, + DCE73E0A2947D090004EE92E /* IdentifiedCollections in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,6 +141,32 @@ ); 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 = ( + CA53F80C295F8BE600DE68FE /* AsyncAlgorithms in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -88,10 +174,11 @@ isa = PBXGroup; children = ( CA4737C3272F090F0012CAC3 /* swiftui-navigation */, - CA4737C9272F095F0012CAC3 /* Inventory */, CA473805272F0D330012CAC3 /* CaseStudies */, - CA473795272F08EF0012CAC3 /* Products */, CA4737F2272F09780012CAC3 /* Frameworks */, + CA4737C9272F095F0012CAC3 /* Inventory */, + CA473795272F08EF0012CAC3 /* Products */, + CA53F807295CC9A900DE68FE /* Standups */, ); sourceTree = ""; }; @@ -100,6 +187,9 @@ children = ( CA4737C8272F095F0012CAC3 /* Inventory.app */, CA473804272F0D330012CAC3 /* CaseStudies.app */, + DC5E07742947CCD700293F45 /* Standups.app */, + DC5E07832947CCD800293F45 /* StandupsTests.xctest */, + CA53F7FA295BEDBD00DE68FE /* StandupsUITests.xctest */, ); name = Products; sourceTree = ""; @@ -148,6 +238,71 @@ path = CaseStudies; sourceTree = ""; }; + CA53F7FB295BEDBE00DE68FE /* StandupsUITests */ = { + isa = PBXGroup; + children = ( + CA53F805295BEE4F00DE68FE /* StandupsListUITests.swift */, + ); + path = StandupsUITests; + sourceTree = ""; + }; + CA53F807295CC9A900DE68FE /* Standups */ = { + isa = PBXGroup; + children = ( + CA53F808295CCA2E00DE68FE /* Readme.md */, + DC5E07752947CCD700293F45 /* Standups */, + DC5E07862947CCD800293F45 /* StandupsTests */, + CA53F7FB295BEDBE00DE68FE /* StandupsUITests */, + ); + path = Standups; + sourceTree = ""; + }; + CA53F809295CE9AD00DE68FE /* Dependencies */ = { + isa = PBXGroup; + children = ( + DCE73E0B2947D163004EE92E /* DataManager.swift */, + CAAA74DF2956956B009A25CA /* OpenSettings.swift */, + DC5E07A82947CFB700293F45 /* SpeechClient.swift */, + ); + path = Dependencies; + sourceTree = ""; + }; + DC5E07752947CCD700293F45 /* Standups */ = { + isa = PBXGroup; + children = ( + DC5E07A42947CFA000293F45 /* EditStandup.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 */ @@ -166,7 +321,7 @@ name = Inventory; packageProductDependencies = ( CA4737F3272F09780012CAC3 /* SwiftUINavigation */, - CA4737FE272F09F20012CAC3 /* IdentifiedCollections */, + DCE73E092947D090004EE92E /* IdentifiedCollections */, ); productName = Inventory; productReference = CA4737C8272F095F0012CAC3 /* Inventory.app */; @@ -192,6 +347,68 @@ 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 = ( + CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */, + ); + productName = StandupsTests; + productReference = DC5E07832947CCD800293F45 /* StandupsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -199,7 +416,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1310; + LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1310; TargetAttributes = { CA4737C7272F095F0012CAC3 = { @@ -210,6 +427,17 @@ 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" */; @@ -222,7 +450,10 @@ ); mainGroup = CA47378B272F08EF0012CAC3; packageReferences = ( - CA4737FD272F09F20012CAC3 /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */, + DCE73E062947D082004EE92E /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, + CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */, ); productRefGroup = CA473795272F08EF0012CAC3 /* Products */; projectDirPath = ""; @@ -230,6 +461,9 @@ targets = ( CA473803272F0D330012CAC3 /* CaseStudies */, CA4737C7272F095F0012CAC3 /* Inventory */, + DC5E07732947CCD700293F45 /* Standups */, + DC5E07822947CCD800293F45 /* StandupsTests */, + CA53F7F9295BEDBD00DE68FE /* StandupsUITests */, ); }; /* End PBXProject section */ @@ -251,6 +485,29 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CA53F7F8295BEDBD00DE68FE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC5E07722947CCD700293F45 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 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 */ @@ -288,8 +545,57 @@ ); 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 */, + CA22CCC22967799600F52F6D /* Helpers.swift in Sources */, + DC5E07A52947CFA000293F45 /* EditStandup.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; @@ -349,6 +655,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -403,6 +710,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; }; name = Release; @@ -527,6 +835,148 @@ }; 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_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_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 */ @@ -557,15 +1007,66 @@ 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 */ - CA4737FD272F09F20012CAC3 /* XCRemoteSwiftPackageReference "swift-identified-collections" */ = { + CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-algorithms"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.3; + }; + }; + CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "http://github.com/pointfreeco/swift-dependencies"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.0; + }; + }; + DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-tagged.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.7.0; + }; + }; + DCE73E062947D082004EE92E /* XCRemoteSwiftPackageReference "swift-identified-collections" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-identified-collections.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.2.0; + minimumVersion = 0.5.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -575,15 +1076,39 @@ isa = XCSwiftPackageProductDependency; productName = SwiftUINavigation; }; - CA4737FE272F09F20012CAC3 /* IdentifiedCollections */ = { + CA47383A272F0DD60012CAC3 /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; - package = CA4737FD272F09F20012CAC3 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; - productName = IdentifiedCollections; + productName = SwiftUINavigation; }; - CA47383A272F0DD60012CAC3 /* SwiftUINavigation */ = { + CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */ = { + isa = XCSwiftPackageProductDependency; + package = CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; + productName = AsyncAlgorithms; + }; + 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" */; + productName = IdentifiedCollections; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = CA47378C272F08EF0012CAC3 /* Project object */; diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 84d7697dcc..adb1c6c530 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,31 @@ { "object": { "pins": [ + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", + "version": "0.9.1" + } + }, { "package": "swift-case-paths", "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "bb436421f57269fbcfe7360735985321585a86e5", - "version": "0.10.1" + "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", + "version": "0.11.0" + } + }, + { + "package": "swift-clocks", + "repositoryURL": "https://github.com/pointfreeco/swift-clocks", + "state": { + "branch": null, + "revision": "20b25ca0dd88ebfb9111ec937814ddc5a8880172", + "version": "0.2.0" } }, { @@ -15,8 +33,8 @@ "repositoryURL": "https://github.com/apple/swift-collections", "state": { "branch": null, - "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", - "version": "1.0.1" + "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version": "1.0.4" } }, { @@ -42,8 +60,17 @@ "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", "state": { "branch": null, - "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", - "version": "0.3.2" + "revision": "a08887de589e3829d488e0b4b707b2ca804b1060", + "version": "0.5.0" + } + }, + { + "package": "swift-tagged", + "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", + "state": { + "branch": null, + "revision": "af06825aaa6adffd636c10a2570b2010c7c07e6a", + "version": "0.9.0" } }, { @@ -51,8 +78,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", - "version": "0.5.0" + "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", + "version": "0.8.0" } } ] diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme new file mode 100644 index 0000000000..0eda908d70 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Standups/Readme.md b/Examples/Standups/Readme.md new file mode 100644 index 0000000000..e9f338b3d3 --- /dev/null +++ b/Examples/Standups/Readme.md @@ -0,0 +1,77 @@ +# 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 +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/Standups/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "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 new file mode 100644 index 0000000000..13613e3ee1 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Standups/Standups/Assets.xcassets/Contents.json b/Examples/Standups/Standups/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/Contents.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 0000000000..849c4cbfca --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..92c0b5a884 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..d9daea3e96 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..f95edce012 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..b20bdf59ea --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..821f22f7de --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..863c8c7235 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..0821af29b5 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..8d29c91c76 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..d6a984fc34 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..b19089a131 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..39065d2a9f --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..91e8248243 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..e42a6726cf --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..a43d657749 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..ce3b3be843 --- /dev/null +++ b/Examples/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 0000000000..1f63ff858b --- /dev/null +++ b/Examples/Standups/Standups/Dependencies/DataManager.swift @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000000..835fe9b876 --- /dev/null +++ b/Examples/Standups/Standups/Dependencies/OpenSettings.swift @@ -0,0 +1,19 @@ +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/SpeechClient.swift b/Examples/Standups/Standups/Dependencies/SpeechClient.swift new file mode 100644 index 0000000000..dc8a3e7d5b --- /dev/null +++ b/Examples/Standups/Standups/Dependencies/SpeechClient.swift @@ -0,0 +1,193 @@ +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/EditStandup.swift b/Examples/Standups/Standups/EditStandup.swift new file mode 100644 index 0000000000..0085f73a0e --- /dev/null +++ b/Examples/Standups/Standups/EditStandup.swift @@ -0,0 +1,136 @@ +import Dependencies +import SwiftUI +import SwiftUINavigation + +class EditStandupModel: 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 EditStandupView: View { + @FocusState var focus: EditStandupModel.Field? + @ObservedObject var model: EditStandupModel + + 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 EditStandup_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + EditStandupView(model: EditStandupModel(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 { + EditStandupView( + model: EditStandupModel( + focus: .attendee(Standup.mock.attendees[3].id), + standup: .mock + ) + ) + } + } + .previewDisplayName("4th attendee focused") + } +} diff --git a/Examples/Standups/Standups/Helpers.swift b/Examples/Standups/Standups/Helpers.swift new file mode 100644 index 0000000000..d078c5c4d9 --- /dev/null +++ b/Examples/Standups/Standups/Helpers.swift @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000000..462b7ec6f6 --- /dev/null +++ b/Examples/Standups/Standups/Models.swift @@ -0,0 +1,117 @@ +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 new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Standups/Standups/RecordMeeting.swift b/Examples/Standups/Standups/RecordMeeting.swift new file mode 100644 index 0000000000..2e02fd88ed --- /dev/null +++ b/Examples/Standups/Standups/RecordMeeting.swift @@ -0,0 +1,394 @@ +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(\.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.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 + } + } + + func task() async { + 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 + } + } + } +} + +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("Seconds Elapsed") + .font(.caption) + Label("\(self.secondsElapsed)", systemImage: "hourglass.bottomhalf.fill") + } + Spacer() + VStack(alignment: .trailing) { + Text("Seconds 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 new file mode 100644 index 0000000000..34f4b6064a --- /dev/null +++ b/Examples/Standups/Standups/StandupDetail.swift @@ -0,0 +1,409 @@ +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(EditStandupModel) + 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 meetingTapping(_ 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() + } + } + + func editButtonTapped() { + self.destination = .edit( + withDependencies(from: self) { + EditStandupModel(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.meetingTapping(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 { + EditStandupView(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 recongition 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 recongition 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 recongition restricted") + } +} diff --git a/Examples/Standups/Standups/StandupsApp.swift b/Examples/Standups/Standups/StandupsApp.swift new file mode 100644 index 0000000000..518fe6d814 --- /dev/null +++ b/Examples/Standups/Standups/StandupsApp.swift @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000000..282f9d7f5a --- /dev/null +++ b/Examples/Standups/Standups/StandupsList.swift @@ -0,0 +1,351 @@ +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(EditStandupModel) + case alert(AlertState) + case detail(StandupDetailModel) + } + enum AlertAction { + case confirmLoadMockData + case dismissFailedAlert + } + + 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) { + EditStandupModel(standup: Standup(id: Standup.ID(self.uuid()))) + } + ) + } + + func dismissAddStandupButtonTapped() { + self.destination = nil + } + + func confirmAddStandupButtonTapped() { + defer { self.destination = nil } + + guard case let .add(editStandupModel) = self.destination + else { return } + var standup = editStandupModel.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 .dismissFailedAlert: + self.standups = [] + } + } +} + +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 { + EditStandupView(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( + EditStandupModel( + 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 new file mode 100644 index 0000000000..e52751185c --- /dev/null +++ b/Examples/Standups/StandupsTests/EditStandupTests.swift @@ -0,0 +1,137 @@ +import Dependencies +import XCTest +import CustomDump +@testable import Standups + +@MainActor +final class EditStandupTests: XCTestCase { + func testAddAttendee() { + let model = withDependencies { + $0.uuid = .incrementing + } operation: { + EditStandupModel( + 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: { + EditStandupModel( + 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 EditStandupModel( + 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 new file mode 100644 index 0000000000..a427609db6 --- /dev/null +++ b/Examples/Standups/StandupsTests/RecordMeetingTests.swift @@ -0,0 +1,323 @@ +import AsyncAlgorithms +import CasePaths +import Dependencies +import XCTest +import CustomDump +@testable import Standups + +@MainActor +final class RecordMeetingTests: XCTestCase { + func testTimer() async throws { + let clock = TestClock() + + let model = withDependencies { + $0.continuousClock = clock + $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)) + + await clock.advance(by: .seconds(1)) + XCTAssertEqual(model.speakerIndex, 2) + XCTAssertEqual(model.durationRemaining, .seconds(1)) + + await clock.advance(by: .seconds(1)) + XCTAssertEqual(model.speakerIndex, 2) + XCTAssertEqual(model.durationRemaining, .seconds(0)) + + await task.value + + self.wait(for: [onMeetingFinishedExpectation], timeout: 0) + XCTAssertEqual(model.isDismissed, true) + } + + func testRecordTranscript() async throws { + let model = withDependencies { + $0.continuousClock = ImmediateClock() + $0.speechClient.authorizationStatus = { .authorized } + $0.speechClient.startTask = { _ in + [ + SpeechRecognitionResult( + bestTranscription: Transcription(formattedString: "I completed the project"), + isFinal: true + ) + ].async.eraseToThrowingStream() + } + } 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.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.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 model = withDependencies { + $0.continuousClock = clock + $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)) + + model.nextButtonTapped() + + XCTAssertEqual(model.speakerIndex, 2) + XCTAssertEqual(model.durationRemaining, .seconds(1)) + + 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)) + + await model.alertButtonTapped(.confirmSave) + + self.wait(for: [onMeetingFinishedExpectation], timeout: 0) + XCTAssertEqual(model.isDismissed, true) + + task.cancel() + await task.value + } + + func testSpeechRecognitionFailure_Continue() async throws { + let model = withDependencies { + $0.continuousClock = ImmediateClock() + $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.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 new file mode 100644 index 0000000000..ed1186f718 --- /dev/null +++ b/Examples/Standups/StandupsTests/StandupDetailTests.swift @@ -0,0 +1,163 @@ +import AsyncAlgorithms +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.speechClient.authorizationStatus = { .authorized } + $0.speechClient.startTask = { _ in + [ + SpeechRecognitionResult( + bestTranscription: Transcription(formattedString: "I completed the project"), + isFinal: true + ) + ].async.eraseToThrowingStream() + } + $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 new file mode 100644 index 0000000000..78c8cacab9 --- /dev/null +++ b/Examples/Standups/StandupsTests/StandupsListTests.swift @@ -0,0 +1,215 @@ +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( + EditStandupModel( + 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( + 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.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 new file mode 100644 index 0000000000..ad25792f17 --- /dev/null +++ b/Examples/Standups/StandupsUITests/StandupsListUITests.swift @@ -0,0 +1,49 @@ +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/Makefile b/Makefile index 71f712fd5e..f0a2a7715b 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,11 @@ test: -workspace SwiftUINavigation.xcworkspace \ -scheme SwiftUINavigation \ -destination platform="$(PLATFORM_WATCHOS)" +test-examples: + xcodebuild test \ + -workspace SwiftUINavigation.xcworkspace \ + -scheme Standups \ + -destination platform="$(PLATFORM_IOS)" DOC_WARNINGS := $(shell xcodebuild clean docbuild \ -scheme SwiftUINavigation \ diff --git a/Package.resolved b/Package.resolved index 1849640f68..7b816a8b17 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "15bba50ebf3a2065388c8d12210debe4f6ada202", - "version": "0.10.0" + "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", + "version": "0.11.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", - "version": "0.5.0" + "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", + "version": "0.8.0" } } ] diff --git a/Package.swift b/Package.swift index 3e008a781a..b58dac9d94 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.10.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.5.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.8.0"), ], targets: [ .target( diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 278b5ce3c5..3bf514a754 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -118,7 +118,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action: @escaping (Value) -> Void = { (_: Never) in fatalError() } + action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -126,7 +126,11 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0, action: action) + Button($0) { action in + Task { + await handler(action) + } + } } }, message: { $0.message.map { Text($0) } } @@ -154,7 +158,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action: @escaping (Value) -> Void = { (_: Never) in fatalError() } + action: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: action) } @@ -162,7 +166,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action: @escaping (Value) -> Void + action handler: @escaping (Value) async -> Void ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -170,7 +174,11 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0, action: action) + Button($0) { action in + Task { + await handler(action) + } + } } }, message: { $0.message.map { Text($0) } } @@ -198,7 +206,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action: @escaping (Value) -> Void + action: @escaping (Value) async -> Void ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: action) } diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55a6c453e4..16c4a75ccd 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,40 @@ { "object": { "pins": [ + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", + "version": "0.9.1" + } + }, + { + "package": "AsyncAlgorithms", + "repositoryURL": "https://github.com/apple/swift-async-algorithms", + "state": { + "branch": null, + "revision": "aed5422380244498344a036b8d94e27f370d9a22", + "version": "0.0.4" + } + }, { "package": "swift-case-paths", "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "15bba50ebf3a2065388c8d12210debe4f6ada202", - "version": "0.10.0" + "revision": "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", + "version": "0.11.0" + } + }, + { + "package": "swift-clocks", + "repositoryURL": "https://github.com/pointfreeco/swift-clocks", + "state": { + "branch": null, + "revision": "20b25ca0dd88ebfb9111ec937814ddc5a8880172", + "version": "0.2.0" } }, { @@ -15,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-collections", "state": { "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" + "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version": "1.0.4" } }, { @@ -28,6 +55,15 @@ "version": "0.6.1" } }, + { + "package": "swift-dependencies", + "repositoryURL": "http://github.com/pointfreeco/swift-dependencies", + "state": { + "branch": null, + "revision": "e49dfe4d9e4c5c06f3334361360b801aef41631c", + "version": "0.1.1" + } + }, { "package": "SwiftDocCPlugin", "repositoryURL": "https://github.com/apple/swift-docc-plugin", @@ -42,8 +78,17 @@ "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections.git", "state": { "branch": null, - "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36", - "version": "0.4.1" + "revision": "fd34c544ad27f3ba6b19142b348005bfa85b6005", + "version": "0.6.0" + } + }, + { + "package": "swift-tagged", + "repositoryURL": "https://github.com/pointfreeco/swift-tagged.git", + "state": { + "branch": null, + "revision": "af06825aaa6adffd636c10a2570b2010c7c07e6a", + "version": "0.9.0" } }, { @@ -51,8 +96,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", - "version": "0.5.0" + "revision": "a9daebf0bf65981fd159c885d504481a65a75f02", + "version": "0.8.0" } } ] diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index b0152c4f13..44505addaf 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -87,3 +87,32 @@ final class AlertTests: XCTestCase { } } } + +import SwiftUI + +// 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) + } + } + + private func alertButtonTapped(_ action: AlertAction) async { + switch action { + case .confirm: + break + case .deny: + break + } + } +} From 83934f8649f63a2ff19bacd328baca9448a3b671 Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Mon, 9 Jan 2023 17:19:30 +0000 Subject: [PATCH 034/181] Run swift-format --- Examples/Standups/Standups/Helpers.swift | 2 +- Examples/Standups/Standups/Models.swift | 2 +- Examples/Standups/Standups/StandupsList.swift | 9 +++++---- Examples/Standups/StandupsTests/EditStandupTests.swift | 9 +++++---- Examples/Standups/StandupsTests/RecordMeetingTests.swift | 6 +++--- Examples/Standups/StandupsTests/StandupDetailTests.swift | 2 +- Tests/SwiftUINavigationTests/AlertTests.swift | 3 +-- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Examples/Standups/Standups/Helpers.swift b/Examples/Standups/Standups/Helpers.swift index d078c5c4d9..0480f3594e 100644 --- a/Examples/Standups/Standups/Helpers.swift +++ b/Examples/Standups/Standups/Helpers.swift @@ -34,7 +34,7 @@ struct Preview: View { struct Preview_Previews: PreviewProvider { static var previews: some View { Preview( - message: + 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 \ diff --git a/Examples/Standups/Standups/Models.swift b/Examples/Standups/Standups/Models.swift index 462b7ec6f6..3c2f537985 100644 --- a/Examples/Standups/Standups/Models.swift +++ b/Examples/Standups/Standups/Models.swift @@ -49,7 +49,7 @@ enum Theme: String, CaseIterable, Equatable, Hashable, Identifiable, Codable { var accentColor: Color { switch self { case .bubblegum, .buttercup, .lavender, .orange, .periwinkle, .poppy, .seafoam, .sky, .tan, - .teal, .yellow: + .teal, .yellow: return .black case .indigo, .magenta, .navy, .oxblood, .purple: return .white diff --git a/Examples/Standups/Standups/StandupsList.swift b/Examples/Standups/Standups/StandupsList.swift index 282f9d7f5a..decb9429eb 100644 --- a/Examples/Standups/Standups/StandupsList.swift +++ b/Examples/Standups/Standups/StandupsList.swift @@ -117,7 +117,7 @@ final class StandupsListModel: ObservableObject { self.standups = [ .mock, .designMock, - .engineeringMock + .engineeringMock, ] } @@ -138,7 +138,8 @@ extension AlertState where Action == StandupsListModel.AlertAction { TextState("No") } } message: { - TextState(""" + TextState( + """ Unfortunately your past data failed to load. Would you like to load some mock data to play \ around with? """) @@ -259,7 +260,7 @@ struct StandupsList_Previews: PreviewProvider { initialData: try! JSONEncoder().encode([ Standup.mock, .engineeringMock, - .designMock + .designMock, ]) ) } operation: { @@ -302,7 +303,7 @@ struct StandupsList_Previews: PreviewProvider { initialData: try! JSONEncoder().encode([ Standup.mock, .engineeringMock, - .designMock + .designMock, ]) ) } operation: { diff --git a/Examples/Standups/StandupsTests/EditStandupTests.swift b/Examples/Standups/StandupsTests/EditStandupTests.swift index e52751185c..91abeaf538 100644 --- a/Examples/Standups/StandupsTests/EditStandupTests.swift +++ b/Examples/Standups/StandupsTests/EditStandupTests.swift @@ -1,6 +1,7 @@ +import CustomDump import Dependencies import XCTest -import CustomDump + @testable import Standups @MainActor @@ -21,7 +22,7 @@ final class EditStandupTests: XCTestCase { XCTAssertNoDifference( model.standup.attendees, [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000000")!), + Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000000")!) ] ) @@ -117,7 +118,7 @@ final class EditStandupTests: XCTestCase { XCTAssertNoDifference( model.standup.attendees, [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!), + Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000001")!) ] ) @@ -130,7 +131,7 @@ final class EditStandupTests: XCTestCase { XCTAssertNoDifference( model.standup.attendees, [ - Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000004")!), + Attendee(id: Attendee.ID(uuidString: "00000000-0000-0000-0000-000000000004")!) ] ) } diff --git a/Examples/Standups/StandupsTests/RecordMeetingTests.swift b/Examples/Standups/StandupsTests/RecordMeetingTests.swift index a427609db6..b083caca23 100644 --- a/Examples/Standups/StandupsTests/RecordMeetingTests.swift +++ b/Examples/Standups/StandupsTests/RecordMeetingTests.swift @@ -1,15 +1,16 @@ import AsyncAlgorithms import CasePaths +import CustomDump import Dependencies import XCTest -import CustomDump + @testable import Standups @MainActor final class RecordMeetingTests: XCTestCase { func testTimer() async throws { let clock = TestClock() - + let model = withDependencies { $0.continuousClock = clock $0.speechClient.authorizationStatus = { .denied } @@ -320,4 +321,3 @@ final class RecordMeetingTests: XCTestCase { await task.value } } - diff --git a/Examples/Standups/StandupsTests/StandupDetailTests.swift b/Examples/Standups/StandupsTests/StandupDetailTests.swift index ed1186f718..cd5ed73659 100644 --- a/Examples/Standups/StandupsTests/StandupDetailTests.swift +++ b/Examples/Standups/StandupsTests/StandupDetailTests.swift @@ -35,7 +35,7 @@ final class StandupDetailTests: XCTestCase { XCTAssertNoDifference(alert, .speechRecognitionDenied) } - + func testOpenSettings() async { let settingsOpened = LockIsolated(false) let model = withDependencies { diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index 44505addaf..d57e887740 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -1,4 +1,5 @@ import CustomDump +import SwiftUI import SwiftUINavigation import XCTest @@ -88,8 +89,6 @@ final class AlertTests: XCTestCase { } } -import SwiftUI - // 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, *) From ddc01cdcddfd30ef7a966049b2e1d251e224ad93 Mon Sep 17 00:00:00 2001 From: John Flanagan <59904321+john-flanagan@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:06:17 -0600 Subject: [PATCH 035/181] Fix typo (#62) --- Examples/Standups/Standups/StandupDetail.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Standups/Standups/StandupDetail.swift b/Examples/Standups/Standups/StandupDetail.swift index 34f4b6064a..e9fc92e605 100644 --- a/Examples/Standups/Standups/StandupDetail.swift +++ b/Examples/Standups/Standups/StandupDetail.swift @@ -366,7 +366,7 @@ struct StandupDetail_Previews: PreviewProvider { ) } } - .previewDisplayName("Speech recongition failed") + .previewDisplayName("Speech recognition failed") Preview( message: """ @@ -385,7 +385,7 @@ struct StandupDetail_Previews: PreviewProvider { ) } } - .previewDisplayName("Speech recongition denied") + .previewDisplayName("Speech recognition denied") Preview( message: """ @@ -404,6 +404,6 @@ struct StandupDetail_Previews: PreviewProvider { ) } } - .previewDisplayName("Speech recongition restricted") + .previewDisplayName("Speech recognition restricted") } } From 6155d6a77c94dce9284da22d9e0b76714d85333e Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Thu, 12 Jan 2023 14:55:39 -0800 Subject: [PATCH 036/181] Update Readme.md --- Examples/Standups/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Standups/Readme.md b/Examples/Standups/Readme.md index e9f338b3d3..52dec1047c 100644 --- a/Examples/Standups/Readme.md +++ b/Examples/Standups/Readme.md @@ -26,7 +26,7 @@ The inspiration for this application comes Apple's [Scrumdinger][scrumdinger] tu 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 -to disk. +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 From a7cd4275fb1689aedd2f073ab290b41d173fd639 Mon Sep 17 00:00:00 2001 From: Daeyoung Kim Date: Fri, 13 Jan 2023 23:13:10 +0900 Subject: [PATCH 037/181] Fix a code typo in DestructuringViews article (#63) Signed-off-by: Daeyoung Kim Signed-off-by: Daeyoung Kim --- .../Documentation.docc/Articles/DestructuringViews.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md index fa69513cf7..b6601391f2 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md @@ -104,7 +104,7 @@ struct EditView: View { } else: { Text("\(self.string)") Button("Edit") { - self.editableString = self.string + self.editableString = .active(self.string) } } .buttonStyle(.borderless) From f3ccc0b3a104d4afc911d8e7f41c009e3187c45d Mon Sep 17 00:00:00 2001 From: Marc Bauer Date: Mon, 16 Jan 2023 00:18:09 +0700 Subject: [PATCH 038/181] Add NSMicrophoneUsageDescription to allow running Standups example on device (#66) --- Examples/Examples.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index b747ae438f..387b3a8a07 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -883,6 +883,7 @@ 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; @@ -915,6 +916,7 @@ 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; From 45a77a21ba0b09de3d80b4dfe1d85692b83643d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Thu, 19 Jan 2023 03:47:31 +0900 Subject: [PATCH 039/181] Gardening some examples (#64) * Gardening some examples * Revert variable names --- .../Documentation.docc/Articles/Bindings.md | 2 +- .../Articles/DestructuringViews.md | 12 ++++++------ Sources/SwiftUINavigation/IfCaseLet.swift | 2 +- Sources/SwiftUINavigation/IfLet.swift | 2 +- Sources/SwiftUINavigation/Switch.swift | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md index 63f6a0f729..99c674c0fd 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md @@ -14,7 +14,7 @@ object and synchronizing it to view state with the `bind` view modifier that shi 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 implement like so: +to refocus the email field. The model can be implemented like so: ```swift class SignInModel: ObservableObject { diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md index b6601391f2..d099b277b6 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/DestructuringViews.md @@ -36,7 +36,7 @@ struct EditView: View { } } } else: { - Text("\(self.string)") + Text(self.string) Button("Edit") { self.editableString = self.string } @@ -47,7 +47,7 @@ struct EditView: View { } ``` -This is the most optimal way to model this domain. Without the ability to deriving a +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: @@ -94,15 +94,15 @@ struct EditView: View { TextField("Edit string", text: $string) HStack { Button("Cancel") { - self.editableString = nil + self.editableString = .inactive } Button("Save") { self.string = string - self.editableString = nil + self.editableString = .inactive } } } else: { - Text("\(self.string)") + Text(self.string) Button("Edit") { self.editableString = .active(self.string) } @@ -136,7 +136,7 @@ enum ItemStatus { case outOfStock(isOnBackOrder: Bool) } -struct InventoryItemView { +struct InventoryItemView: View { @State var status: ItemStatus var body: some View { diff --git a/Sources/SwiftUINavigation/IfCaseLet.swift b/Sources/SwiftUINavigation/IfCaseLet.swift index 95f108942a..ba0e69e3ba 100644 --- a/Sources/SwiftUINavigation/IfCaseLet.swift +++ b/Sources/SwiftUINavigation/IfCaseLet.swift @@ -15,7 +15,7 @@ import SwiftUI /// case outOfStock(isOnBackOrder: Bool) /// } /// -/// struct InventoryItemView { +/// struct InventoryItemView: View { /// @State var status: ItemStatus /// /// var body: some View { diff --git a/Sources/SwiftUINavigation/IfLet.swift b/Sources/SwiftUINavigation/IfLet.swift index 33e6952588..429f624d98 100644 --- a/Sources/SwiftUINavigation/IfLet.swift +++ b/Sources/SwiftUINavigation/IfLet.swift @@ -11,7 +11,7 @@ import SwiftUI /// optional binding. /// /// ```swift -/// struct InventoryItemView { +/// struct InventoryItemView: View { /// @State var quantity: Int? /// /// var body: some View { diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index d4d702da83..1d34b057d5 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -15,7 +15,7 @@ import SwiftUI /// case outOfStock(isOnBackOrder: Bool) /// } /// -/// struct InventoryItemView { +/// struct InventoryItemView: View { /// @State var status: ItemStatus /// /// var body: some View { From 5e97ce756293f941c2c336693283493a965458f6 Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:02:00 -0800 Subject: [PATCH 040/181] Small improvements (#67) * wip * wip * wip * simplfiy * wip * wip * wip --- Examples/Examples.xcodeproj/project.pbxproj | 41 ++++++++-------- Examples/Standups/Resources/ding.wav | Bin 0 -> 535904 bytes .../Dependencies/SoundEffectClient.swift | 45 ++++++++++++++++++ .../Standups/Standups/RecordMeeting.swift | 14 ++++-- .../Standups/Standups/StandupDetail.swift | 10 ++-- .../{EditStandup.swift => StandupForm.swift} | 16 +++---- Examples/Standups/Standups/StandupsList.swift | 16 +++---- .../StandupsTests/EditStandupTests.swift | 8 ++-- .../StandupsTests/RecordMeetingTests.swift | 34 ++++++++++--- .../StandupsTests/StandupDetailTests.swift | 17 ++++--- .../StandupsTests/StandupsListTests.swift | 11 +---- .../xcshareddata/swiftpm/Package.resolved | 9 ---- 12 files changed, 139 insertions(+), 82 deletions(-) create mode 100644 Examples/Standups/Resources/ding.wav create mode 100644 Examples/Standups/Standups/Dependencies/SoundEffectClient.swift rename Examples/Standups/Standups/{EditStandup.swift => StandupForm.swift} (90%) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 387b3a8a07..f39b997361 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 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 */; }; - CA53F80C295F8BE600DE68FE /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */; }; 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 */; }; @@ -35,11 +34,13 @@ 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 /* EditStandup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5E07A42947CFA000293F45 /* EditStandup.swift */; }; + 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 */; }; @@ -104,13 +105,15 @@ 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 /* EditStandup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStandup.swift; sourceTree = ""; }; + 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 = ""; }; @@ -163,7 +166,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CA53F80C295F8BE600DE68FE /* AsyncAlgorithms in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -250,6 +252,7 @@ isa = PBXGroup; children = ( CA53F808295CCA2E00DE68FE /* Readme.md */, + CADF861F297765F000B7695B /* Resources */, DC5E07752947CCD700293F45 /* Standups */, DC5E07862947CCD800293F45 /* StandupsTests */, CA53F7FB295BEDBE00DE68FE /* StandupsUITests */, @@ -262,15 +265,24 @@ 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 /* EditStandup.swift */, + DC5E07A42947CFA000293F45 /* StandupForm.swift */, DC5E07AA2947CFCA00293F45 /* Models.swift */, DC5E07AC2947CFD300293F45 /* RecordMeeting.swift */, DC5E07782947CCD700293F45 /* StandupDetail.swift */, @@ -403,7 +415,6 @@ ); name = StandupsTests; packageProductDependencies = ( - CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */, ); productName = StandupsTests; productReference = DC5E07832947CCD800293F45 /* StandupsTests.xctest */; @@ -452,7 +463,6 @@ packageReferences = ( DCE73E032947D063004EE92E /* XCRemoteSwiftPackageReference "swift-tagged" */, DCE73E062947D082004EE92E /* XCRemoteSwiftPackageReference "swift-identified-collections" */, - CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */, ); productRefGroup = CA473795272F08EF0012CAC3 /* Products */; @@ -496,6 +506,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + CADF8621297765F000B7695B /* ding.wav in Resources */, DC5E077E2947CCD800293F45 /* Preview Assets.xcassets in Resources */, DC5E077B2947CCD800293F45 /* Assets.xcassets in Resources */, ); @@ -560,8 +571,9 @@ 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 /* EditStandup.swift in Sources */, + DC5E07A52947CFA000293F45 /* StandupForm.swift in Sources */, DC5E07AB2947CFCA00293F45 /* Models.swift in Sources */, DC5E07AD2947CFD300293F45 /* RecordMeeting.swift in Sources */, DC5E07A92947CFB700293F45 /* SpeechClient.swift in Sources */, @@ -1039,14 +1051,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-async-algorithms"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.3; - }; - }; CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "http://github.com/pointfreeco/swift-dependencies"; @@ -1082,11 +1086,6 @@ isa = XCSwiftPackageProductDependency; productName = SwiftUINavigation; }; - CA53F80B295F8BE600DE68FE /* AsyncAlgorithms */ = { - isa = XCSwiftPackageProductDependency; - package = CA53F80A295F8BE600DE68FE /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; - productName = AsyncAlgorithms; - }; CA6453992968A06E00802931 /* Dependencies */ = { isa = XCSwiftPackageProductDependency; package = CA6453982968A06E00802931 /* XCRemoteSwiftPackageReference "swift-dependencies" */; diff --git a/Examples/Standups/Resources/ding.wav b/Examples/Standups/Resources/ding.wav new file mode 100644 index 0000000000000000000000000000000000000000..5831df269dda704add693c6ff8468400559e22de GIT binary patch literal 535904 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

7{1zAqfq$7$2 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 literal 0 HcmV?d00001 diff --git a/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift b/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift new file mode 100644 index 0000000000..6596c4e330 --- /dev/null +++ b/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift @@ -0,0 +1,45 @@ +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/RecordMeeting.swift b/Examples/Standups/Standups/RecordMeeting.swift index 2e02fd88ed..bf569945d9 100644 --- a/Examples/Standups/Standups/RecordMeeting.swift +++ b/Examples/Standups/Standups/RecordMeeting.swift @@ -15,6 +15,7 @@ class RecordMeetingModel: ObservableObject { private var transcript = "" @Dependency(\.continuousClock) var clock + @Dependency(\.soundEffectClient) var soundEffectClient @Dependency(\.speechClient) var speechClient var onMeetingFinished: (String) async -> Void = unimplemented( @@ -58,6 +59,7 @@ class RecordMeetingModel: ObservableObject { } self.speakerIndex += 1 + self.soundEffectClient.play() self.secondsElapsed = self.speakerIndex * Int(self.standup.durationPerAttendee.components.seconds) } @@ -77,6 +79,8 @@ class RecordMeetingModel: ObservableObject { } func task() async { + self.soundEffectClient.load("ding.wav") + let authorization = await self.speechClient.authorizationStatus() == .notDetermined ? self.speechClient.requestAuthorization() @@ -127,6 +131,7 @@ class RecordMeetingModel: ObservableObject { break } self.speakerIndex += 1 + self.soundEffectClient.play() } } } @@ -230,13 +235,16 @@ struct MeetingHeaderView: View { .progressViewStyle(MeetingProgressViewStyle(theme: self.theme)) HStack { VStack(alignment: .leading) { - Text("Seconds Elapsed") + Text("Time Elapsed") .font(.caption) - Label("\(self.secondsElapsed)", systemImage: "hourglass.bottomhalf.fill") + Label( + Duration.seconds(self.secondsElapsed).formatted(.units()), + systemImage: "hourglass.bottomhalf.fill" + ) } Spacer() VStack(alignment: .trailing) { - Text("Seconds Remaining") + Text("Time Remaining") .font(.caption) Label(self.durationRemaining.formatted(.units()), systemImage: "hourglass.tophalf.fill") .font(.body.monospacedDigit()) diff --git a/Examples/Standups/Standups/StandupDetail.swift b/Examples/Standups/Standups/StandupDetail.swift index e9fc92e605..9cc942b59b 100644 --- a/Examples/Standups/Standups/StandupDetail.swift +++ b/Examples/Standups/Standups/StandupDetail.swift @@ -23,7 +23,7 @@ class StandupDetailModel: ObservableObject { enum Destination { case alert(AlertState) - case edit(EditStandupModel) + case edit(StandupFormModel) case meeting(Meeting) case record(RecordMeetingModel) } @@ -46,7 +46,7 @@ class StandupDetailModel: ObservableObject { self.standup.meetings.remove(atOffsets: indices) } - func meetingTapping(_ meeting: Meeting) { + func meetingTapped(_ meeting: Meeting) { self.destination = .meeting(meeting) } @@ -75,7 +75,7 @@ class StandupDetailModel: ObservableObject { func editButtonTapped() { self.destination = .edit( withDependencies(from: self) { - EditStandupModel(standup: self.standup) + StandupFormModel(standup: self.standup) } ) } @@ -175,7 +175,7 @@ struct StandupDetailView: View { Section { ForEach(self.model.standup.meetings) { meeting in Button { - self.model.meetingTapping(meeting) + self.model.meetingTapped(meeting) } label: { HStack { Image(systemName: "calendar") @@ -237,7 +237,7 @@ struct StandupDetailView: View { case: /StandupDetailModel.Destination.edit ) { $editModel in NavigationStack { - EditStandupView(model: editModel) + StandupFormView(model: editModel) .navigationTitle(self.model.standup.title) .toolbar { ToolbarItem(placement: .cancellationAction) { diff --git a/Examples/Standups/Standups/EditStandup.swift b/Examples/Standups/Standups/StandupForm.swift similarity index 90% rename from Examples/Standups/Standups/EditStandup.swift rename to Examples/Standups/Standups/StandupForm.swift index 0085f73a0e..5aa9698e9e 100644 --- a/Examples/Standups/Standups/EditStandup.swift +++ b/Examples/Standups/Standups/StandupForm.swift @@ -2,7 +2,7 @@ import Dependencies import SwiftUI import SwiftUINavigation -class EditStandupModel: ObservableObject { +class StandupFormModel: ObservableObject { @Published var focus: Field? @Published var standup: Standup @@ -42,9 +42,9 @@ class EditStandupModel: ObservableObject { } } -struct EditStandupView: View { - @FocusState var focus: EditStandupModel.Field? - @ObservedObject var model: EditStandupModel +struct StandupFormView: View { + @FocusState var focus: StandupFormModel.Field? + @ObservedObject var model: StandupFormModel var body: some View { Form { @@ -109,10 +109,10 @@ extension Duration { } } -struct EditStandup_Previews: PreviewProvider { +struct StandupForm_Previews: PreviewProvider { static var previews: some View { NavigationStack { - EditStandupView(model: EditStandupModel(standup: .mock)) + StandupFormView(model: StandupFormModel(standup: .mock)) } .previewDisplayName("Edit") @@ -123,8 +123,8 @@ struct EditStandup_Previews: PreviewProvider { """ ) { NavigationStack { - EditStandupView( - model: EditStandupModel( + StandupFormView( + model: StandupFormModel( focus: .attendee(Standup.mock.attendees[3].id), standup: .mock ) diff --git a/Examples/Standups/Standups/StandupsList.swift b/Examples/Standups/Standups/StandupsList.swift index decb9429eb..59959f0184 100644 --- a/Examples/Standups/Standups/StandupsList.swift +++ b/Examples/Standups/Standups/StandupsList.swift @@ -19,13 +19,12 @@ final class StandupsListModel: ObservableObject { @Dependency(\.uuid) var uuid enum Destination { - case add(EditStandupModel) + case add(StandupFormModel) case alert(AlertState) case detail(StandupDetailModel) } enum AlertAction { case confirmLoadMockData - case dismissFailedAlert } init( @@ -57,7 +56,7 @@ final class StandupsListModel: ObservableObject { func addStandupButtonTapped() { self.destination = .add( withDependencies(from: self) { - EditStandupModel(standup: Standup(id: Standup.ID(self.uuid()))) + StandupFormModel(standup: Standup(id: Standup.ID(self.uuid()))) } ) } @@ -69,9 +68,9 @@ final class StandupsListModel: ObservableObject { func confirmAddStandupButtonTapped() { defer { self.destination = nil } - guard case let .add(editStandupModel) = self.destination + guard case let .add(standupFormModel) = self.destination else { return } - var standup = editStandupModel.standup + var standup = standupFormModel.standup standup.attendees.removeAll { attendee in attendee.name.allSatisfy(\.isWhitespace) @@ -120,9 +119,6 @@ final class StandupsListModel: ObservableObject { .engineeringMock, ] } - - case .dismissFailedAlert: - self.standups = [] } } } @@ -174,7 +170,7 @@ struct StandupsList: View { case: /StandupsListModel.Destination.add ) { $model in NavigationStack { - EditStandupView(model: model) + StandupFormView(model: model) .navigationTitle("New standup") .toolbar { ToolbarItem(placement: .cancellationAction) { @@ -338,7 +334,7 @@ struct StandupsList_Previews: PreviewProvider { let _ = standup.attendees.append(lastAttendee) return StandupsListModel( destination: .add( - EditStandupModel( + StandupFormModel( focus: .attendee(lastAttendee.id), standup: standup ) diff --git a/Examples/Standups/StandupsTests/EditStandupTests.swift b/Examples/Standups/StandupsTests/EditStandupTests.swift index 91abeaf538..2063f05156 100644 --- a/Examples/Standups/StandupsTests/EditStandupTests.swift +++ b/Examples/Standups/StandupsTests/EditStandupTests.swift @@ -5,12 +5,12 @@ import XCTest @testable import Standups @MainActor -final class EditStandupTests: XCTestCase { +final class StandupFormTests: XCTestCase { func testAddAttendee() { let model = withDependencies { $0.uuid = .incrementing } operation: { - EditStandupModel( + StandupFormModel( standup: Standup( id: Standup.ID(), attendees: [], @@ -41,7 +41,7 @@ final class EditStandupTests: XCTestCase { let model = withDependencies { $0.uuid = .incrementing } operation: { - EditStandupModel( + StandupFormModel( standup: Standup( id: Standup.ID(), attendees: [], @@ -66,7 +66,7 @@ final class EditStandupTests: XCTestCase { } operation: { @Dependency(\.uuid) var uuid - return EditStandupModel( + return StandupFormModel( standup: Standup( id: Standup.ID(), attendees: [ diff --git a/Examples/Standups/StandupsTests/RecordMeetingTests.swift b/Examples/Standups/StandupsTests/RecordMeetingTests.swift index b083caca23..519ba727ab 100644 --- a/Examples/Standups/StandupsTests/RecordMeetingTests.swift +++ b/Examples/Standups/StandupsTests/RecordMeetingTests.swift @@ -1,4 +1,3 @@ -import AsyncAlgorithms import CasePaths import CustomDump import Dependencies @@ -10,9 +9,12 @@ import XCTest 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( @@ -50,32 +52,40 @@ final class RecordMeetingTests: XCTestCase { 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 - [ - SpeechRecognitionResult( - bestTranscription: Transcription(formattedString: "I completed the project"), - isFinal: true + AsyncThrowingStream { continuation in + continuation.yield( + SpeechRecognitionResult( + bestTranscription: Transcription(formattedString: "I completed the project"), + isFinal: true + ) ) - ].async.eraseToThrowingStream() + continuation.finish() + } } } operation: { RecordMeetingModel( @@ -104,6 +114,7 @@ final class RecordMeetingTests: XCTestCase { let model = withDependencies { $0.continuousClock = clock + $0.soundEffectClient = .noop $0.speechClient.authorizationStatus = { .denied } } operation: { RecordMeetingModel(standup: .mock) @@ -144,6 +155,7 @@ final class RecordMeetingTests: XCTestCase { let model = withDependencies { $0.continuousClock = clock + $0.soundEffectClient = .noop $0.speechClient.authorizationStatus = { .denied } } operation: { RecordMeetingModel(standup: .mock) @@ -171,8 +183,12 @@ final class RecordMeetingTests: XCTestCase { 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: { @@ -203,11 +219,13 @@ final class RecordMeetingTests: XCTestCase { 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() @@ -219,11 +237,13 @@ final class RecordMeetingTests: XCTestCase { 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 @@ -232,6 +252,7 @@ final class RecordMeetingTests: XCTestCase { func testSpeechRecognitionFailure_Continue() async throws { let model = withDependencies { $0.continuousClock = ImmediateClock() + $0.soundEffectClient = .noop $0.speechClient.authorizationStatus = { .authorized } $0.speechClient.startTask = { _ in AsyncThrowingStream { @@ -286,6 +307,7 @@ final class RecordMeetingTests: XCTestCase { 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 {} diff --git a/Examples/Standups/StandupsTests/StandupDetailTests.swift b/Examples/Standups/StandupsTests/StandupDetailTests.swift index cd5ed73659..c3b2378497 100644 --- a/Examples/Standups/StandupsTests/StandupDetailTests.swift +++ b/Examples/Standups/StandupsTests/StandupDetailTests.swift @@ -1,4 +1,3 @@ -import AsyncAlgorithms import CasePaths import CustomDump import Dependencies @@ -45,7 +44,7 @@ final class StandupDetailTests: XCTestCase { destination: .alert(.speechRecognitionDenied), standup: .mock ) - } + } await model.alertButtonTapped(.openSettings) @@ -83,14 +82,18 @@ final class StandupDetailTests: XCTestCase { 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 - [ - SpeechRecognitionResult( - bestTranscription: Transcription(formattedString: "I completed the project"), - isFinal: true + AsyncThrowingStream { continuation in + continuation.yield( + SpeechRecognitionResult( + bestTranscription: Transcription(formattedString: "I completed the project"), + isFinal: true + ) ) - ].async.eraseToThrowingStream() + continuation.finish() + } } $0.uuid = .incrementing } operation: { diff --git a/Examples/Standups/StandupsTests/StandupsListTests.swift b/Examples/Standups/StandupsTests/StandupsListTests.swift index 78c8cacab9..16683f9cc5 100644 --- a/Examples/Standups/StandupsTests/StandupsListTests.swift +++ b/Examples/Standups/StandupsTests/StandupsListTests.swift @@ -69,7 +69,7 @@ final class StandupsListTests: XCTestCase { } operation: { StandupsListModel( destination: .add( - EditStandupModel( + StandupFormModel( standup: Standup( id: Standup.ID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!, attendees: [ @@ -106,14 +106,7 @@ final class StandupsListTests: XCTestCase { func testDelete() 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")!) - ] - ) - ]) + initialData: try JSONEncoder().encode([Standup.mock]) ) dependencies.mainQueue = mainQueue.eraseToAnyScheduler() } operation: { diff --git a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 16c4a75ccd..6c3cf7fefd 100644 --- a/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,15 +10,6 @@ "version": "0.9.1" } }, - { - "package": "AsyncAlgorithms", - "repositoryURL": "https://github.com/apple/swift-async-algorithms", - "state": { - "branch": null, - "revision": "aed5422380244498344a036b8d94e27f370d9a22", - "version": "0.0.4" - } - }, { "package": "swift-case-paths", "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", From 1db1bcfd1e9f533a17074b7e95613d0d9a78262c Mon Sep 17 00:00:00 2001 From: mbrandonw Date: Wed, 18 Jan 2023 22:07:57 +0000 Subject: [PATCH 041/181] Run swift-format --- Examples/Standups/Standups/Dependencies/SoundEffectClient.swift | 2 +- Examples/Standups/StandupsTests/StandupDetailTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift b/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift index 6596c4e330..d09aa6c5ce 100644 --- a/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift +++ b/Examples/Standups/Standups/Dependencies/SoundEffectClient.swift @@ -33,7 +33,7 @@ extension SoundEffectClient: DependencyKey { static let noop = Self( load: { _ in }, - play: { } + play: {} ) } diff --git a/Examples/Standups/StandupsTests/StandupDetailTests.swift b/Examples/Standups/StandupsTests/StandupDetailTests.swift index c3b2378497..3ce8e61752 100644 --- a/Examples/Standups/StandupsTests/StandupDetailTests.swift +++ b/Examples/Standups/StandupsTests/StandupDetailTests.swift @@ -44,7 +44,7 @@ final class StandupDetailTests: XCTestCase { destination: .alert(.speechRecognitionDenied), standup: .mock ) - } + } await model.alertButtonTapped(.openSettings) From 2500304089193dcd2ac722b4ee3a4f02cc4ec4e2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 19 Jan 2023 13:26:24 -0800 Subject: [PATCH 042/181] Alert/ConfirmationDialog updates (#68) * Always open up closures with global actors Going point-free style when `async` is involved can be problematic, and lose things like the current actor. * Support async confirmation dialog actions --- Examples/Inventory/ItemRow.swift | 7 +++-- Sources/SwiftUINavigation/Alert.swift | 12 +++---- .../ConfirmationDialog.swift | 31 +++++++++++++------ .../Articles/AlertsDialogs.md | 22 ++++++------- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index fdbde13c60..e0107ee983 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -113,9 +113,10 @@ struct ItemRowView: View { .foregroundColor(self.model.item.status.isInStock ? nil : Color.gray) .alert( unwrapping: self.$model.destination, - case: /ItemRowModel.Destination.alert, - action: self.model.alertButtonTapped - ) + case: /ItemRowModel.Destination.alert + ) { + self.model.alertButtonTapped($0) + } .popover( unwrapping: self.$model.destination, case: /ItemRowModel.Destination.duplicate diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 3bf514a754..551111f13a 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -113,7 +113,7 @@ extension View { /// 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. - /// - action: A closure that is called with an action from a particular alert button when + /// - 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( @@ -152,15 +152,15 @@ extension View { /// 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. - /// - action: A closure that is called with an action from a particular alert button when + /// - 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: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: action) + self.alert(unwrapping: `enum`.case(casePath), action: handler) } #else @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) @@ -206,9 +206,9 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action: @escaping (Value) async -> Void + action handler: @escaping (Value) async -> Void ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: action) + self.alert(unwrapping: `enum`.case(casePath), action: handler) } @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 6d301fdb2b..e3de28147b 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -121,12 +121,12 @@ extension View { /// 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. - /// - action: A closure that is called with an action from a particular dialog button when + /// - 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: @escaping (Value) -> Void = { (_: Never) in fatalError() } + action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -135,7 +135,11 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0, action: action) + Button($0) { action in + Task { + await handler(action) + } + } } }, message: { $0.message.map { Text($0) } } @@ -155,24 +159,24 @@ extension View { /// 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. - /// - action: A closure that is called with an action from a particular dialog button when + /// - 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: @escaping (Value) -> Void = { (_: Never) in fatalError() } + action handler: @escaping (Value) async -> Void = { (_: Void) async in } ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), - action: action + action: handler ) } #else @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?>, - action: @escaping (Value) -> Void + action handler: @escaping (Value) async -> Void ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -181,12 +185,17 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0, action: action) + Button($0) { action in + Task { + await handler(action) + } + } } }, message: { $0.message.map { Text($0) } } ) } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?> @@ -196,17 +205,19 @@ extension View { action: { (_: Never) in fatalError() } ) } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping `enum`: Binding, case casePath: CasePath>, - action: @escaping (Value) -> Void + action handler: @escaping (Value) async -> Void ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), - action: action + action: handler ) } + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping `enum`: Binding, diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md index ec5f4aba11..075e90c7d0 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/AlertsDialogs.md @@ -78,10 +78,9 @@ struct ContentView: View { List { // ... } - .alert( - unwrapping: self.$model.alert, - action: self.alertButtonTapped - ) + .alert(unwrapping: self.$model.alert) { action in + self.model.alertButtonTapped(action) + } } } ``` @@ -129,11 +128,9 @@ 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: self.alertButtonTapped -) +.alert(unwrapping: self.$model.destination, case: /Destination.alert) { action in + self.model.alertButtonTapped(action) +} ``` Note that the `case` argument is specified via a concept known as "case paths", which are like @@ -185,10 +182,9 @@ struct ContentView: View { List { // ... } - .confirmationDialog( - unwrapping: self.$model.dialog, - action: self.dialogButtonTapped - ) + .confirmationDialog(unwrapping: self.$model.dialog) { action in + self.dialogButtonTapped(action) + } } } ``` From 34af246f52cc31c846c2e6a51604220b61857213 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 23 Jan 2023 16:52:59 -0800 Subject: [PATCH 043/181] Support synchronous `ButtonState` action closures (#70) * Support synchronous action closures When we added async support for alert/dialog action closures, we broke implicit animations, because it's impossible to run an async closure inside `SwiftUI.withAnimation`. Let's bring back this functionality, and runtime warn loudly whenever an animated action is emitted to an async action handler. * wip --- Examples/Inventory/Inventory.swift | 12 +- Sources/SwiftUINavigation/Alert.swift | 103 ++++++++++++-- .../ConfirmationDialog.swift | 107 +++++++++++++-- Sources/SwiftUINavigation/Switch.swift | 1 + .../_SwiftUINavigationState/AlertState.swift | 23 ++++ .../_SwiftUINavigationState/ButtonState.swift | 127 +++++++++++++++++- .../Internal/RuntimeWarnings.swift | 4 +- .../ButtonStateTests.swift | 32 +++++ 8 files changed, 376 insertions(+), 33 deletions(-) rename Sources/{SwiftUINavigation => _SwiftUINavigationState}/Internal/RuntimeWarnings.swift (97%) create mode 100644 Tests/SwiftUINavigationTests/ButtonStateTests.swift diff --git a/Examples/Inventory/Inventory.swift b/Examples/Inventory/Inventory.swift index be21b3574f..01e060b1cd 100644 --- a/Examples/Inventory/Inventory.swift +++ b/Examples/Inventory/Inventory.swift @@ -23,9 +23,7 @@ class InventoryModel: ObservableObject { } func delete(item: Item) { - withAnimation { - _ = self.inventory.remove(id: item.id) - } + _ = self.inventory.remove(id: item.id) } func add(item: Item) { @@ -56,15 +54,11 @@ class InventoryModel: ObservableObject { for itemRowModel in self.inventory { itemRowModel.onDelete = { [weak self, weak itemRowModel] in guard let self, let itemRowModel else { return } - withAnimation { - self.delete(item: itemRowModel.item) - } + self.delete(item: itemRowModel.item) } itemRowModel.onDuplicate = { [weak self] item in guard let self else { return } - withAnimation { - self.add(item: item) - } + self.add(item: item) } itemRowModel.onTap = { [weak self, weak itemRowModel] in guard let self, let itemRowModel else { return } diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 551111f13a..57ec88c600 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -105,7 +105,40 @@ extension View { #if swift(>=5.7) /// Presents an alert from a binding to optional ``AlertState``. /// - /// See for more information on how to use this API. + /// 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 = { (_: Void) 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``. + /// + /// 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 @@ -126,11 +159,7 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0) { action in - Task { - await handler(action) - } - } + Button($0, action: handler) } }, message: { $0.message.map { Text($0) } } @@ -155,6 +184,35 @@ extension View { /// - 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 = { (_: Void) 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>, @@ -163,6 +221,24 @@ extension 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 value: Binding?>, @@ -174,11 +250,7 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0) { action in - Task { - await handler(action) - } - } + Button($0, action: handler) } }, message: { $0.message.map { Text($0) } } @@ -202,6 +274,15 @@ extension View { ) } + @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, diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index e3de28147b..d2fa256b8c 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -124,6 +124,40 @@ extension View { /// - 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 = { (_: Void) 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``. + /// + /// 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 = { (_: Void) async in } @@ -135,11 +169,7 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0) { action in - Task { - await handler(action) - } - } + Button($0, action: handler) } }, message: { $0.message.map { Text($0) } } @@ -162,6 +192,36 @@ extension View { /// - 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 = { (_: Void) 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>, @@ -173,6 +233,25 @@ extension View { ) } #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 value: Binding?>, @@ -185,11 +264,7 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0) { action in - Task { - await handler(action) - } - } + Button($0, action: handler) } }, message: { $0.message.map { Text($0) } } @@ -206,6 +281,18 @@ extension View { ) } + @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, diff --git a/Sources/SwiftUINavigation/Switch.swift b/Sources/SwiftUINavigation/Switch.swift index 1d34b057d5..4e600dfebc 100644 --- a/Sources/SwiftUINavigation/Switch.swift +++ b/Sources/SwiftUINavigation/Switch.swift @@ -1,4 +1,5 @@ import SwiftUI +@_spi(RuntimeWarn) import _SwiftUINavigationState /// 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/_SwiftUINavigationState/AlertState.swift index 20eff155fe..32231cd1b9 100644 --- a/Sources/_SwiftUINavigationState/AlertState.swift +++ b/Sources/_SwiftUINavigationState/AlertState.swift @@ -210,6 +210,29 @@ 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) 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) } + ) + } + } } // MARK: - Deprecations diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift index a0bb5c283f..9d0c5bb869 100644 --- a/Sources/_SwiftUINavigationState/ButtonState.swift +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -83,7 +83,7 @@ public struct ButtonState: Identifiable { switch self.action?.type { case let .send(action): perform(action) - case let .animatedSend(action, animation: animation): + case let .animatedSend(action, animation): withAnimation(animation) { perform(action) } @@ -91,6 +91,35 @@ public struct ButtonState: Identifiable { return } } + + /// 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 { + guard let handler = self.action else { return } + switch handler.type { + case let .send(action): + await perform(action) + case let .animatedSend(action, _): + var output = "" + customDump(handler, 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) + } + } } extension ButtonState: CustomDumpReflectable { @@ -166,6 +195,11 @@ extension ButtonState: Hashable where Action: Hashable { // 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 { @@ -177,6 +211,26 @@ extension Alert.Button { 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) + } + } } @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) @@ -192,6 +246,11 @@ extension ButtonRole { } 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( @@ -201,6 +260,24 @@ extension Button where Label == Text { Text(button.label) } } + + /// 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) + } + } } // MARK: - Deprecations @@ -257,3 +334,51 @@ extension ButtonState { } } } + +@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 +} + +@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 +} diff --git a/Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift similarity index 97% rename from Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift rename to Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift index d0747c8a94..f01906c022 100644 --- a/Sources/SwiftUINavigation/Internal/RuntimeWarnings.swift +++ b/Sources/_SwiftUINavigationState/Internal/RuntimeWarnings.swift @@ -1,7 +1,7 @@ +@_spi(RuntimeWarn) @_transparent @inline(__always) -@usableFromInline -func runtimeWarn( +public func runtimeWarn( _ message: @autoclosure () -> String, category: String? = "SwiftUINavigation", file: StaticString? = nil, diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift new file mode 100644 index 0000000000..b69aee19b3 --- /dev/null +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -0,0 +1,32 @@ +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: … + + Action: + ButtonState.Handler.send( + (), + animation: Animation.easeInOut + ) + + Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, or \ + use 'SwiftUI.withAnimation' explicitly. + """ + } + + let button = ButtonState(action: .send((), animation: .default)) { + TextState("Animate!") + } + + await button.withAction { + await Task.yield() + } + } +} From a2154e67b65c9ee9816f1a3273467ecf6c9520b1 Mon Sep 17 00:00:00 2001 From: stephencelis Date: Tue, 24 Jan 2023 01:00:41 +0000 Subject: [PATCH 044/181] Run swift-format --- Sources/SwiftUINavigation/ConfirmationDialog.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index d2fa256b8c..56240108eb 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -211,7 +211,7 @@ extension View { /// /// > 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 From bf0fb9d53019cbde1a1e0cf290b560a0a0411282 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 25 Jan 2023 10:49:28 -0800 Subject: [PATCH 045/181] Make `ButtonState` actions more flexible (#71) * Allow alert state actions to be mapped Co-authored-by: Skyler Smith * wip * wip * reorganize deprecations * fix Co-authored-by: Skyler Smith --- Examples/CaseStudies/09-Routing.swift | 12 +- Examples/Inventory/Item.swift | 2 +- Examples/Inventory/ItemRow.swift | 6 +- .../Standups/Standups/RecordMeeting.swift | 9 +- .../Standups/Standups/StandupDetail.swift | 11 +- Examples/Standups/Standups/StandupsList.swift | 6 +- Sources/SwiftUINavigation/Alert.swift | 20 +- .../ConfirmationDialog.swift | 26 +- .../Internal/Deprecations.swift | 163 +++++++++ .../_SwiftUINavigationState/AlertState.swift | 134 ++------ .../_SwiftUINavigationState/ButtonState.swift | 226 ++++++------- .../ConfirmationDialogState.swift | 206 ++++-------- .../Internal/Deprecations.swift | 309 ++++++++++++++++++ Tests/SwiftUINavigationTests/AlertTests.swift | 26 +- .../ButtonStateTests.swift | 4 +- 15 files changed, 733 insertions(+), 427 deletions(-) create mode 100644 Sources/_SwiftUINavigationState/Internal/Deprecations.swift diff --git a/Examples/CaseStudies/09-Routing.swift b/Examples/CaseStudies/09-Routing.swift index 4140c1b1cb..bf0af7ede1 100644 --- a/Examples/CaseStudies/09-Routing.swift +++ b/Examples/CaseStudies/09-Routing.swift @@ -91,10 +91,12 @@ struct Routing: View { .navigationTitle("Routing") .alert(unwrapping: self.$destination, case: /Destination.alert) { action in switch action { - case .randomize: + case .randomize?: self.count = .random(in: 0...1_000) - case .reset: + case .reset?: self.count = 0 + case nil: + break } } .confirmationDialog( @@ -102,10 +104,12 @@ struct Routing: View { case: /Destination.confirmationDialog ) { action in switch action { - case .decrement: + case .decrement?: self.count -= 1 - case .increment: + case .increment?: self.count += 1 + case nil: + break } } .sheet(unwrapping: self.$destination, case: /Destination.sheet) { $count in diff --git a/Examples/Inventory/Item.swift b/Examples/Inventory/Item.swift index 640e9ec8b0..6e1ca7edf8 100644 --- a/Examples/Inventory/Item.swift +++ b/Examples/Inventory/Item.swift @@ -23,7 +23,7 @@ struct Item: Equatable, Identifiable { var green: CGFloat = 0 var blue: CGFloat = 0 - static var defaults: [Self] = [ + static let defaults: [Self] = [ .red, .green, .blue, diff --git a/Examples/Inventory/ItemRow.swift b/Examples/Inventory/ItemRow.swift index e0107ee983..83ba136935 100644 --- a/Examples/Inventory/ItemRow.swift +++ b/Examples/Inventory/ItemRow.swift @@ -39,10 +39,12 @@ class ItemRowModel: Identifiable, ObservableObject { ) } - func alertButtonTapped(_ action: AlertAction) { + func alertButtonTapped(_ action: AlertAction?) { switch action { - case .deleteConfirmation: + case .deleteConfirmation?: self.onDelete() + case nil: + break } } diff --git a/Examples/Standups/Standups/RecordMeeting.swift b/Examples/Standups/Standups/RecordMeeting.swift index bf569945d9..35bb6f16e5 100644 --- a/Examples/Standups/Standups/RecordMeeting.swift +++ b/Examples/Standups/Standups/RecordMeeting.swift @@ -68,13 +68,14 @@ class RecordMeetingModel: ObservableObject { self.destination = .alert(.endMeeting(isDiscardable: true)) } - func alertButtonTapped(_ action: AlertAction) async { + func alertButtonTapped(_ action: AlertAction?) async { switch action { - case .confirmSave: + case .confirmSave?: await self.finishMeeting() - - case .confirmDiscard: + case .confirmDiscard?: self.isDismissed = true + case nil: + break } } diff --git a/Examples/Standups/Standups/StandupDetail.swift b/Examples/Standups/Standups/StandupDetail.swift index 9cc942b59b..17d4b6ff9e 100644 --- a/Examples/Standups/Standups/StandupDetail.swift +++ b/Examples/Standups/Standups/StandupDetail.swift @@ -54,21 +54,24 @@ class StandupDetailModel: ObservableObject { self.destination = .alert(.deleteStandup) } - func alertButtonTapped(_ action: AlertAction) async { + func alertButtonTapped(_ action: AlertAction?) async { switch action { - case .confirmDeletion: + case .confirmDeletion?: self.onConfirmDeletion() self.isDismissed = true - case .continueWithoutRecording: + case .continueWithoutRecording?: self.destination = .record( withDependencies(from: self) { RecordMeetingModel(standup: self.standup) } ) - case .openSettings: + case .openSettings?: await self.openSettings() + + case nil: + break } } diff --git a/Examples/Standups/Standups/StandupsList.swift b/Examples/Standups/Standups/StandupsList.swift index 59959f0184..0fbb946993 100644 --- a/Examples/Standups/Standups/StandupsList.swift +++ b/Examples/Standups/Standups/StandupsList.swift @@ -109,9 +109,9 @@ final class StandupsListModel: ObservableObject { } } - func alertButtonTapped(_ action: AlertAction) { + func alertButtonTapped(_ action: AlertAction?) { switch action { - case .confirmLoadMockData: + case .confirmLoadMockData?: withAnimation { self.standups = [ .mock, @@ -119,6 +119,8 @@ final class StandupsListModel: ObservableObject { .engineeringMock, ] } + case nil: + break } } } diff --git a/Sources/SwiftUINavigation/Alert.swift b/Sources/SwiftUINavigation/Alert.swift index 57ec88c600..b3cb1521a2 100644 --- a/Sources/SwiftUINavigation/Alert.swift +++ b/Sources/SwiftUINavigation/Alert.swift @@ -118,7 +118,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value) -> Void = { (_: Void) in } + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -151,7 +151,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -187,7 +187,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) -> Void = { (_: Void) in } + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: handler) } @@ -216,7 +216,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: handler) } @@ -224,7 +224,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value) -> Void + action handler: @escaping (Value?) -> Void ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -242,7 +242,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func alert( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void + action handler: @escaping (Value?) async -> Void ) -> some View { self.alert( (value.wrappedValue?.title).map(Text.init) ?? Text(""), @@ -267,7 +267,7 @@ extension View { presenting: value.wrappedValue, actions: { ForEach($0.buttons) { - Button($0, action: { (_: Never) in fatalError() }) + Button($0) { _ in } } }, message: { $0.message.map { Text($0) } } @@ -278,7 +278,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) -> Void + action handler: @escaping (Value?) -> Void ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: handler) } @@ -287,7 +287,7 @@ extension View { public func alert( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void + action handler: @escaping (Value?) async -> Void ) -> some View { self.alert(unwrapping: `enum`.case(casePath), action: handler) } @@ -297,7 +297,7 @@ extension View { unwrapping `enum`: Binding, case casePath: CasePath> ) -> some View { - self.alert(unwrapping: `enum`.case(casePath), action: { (_: Never) in fatalError() }) + self.alert(unwrapping: `enum`.case(casePath)) { (_: Never?) in } } #endif diff --git a/Sources/SwiftUINavigation/ConfirmationDialog.swift b/Sources/SwiftUINavigation/ConfirmationDialog.swift index 56240108eb..598f646935 100644 --- a/Sources/SwiftUINavigation/ConfirmationDialog.swift +++ b/Sources/SwiftUINavigation/ConfirmationDialog.swift @@ -126,7 +126,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value) -> Void = { (_: Void) in } + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -160,7 +160,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -195,7 +195,7 @@ extension View { public func confirmationDialog( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) -> Void = { (_: Void) in } + action handler: @escaping (Value?) -> Void = { (_: Never?) in } ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), @@ -225,7 +225,7 @@ extension View { public func confirmationDialog( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void = { (_: Void) async in } + action handler: @escaping (Value?) async -> Void = { (_: Never?) async in } ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), @@ -236,7 +236,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value) -> Void + action handler: @escaping (Value?) -> Void ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -255,7 +255,7 @@ extension View { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) public func confirmationDialog( unwrapping value: Binding?>, - action handler: @escaping (Value) async -> Void + action handler: @escaping (Value?) async -> Void ) -> some View { self.confirmationDialog( value.wrappedValue.flatMap { Text($0.title) } ?? Text(""), @@ -275,17 +275,14 @@ extension View { public func confirmationDialog( unwrapping value: Binding?> ) -> some View { - self.confirmationDialog( - unwrapping: value, - action: { (_: Never) in fatalError() } - ) + 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 + action handler: @escaping (Value?) -> Void ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), @@ -297,7 +294,7 @@ extension View { public func confirmationDialog( unwrapping `enum`: Binding, case casePath: CasePath>, - action handler: @escaping (Value) async -> Void + action handler: @escaping (Value?) async -> Void ) -> some View { self.confirmationDialog( unwrapping: `enum`.case(casePath), @@ -310,10 +307,7 @@ extension View { unwrapping `enum`: Binding, case casePath: CasePath> ) -> some View { - self.confirmationDialog( - unwrapping: `enum`.case(casePath), - action: { (_: Never) in fatalError() } - ) + self.confirmationDialog(unwrapping: `enum`.case(casePath)) { _ in } } #endif diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index f0038c1994..98f3d6348b 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -1,5 +1,168 @@ import SwiftUI +// 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: """ + '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) + } + } + } + + @_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) + } + } + } + + @_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) + } + } + } + #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) + } + } + } + + @_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) + } + } + } + + @_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) + } + } + } + + @_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) + } + } + } + #endif +} + // NB: Deprecated after 0.3.0 @available(*, deprecated, renamed: "init(_:pattern:then:else:)") diff --git a/Sources/_SwiftUINavigationState/AlertState.swift b/Sources/_SwiftUINavigationState/AlertState.swift index 32231cd1b9..929de41967 100644 --- a/Sources/_SwiftUINavigationState/AlertState.swift +++ b/Sources/_SwiftUINavigationState/AlertState.swift @@ -127,11 +127,23 @@ import SwiftUI /// model.alert = nil /// ``` public struct AlertState: Identifiable { - public let id = UUID() + 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 + } + /// Creates alert state. /// /// - Parameters: @@ -144,9 +156,21 @@ public struct AlertState: Identifiable { @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, message: (() -> TextState)? = nil ) { - self.title = title() - self.message = message?() - self.buttons = actions() + 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 + ) } } @@ -194,7 +218,7 @@ extension Alert { /// - 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) { + public init(_ state: AlertState, action: @escaping (Action?) -> Void) { if state.buttons.count == 2 { self.init( title: Text(state.title), @@ -217,7 +241,7 @@ extension Alert { /// - 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 (Action?) async -> Void) { if state.buttons.count == 2 { self.init( title: Text(state.title), @@ -234,101 +258,3 @@ extension Alert { } } } - -// MARK: - Deprecations - -extension AlertState { - @available(*, deprecated, message: "Use 'ButtonState' instead.") - public typealias Button = ButtonState - - @available(*, deprecated, message: "Use 'ButtonState.ButtonAction' instead.") - public typealias ButtonAction = ButtonState.ButtonAction - - @available(*, deprecated, message: "Use 'ButtonState.Role' instead.") - public typealias ButtonRole = ButtonState.Role - - @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.title = title - self.message = message - self.buttons = buttons - } - - @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.title = title - self.message = message - self.buttons = dismissButton.map { [$0] } ?? [] - } - - @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.title = title - self.message = message - self.buttons = [primaryButton, secondaryButton] - } -} diff --git a/Sources/_SwiftUINavigationState/ButtonState.swift b/Sources/_SwiftUINavigationState/ButtonState.swift index 9d0c5bb869..f8bb7799ea 100644 --- a/Sources/_SwiftUINavigationState/ButtonState.swift +++ b/Sources/_SwiftUINavigationState/ButtonState.swift @@ -2,44 +2,23 @@ import CustomDump import SwiftUI public struct ButtonState: Identifiable { - /// A type that wraps an action with additional context, _e.g._ for animation. - public struct Handler { - public let type: _ActionType - - 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 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 Role { - /// A role that indicates a cancel button. - /// - /// See `SwiftUI.ButtonRole.cancel` for more information. - case cancel + public let id: UUID + public let action: ButtonStateAction + public let label: TextState + public let role: ButtonStateRole? - /// A role that indicates a destructive button. - /// - /// See `SwiftUI.ButtonRole.destructive` for more information. - case destructive + init( + id: UUID, + action: ButtonStateAction, + label: TextState, + role: ButtonStateRole? + ) { + self.id = id + self.action = action + self.label = label + self.role = role } - public let id = UUID() - public let action: Handler? - public let label: TextState - public let role: Role? - /// Creates button state. /// /// - Parameters: @@ -48,13 +27,11 @@ public struct ButtonState: Identifiable { /// - 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: Role? = nil, - action: Handler? = nil, + role: ButtonStateRole? = nil, + action: ButtonStateAction = .send(nil), label: () -> TextState ) { - self.role = role - self.action = action - self.label = label() + self.init(id: UUID(), action: action, label: label(), role: role) } /// Creates button state. @@ -65,13 +42,11 @@ public struct ButtonState: Identifiable { /// - 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: Role? = nil, + role: ButtonStateRole? = nil, action: Action, label: () -> TextState ) { - self.role = role - self.action = .send(action) - self.label = label() + self.init(id: UUID(), action: .send(action), label: label(), role: role) } /// Handle the button's action in a closure. @@ -79,16 +54,14 @@ public struct ButtonState: Identifiable { /// - 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 { + 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) } - case .none: - return } } @@ -98,14 +71,13 @@ public struct ButtonState: Identifiable { /// > 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 { - guard let handler = self.action else { return } - switch handler.type { + 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(handler, to: &output, indent: 4) + customDump(self.action, to: &output, indent: 4) runtimeWarn( """ An animated action was performed asynchronously: … @@ -120,6 +92,71 @@ public struct ButtonState: Identifiable { 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 + ) + } +} + +/// 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?, 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 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?) + } +} + +/// A value that describes the purpose of a button. +/// +/// See `SwiftUI.ButtonRole` for more information. +public enum ButtonStateRole { + /// 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 ButtonState: CustomDumpReflectable { @@ -128,9 +165,7 @@ extension ButtonState: CustomDumpReflectable { if let role = self.role { children.append(("role", role)) } - if let action = self.action { - children.append(("action", action)) - } + children.append(("action", self.action)) children.append(("label", self.label)) return Mirror( self, @@ -140,14 +175,14 @@ extension ButtonState: CustomDumpReflectable { } } -extension ButtonState.Handler: CustomDumpReflectable { +extension ButtonStateAction: CustomDumpReflectable { public var customDumpMirror: Mirror { switch self.type { case let .send(action): return Mirror( self, children: [ - "send": action + "send": action as Any ], displayStyle: .enum ) @@ -163,9 +198,9 @@ extension ButtonState.Handler: CustomDumpReflectable { } } -extension ButtonState.Handler: Equatable where Action: Equatable {} -extension ButtonState.Handler._ActionType: Equatable where Action: Equatable {} -extension ButtonState.Role: Equatable {} +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 @@ -174,8 +209,8 @@ extension ButtonState: Equatable where Action: Equatable { } } -extension ButtonState.Handler: Hashable where Action: Hashable {} -extension ButtonState.Handler._ActionType: Hashable where Action: Hashable { +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: _): @@ -183,7 +218,7 @@ extension ButtonState.Handler._ActionType: Hashable where Action: Hashable { } } } -extension ButtonState.Role: Hashable {} +extension ButtonStateRole: Hashable {} extension ButtonState: Hashable where Action: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(self.action) @@ -200,7 +235,7 @@ extension Alert.Button { /// - Parameters: /// - button: Button state. /// - action: An action closure that is invoked when the button is tapped. - public init(_ button: ButtonState, action: @escaping (Action) -> Void) { + public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { let action = { button.withAction(action) } switch button.role { case .cancel: @@ -220,7 +255,7 @@ extension Alert.Button { /// - 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 (Action?) async -> Void) { let action = { _ = Task { await button.withAction(action) } } switch button.role { case .cancel: @@ -235,7 +270,7 @@ extension Alert.Button { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) extension ButtonRole { - public init(_ role: ButtonState.Role) { + public init(_ role: ButtonStateRole) { switch role { case .cancel: self = .cancel @@ -252,7 +287,7 @@ extension Button where Label == Text { /// - 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) { + public init(_ button: ButtonState, action: @escaping (Action?) -> Void) { self.init( role: button.role.map(ButtonRole.init), action: { button.withAction(action) } @@ -270,7 +305,7 @@ extension Button where Label == Text { /// - 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 (Action?) async -> Void) { self.init( role: button.role.map(ButtonRole.init), action: { Task { await button.withAction(action) } } @@ -280,61 +315,6 @@ extension Button where Label == Text { } } -// MARK: - Deprecations - -extension ButtonState { - @available(*, deprecated, renamed: "Handler") - public typealias ButtonAction = Handler -} - -extension ButtonState.Handler { - @available(*, deprecated, message: "Use 'ButtonState.withAction' instead.") - public typealias ActionType = _ActionType -} - -@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: Handler? = nil) -> Self { - Self(role: .cancel, action: action) { - label - } - } - - public static func `default`(_ label: TextState, action: Handler? = nil) -> Self { - Self(action: action) { - label - } - } - - public static func destructive(_ label: TextState, action: Handler? = nil) -> Self { - Self(role: .destructive, action: action) { - label - } - } -} - @usableFromInline func debugCaseOutput(_ value: Any) -> String { func debugCaseOutputHelp(_ value: Any) -> String { diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift index addfa24170..650f6d4c3b 100644 --- a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift +++ b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift @@ -116,11 +116,25 @@ import SwiftUI @available(tvOS 13, *) @available(watchOS 6, *) public struct ConfirmationDialogState: Identifiable { - public let id = UUID() + public let id: UUID public var buttons: [ButtonState] public var message: TextState? public var title: TextState - public var titleVisibility: Visibility + 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. /// @@ -134,15 +148,18 @@ public struct ConfirmationDialogState: Identifiable { @available(tvOS 15, *) @available(watchOS 8, *) public init( - titleVisibility: Visibility, + titleVisibility: ConfirmationDialogStateTitleVisibility, title: () -> TextState, @ButtonStateBuilder actions: () -> [ButtonState], message: (() -> TextState)? = nil ) { - self.buttons = actions() - self.message = message?() - self.title = title() - self.titleVisibility = titleVisibility + self.init( + id: UUID(), + buttons: actions(), + message: message?(), + title: title(), + titleVisibility: titleVisibility + ) } /// Creates confirmation dialog state. @@ -156,19 +173,49 @@ public struct ConfirmationDialogState: Identifiable { @ButtonStateBuilder actions: () -> [ButtonState], message: (() -> TextState)? = nil ) { - self.buttons = actions() - self.message = message?() - self.title = title() - self.titleVisibility = .automatic + self.init( + id: UUID(), + buttons: actions(), + message: message?(), + title: title(), + titleVisibility: .automatic + ) } - public enum Visibility { - case automatic - case hidden - case visible + 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 { + /// 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, *) @@ -222,7 +269,7 @@ extension ConfirmationDialogState: Hashable where Action: Hashable { @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) extension Visibility { - public init(_ visibility: ConfirmationDialogState.Visibility) { + public init(_ visibility: ConfirmationDialogStateTitleVisibility) { switch visibility { case .automatic: self = .automatic @@ -233,130 +280,3 @@ extension Visibility { } } } - -// MARK: - Deprecations - -@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( - 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: Visibility, - message: TextState? = nil, - buttons: [ButtonState] = [] - ) { - self.buttons = buttons - self.message = message - self.title = title - self.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.buttons = buttons - self.message = message - self.title = title - self.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) } - ) - } -} diff --git a/Sources/_SwiftUINavigationState/Internal/Deprecations.swift b/Sources/_SwiftUINavigationState/Internal/Deprecations.swift new file mode 100644 index 0000000000..a706de75ae --- /dev/null +++ b/Sources/_SwiftUINavigationState/Internal/Deprecations.swift @@ -0,0 +1,309 @@ +import SwiftUI + +// NB: Deprecated after 0.5.0 + +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 'ButtonStateRole' instead.") + public typealias Role = ButtonStateRole +} + +extension ButtonStateAction { + @available(*, deprecated, message: "Use 'ButtonState.withAction' instead.") + public typealias ActionType = _ActionType +} + +// NB: Deprecated after 0.3.0 + +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 '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: 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( + 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, + 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 + } + } + + 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 '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) } + ) + } +} diff --git a/Tests/SwiftUINavigationTests/AlertTests.swift b/Tests/SwiftUINavigationTests/AlertTests.swift index d57e887740..34140816da 100644 --- a/Tests/SwiftUINavigationTests/AlertTests.swift +++ b/Tests/SwiftUINavigationTests/AlertTests.swift @@ -30,16 +30,18 @@ final class AlertTests: XCTestCase { title: "Alert!", actions: [ [0]: ButtonState( - role: ButtonState.Role.destructive, - action: ButtonState.Handler.send( + role: ButtonStateRole.destructive, + action: ButtonStateAction.send( true, animation: Animation.easeInOut ), label: "Destroy" ), [1]: ButtonState( - role: ButtonState.Role.cancel, - action: ButtonState.Handler.send(false), + role: ButtonStateRole.cancel, + action: ButtonStateAction.send( + false + ), label: "Cancel" ) ], @@ -68,16 +70,18 @@ final class AlertTests: XCTestCase { title: "Alert!", actions: [ [0]: ButtonState( - role: ButtonState.Role.destructive, - action: ButtonState.Handler.send( + role: ButtonStateRole.destructive, + action: ButtonStateAction.send( true, animation: Animation.easeInOut ), label: "Destroy" ), [1]: ButtonState( - role: ButtonState.Role.cancel, - action: ButtonState.Handler.send(false), + role: ButtonStateRole.cancel, + action: ButtonStateAction.send( + false + ), label: "Cancel" ) ], @@ -106,11 +110,9 @@ private struct TestView: View { } } - private func alertButtonTapped(_ action: AlertAction) async { + private func alertButtonTapped(_ action: AlertAction?) async { switch action { - case .confirm: - break - case .deny: + case .some(.confirm), .some(.deny), .none: break } } diff --git a/Tests/SwiftUINavigationTests/ButtonStateTests.swift b/Tests/SwiftUINavigationTests/ButtonStateTests.swift index b69aee19b3..904694d56b 100644 --- a/Tests/SwiftUINavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftUINavigationTests/ButtonStateTests.swift @@ -11,7 +11,7 @@ final class ButtonStateTests: XCTestCase { An animated action was performed asynchronously: … Action: - ButtonState.Handler.send( + ButtonStateAction.send( (), animation: Animation.easeInOut ) @@ -25,7 +25,7 @@ final class ButtonStateTests: XCTestCase { TextState("Animate!") } - await button.withAction { + await button.withAction { _ in await Task.yield() } } From a6a42363204d550288c17f7b7258cc6ea73b759c Mon Sep 17 00:00:00 2001 From: stephencelis Date: Wed, 25 Jan 2023 19:03:38 +0000 Subject: [PATCH 046/181] Run swift-format --- .../Internal/Deprecations.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftUINavigation/Internal/Deprecations.swift b/Sources/SwiftUINavigation/Internal/Deprecations.swift index 98f3d6348b..7c1261a577 100644 --- a/Sources/SwiftUINavigation/Internal/Deprecations.swift +++ b/Sources/SwiftUINavigation/Internal/Deprecations.swift @@ -9,7 +9,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -28,7 +29,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -48,7 +50,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -67,7 +70,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -87,7 +91,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -106,7 +111,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -126,7 +132,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) @@ -145,7 +152,8 @@ extension View { @available( *, deprecated, - message: """ + message: + """ 'View.alert' now passes an optional action to its handler to allow you to handle action-less dismissals. """ ) From 712894cabcd8379c23ac4da8c2cced27af976dc4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 30 Jan 2023 12:25:05 -0800 Subject: [PATCH 047/181] Update CoC --- .github/CODE_OF_CONDUCT.md | 106 +++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 29 deletions(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 703a4725a4..6f9886f04d 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,83 +2,131 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to a positive environment for our community include: +Examples of behavior that contributes to a positive environment for our +community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or - advances of any kind +* The use of sexualized language or imagery, and sexual attention or advances of + any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at support@pointfree.co. All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the reporter of any incident. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. -**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. ### 2. Warning -**Community Impact**: A violation through a single incident or series of actions. +**Community Impact**: A violation through a single incident or series of +actions. -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. ### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within the community. +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations From 270a754308f5440be52fc295242eb7031638bd15 Mon Sep 17 00:00:00 2001 From: Vincent Isambart Date: Tue, 31 Jan 2023 05:39:42 +0900 Subject: [PATCH 048/181] Real dependencies (#74) * Move XCTestDynamicOverlay to real place of use * Remove unused dependencies --- Package.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index b58dac9d94..ec43ec4d35 100644 --- a/Package.swift +++ b/Package.swift @@ -30,8 +30,8 @@ let package = Package( .target( name: "_SwiftUINavigationState", dependencies: [ - .product(name: "CasePaths", package: "swift-case-paths"), .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), .target( @@ -39,8 +39,6 @@ let package = Package( dependencies: [ "_SwiftUINavigationState", .product(name: "CasePaths", package: "swift-case-paths"), - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), .testTarget( From 2f4074d2b5d02ebdb12c949ddf1a90cdf08ad1aa Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Mon, 30 Jan 2023 12:54:35 -0800 Subject: [PATCH 049/181] Update README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index b75873f245..b663e5746c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # SwiftUI Navigation [![CI](https://github.com/pointfreeco/swiftui-navigation/actions/workflows/ci.yml/badge.svg)](https://github.com/pointfreeco/swiftui-navigation/actions/workflows/ci.yml) +[![Slack](https://img.shields.io/badge/slack-chat-informational.svg?label=Slack&logo=slack)](http://pointfree.co/slack-invite) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswiftui-navigation%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/swiftui-navigation) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswiftui-navigation%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/swiftui-navigation) @@ -9,6 +10,7 @@ Tools for making SwiftUI navigation simpler, more ergonomic and more precise. * [Overview](#overview) * [Examples](#examples) * [Learn more](#learn-more) + * [Community](#community) * [Installation](#installation) * [Documentation](#documentation) * [License](#license) @@ -85,6 +87,15 @@ You can watch all of the episodes [here](https://www.pointfree.co/collections/sw video poster image +## Community + +If you want to discuss this library or have a question about how to use it to solve +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). + ## Installation You can add SwiftUI Navigation to an Xcode project by adding it as a package dependency. From 5ccf329cf35efa40158d52c9709a7d6a4df77f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Fri, 3 Feb 2023 06:00:35 +0900 Subject: [PATCH 050/181] Fix mismatched words in Routing case study (#78) --- Examples/CaseStudies/09-Routing.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/CaseStudies/09-Routing.swift b/Examples/CaseStudies/09-Routing.swift index bf0af7ede1..4d60633b10 100644 --- a/Examples/CaseStudies/09-Routing.swift +++ b/Examples/CaseStudies/09-Routing.swift @@ -5,7 +5,7 @@ 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 three navigation destinations: an alert, a navigation link to a count stepper, \ + 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. @@ -77,7 +77,7 @@ struct Routing: View { } } destination: { $count in Form { - Stepper("Number: \(count)", value: $count) + Stepper("Count: \(count)", value: $count) } .navigationTitle("Routing link") } label: { @@ -115,7 +115,7 @@ struct Routing: View { .sheet(unwrapping: self.$destination, case: /Destination.sheet) { $count in NavigationView { Form { - Stepper("Number: \(count)", value: $count) + Stepper("Count: \(count)", value: $count) } .navigationTitle("Routing sheet") } From ff448510c52070473975afe9686e1b27a5c98489 Mon Sep 17 00:00:00 2001 From: Armen Mkrtchian Date: Tue, 21 Feb 2023 16:01:46 -0600 Subject: [PATCH 051/181] Remove duplicate paragraph in Readme (#83) --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index b663e5746c..36d640418e 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,7 @@ 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, but unfortunately SwiftUI does not ship with all the tools necessary to model our -domains as concisely as possible and use these navigation APIs. - -Unfortunately, SwiftUI does not ship with all of the tools necessary to model our domains with +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. From 92c82d73a4fe11d4666a6b4b2956fc062a9219ca Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 5 Mar 2023 16:34:15 -0800 Subject: [PATCH 052/181] Don't require confirmation dialog actions (#85) We already have this default for `AlertState`, so let's do the same for `ConfirmationDialogState`. --- .../_SwiftUINavigationState/ConfirmationDialogState.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift index 650f6d4c3b..07421173e5 100644 --- a/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift +++ b/Sources/_SwiftUINavigationState/ConfirmationDialogState.swift @@ -150,7 +150,7 @@ public struct ConfirmationDialogState: Identifiable { public init( titleVisibility: ConfirmationDialogStateTitleVisibility, title: () -> TextState, - @ButtonStateBuilder actions: () -> [ButtonState], + @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, message: (() -> TextState)? = nil ) { self.init( @@ -170,7 +170,7 @@ public struct ConfirmationDialogState: Identifiable { /// - message: The message for the dialog. public init( title: () -> TextState, - @ButtonStateBuilder actions: () -> [ButtonState], + @ButtonStateBuilder actions: () -> [ButtonState] = { [] }, message: (() -> TextState)? = nil ) { self.init( @@ -200,8 +200,8 @@ public struct ConfirmationDialogState: Identifiable { /// /// See `SwiftUI.Visibility` for more information. public enum ConfirmationDialogStateTitleVisibility { - /// The element may be visible or hidden depending on the policies of the - /// component accepting the visibility configuration. + /// 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 From 0a0e1b321d70ee6a464ecfe6b0136d9eff77ebfc Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:18:27 -0800 Subject: [PATCH 053/181] Create release.yml --- .github/workflows/release.yml | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..894af77613 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Release +on: + release: + types: [published] + workflow_dispatch: +jobs: + project-channel: + runs-on: ubuntu-latest + steps: + - name: Dump Github context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Slack Notification on SUCCESS + if: success() + uses: tokorom/action-slack-incoming-webhook@main + env: + INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_PROJECT_CHANNEL_WEBHOOK_URL }} + with: + text: swiftui-navigation ${{ github.event.release.tag_name }} has been released. + blocks: | + [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "swiftui-navigation ${{ github.event.release.tag_name}}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(github.event.release.body) }} + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ github.event.release.html_url }}" + } + } + ] + + releases-channel: + runs-on: ubuntu-latest + steps: + - name: Dump Github context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Slack Notification on SUCCESS + if: success() + uses: tokorom/action-slack-incoming-webhook@main + env: + INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }} + with: + text: swiftui-navigation ${{ github.event.release.tag_name }} has been released. + blocks: | + [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "swiftui-navigation ${{ github.event.release.tag_name}}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(github.event.release.body) }} + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ github.event.release.html_url }}" + } + } + ] From 95df6038296f7161947767b392e2c18fad659212 Mon Sep 17 00:00:00 2001 From: Dave Reed Date: Sun, 12 Mar 2023 15:27:24 -0400 Subject: [PATCH 054/181] fix minor typo in Navigation.md documentation (#88) --- .../SwiftUINavigation/Documentation.docc/Articles/Navigation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md index f5787d7bbd..e9c8486e3a 100644 --- a/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md +++ b/Sources/SwiftUINavigation/Documentation.docc/Articles/Navigation.md @@ -105,7 +105,7 @@ Button { .navigationDestination( unwrapping: self.$model.destination, case: /Destination.counter -) { $item in +) { $number in CounterView(number: $number) } ``` From 17c342abac2bdf29ab0680024cd567ccd526befc Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 14 Mar 2023 10:43:38 -0700 Subject: [PATCH 055/181] Update issue templates (#92) * Update issue templates * Update config.yml --- .github/ISSUE_TEMPLATE/bug_report.md | 34 ------------- .github/ISSUE_TEMPLATE/bug_report.yml | 73 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 15 ++++++ .github/ISSUE_TEMPLATE/question.md | 10 ---- 4 files changed, 88 insertions(+), 44 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index bb810f7097..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -Give a clear and concise description of what the bug is. - -**To Reproduce** -Zip up a project that reproduces the behavior and attach it by dragging it here. - -```swift -// And/or enter code that reproduces the behavior here. - -``` - -**Expected behavior** -Give a clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment** - - swiftui-navigation version [e.g. 0.1.0] - - Xcode [e.g. 13.1] - - Swift [e.g. 5.5] - - OS: [e.g. iOS 15] - -**Additional context** -Add any more context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..e54783919e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: Something isn't working as expected +labels: [bug] +body: +- type: markdown + attributes: + value: | + Thank you for contributing to the SwiftUI Navigation! + + Before you submit your issue, please complete each text area below with the relevant details for your bug, and complete the steps in the checklist. +- type: textarea + attributes: + label: Description + description: | + A short description of the incorrect behavior. + + If you think this issue has been recently introduced and did not occur in an earlier version, please note that. If possible, include the last version that the behavior was correct in addition to your current version. + validations: + required: true +- type: checkboxes + attributes: + label: Checklist + options: + - label: I have determined whether this bug is also reproducible in a vanilla SwiftUI project. + required: false + - label: If possible, I've reproduced the issue using the `main` branch of this package. + required: false + - label: This issue hasn't been addressed in an [existing GitHub issue](https://github.com/pointfreeco/swiftui-navigation/issues) or [discussion](https://github.com/pointfreeco/swiftui-navigation/discussions). + required: true +- type: textarea + attributes: + label: Expected behavior + description: Describe what you expected to happen. + validations: + required: false +- type: textarea + attributes: + label: Actual behavior + description: Describe or copy/paste the behavior you observe. + validations: + required: false +- type: textarea + attributes: + label: Steps to reproduce + description: | + Explanation of how to reproduce the incorrect behavior. + + This could include an attached project or link to code that is exhibiting the issue, and/or a screen recording. + placeholder: | + 1. ... + validations: + required: false +- type: input + attributes: + label: SwiftUI Navigation version information + description: The version of SwiftUI Navigation used to reproduce this issue. + placeholder: "'0.7.0' for example, or a commit hash" +- type: input + attributes: + label: Destination operating system + description: The OS running your application. + placeholder: "'iOS 16' for example" +- type: input + attributes: + label: Xcode version information + description: The version of Xcode used to reproduce this issue. + placeholder: "The version displayed from 'Xcode 〉About Xcode'" +- type: textarea + attributes: + label: Swift Compiler version information + description: The version of Swift used to reproduce this issue. + placeholder: Output from 'xcrun swiftc --version' + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..54008c23e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,15 @@ +blank_issues_enabled: false + +contact_links: + - name: Project Discussion + url: https://github.com/pointfreeco/swiftui-navigation/discussions + about: SwiftUI Navigation Q&A, ideas, and more + - name: Documentation + url: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/ + about: Read SwiftUI Navigation's documentation + - name: Videos + url: https://www.pointfree.co/collections/swiftui-navigation + about: Watch videos to get a behind-the-scenes look at how SwiftUI Navigation was motivated and built + - name: Slack + url: https://www.pointfree.co/slack-invite + about: Community chat diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index e9a7a08d03..0000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Question -about: Have a question about the SwiftUI Navigation? -title: '' -labels: '' -assignees: '' - ---- - -SwiftUI Navigation uses GitHub issues for bugs. For more general discussion and help, please use [GitHub Discussions](https://github.com/pointfreeco/swiftui-navigation/discussions). From 1bc2bc91112a1c0d0f8585a5c9630a98059cc5cf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 14 Mar 2023 10:46:38 -0700 Subject: [PATCH 056/181] Fix main actor warnings (#89) --- Examples/CaseStudies/01-Alerts.swift | 3 ++- Examples/CaseStudies/02-ConfirmationDialogs.swift | 3 ++- Examples/CaseStudies/03-Sheets.swift | 3 ++- Examples/CaseStudies/04-Popovers.swift | 3 ++- Examples/CaseStudies/05-FullScreenCovers.swift | 3 ++- Examples/CaseStudies/06-NavigationDestinations.swift | 3 ++- Examples/CaseStudies/07-NavigationLinks.swift | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index 23c142e7ac..af872c2e2f 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -36,13 +36,14 @@ struct OptionalAlerts: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var isLoading = false @Published var fact: Fact? func numberFactButtonTapped() { - Task { @MainActor in + Task { self.isLoading = true defer { self.isLoading = false } self.fact = await getNumberFact(self.count) diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index 48ee1b61b2..e4feac944c 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -34,13 +34,14 @@ struct OptionalConfirmationDialogs: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var isLoading = false @Published var fact: Fact? func numberFactButtonTapped() { - Task { @MainActor in + Task { self.isLoading = true defer { self.isLoading = false } self.fact = await getNumberFact(self.count) diff --git a/Examples/CaseStudies/03-Sheets.swift b/Examples/CaseStudies/03-Sheets.swift index 87fcea317d..1067768a00 100644 --- a/Examples/CaseStudies/03-Sheets.swift +++ b/Examples/CaseStudies/03-Sheets.swift @@ -67,6 +67,7 @@ private struct FactEditor: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @@ -81,7 +82,7 @@ private class FeatureModel: ObservableObject { func numberFactButtonTapped() { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { @MainActor in + self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false try Task.checkCancellation() diff --git a/Examples/CaseStudies/04-Popovers.swift b/Examples/CaseStudies/04-Popovers.swift index 68497ab4ee..3582d89649 100644 --- a/Examples/CaseStudies/04-Popovers.swift +++ b/Examples/CaseStudies/04-Popovers.swift @@ -63,6 +63,7 @@ private struct FactEditor: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @@ -77,7 +78,7 @@ private class FeatureModel: ObservableObject { func numberFactButtonTapped() { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { @MainActor in + self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false try Task.checkCancellation() diff --git a/Examples/CaseStudies/05-FullScreenCovers.swift b/Examples/CaseStudies/05-FullScreenCovers.swift index e11025792c..588d1c7663 100644 --- a/Examples/CaseStudies/05-FullScreenCovers.swift +++ b/Examples/CaseStudies/05-FullScreenCovers.swift @@ -67,6 +67,7 @@ private struct FactEditor: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @@ -77,7 +78,7 @@ private class FeatureModel: ObservableObject { func numberFactButtonTapped() { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { @MainActor in + self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false try Task.checkCancellation() diff --git a/Examples/CaseStudies/06-NavigationDestinations.swift b/Examples/CaseStudies/06-NavigationDestinations.swift index 3582c5e9a9..27b8c7aa9a 100644 --- a/Examples/CaseStudies/06-NavigationDestinations.swift +++ b/Examples/CaseStudies/06-NavigationDestinations.swift @@ -71,6 +71,7 @@ private struct FactEditor: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @@ -86,7 +87,7 @@ private class FeatureModel: ObservableObject { if isActive { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { @MainActor in + self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false try Task.checkCancellation() diff --git a/Examples/CaseStudies/07-NavigationLinks.swift b/Examples/CaseStudies/07-NavigationLinks.swift index 2e1c769053..51ce98e2c3 100644 --- a/Examples/CaseStudies/07-NavigationLinks.swift +++ b/Examples/CaseStudies/07-NavigationLinks.swift @@ -67,6 +67,7 @@ private struct FactEditor: View { } } +@MainActor private class FeatureModel: ObservableObject { @Published var count = 0 @Published var fact: Fact? @@ -82,7 +83,7 @@ private class FeatureModel: ObservableObject { if isActive { self.isLoading = true self.fact = Fact(description: "\(self.count) is still loading...", number: self.count) - self.task = Task { @MainActor in + self.task = Task { let fact = await getNumberFact(self.count) self.isLoading = false try Task.checkCancellation() From 47dd574b900ba5ba679f56ea00d4d282fc7305a6 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 14 Mar 2023 11:31:25 -0700 Subject: [PATCH 057/181] Scope `navigationDestination` bind workaround (#93) * Scope `navigationDestination` bind workaround * Update NavigationDestination.swift * wip --- .../NavigationDestination.swift | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftUINavigation/NavigationDestination.swift b/Sources/SwiftUINavigation/NavigationDestination.swift index cc1fa3a589..6f9e611b4a 100644 --- a/Sources/SwiftUINavigation/NavigationDestination.swift +++ b/Sources/SwiftUINavigation/NavigationDestination.swift @@ -38,16 +38,23 @@ /// the source of truth. Likewise, changes to `value` are instantly reflected in the /// destination. If `value` 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 ) -> some View { - self.modifier( - _NavigationDestination( - isPresented: value.isPresent(), - destination: Binding(unwrapping: value).map(destination) + 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) + } + } } /// Pushes a view onto a `NavigationStack` using a binding and case path as a data source for @@ -79,7 +86,7 @@ // 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 _NavigationDestination: ViewModifier { + private struct _NavigationDestinationBindWorkaround: ViewModifier { @Binding var isPresented: Bool let destination: Destination @@ -91,4 +98,10 @@ .bind(self.$isPresented, to: self.$isPresentedState) } } + + private let requiresBindWorkaround = { + guard #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) + else { return true } + return false + }() #endif From 7255c8dd4b3a86a449f6185a2b59b68298c5a6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=9E=AC=ED=98=B8?= Date: Tue, 21 Mar 2023 10:42:29 +0900 Subject: [PATCH 058/181] Fix MainActor with defer compile error (#94) --- Examples/CaseStudies/01-Alerts.swift | 2 +- Examples/CaseStudies/02-ConfirmationDialogs.swift | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/CaseStudies/01-Alerts.swift b/Examples/CaseStudies/01-Alerts.swift index af872c2e2f..dc1baeafaf 100644 --- a/Examples/CaseStudies/01-Alerts.swift +++ b/Examples/CaseStudies/01-Alerts.swift @@ -45,8 +45,8 @@ private class FeatureModel: ObservableObject { func numberFactButtonTapped() { Task { self.isLoading = true - defer { self.isLoading = false } self.fact = await getNumberFact(self.count) + self.isLoading = false } } } diff --git a/Examples/CaseStudies/02-ConfirmationDialogs.swift b/Examples/CaseStudies/02-ConfirmationDialogs.swift index e4feac944c..312b080ad0 100644 --- a/Examples/CaseStudies/02-ConfirmationDialogs.swift +++ b/Examples/CaseStudies/02-ConfirmationDialogs.swift @@ -43,8 +43,8 @@ private class FeatureModel: ObservableObject { func numberFactButtonTapped() { Task { self.isLoading = true - defer { self.isLoading = false } self.fact = await getNumberFact(self.count) + self.isLoading = false } } } diff --git a/README.md b/README.md index 36d640418e..51b7438645 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.4.5") + .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.7.1") ] ``` 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 059/181] 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 060/181] 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 061/181] 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 062/181] 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 063/181] 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 064/181] 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 065/181] 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 066/181] 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 067/181] 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 068/181] 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 069/181] 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 070/181] 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 071/181] 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 072/181] 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 073/181] 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 074/181] 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 075/181] 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 076/181] 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 077/181] 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 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 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 078/181] 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 079/181] 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 080/181] 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 081/181] 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 082/181] 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 083/181] 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 084/181] 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 085/181] 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 086/181] 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 087/181] [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 088/181] 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 089/181] 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 090/181] 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 091/181] 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 092/181] 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 093/181] 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 094/181] 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 095/181] 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 096/181] 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 097/181] 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 098/181] 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 099/181] 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 100/181] 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 101/181] 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 102/181] 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 103/181] 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 104/181] 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 105/181] 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 106/181] 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 107/181] 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 108/181] 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 109/181] 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 110/181] 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 111/181] 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 112/181] 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 113/181] 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 114/181] 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 115/181] 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 116/181] 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 117/181] 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 118/181] 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 119/181] 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