Adds presentation background/content interactions and corner radius

This commit is contained in:
Shaps Benkau 2023-02-22 01:02:07 +00:00
parent e6d5b78c69
commit 00b13d90ea
8 changed files with 480 additions and 93 deletions

View File

@ -0,0 +1,163 @@
import SwiftUI
import SwiftBackports
@available(iOS, deprecated: 16.4)
@available(tvOS, deprecated: 16.4)
@available(macOS, deprecated: 13.3)
@available(watchOS, deprecated: 9.4)
extension Backport<Any> {
/// The kinds of interaction available to views behind a presentation.
///
/// Use values of this type with the
/// ``View/presentationBackgroundInteraction(_:)`` modifier.
public struct PresentationBackgroundInteraction: Hashable {
enum Interaction: Hashable {
case automatic
case enabled
case upThrough(detent: Backport.PresentationDetent)
case disabled
}
let interaction: Interaction
/// The default background interaction for the presentation.
public static var automatic: Self { .init(interaction: .automatic) }
/// People can interact with the view behind a presentation.
public static var enabled: Self { .init(interaction: .enabled) }
/// People can interact with the view behind a presentation up through a
/// specified detent.
///
/// At detents larger than the one you specify, SwiftUI disables
/// interaction.
///
/// - Parameter detent: The largest detent at which people can interact with
/// the view behind the presentation.
public static func enabled(upThrough detent: Backport.PresentationDetent) -> Self { .init(interaction: .upThrough(detent: detent))}
/// People can't interact with the view behind a presentation.
public static var disabled: Self { .init(interaction: .disabled) }
}
}
@available(iOS, deprecated: 16.4)
@available(tvOS, deprecated: 16.4)
@available(macOS, deprecated: 13.3)
@available(watchOS, deprecated: 9.4)
public extension Backport where Wrapped: View {
/// Controls whether people can interact with the view behind a
/// presentation.
///
/// On many platforms, SwiftUI automatically disables the view behind a
/// sheet that you present, so that people can't interact with the backing
/// view until they dismiss the sheet. Use this modifier if you want to
/// enable interaction.
///
/// The following example enables people to interact with the view behind
/// the sheet when the sheet is at the smallest detent, but not at the other
/// detents:
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents(
/// [.medium, .large])
/// .presentationBackgroundInteraction(
/// .enabled(upThrough: .medium))
/// }
/// }
/// }
///
/// - Parameters:
/// - interaction: A specification of how people can interact with the
/// view behind a presentation.
@ViewBuilder
func presentationBackgroundInteraction(_ interaction: Backport<Any>.PresentationBackgroundInteraction) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
wrapped.background(Backport<Any>.Representable(interaction: interaction))
} else {
wrapped
}
#else
wrapped
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
private extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let interaction: Backport<Any>.PresentationBackgroundInteraction
func makeUIViewController(context: Context) -> Backport.Representable.Controller {
Controller(interaction: interaction)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) {
controller.update(interaction: interaction)
}
}
}
@available(iOS 15, *)
private extension Backport.Representable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var interaction: Backport<Any>.PresentationBackgroundInteraction
weak var _delegate: UISheetPresentationControllerDelegate?
init(interaction: Backport<Any>.PresentationBackgroundInteraction) {
self.interaction = interaction
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(interaction: interaction)
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
update(interaction: interaction)
}
func update(interaction: Backport<Any>.PresentationBackgroundInteraction) {
self.interaction = interaction
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
switch interaction.interaction {
case .automatic:
controller.largestUndimmedDetentIdentifier = nil
controller.presentingViewController.view?.tintAdjustmentMode = .automatic
case .disabled:
controller.largestUndimmedDetentIdentifier = nil
controller.presentingViewController.view?.tintAdjustmentMode = .automatic
case .enabled:
controller.largestUndimmedDetentIdentifier = .large
controller.presentingViewController.view?.tintAdjustmentMode = .normal
case .upThrough(let detent):
controller.largestUndimmedDetentIdentifier = .init(detent.id.rawValue)
let selectedId = controller.selectedDetentIdentifier ?? .large
let selected = Backport<Any>.PresentationDetent(id: .init(rawValue: selectedId.rawValue))
controller.presentingViewController.view?.tintAdjustmentMode = selected > detent ? .dimmed : .normal
}
}
}
}
}
}
#endif

View File

@ -0,0 +1,113 @@
import SwiftUI
import SwiftBackports
@available(iOS, deprecated: 16.4)
@available(tvOS, deprecated: 16.4)
@available(macOS, deprecated: 13.3)
@available(watchOS, deprecated: 9.4)
public extension Backport<Any> {
/// A behavior that you can use to influence how a presentation responds to
/// swipe gestures.
///
/// Use values of this type with the
/// ``View/presentationContentInteraction(_:)`` modifier.
struct PresentationContentInteraction: Hashable {
enum Interaction: Hashable {
case automatic
case resizes
case scrolls
}
let interaction: Interaction
/// The default swipe behavior for the presentation.
public static var automatic: PresentationContentInteraction { .init(interaction: .automatic) }
/// A behavior that prioritizes resizing a presentation when swiping, rather
/// than scrolling the content of the presentation.
public static var resizes: PresentationContentInteraction { .init(interaction: .resizes) }
/// A behavior that prioritizes scrolling the content of a presentation when
/// swiping, rather than resizing the presentation.
public static var scrolls: PresentationContentInteraction { .init(interaction: .scrolls) }
}
}
@available(iOS, deprecated: 16.4)
@available(tvOS, deprecated: 16.4)
@available(macOS, deprecated: 13.3)
@available(watchOS, deprecated: 9.4)
public extension Backport where Wrapped: View {
@ViewBuilder
func presentationContentInteraction(_ interaction: Backport<Any>.PresentationContentInteraction) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
wrapped.background(Backport<Any>.Representable(interaction: interaction))
} else {
wrapped
}
#else
wrapped
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
private extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let interaction: Backport<Any>.PresentationContentInteraction
func makeUIViewController(context: Context) -> Backport.Representable.Controller {
Controller(interaction: interaction)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) {
controller.update(interaction: interaction)
}
}
}
@available(iOS 15, *)
private extension Backport.Representable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var interaction: Backport<Any>.PresentationContentInteraction
init(interaction: Backport<Any>.PresentationContentInteraction) {
self.interaction = interaction
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(interaction: interaction)
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
update(interaction: interaction)
}
func update(interaction: Backport<Any>.PresentationContentInteraction) {
self.interaction = interaction
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
switch interaction.interaction {
case .automatic, .resizes:
controller.prefersScrollingExpandsWhenScrolledToEdge = true
case .scrolls:
controller.prefersScrollingExpandsWhenScrolledToEdge = false
}
controller.preferredCornerRadius = 50
}
}
}
}
}
#endif

View File

@ -0,0 +1,74 @@
import SwiftUI
import SwiftBackports
@available(iOS, deprecated: 16.4)
@available(tvOS, deprecated: 16.4)
@available(macOS, deprecated: 13.3)
@available(watchOS, deprecated: 9.4)
public extension Backport where Wrapped: View {
@ViewBuilder
func presentationCornerRadius(_ cornerRadius: CGFloat?) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
wrapped.background(Backport<Any>.Representable(cornerRadius: cornerRadius))
} else {
wrapped
}
#else
wrapped
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
private extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let cornerRadius: CGFloat?
func makeUIViewController(context: Context) -> Backport.Representable.Controller {
Controller(cornerRadius: cornerRadius)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) {
controller.update(cornerRadius: cornerRadius)
}
}
}
@available(iOS 15, *)
private extension Backport.Representable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var cornerRadius: CGFloat?
init(cornerRadius: CGFloat?) {
self.cornerRadius = cornerRadius
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(cornerRadius: cornerRadius)
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
update(cornerRadius: cornerRadius)
}
func update(cornerRadius: CGFloat?) {
self.cornerRadius = cornerRadius
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.preferredCornerRadius = cornerRadius
}
}
}
}
}
#endif

View File

@ -5,7 +5,6 @@ import SwiftBackports
@available(macOS, deprecated: 13) @available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9) @available(watchOS, deprecated: 9)
public extension Backport where Wrapped: View { public extension Backport where Wrapped: View {
/// Sets the available detents for the enclosing sheet. /// Sets the available detents for the enclosing sheet.
/// ///
/// By default, sheets support the ``PresentationDetent/large`` detent. /// By default, sheets support the ``PresentationDetent/large`` detent.
@ -31,39 +30,7 @@ public extension Backport where Wrapped: View {
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+") @available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>) -> some View { func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>) -> some View {
#if os(iOS) #if os(iOS)
wrapped.background(Backport<Any>.Representable(detents: detents, selection: nil, largestUndimmed: .large)) wrapped.background(Backport<Any>.Representable(detents: detents, selection: nil))
#else
wrapped
#endif
}
/// Sets the available detents for the enclosing sheet.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents([.medium, .large])
/// }
/// }
/// }
///
/// - Parameter detents: A set of supported detents for the sheet.
/// If you provide more than one detent, people can drag the sheet
/// to resize it.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>, largestUndimmedDetent: Backport<Any>.PresentationDetent? = nil) -> some View {
#if os(iOS)
wrapped.background(Backport<Any>.Representable(detents: detents, selection: nil, largestUndimmed: largestUndimmedDetent))
#else #else
wrapped wrapped
#endif #endif
@ -104,59 +71,18 @@ public extension Backport where Wrapped: View {
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+") @available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>) -> some View { func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>) -> some View {
#if os(iOS) #if os(iOS)
wrapped.background(Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: .large)) wrapped.background(Backport<Any>.Representable(detents: detents, selection: selection))
#else #else
wrapped wrapped
#endif #endif
} }
/// Sets the available detents for the enclosing sheet, giving you
/// programmatic control of the currently selected detent.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
/// @State private var settingsDetent = PresentationDetent.medium
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:(
/// [.medium, .large],
/// selection: $settingsDetent
/// )
/// }
/// }
/// }
///
/// - Parameters:
/// - detents: A set of supported detents for the sheet.
/// If you provide more that one detent, people can drag the sheet
/// to resize it.
/// - selection: A ``Binding`` to the currently selected detent.
/// Ensure that the value matches one of the detents that you
/// provide for the `detents` parameter.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>, largestUndimmedDetent: Backport<Any>.PresentationDetent? = nil) -> some View {
#if os(iOS)
wrapped.background(Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: largestUndimmedDetent))
#else
wrapped
#endif
}
} }
@available(iOS, deprecated: 16) @available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16) @available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13) @available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9) @available(watchOS, deprecated: 9)
public extension Backport where Wrapped == Any { public extension Backport<Any> {
/// A type that represents a height where a sheet naturally rests. /// A type that represents a height where a sheet naturally rests.
struct PresentationDetent: Hashable, Comparable { struct PresentationDetent: Hashable, Comparable {
@ -206,18 +132,17 @@ public extension Backport where Wrapped == Any {
#if os(iOS) #if os(iOS)
@available(iOS 15, *) @available(iOS 15, *)
private extension Backport where Wrapped == Any { private extension Backport<Any> {
struct Representable: UIViewControllerRepresentable { struct Representable: UIViewControllerRepresentable {
let detents: Set<Backport<Any>.PresentationDetent> let detents: Set<Backport<Any>.PresentationDetent>
let selection: Binding<Backport<Any>.PresentationDetent>? let selection: Binding<Backport<Any>.PresentationDetent>?
let largestUndimmed: Backport<Any>.PresentationDetent?
func makeUIViewController(context: Context) -> Backport.Representable.Controller { func makeUIViewController(context: Context) -> Backport.Representable.Controller {
Controller(detents: detents, selection: selection, largestUndimmed: largestUndimmed) Controller(detents: detents, selection: selection)
} }
func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) { func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) {
controller.update(detents: detents, selection: selection, largestUndimmed: largestUndimmed) controller.update(detents: detents, selection: selection)
} }
} }
} }
@ -231,10 +156,9 @@ private extension Backport.Representable {
var largestUndimmed: Backport<Any>.PresentationDetent? var largestUndimmed: Backport<Any>.PresentationDetent?
weak var _delegate: UISheetPresentationControllerDelegate? weak var _delegate: UISheetPresentationControllerDelegate?
init(detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?, largestUndimmed: Backport<Any>.PresentationDetent?) { init(detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?) {
self.detents = detents self.detents = detents
self.selection = selection self.selection = selection
self.largestUndimmed = largestUndimmed
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@ -250,18 +174,17 @@ private extension Backport.Representable {
controller.delegate = self controller.delegate = self
} }
} }
update(detents: detents, selection: selection, largestUndimmed: largestUndimmed) update(detents: detents, selection: selection)
} }
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator) super.willTransition(to: newCollection, with: coordinator)
update(detents: detents, selection: selection, largestUndimmed: largestUndimmed) update(detents: detents, selection: selection)
} }
func update(detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?, largestUndimmed: Backport<Any>.PresentationDetent?) { func update(detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?) {
self.detents = detents self.detents = detents
self.selection = selection self.selection = selection
self.largestUndimmed = largestUndimmed
if let controller = parent?.sheetPresentationController { if let controller = parent?.sheetPresentationController {
controller.animateChanges { controller.animateChanges {
@ -274,10 +197,6 @@ private extension Backport.Representable {
} }
} }
controller.largestUndimmedDetentIdentifier = largestUndimmed.flatMap {
.init(rawValue: $0.id.rawValue)
}
if let selection = selection { if let selection = selection {
controller.selectedDetentIdentifier = .init(selection.wrappedValue.id.rawValue) controller.selectedDetentIdentifier = .init(selection.wrappedValue.id.rawValue)
} }
@ -286,8 +205,8 @@ private extension Backport.Representable {
} }
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
if let undimmed = largestUndimmed { if let undimmed = controller.largestUndimmedDetentIdentifier {
controller.presentingViewController.view?.tintAdjustmentMode = (selection?.wrappedValue ?? .large) >= undimmed ? .automatic : .normal controller.presentingViewController.view?.tintAdjustmentMode = (selection?.wrappedValue ?? .large) >= .init(id: .init(rawValue: undimmed.rawValue)) ? .automatic : .normal
} else { } else {
controller.presentingViewController.view?.tintAdjustmentMode = .automatic controller.presentingViewController.view?.tintAdjustmentMode = .automatic
} }

View File

@ -0,0 +1,118 @@
import SwiftUI
import SwiftBackports
///// Strategies for adapting a presentation to a different size class.
/////
///// Use values of this type with the ``View/presentationCompactAdaptation(_:)``
///// and ``View/presentationCompactAdaptation(horizontal:vertical:)`` modifiers.
//@available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *)
//public struct PresentationAdaptation {
//
// /// Use the default presentation adaptation.
// public static var automatic: PresentationAdaptation { get }
//
// /// Don't adapt for the size class, if possible.
// public static var none: PresentationAdaptation { get }
//
// /// Prefer a popover appearance when adapting for size classes.
// public static var popover: PresentationAdaptation { get }
//
// /// Prefer a sheet appearance when adapting for size classes.
// public static var sheet: PresentationAdaptation { get }
//
// /// Prefer a full-screen-cover appearance when adapting for size classes.
// public static var fullScreenCover: PresentationAdaptation { get }
//}
//
//
///// Specifies how to adapt a presentation to compact size classes.
/////
///// Some presentations adapt their appearance depending on the context. For
///// example, a sheet presentation over a vertically-compact view uses a
///// full-screen-cover appearance by default. Use this modifier to indicate
///// a custom adaptation preference. For example, the following code
///// uses a presentation adaptation value of ``PresentationAdaptation/none``
///// to request that the system not adapt the sheet in compact size classes:
/////
///// struct ContentView: View {
///// @State private var showSettings = false
/////
///// var body: some View {
///// Button("View Settings") {
///// showSettings = true
///// }
///// .sheet(isPresented: $showSettings) {
///// SettingsView()
///// .presentationDetents([.medium, .large])
///// .presentationCompactAdaptation(.none)
///// }
///// }
///// }
/////
///// If you want to specify different adaptations for each dimension,
///// use the ``View/presentationCompactAdaptation(horizontal:vertical:)``
///// method instead.
/////
///// - Parameter adaptation: The adaptation to use in either a horizontally
///// or vertically compact size class.
//public func presentationCompactAdaptation(_ adaptation: PresentationAdaptation) -> some View
//
//
///// Specifies how to adapt a presentation to horizontally and vertically
///// compact size classes.
/////
///// Some presentations adapt their appearance depending on the context. For
///// example, a popover presentation over a horizontally-compact view uses a
///// sheet appearance by default. Use this modifier to indicate a custom
///// adaptation preference.
/////
///// struct ContentView: View {
///// @State private var showInfo = false
/////
///// var body: some View {
///// Button("View Info") {
///// showInfo = true
///// }
///// .popover(isPresented: $showInfo) {
///// InfoView()
///// .presentationCompactAdaptation(
///// horizontal: .popover,
///// vertical: .sheet)
///// }
///// }
///// }
/////
///// If you want to specify the same adaptation for both dimensions,
///// use the ``View/presentationCompactAdaptation(_:)`` method instead.
/////
///// - Parameters:
///// - horizontalAdaptation: The adaptation to use in a horizontally
///// compact size class.
///// - verticalAdaptation: The adaptation to use in a vertically compact
///// size class. In a size class that is both horizontally and vertically
///// compact, SwiftUI uses the `verticalAdaptation` value.
//public func presentationCompactAdaptation(horizontal horizontalAdaptation: PresentationAdaptation, vertical verticalAdaptation: PresentationAdaptation) -> some View
//
//
///// Requests that the presentation have a specific corner radius.
/////
///// Use this modifier to change the corner radius of a presentation.
/////
///// struct ContentView: View {
///// @State private var showSettings = false
/////
///// var body: some View {
///// Button("View Settings") {
///// showSettings = true
///// }
///// .sheet(isPresented: $showSettings) {
///// SettingsView()
///// .presentationDetents([.medium, .large])
///// .presentationCornerRadius(21)
///// }
///// }
///// }
/////
///// - Parameter cornerRadius: The corner radius, or `nil` to use the system
///// default.
//public func presentationCornerRadius(_ cornerRadius: CGFloat?) -> some View