diff --git a/Sources/SwiftUIBackports/iOS/Presentation/BackgroundInteraction.swift b/Sources/SwiftUIBackports/iOS/Presentation/BackgroundInteraction.swift new file mode 100644 index 0000000..c432a8f --- /dev/null +++ b/Sources/SwiftUIBackports/iOS/Presentation/BackgroundInteraction.swift @@ -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 { + /// 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.PresentationBackgroundInteraction) -> some View { + #if os(iOS) + if #available(iOS 15, *) { + wrapped.background(Backport.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.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.PresentationBackgroundInteraction + weak var _delegate: UISheetPresentationControllerDelegate? + + init(interaction: Backport.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.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.PresentationDetent(id: .init(rawValue: selectedId.rawValue)) + controller.presentingViewController.view?.tintAdjustmentMode = selected > detent ? .dimmed : .normal + } + } + } + } + } +} +#endif diff --git a/Sources/SwiftUIBackports/iOS/Presentation/ContentInteraction.swift b/Sources/SwiftUIBackports/iOS/Presentation/ContentInteraction.swift new file mode 100644 index 0000000..daa83b8 --- /dev/null +++ b/Sources/SwiftUIBackports/iOS/Presentation/ContentInteraction.swift @@ -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 { + /// 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.PresentationContentInteraction) -> some View { + #if os(iOS) + if #available(iOS 15, *) { + wrapped.background(Backport.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.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.PresentationContentInteraction + + init(interaction: Backport.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.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 diff --git a/Sources/SwiftUIBackports/iOS/Presentation/CornerRadius.swift b/Sources/SwiftUIBackports/iOS/Presentation/CornerRadius.swift new file mode 100644 index 0000000..00e3e58 --- /dev/null +++ b/Sources/SwiftUIBackports/iOS/Presentation/CornerRadius.swift @@ -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.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 diff --git a/Sources/SwiftUIBackports/iOS/PresentationDetents/Detents.swift b/Sources/SwiftUIBackports/iOS/Presentation/Detents.swift similarity index 65% rename from Sources/SwiftUIBackports/iOS/PresentationDetents/Detents.swift rename to Sources/SwiftUIBackports/iOS/Presentation/Detents.swift index ab57729..4f25911 100644 --- a/Sources/SwiftUIBackports/iOS/PresentationDetents/Detents.swift +++ b/Sources/SwiftUIBackports/iOS/Presentation/Detents.swift @@ -5,7 +5,6 @@ import SwiftBackports @available(macOS, deprecated: 13) @available(watchOS, deprecated: 9) public extension Backport where Wrapped: View { - /// Sets the available detents for the enclosing sheet. /// /// 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+") func presentationDetents(_ detents: Set.PresentationDetent>) -> some View { #if os(iOS) - wrapped.background(Backport.Representable(detents: detents, selection: nil, largestUndimmed: .large)) - #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.PresentationDetent>, largestUndimmedDetent: Backport.PresentationDetent? = nil) -> some View { - #if os(iOS) - wrapped.background(Backport.Representable(detents: detents, selection: nil, largestUndimmed: largestUndimmedDetent)) + wrapped.background(Backport.Representable(detents: detents, selection: nil)) #else wrapped #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+") func presentationDetents(_ detents: Set.PresentationDetent>, selection: Binding.PresentationDetent>) -> some View { #if os(iOS) - wrapped.background(Backport.Representable(detents: detents, selection: selection, largestUndimmed: .large)) + wrapped.background(Backport.Representable(detents: detents, selection: selection)) #else wrapped #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.PresentationDetent>, selection: Binding.PresentationDetent>, largestUndimmedDetent: Backport.PresentationDetent? = nil) -> some View { - #if os(iOS) - wrapped.background(Backport.Representable(detents: detents, selection: selection, largestUndimmed: largestUndimmedDetent)) - #else - wrapped - #endif - } - } @available(iOS, deprecated: 16) @available(tvOS, deprecated: 16) @available(macOS, deprecated: 13) @available(watchOS, deprecated: 9) -public extension Backport where Wrapped == Any { +public extension Backport { /// A type that represents a height where a sheet naturally rests. struct PresentationDetent: Hashable, Comparable { @@ -206,18 +132,17 @@ public extension Backport where Wrapped == Any { #if os(iOS) @available(iOS 15, *) -private extension Backport where Wrapped == Any { +private extension Backport { struct Representable: UIViewControllerRepresentable { let detents: Set.PresentationDetent> let selection: Binding.PresentationDetent>? - let largestUndimmed: Backport.PresentationDetent? 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) { - 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.PresentationDetent? weak var _delegate: UISheetPresentationControllerDelegate? - init(detents: Set.PresentationDetent>, selection: Binding.PresentationDetent>?, largestUndimmed: Backport.PresentationDetent?) { + init(detents: Set.PresentationDetent>, selection: Binding.PresentationDetent>?) { self.detents = detents self.selection = selection - self.largestUndimmed = largestUndimmed super.init(nibName: nil, bundle: nil) } @@ -250,18 +174,17 @@ private extension Backport.Representable { controller.delegate = self } } - update(detents: detents, selection: selection, largestUndimmed: largestUndimmed) + update(detents: detents, selection: selection) } override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) - update(detents: detents, selection: selection, largestUndimmed: largestUndimmed) + update(detents: detents, selection: selection) } - func update(detents: Set.PresentationDetent>, selection: Binding.PresentationDetent>?, largestUndimmed: Backport.PresentationDetent?) { + func update(detents: Set.PresentationDetent>, selection: Binding.PresentationDetent>?) { self.detents = detents self.selection = selection - self.largestUndimmed = largestUndimmed if let controller = parent?.sheetPresentationController { controller.animateChanges { @@ -274,10 +197,6 @@ private extension Backport.Representable { } } - controller.largestUndimmedDetentIdentifier = largestUndimmed.flatMap { - .init(rawValue: $0.id.rawValue) - } - if let selection = selection { controller.selectedDetentIdentifier = .init(selection.wrappedValue.id.rawValue) } @@ -286,8 +205,8 @@ private extension Backport.Representable { } UIView.animate(withDuration: 0.25) { - if let undimmed = largestUndimmed { - controller.presentingViewController.view?.tintAdjustmentMode = (selection?.wrappedValue ?? .large) >= undimmed ? .automatic : .normal + if let undimmed = controller.largestUndimmedDetentIdentifier { + controller.presentingViewController.view?.tintAdjustmentMode = (selection?.wrappedValue ?? .large) >= .init(id: .init(rawValue: undimmed.rawValue)) ? .automatic : .normal } else { controller.presentingViewController.view?.tintAdjustmentMode = .automatic } diff --git a/Sources/SwiftUIBackports/iOS/PresentationDetents/DragIndicator.swift b/Sources/SwiftUIBackports/iOS/Presentation/DragIndicator.swift similarity index 100% rename from Sources/SwiftUIBackports/iOS/PresentationDetents/DragIndicator.swift rename to Sources/SwiftUIBackports/iOS/Presentation/DragIndicator.swift diff --git a/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDetent.swift b/Sources/SwiftUIBackports/iOS/Presentation/InteractiveDetent.swift similarity index 100% rename from Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDetent.swift rename to Sources/SwiftUIBackports/iOS/Presentation/InteractiveDetent.swift diff --git a/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift b/Sources/SwiftUIBackports/iOS/Presentation/InteractiveDismiss.swift similarity index 100% rename from Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift rename to Sources/SwiftUIBackports/iOS/Presentation/InteractiveDismiss.swift diff --git a/Sources/SwiftUIBackports/iOS/Presentation/test.swift b/Sources/SwiftUIBackports/iOS/Presentation/test.swift new file mode 100644 index 0000000..36e5138 --- /dev/null +++ b/Sources/SwiftUIBackports/iOS/Presentation/test.swift @@ -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