Skip to content

Commit 72dbb2a

Browse files
authored
Add sheet(item:id:) (pointfreeco#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
1 parent 2b7a69b commit 72dbb2a

File tree

4 files changed

+95
-10
lines changed

4 files changed

+95
-10
lines changed

Sources/SwiftUINavigation/FullScreenCover.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
#if canImport(SwiftUI)
22
import SwiftUI
33

4+
@available(iOS 14, tvOS 14, watchOS 7, *)
5+
@available(macOS, unavailable)
46
extension View {
7+
/// Presents a full-screen cover using a binding as a data source for the sheet's content based
8+
/// on the identity of the underlying item.
9+
///
10+
/// - Parameters:
11+
/// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`,
12+
/// the system passes the item's content to the modifier's closure. You display this content
13+
/// in a sheet that you create that the system displays to the user. If `item` changes, the
14+
/// system dismisses the sheet and replaces it with a new one using the same process.
15+
/// - id: The key path to the provided item's identifier.
16+
/// - onDismiss: The closure to execute when dismissing the sheet.
17+
/// - content: A closure returning the content of the sheet.
18+
public func fullScreenCover<Item, ID: Hashable, Content: View>(
19+
item: Binding<Item?>,
20+
id: KeyPath<Item, ID>,
21+
onDismiss: (() -> Void)? = nil,
22+
@ViewBuilder content: @escaping (Item) -> Content
23+
) -> some View {
24+
self.fullScreenCover(item: item[id: id], onDismiss: onDismiss) { _ in
25+
item.wrappedValue.map(content)
26+
}
27+
}
28+
529
/// Presents a full-screen cover using a binding as a data source for the sheet's content.
630
///
731
/// SwiftUI comes with a `fullScreenCover(item:)` view modifier that is powered by a binding to
@@ -36,15 +60,13 @@
3660
///
3761
/// - Parameters:
3862
/// - value: A binding to a source of truth for the sheet. When `value` is non-`nil`, a
39-
/// non-optional binding to the value is passed to the `content` closure. You use this binding
40-
/// to produce content that the system presents to the user in a sheet. Changes made to the
41-
/// sheet's binding will be reflected back in the source of truth. Likewise, changes to
42-
/// `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is
63+
/// non-optional binding to the value is passed to the `content` closure. You use this
64+
/// binding to produce content that the system presents to the user in a sheet. Changes made
65+
/// to the sheet's binding will be reflected back in the source of truth. Likewise, changes
66+
/// to `value` are instantly reflected in the sheet. If `value` becomes `nil`, the sheet is
4367
/// dismissed.
4468
/// - onDismiss: The closure to execute when dismissing the sheet.
4569
/// - content: A closure returning the content of the sheet.
46-
@available(iOS 14, tvOS 14, watchOS 7, *)
47-
@available(macOS, unavailable)
4870
public func fullScreenCover<Value, Content>(
4971
unwrapping value: Binding<Value?>,
5072
onDismiss: (() -> Void)? = nil,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
struct Identified<ID: Hashable>: Identifiable {
2+
let id: ID
3+
}
4+
5+
extension Optional {
6+
subscript<ID: Hashable>(id keyPath: KeyPath<Wrapped, ID>) -> Identified<ID>? {
7+
get { (self?[keyPath: keyPath]).map(Identified.init) }
8+
set { if newValue == nil { self = nil } }
9+
}
10+
}

Sources/SwiftUINavigation/Popover.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
#if canImport(SwiftUI)
22
import SwiftUI
33

4+
@available(tvOS, unavailable)
5+
@available(watchOS, unavailable)
46
extension View {
7+
/// Presents a popover using a binding as a data source for the sheet's content based on the
8+
/// identity of the underlying item.
9+
///
10+
/// - Parameters:
11+
/// - item: A binding to an optional source of truth for the popover. When `item` is
12+
/// non-`nil`, the system passes the item's content to the modifier's closure. You display
13+
/// this content in a popover that you create that the system displays to the user. If `item`
14+
/// changes, the system dismisses the popover and replaces it with a new one using the same
15+
/// process.
16+
/// - id: The key path to the provided item's identifier.
17+
/// - attachmentAnchor: The positioning anchor that defines the attachment point of the
18+
/// popover.
19+
/// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's
20+
/// arrow.
21+
/// - content: A closure returning the content of the popover.
22+
public func popover<Item, ID: Hashable, Content: View>(
23+
item: Binding<Item?>,
24+
id: KeyPath<Item, ID>,
25+
attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds),
26+
arrowEdge: Edge = .top,
27+
@ViewBuilder content: @escaping (Item) -> Content
28+
) -> some View {
29+
self.popover(
30+
item: item[id: id],
31+
attachmentAnchor: attachmentAnchor,
32+
arrowEdge: arrowEdge
33+
) { _ in
34+
item.wrappedValue.map(content)
35+
}
36+
}
37+
538
/// Presents a popover using a binding as a data source for the popover's content.
639
///
740
/// SwiftUI comes with a `popover(item:)` view modifier that is powered by a binding to some
@@ -46,14 +79,12 @@
4679
/// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's
4780
/// arrow.
4881
/// - content: A closure returning the content of the popover.
49-
@available(tvOS, unavailable)
50-
@available(watchOS, unavailable)
51-
public func popover<Value, Content>(
82+
public func popover<Value, Content: View>(
5283
unwrapping value: Binding<Value?>,
5384
attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds),
5485
arrowEdge: Edge = .top,
5586
@ViewBuilder content: @escaping (Binding<Value>) -> Content
56-
) -> some View where Content: View {
87+
) -> some View {
5788
self.popover(
5889
isPresented: value.isPresent(),
5990
attachmentAnchor: attachmentAnchor,

Sources/SwiftUINavigation/Sheet.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@
88
#endif
99

1010
extension View {
11+
/// Presents a sheet using a binding as a data source for the sheet's content based on the
12+
/// identity of the underlying item.
13+
///
14+
/// - Parameters:
15+
/// - item: A binding to an optional source of truth for the sheet. When `item` is non-`nil`,
16+
/// the system passes the item's content to the modifier's closure. You display this content
17+
/// in a sheet that you create that the system displays to the user. If `item` changes, the
18+
/// system dismisses the sheet and replaces it with a new one using the same process.
19+
/// - id: The key path to the provided item's identifier.
20+
/// - onDismiss: The closure to execute when dismissing the sheet.
21+
/// - content: A closure returning the content of the sheet.
22+
public func sheet<Item, ID: Hashable, Content: View>(
23+
item: Binding<Item?>,
24+
id: KeyPath<Item, ID>,
25+
onDismiss: (() -> Void)? = nil,
26+
@ViewBuilder content: @escaping (Item) -> Content
27+
) -> some View {
28+
self.sheet(item: item[id: id], onDismiss: onDismiss) { _ in
29+
item.wrappedValue.map(content)
30+
}
31+
}
32+
1133
/// Presents a sheet using a binding as a data source for the sheet's content.
1234
///
1335
/// SwiftUI comes with a `sheet(item:)` view modifier that is powered by a binding to some

0 commit comments

Comments
 (0)