diff --git a/MijickPopupView.podspec b/MijickPopupView.podspec index 674a81fa2..3578b04ec 100644 --- a/MijickPopupView.podspec +++ b/MijickPopupView.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| PopupView is a free and open-source library dedicated for SwiftUI that makes the process of presenting popups easier and much cleaner. DESC - s.version = '2.6.0' + s.version = '2.7.0' s.ios.deployment_target = '14.0' s.osx.deployment_target = '13.0' s.swift_version = '5.0' diff --git a/Sources/Internal/Extensions/Array++.swift b/Sources/Internal/Extensions/Array++.swift index 2b6e482fc..2741e006c 100644 --- a/Sources/Internal/Extensions/Array++.swift +++ b/Sources/Internal/Extensions/Array++.swift @@ -10,6 +10,7 @@ import Foundation +// MARK: Mutable extension Array { @inlinable mutating func append(_ newElement: Element, if prerequisite: Bool) { if prerequisite { append(newElement) } } @inlinable mutating func removeAllUpToElement(where predicate: (Element) -> Bool) { if let index = lastIndex(where: predicate) { removeLast(count - index - 1) } } @@ -21,6 +22,13 @@ extension Array { } }} } + +// MARK: Immutable +extension Array { + @inlinable func appending(_ newElement: Element) -> Self { self + [newElement] } +} + +// MARK: Others extension Array { var nextToLast: Element? { count >= 2 ? self[count - 2] : nil } } diff --git a/Sources/Internal/Extensions/View++.swift b/Sources/Internal/Extensions/View++.swift index 9d7adb7a6..22f39e598 100644 --- a/Sources/Internal/Extensions/View++.swift +++ b/Sources/Internal/Extensions/View++.swift @@ -12,7 +12,11 @@ import SwiftUI // MARK: - Alignments extension View { - func align(to edge: Edge, _ value: CGFloat?) -> some View { padding(.init(edge), value).frame(maxHeight: value != nil ? .infinity : nil, alignment: edge.toAlignment()) } + func align(to edge: Edge, _ value: CGFloat?) -> some View { + padding(.init(edge), value) + .frame(height: value != nil ? ScreenManager.shared.size.height : nil, alignment: edge.toAlignment()) + .frame(maxHeight: value != nil ? .infinity : nil, alignment: edge.toAlignment()) + } } fileprivate extension Edge { func toAlignment() -> Alignment { diff --git a/Sources/Internal/Protocols/PopupStack.swift b/Sources/Internal/Protocols/PopupStack.swift index 0927bac4b..7c24065b2 100644 --- a/Sources/Internal/Protocols/PopupStack.swift +++ b/Sources/Internal/Protocols/PopupStack.swift @@ -15,6 +15,7 @@ protocol PopupStack: View { var items: [AnyPopup] { get } var heights: [ID: CGFloat] { get } + var dragHeights: [ID: CGFloat] { get } var globalConfig: GlobalConfig { get } var gestureTranslation: CGFloat { get } var isGestureActive: Bool { get } @@ -30,6 +31,7 @@ protocol PopupStack: View { } extension PopupStack { var heights: [ID: CGFloat] { [:] } + var dragHeights: [ID: CGFloat] { [:] } var gestureTranslation: CGFloat { 0 } var isGestureActive: Bool { false } var translationProgress: CGFloat { 1 } @@ -101,7 +103,20 @@ extension PopupStack { // MARK: - Stack Offset extension PopupStack { - func getOffset(_ item: AnyPopup) -> CGFloat { isLast(item) ? gestureTranslation : invertedIndex(item).floatValue * stackOffsetValue } + func getOffset(_ item: AnyPopup) -> CGFloat { switch isLast(item) { + case true: calculateOffsetForLastItem() + case false: calculateOffsetForOtherItems(item) + }} +} +private extension PopupStack { + func calculateOffsetForLastItem() -> CGFloat { switch items { + case _ as [AnyPopup]: max(gestureTranslation - getLastDragHeight(), 0) + case _ as [AnyPopup]: min(gestureTranslation + getLastDragHeight(), 0) + default: 0 + }} + func calculateOffsetForOtherItems(_ item: AnyPopup) -> CGFloat { + invertedIndex(item).floatValue * stackOffsetValue + } } // MARK: - Initial Height @@ -114,6 +129,16 @@ extension PopupStack { } } +// MARK: - Last Popup Height +extension PopupStack { + func getLastPopupHeight() -> CGFloat? { heights[items.last?.id ?? .init()] } +} + +// MARK: - Drag Height Value +extension PopupStack { + func getLastDragHeight() -> CGFloat { dragHeights[items.last?.id ?? .init()] ?? 0 } +} + // MARK: - Item ZIndex extension PopupStack { func getZIndex(_ item: AnyPopup) -> Double { .init(items.firstIndex(of: item) ?? 2137) } diff --git a/Sources/Internal/Views/PopupBottomStackView.swift b/Sources/Internal/Views/PopupBottomStackView.swift index bc7694811..5354201af 100644 --- a/Sources/Internal/Views/PopupBottomStackView.swift +++ b/Sources/Internal/Views/PopupBottomStackView.swift @@ -15,6 +15,7 @@ struct PopupBottomStackView: PopupStack { let globalConfig: GlobalConfig @State var gestureTranslation: CGFloat = 0 @State var heights: [ID: CGFloat] = [:] + @State var dragHeights: [ID: CGFloat] = [:] @GestureState var isGestureActive: Bool = false @ObservedObject private var screenManager: ScreenManager = .shared @ObservedObject private var keyboardManager: KeyboardManager = .shared @@ -59,43 +60,130 @@ private extension PopupBottomStackView { } } -// MARK: - Gesture +// MARK: - Gestures + +// MARK: On Changed +private extension PopupBottomStackView { + func onPopupDragGestureChanged(_ value: CGFloat) { if canDragGestureBeUsed() { + updateGestureTranslation(value) + }} +} +private extension PopupBottomStackView { + func canDragGestureBeUsed() -> Bool { lastPopupConfig.dragGestureEnabled ?? globalConfig.bottom.dragGestureEnabled } + func updateGestureTranslation(_ value: CGFloat) { switch lastPopupConfig.dragDetents.isEmpty { + case true: gestureTranslation = calculateGestureTranslationWhenNoDragDetents(value) + case false: gestureTranslation = calculateGestureTranslationWhenDragDetents(value) + }} +} +private extension PopupBottomStackView { + func calculateGestureTranslationWhenNoDragDetents(_ value: CGFloat) -> CGFloat { max(value, 0) } + func calculateGestureTranslationWhenDragDetents(_ value: CGFloat) -> CGFloat { guard value < 0, let lastPopupHeight = getLastPopupHeight() else { return value } + let maxHeight = calculateMaxHeightForDragGesture(lastPopupHeight) + let dragTranslation = calculateDragTranslation(maxHeight, lastPopupHeight) + return max(dragTranslation, value) + } +} private extension PopupBottomStackView { - func onPopupDragGestureChanged(_ value: CGFloat) { - if lastPopupConfig.dragGestureEnabled ?? globalConfig.bottom.dragGestureEnabled { gestureTranslation = max(0, value) } + func calculateMaxHeightForDragGesture(_ lastPopupHeight: CGFloat) -> CGFloat { + let maxHeight1 = (calculatePopupTargetHeightsFromDragDetents(lastPopupHeight).max() ?? 0) + dragTranslationThreshold + let maxHeight2 = screenManager.size.height + return min(maxHeight1, maxHeight2) } - func onPopupDragGestureEnded(_ value: CGFloat) { + func calculateDragTranslation(_ maxHeight: CGFloat, _ lastPopupHeight: CGFloat) -> CGFloat { + let translation = maxHeight - lastPopupHeight - getLastDragHeight() + return -translation + } +} +private extension PopupBottomStackView { + var dragTranslationThreshold: CGFloat { 8 } +} + +// MARK: On Ended +private extension PopupBottomStackView { + func onPopupDragGestureEnded(_ value: CGFloat) { guard value != 0 else { return } dismissLastItemIfNeeded() - resetGestureTranslationOnEnd() + updateTranslationValues() } } private extension PopupBottomStackView { - func dismissLastItemIfNeeded() { - if translationProgress >= gestureClosingThresholdFactor { items.last?.remove() } + func dismissLastItemIfNeeded() { if shouldDismissPopup() { + items.last?.remove() + }} + func updateTranslationValues() { if let lastPopupHeight = getLastPopupHeight() { + let currentPopupHeight = calculateCurrentPopupHeight(lastPopupHeight) + let popupTargetHeights = calculatePopupTargetHeightsFromDragDetents(lastPopupHeight) + let targetHeight = calculateTargetPopupHeight(currentPopupHeight, popupTargetHeights) + let targetDragHeight = calculateTargetDragHeight(targetHeight, lastPopupHeight) + + resetGestureTranslation() + updateDragHeight(targetDragHeight) + }} +} +private extension PopupBottomStackView { + func calculateCurrentPopupHeight(_ lastPopupHeight: CGFloat) -> CGFloat { + let lastDragHeight = getLastDragHeight() + let currentDragHeight = lastDragHeight - gestureTranslation + + let currentPopupHeight = lastPopupHeight + currentDragHeight + return currentPopupHeight + } + func calculatePopupTargetHeightsFromDragDetents(_ lastPopupHeight: CGFloat) -> [CGFloat] { lastPopupConfig.dragDetents + .map { switch $0 { + case .fixed(let targetHeight): min(targetHeight, getMaxHeight()) + case .fraction(let fraction): min(fraction * lastPopupHeight, getMaxHeight()) + case .fullscreen(let stackVisible): stackVisible ? getMaxHeight() : screenManager.size.height + }} + .appending(lastPopupHeight) + .sorted(by: <) + } + func calculateTargetPopupHeight(_ currentPopupHeight: CGFloat, _ popupTargetHeights: [CGFloat]) -> CGFloat { + guard let lastPopupHeight = getLastPopupHeight(), + currentPopupHeight < screenManager.size.height + else { return popupTargetHeights.last ?? 0 } + + let initialIndex = popupTargetHeights.firstIndex(where: { $0 >= currentPopupHeight }) ?? popupTargetHeights.count - 1, + targetIndex = gestureTranslation <= 0 ? initialIndex : max(0, initialIndex - 1) + let previousPopupHeight = getLastDragHeight() + lastPopupHeight, + popupTargetHeight = popupTargetHeights[targetIndex], + deltaHeight = abs(previousPopupHeight - popupTargetHeight) + let progress = abs(currentPopupHeight - previousPopupHeight) / deltaHeight + + if progress < gestureClosingThresholdFactor { + let index = gestureTranslation <= 0 ? max(0, initialIndex - 1) : initialIndex + return popupTargetHeights[index] + } + return popupTargetHeights[targetIndex] + } + func calculateTargetDragHeight(_ targetHeight: CGFloat, _ lastPopupHeight: CGFloat) -> CGFloat { + targetHeight - lastPopupHeight } - func resetGestureTranslationOnEnd() { - let resetAfter = items.count == 1 && translationProgress >= gestureClosingThresholdFactor ? 0.25 : 0 + func updateDragHeight(_ targetDragHeight: CGFloat) { if let id = items.last?.id { + dragHeights[id] = targetDragHeight + }} + func resetGestureTranslation() { + let resetAfter = items.count == 1 && shouldDismissPopup() ? 0.25 : 0 DispatchQueue.main.asyncAfter(deadline: .now() + resetAfter) { gestureTranslation = 0 } } + func shouldDismissPopup() -> Bool { + translationProgress >= gestureClosingThresholdFactor + } } // MARK: - View Modifiers private extension PopupBottomStackView { - func getCorners() -> RectCorner { - switch popupBottomPadding { - case 0: return [.topLeft, .topRight] - default: return .allCorners - } - } + func getCorners() -> RectCorner { switch popupBottomPadding { + case 0: return [.topLeft, .topRight] + default: return .allCorners + }} func saveHeight(_ height: CGFloat, for item: AnyPopup) { if !isGestureActive { let config = item.configurePopup(popup: .init()) - if config.contentFillsEntireScreen { return heights[item.id] = screenManager.size.height + screenManager.safeArea.top } + if config.contentFillsEntireScreen { return heights[item.id] = screenManager.size.height } if config.contentFillsWholeHeight { return heights[item.id] = getMaxHeight() } return heights[item.id] = min(height, maxHeight) }} func getMaxHeight() -> CGFloat { - let basicHeight = screenManager.size.height - screenManager.safeArea.top + let basicHeight = screenManager.size.height - screenManager.safeArea.top - popupBottomPadding let stackedViewsCount = min(max(0, globalConfig.bottom.stackLimit - 1), items.count - 1) let stackedViewsHeight = globalConfig.bottom.stackOffset * .init(stackedViewsCount) * maxHeightStackedFactor return basicHeight - stackedViewsHeight @@ -106,7 +194,13 @@ private extension PopupBottomStackView { return max(screenManager.safeArea.bottom - popupBottomPadding, 0) } - func getContentTopPadding() -> CGFloat { lastPopupConfig.contentFillsEntireScreen && !lastPopupConfig.contentIgnoresSafeArea ? screenManager.safeArea.top : 0 } + func getContentTopPadding() -> CGFloat { + if lastPopupConfig.contentIgnoresSafeArea { return 0 } + + let heightWithoutTopSafeArea = screenManager.size.height - screenManager.safeArea.top + let topPadding = height - heightWithoutTopSafeArea + return max(topPadding, 0) + } func getHeight(_ item: AnyPopup) -> CGFloat? { getConfig(item).contentFillsEntireScreen ? nil : height } func getFixedSize(_ item: AnyPopup) -> Bool { !(getConfig(item).contentFillsEntireScreen || getConfig(item).contentFillsWholeHeight || height == maxHeight) } func getBackgroundColour(for item: AnyPopup) -> Color { item.configurePopup(popup: .init()).backgroundColour ?? globalConfig.bottom.backgroundColour } @@ -117,7 +211,17 @@ extension PopupBottomStackView { var popupBottomPadding: CGFloat { lastPopupConfig.popupPadding.bottom } var popupHorizontalPadding: CGFloat { lastPopupConfig.popupPadding.horizontal } var popupShadow: Shadow { globalConfig.bottom.shadow } - var height: CGFloat { heights.first { $0.key == items.last?.id }?.value ?? (lastPopupConfig.contentFillsEntireScreen ? screenManager.size.height : getInitialHeight()) } + var height: CGFloat { + let lastDragHeight = getLastDragHeight(), + lastPopupHeight = getLastPopupHeight() ?? (lastPopupConfig.contentFillsEntireScreen ? screenManager.size.height : getInitialHeight()) + let dragTranslation = lastPopupHeight + lastDragHeight - gestureTranslation + let newHeight = max(lastPopupHeight, dragTranslation) + + switch lastPopupHeight + lastDragHeight > screenManager.size.height && !lastPopupConfig.contentIgnoresSafeArea { + case true: return newHeight == screenManager.size.height ? newHeight : newHeight - screenManager.safeArea.top + case false: return newHeight + } + } var maxHeight: CGFloat { getMaxHeight() - popupBottomPadding } var distanceFromKeyboard: CGFloat { lastPopupConfig.distanceFromKeyboard ?? globalConfig.bottom.distanceFromKeyboard } var cornerRadius: CGFloat { let cornerRadius = lastPopupConfig.cornerRadius ?? globalConfig.bottom.cornerRadius; return lastPopupConfig.contentFillsEntireScreen ? min(cornerRadius, screenManager.cornerRadius ?? 0) : cornerRadius } @@ -129,7 +233,7 @@ extension PopupBottomStackView { var stackOffsetValue: CGFloat { -globalConfig.bottom.stackOffset } var stackCornerRadiusMultiplier: CGFloat { globalConfig.bottom.stackCornerRadiusMultiplier } - var translationProgress: CGFloat { abs(gestureTranslation) / height } + var translationProgress: CGFloat { guard let popupHeight = getLastPopupHeight() else { return 0 }; return max(gestureTranslation - getLastDragHeight(), 0) / popupHeight } var gestureClosingThresholdFactor: CGFloat { globalConfig.bottom.dragGestureProgressToClose } var transition: AnyTransition { .move(edge: .bottom) } diff --git a/Sources/Internal/Views/PopupTopStackView.swift b/Sources/Internal/Views/PopupTopStackView.swift index 579b21887..8f4a831ce 100644 --- a/Sources/Internal/Views/PopupTopStackView.swift +++ b/Sources/Internal/Views/PopupTopStackView.swift @@ -15,6 +15,7 @@ struct PopupTopStackView: PopupStack { let globalConfig: GlobalConfig @State var gestureTranslation: CGFloat = 0 @State var heights: [ID: CGFloat] = [:] + @State var dragHeights: [ID: CGFloat] = [:] @GestureState var isGestureActive: Bool = false @ObservedObject private var screenManager: ScreenManager = .shared @@ -41,7 +42,7 @@ private extension PopupTopStackView { .padding(.leading, screenManager.safeArea.left) .padding(.trailing, screenManager.safeArea.right) .readHeight { saveHeight($0, for: item) } - .frame(height: height).frame(maxWidth: .infinity) + .frame(height: height, alignment: .bottom).frame(maxWidth: .infinity) .background(getBackgroundColour(for: item), overlayColour: getStackOverlayColour(item), radius: getCornerRadius(item), corners: getCorners(), shadow: popupShadow) .padding(.horizontal, lastPopupConfig.popupPadding.horizontal) .offset(y: getOffset(item)) @@ -56,23 +57,110 @@ private extension PopupTopStackView { } // MARK: - Gestures + +// MARK: On Changed +private extension PopupTopStackView { + func onPopupDragGestureChanged(_ value: CGFloat) { if canDragGestureBeUsed() { + updateGestureTranslation(value) + }} +} +private extension PopupTopStackView { + func canDragGestureBeUsed() -> Bool { lastPopupConfig.dragGestureEnabled ?? globalConfig.bottom.dragGestureEnabled } + func updateGestureTranslation(_ value: CGFloat) { switch lastPopupConfig.dragDetents.isEmpty { + case true: gestureTranslation = calculateGestureTranslationWhenNoDragDetents(value) + case false: gestureTranslation = calculateGestureTranslationWhenDragDetents(value) + }} +} +private extension PopupTopStackView { + func calculateGestureTranslationWhenNoDragDetents(_ value: CGFloat) -> CGFloat { min(value, 0) } + func calculateGestureTranslationWhenDragDetents(_ value: CGFloat) -> CGFloat { guard value > 0, let lastPopupHeight = getLastPopupHeight() else { return value } + let maxHeight = calculateMaxHeightForDragGesture(lastPopupHeight) + let dragTranslation = calculateDragTranslation(maxHeight, lastPopupHeight) + return min(dragTranslation, value) + } +} private extension PopupTopStackView { - func onPopupDragGestureChanged(_ value: CGFloat) { - if lastPopupConfig.dragGestureEnabled ?? globalConfig.top.dragGestureEnabled { gestureTranslation = min(0, value) } + func calculateMaxHeightForDragGesture(_ lastPopupHeight: CGFloat) -> CGFloat { + let maxHeight1 = (calculatePopupTargetHeightsFromDragDetents(lastPopupHeight).max() ?? 0) + dragTranslationThreshold + let maxHeight2 = screenManager.size.height + return min(maxHeight1, maxHeight2) + } + func calculateDragTranslation(_ maxHeight: CGFloat, _ lastPopupHeight: CGFloat) -> CGFloat { + let translation = maxHeight - lastPopupHeight - getLastDragHeight() + return translation } - func onPopupDragGestureEnded(_ value: CGFloat) { +} +private extension PopupTopStackView { + var dragTranslationThreshold: CGFloat { 8 } +} + +// MARK: On Ended +private extension PopupTopStackView { + func onPopupDragGestureEnded(_ value: CGFloat) { guard value != 0 else { return } dismissLastItemIfNeeded() - resetGestureTranslationOnEnd() + updateTranslationValues() } } private extension PopupTopStackView { - func dismissLastItemIfNeeded() { - if translationProgress >= gestureClosingThresholdFactor { items.last?.remove() } + func dismissLastItemIfNeeded() { if shouldDismissPopup() { + items.last?.remove() + }} + func updateTranslationValues() { if let lastPopupHeight = getLastPopupHeight() { + let currentPopupHeight = calculateCurrentPopupHeight(lastPopupHeight) + let popupTargetHeights = calculatePopupTargetHeightsFromDragDetents(lastPopupHeight) + let targetHeight = calculateTargetPopupHeight(currentPopupHeight, popupTargetHeights) + let targetDragHeight = calculateTargetDragHeight(targetHeight, lastPopupHeight) + + resetGestureTranslation() + updateDragHeight(targetDragHeight) + }} +} +private extension PopupTopStackView { + func calculateCurrentPopupHeight(_ lastPopupHeight: CGFloat) -> CGFloat { + let lastDragHeight = getLastDragHeight() + let currentDragHeight = lastDragHeight + gestureTranslation + + let currentPopupHeight = lastPopupHeight + currentDragHeight + return currentPopupHeight } - func resetGestureTranslationOnEnd() { - let resetAfter = items.count == 1 && translationProgress >= gestureClosingThresholdFactor ? 0.25 : 0 + func calculatePopupTargetHeightsFromDragDetents(_ lastPopupHeight: CGFloat) -> [CGFloat] { lastPopupConfig.dragDetents + .map { switch $0 { + case .fixed(let targetHeight): min(targetHeight, screenManager.size.height) + case .fraction(let fraction): min(fraction * lastPopupHeight, screenManager.size.height) + case .fullscreen(let stackVisible): stackVisible ? screenManager.size.height - screenManager.safeArea.bottom : screenManager.size.height + }} + .appending(lastPopupHeight) + .sorted(by: <) + } + func calculateTargetPopupHeight(_ currentPopupHeight: CGFloat, _ popupTargetHeights: [CGFloat]) -> CGFloat { + guard let lastPopupHeight = getLastPopupHeight(), + currentPopupHeight < screenManager.size.height + else { return popupTargetHeights.last ?? 0 } + + let initialIndex = popupTargetHeights.firstIndex(where: { $0 >= currentPopupHeight }) ?? popupTargetHeights.count - 1, + targetIndex = gestureTranslation > 0 ? initialIndex : max(0, initialIndex - 1) + let previousPopupHeight = getLastDragHeight() + lastPopupHeight, + popupTargetHeight = popupTargetHeights[targetIndex], + deltaHeight = abs(previousPopupHeight - popupTargetHeight) + let progress = abs(currentPopupHeight - previousPopupHeight) / deltaHeight + + if progress < gestureClosingThresholdFactor { + let index = gestureTranslation > 0 ? max(0, initialIndex - 1) : initialIndex + return popupTargetHeights[index] + } + return popupTargetHeights[targetIndex] + } + func calculateTargetDragHeight(_ targetHeight: CGFloat, _ lastPopupHeight: CGFloat) -> CGFloat { + targetHeight - lastPopupHeight + } + func updateDragHeight(_ targetDragHeight: CGFloat) { if let id = items.last?.id { + dragHeights[id] = targetDragHeight + }} + func resetGestureTranslation() { + let resetAfter = items.count == 1 && shouldDismissPopup() ? 0.25 : 0 DispatchQueue.main.asyncAfter(deadline: .now() + resetAfter) { gestureTranslation = 0 } } + func shouldDismissPopup() -> Bool { translationProgress >= gestureClosingThresholdFactor } } // MARK: - View Modifiers @@ -92,7 +180,17 @@ extension PopupTopStackView { var contentTopPadding: CGFloat { lastPopupConfig.contentIgnoresSafeArea ? 0 : max(screenManager.safeArea.top - popupTopPadding, 0) } var popupTopPadding: CGFloat { lastPopupConfig.popupPadding.top } var popupShadow: Shadow { globalConfig.top.shadow } - var height: CGFloat { heights.first { $0.key == items.last?.id }?.value ?? getInitialHeight() } + var height: CGFloat { + let lastDragHeight = getLastDragHeight(), + lastPopupHeight = getLastPopupHeight() ?? getInitialHeight() + let dragTranslation = lastPopupHeight + lastDragHeight + gestureTranslation - popupTopPadding + let newHeight = max(lastPopupHeight, dragTranslation) + + switch lastPopupHeight + lastDragHeight > screenManager.size.height && !lastPopupConfig.contentIgnoresSafeArea { + case true: return newHeight == screenManager.size.height ? newHeight : newHeight - screenManager.safeArea.top + case false: return newHeight + } + } var cornerRadius: CGFloat { lastPopupConfig.cornerRadius ?? globalConfig.top.cornerRadius } var stackLimit: Int { globalConfig.top.stackLimit } @@ -100,7 +198,7 @@ extension PopupTopStackView { var stackOffsetValue: CGFloat { globalConfig.top.stackOffset } var stackCornerRadiusMultiplier: CGFloat { globalConfig.top.stackCornerRadiusMultiplier } - var translationProgress: CGFloat { abs(gestureTranslation) / height } + var translationProgress: CGFloat { guard let popupHeight = getLastPopupHeight() else { return 0 }; return abs(min(gestureTranslation + getLastDragHeight(), 0)) / popupHeight } var gestureClosingThresholdFactor: CGFloat { globalConfig.top.dragGestureProgressToClose } var transition: AnyTransition { .move(edge: .top) } diff --git a/Sources/Internal/Views/PopupView.swift b/Sources/Internal/Views/PopupView.swift index 7034493ef..d798031c8 100644 --- a/Sources/Internal/Views/PopupView.swift +++ b/Sources/Internal/Views/PopupView.swift @@ -45,6 +45,7 @@ private extension PopupView { createPopupStackView() .ignoresSafeArea() .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() .animation(stackAnimation, value: popupManager.views.map(\.id)) .onChange(popupManager.views.count, completion: onViewsCountChange) } diff --git a/Sources/Public/Configurables/LocalConfig.BottomPopup.swift b/Sources/Public/Configurables/LocalConfig.BottomPopup.swift index d35329de7..08d83adbd 100644 --- a/Sources/Public/Configurables/LocalConfig.BottomPopup.swift +++ b/Sources/Public/Configurables/LocalConfig.BottomPopup.swift @@ -49,6 +49,9 @@ public extension BottomPopupConfig { /// Popup can be closed with drag gesture if enabled func dragGestureEnabled(_ value: Bool) -> Self { changing(path: \.dragGestureEnabled, to: value) } + + /// Sets available detents for the popupSets the available detents for the enclosing sheet + func dragDetents(_ value: [DragDetent]) -> Self { changing(path: \.dragDetents, to: value) } } @@ -65,4 +68,5 @@ public struct BottomPopupConfig: Configurable { public init() {} private(set) var tapOutsideClosesView: Bool? = nil private(set) var dragGestureEnabled: Bool? = nil + private(set) var dragDetents: [DragDetent] = [] } diff --git a/Sources/Public/Configurables/LocalConfig.TopPopup.swift b/Sources/Public/Configurables/LocalConfig.TopPopup.swift index 717111e74..6f480f462 100644 --- a/Sources/Public/Configurables/LocalConfig.TopPopup.swift +++ b/Sources/Public/Configurables/LocalConfig.TopPopup.swift @@ -38,6 +38,9 @@ public extension TopPopupConfig { /// Popup can be closed with drag gesture if enabled func dragGestureEnabled(_ value: Bool) -> Self { changing(path: \.dragGestureEnabled, to: value) } + + /// Sets available detents for the popupSets the available detents for the enclosing sheet + func dragDetents(_ value: [DragDetent]) -> Self { changing(path: \.dragDetents, to: value) } } @@ -51,4 +54,5 @@ public struct TopPopupConfig: Configurable { public init() {} private(set) var tapOutsideClosesView: Bool? = nil private(set) var dragGestureEnabled: Bool? = nil + private(set) var dragDetents: [DragDetent] = [] } diff --git a/Sources/Public/Public+PopupSceneDelegate.swift b/Sources/Public/Delegates/Public+PopupSceneDelegate.swift similarity index 100% rename from Sources/Public/Public+PopupSceneDelegate.swift rename to Sources/Public/Delegates/Public+PopupSceneDelegate.swift diff --git a/Sources/Public/Utilities/Public+DragDetent.swift b/Sources/Public/Utilities/Public+DragDetent.swift new file mode 100644 index 000000000..10ce38986 --- /dev/null +++ b/Sources/Public/Utilities/Public+DragDetent.swift @@ -0,0 +1,18 @@ +// +// Public+DragDetent.swift of PopupView +// +// Created by Tomasz Kurylik +// - Twitter: https://twitter.com/tkurylik +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// +// Copyright ©2024 Mijick. Licensed under MIT License. + + +import Foundation + +public enum DragDetent { + case fixed(CGFloat) + case fraction(CGFloat) + case fullscreen(stackVisible: Bool) +}