Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iPad Bottom Sheet, but at the Bottom ✨ #100

Merged
merged 8 commits into from Sep 9, 2022
Merged
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ The ViewModifiers are used to customise the look and feel of the BottomSheet.
- Changing the threshold does not affect whether either option is enabled.
- The default threshold is 30% (0.3).

`.iPadFloatingSheet(Bool)`: Makes the iPad's bottom sheet be an actual bottom sheet when set to true.
- The default is `false`.

`.relativeSheetWidth(Double?)`: Sets the relative width of the sheet.
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
- Relative to the available width.
- Max 1, min 0.
- Set to `nil` to disable, and let the library decide the width.

`.accountForKeyboardHeight(Bool)`: Adds bottom padding to the main content equal to the keyboard height when the keyboard appears.
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
- The default is `false`.
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
- Will not move the sheet further up if the sheet has a smaller height than the keyboard.

## BottomSheetPosition

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// BottomSheet+AccountForKeyboardHeight.swift
//
//
// Created by Robin Pel on 06/09/2022.
//

import Foundation

public extension BottomSheet {

/// Adds padding to the bottom of the main content when the keyboard appears so all of the main content is visible.
///
/// - Note: If the height of the sheet is smaller than the height of the keyboard, this modifier will not make the content visible.
///
/// - Parameters:
/// - bool: A boolean whether the option is enabled.
///
/// - Returns: A BottomSheet with its main content shifted up to account for the keyboard when it has appeared.
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
func accountForKeyboardHeight(_ bool: Bool = true) -> BottomSheet {
self.configuration.accountForKeyboardHeight = bool
return self
}
}
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// BottomSheet+IPadFloatingSheet.swift
//
//
// Created by Robin Pel on 05/09/2022.
//

import Foundation

public extension BottomSheet {

/// Makes it possible to make the sheet appear like on iPhone.
///
/// - Parameters:
/// - bool: A boolean whether the option is enabled.
///
/// - Returns: A BottomSheet that will actually appear at the bottom.
func iPadFloatingSheet(_ bool: Bool = true) -> BottomSheet {
self.configuration.iPadFloatingSheet = bool
return self
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// BottomSheet+RelativeSheetWidth.swift
//
//
// Created by Robin Pel on 05/09/2022.
//

import Foundation

public extension BottomSheet {

/// Makes it possible to configure a custom sheet width.
///
/// - Parameters:
/// - width: The amount of width of the screen the sheet should occupy, 0.5 = 50%, 0.251 = 25.1%, etc.
///
/// - Returns: A BottomSheet with the configured width.
func relativeSheetWidth(_ width: Double? = nil) -> BottomSheet {
self.configuration.relativeSheetWidth = width
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,44 @@ import SwiftUI

internal extension BottomSheetView {

// For iPad and Mac support
var isIPadOrMac: Bool {
var isIPad: Bool {
#if os(macOS)
return true
return false
#else
if self.horizontalSizeClass == .regular && self.verticalSizeClass == .regular {
return true
} else {
return false
}
return self.horizontalSizeClass == .regular && self.verticalSizeClass == .regular
#endif
}

var iPadAndMacTopPadding: CGFloat {
if self.isIPadOrMac {
#if !os(macOS)
return UIApplication.shared.windows.first?.safeAreaInsets.top ?? 10
var isMac: Bool {
#if os(macOS)
return true
#else
return false
#endif
}

var isIPadOrMac: Bool {
return self.isIPad || self.isMac
}

var isIPadFloating: Bool {
return self.isIPad && self.configuration.iPadFloatingSheet
}

var isIPadFloatingOrMac: Bool {
return self.isIPadFloating || self.isMac
}

var isIPadBottom: Bool {
return self.isIPad && !self.configuration.iPadFloatingSheet
}

var topPadding: CGFloat {
if self.isIPadFloatingOrMac {
#if os(macOS)
return NSApplication.shared.mainMenu?.menuBarHeight ?? 20
#else
return UIApplication.shared.windows.first?.safeAreaInsets.top ?? 10
#endif
} else {
return 0
Expand All @@ -53,8 +72,8 @@ internal extension BottomSheetView {

// The height of the safe area when position is bottom
var bottomPositionSafeAreaHeight: CGFloat {
// Only add safe area when `dynamicBottom` and not on iPad or Mac
if self.bottomSheetPosition == .dynamicBottom && !self.isIPadOrMac {
// Only add safe area when `dynamicBottom` and not on iPad floating or Mac
if self.bottomSheetPosition == .dynamicBottom && !self.isIPadFloatingOrMac {
#if !os(macOS)
// Safe area as height (iPhone)
return UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 20
Expand All @@ -63,7 +82,7 @@ internal extension BottomSheetView {
return 0
#endif
} else {
// When not `.dynamicBottom` or when iPad or Mac don't add safe area
// When not `.dynamicBottom` or when iPad floating or Mac don't add safe area
return 0
}
}
Expand All @@ -79,7 +98,7 @@ internal extension BottomSheetView {
// The maximum height of the BottomSheet
func maxBottomSheetHeight(with geometry: GeometryProxy) -> CGFloat {
// Screen height without safe areas and padding
return geometry.size.height - (self.isIPadOrMac ? 20 : 0) - self.iPadAndMacTopPadding
return geometry.size.height - (self.isIPadFloatingOrMac ? 20 : 0) - self.topPadding
}

// The current height of the BottomSheet (without translation)
Expand Down Expand Up @@ -136,18 +155,25 @@ internal extension BottomSheetView {

// For iPhone landscape, iPad and Mac support
func width(with geometry: GeometryProxy) -> CGFloat {

lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
if let widthOverride = self.configuration.relativeSheetWidth {
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
// When the width is overriden, use it
// But don't allow it to be smaller than zero, or larger than one
return geometry.size.width * max(0, min(1, widthOverride))
lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
}

#if os(macOS)
// On Mac use 30% of the width
return geometry.size.width * 0.3
#else
if self.isIPadOrMac {
if self.isIPad {
// On iPad use 30% of the width
return geometry.size.width * 0.3
} else if UIDevice.current.orientation.isLandscape {
// On iPhone landscape use of the 40% width
// On iPhone landscape use 40% of the width
return geometry.size.width * 0.4
} else {
// On iPhone portrait use of the 100% width
// On iPhone portrait use 100% of the width
return geometry.size.width
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ internal extension BottomSheetView {
// Perform custom onChanged action
self.configuration.onDragChanged(value)

// Update translation; on iPad and Mac the drag direction is reversed
self.translation = self.isIPadOrMac ? -value.translation.height : value.translation.height
// Update translation; on iPad floating and Mac the drag direction is reversed
self.translation = self.isIPadFloatingOrMac ? -value.translation.height : value.translation.height
// Dismiss the keyboard on drag
self.endEditing()
}
Expand Down Expand Up @@ -51,8 +51,8 @@ internal extension BottomSheetView {

// Notify the ScrollView that the user is dragging
self.dragState = .none
// Update translation; on iPad and Mac the drag direction is reversed
self.translation = self.isIPadOrMac ? -value.translation.height : value.translation.height
// Update translation; on iPad floating and Mac the drag direction is reversed
self.translation = self.isIPadFloatingOrMac ? -value.translation.height : value.translation.height
}

// Dismiss the keyboard on dragging/scrolling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,46 +36,47 @@ internal extension BottomSheetView {
alignment: .center,
spacing: 0
) {
// Drag indicator on the top (iPhone)
if self.configuration.isResizable && self.configuration.isDragIndicatorShown && !self.isIPadOrMac {
// Drag indicator on the top (iPhone and iPad not floating)
if self.configuration.isResizable && self.configuration.isDragIndicatorShown && !self.isIPadFloatingOrMac {
self.dragIndicator( with: geometry)
}

// The header an main content
self.bottomSheetContent(with: geometry)

// Drag indicator on the bottom (iPad and Mac)
if self.configuration.isResizable && self.configuration.isDragIndicatorShown && self.isIPadOrMac {
// Drag indicator on the bottom (iPad floating and Mac)
if self.configuration.isResizable && self.configuration.isDragIndicatorShown && self.isIPadFloatingOrMac {
self.dragIndicator(with: geometry)
}
}
// Set the height and width to its calculated values
// The content should be aligned to the top on iPhone,
// on iPad and Mac to the bottom for transition to work correctly
// Don't set height if `.dynamic...` and currently not dragging
// on iPad floating and Mac to the bottom for transition
// to work correctly. Don't set height if `.dynamic...`
// and currently not dragging
.frame(
width: self.width(with: geometry),
height: self.bottomSheetPosition.isDynamic && self.translation == 0 ? nil : self.height(with: geometry),
alignment: self.isIPadOrMac ? .bottom : .top
alignment: self.isIPadFloatingOrMac ? .bottom : .top
)
// Clip BottomSheet for transition to work correctly for iPad and Mac
.clipped()
// BottomSheet background
.background(
self.bottomSheetBackground(with: geometry)
)
// On iPad and Mac the BottomSheet has a padding
// On iPad floating and Mac the BottomSheet has a padding
.padding(
self.isIPadOrMac ? 10 : 0
self.isIPadFloatingOrMac ? 10 : 0
)
// Add safe area top padding on iPad and Mac
.padding(
.top,
self.iPadAndMacTopPadding
self.topPadding
)
// Make the BottomSheet transition via move
.transition(.move(
edge: self.isIPadOrMac ? .top : .bottom
edge: self.isIPadFloatingOrMac ? .top : .bottom
))
}

Expand Down Expand Up @@ -162,8 +163,8 @@ internal extension BottomSheetView {
VStack(alignment: .center, spacing: 0) {
if self.configuration.isAppleScrollBehaviorEnabled && self.configuration.isResizable {
// Content for `appleScrollBehaviour`
if self.isIPadOrMac {
// On iPad an Mac use a normal ScrollView
if self.isIPadFloatingOrMac {
// On iPad floating an Mac use a normal ScrollView
ScrollView {
self.mainContent
}
Expand All @@ -187,7 +188,7 @@ internal extension BottomSheetView {
// Align content correctly and make it use all available space to fix transition
.frame(
maxHeight: self.maxMainContentHeight(with: geometry),
alignment: self.isIPadOrMac ? .bottom : .top
alignment: self.isIPadFloatingOrMac ? .bottom : .top
)
// Clip main content so that it doesn't go beneath the header content
.clipped()
Expand All @@ -196,9 +197,14 @@ internal extension BottomSheetView {
.top,
self.headerContentHeight
)
// Add padding to the bottom to compensate for the keyboard (if desired)
.padding(
.bottom,
self.mainContentBottomPadding
)
// Make the main content transition via move
.transition(.move(
edge: self.isIPadOrMac ? .top : .bottom
edge: self.isIPadFloatingOrMac ? .top : .bottom
))
}

Expand Down Expand Up @@ -303,7 +309,7 @@ internal extension BottomSheetView {
// Only add top padding if no drag indicator
.padding(
(!self.configuration.isDragIndicatorShown || !self.configuration.isResizable) ||
self.isIPadOrMac ? .top : []
self.isIPadFloatingOrMac ? .top : []
)
}
}
Expand Down Expand Up @@ -362,11 +368,11 @@ internal extension BottomSheetView {
// Default BottomSheet background
VisualEffectView(visualEffect: .system)
// Add corner radius to BottomSheet background
// On iPhone only to the top corners,
// on iPad and Mac to all corners
// On iPhone and iPad not floating only to the top corners,
// on iPad floating and Mac to all corners
.cornerRadius(
10,
corners: self.isIPadOrMac ? .allCorners : [
corners: self.isIPadFloatingOrMac ? .allCorners : [
.topRight,
.topLeft
]
Expand All @@ -378,4 +384,18 @@ internal extension BottomSheetView {
self.configuration.isResizable ? self.dragGesture(with: geometry) : nil
)
}

var mainContentBottomPadding: CGFloat {

lucaszischka marked this conversation as resolved.
Show resolved Hide resolved
if !self.isIPadFloatingOrMac && self.configuration.accountForKeyboardHeight {
#if !os(macOS)
return keyboardHeight.value
#else
// Should not be reached
return 0
#endif
} else {
return 0
}
}
}
Loading