Skip to content

Commit

Permalink
Add sheet(item:id:) (#155)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephencelis authored May 27, 2024
1 parent 2b7a69b commit 72dbb2a
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 10 deletions.
34 changes: 28 additions & 6 deletions Sources/SwiftUINavigation/FullScreenCover.swift
Original file line number Diff line number Diff line change
@@ -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, ID: Hashable, Content: View>(
item: Binding<Item?>,
id: KeyPath<Item, ID>,
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
Expand Down Expand Up @@ -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<Value, Content>(
unwrapping value: Binding<Value?>,
onDismiss: (() -> Void)? = nil,
Expand Down
10 changes: 10 additions & 0 deletions Sources/SwiftUINavigation/Internal/Identified.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
struct Identified<ID: Hashable>: Identifiable {
let id: ID
}

extension Optional {
subscript<ID: Hashable>(id keyPath: KeyPath<Wrapped, ID>) -> Identified<ID>? {
get { (self?[keyPath: keyPath]).map(Identified.init) }
set { if newValue == nil { self = nil } }
}
}
39 changes: 35 additions & 4 deletions Sources/SwiftUINavigation/Popover.swift
Original file line number Diff line number Diff line change
@@ -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, ID: Hashable, Content: View>(
item: Binding<Item?>,
id: KeyPath<Item, ID>,
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
Expand Down Expand Up @@ -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<Value, Content>(
public func popover<Value, Content: View>(
unwrapping value: Binding<Value?>,
attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds),
arrowEdge: Edge = .top,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Content: View {
) -> some View {
self.popover(
isPresented: value.isPresent(),
attachmentAnchor: attachmentAnchor,
Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftUINavigation/Sheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, ID: Hashable, Content: View>(
item: Binding<Item?>,
id: KeyPath<Item, ID>,
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
Expand Down

0 comments on commit 72dbb2a

Please sign in to comment.