diff --git a/MijickPopupView.podspec b/MijickPopupView.podspec index d2114d25e7..13ecb5c29a 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.3.0' + s.version = '2.3.1' s.ios.deployment_target = '14.0' s.osx.deployment_target = '12.0' s.swift_version = '5.0' diff --git a/Sources/Internal/Extensions/DispatchSource++.swift b/Sources/Internal/Extensions/DispatchSource++.swift new file mode 100644 index 0000000000..a4cb2f6501 --- /dev/null +++ b/Sources/Internal/Extensions/DispatchSource++.swift @@ -0,0 +1,22 @@ +// +// DispatchSource++.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 + +extension DispatchSource { + static func createAction(deadline seconds: Double, event: @escaping () -> ()) -> DispatchSourceTimer { + let action = DispatchSource.makeTimerSource(queue: .main) + action.schedule(deadline: .now() + max(0.6, seconds)) + action.setEventHandler(handler: event) + action.resume() + return action + } +} diff --git a/Sources/Internal/Managers/PopupManager.swift b/Sources/Internal/Managers/PopupManager.swift index 9ee29a9b24..e670ee36cf 100644 --- a/Sources/Internal/Managers/PopupManager.swift +++ b/Sources/Internal/Managers/PopupManager.swift @@ -14,6 +14,7 @@ public class PopupManager: ObservableObject { @Published private(set) var views: [any Popup] = [] private(set) var presenting: Bool = true private(set) var popupsWithoutOverlay: [String] = [] + private(set) var popupsToBeDismissed: [String: DispatchSourceTimer] = [:] static let shared: PopupManager = .init() private init() {} @@ -26,12 +27,20 @@ enum StackOperation { } extension PopupManager { static func performOperation(_ operation: StackOperation) { DispatchQueue.main.async { + removePopupFromStackToBeDismissed(operation) updateOperationType(operation) shared.views.perform(operation) }} + static func dismissPopupAfter(_ popup: any Popup, _ seconds: Double) { shared.popupsToBeDismissed[popup.id] = DispatchSource.createAction(deadline: seconds) { performOperation(.remove(id: popup.id)) } } static func hideOverlay(_ popup: any Popup) { shared.popupsWithoutOverlay.append(popup.id) } } private extension PopupManager { + static func removePopupFromStackToBeDismissed(_ operation: StackOperation) { switch operation { + case .removeLast: shared.popupsToBeDismissed.removeValue(forKey: shared.views.last?.id ?? "") + case .remove(let id): shared.popupsToBeDismissed.removeValue(forKey: id) + case .removeAllUpTo, .removeAll: shared.popupsToBeDismissed.removeAll() + default: break + }} static func updateOperationType(_ operation: StackOperation) { switch operation { case .insertAndReplace, .insertAndStack: shared.presenting = true case .removeLast, .remove, .removeAllUpTo, .removeAll: shared.presenting = false diff --git a/Sources/Internal/Other/AnimationType.swift b/Sources/Internal/Other/AnimationType.swift index cb450c353d..49f6432056 100644 --- a/Sources/Internal/Other/AnimationType.swift +++ b/Sources/Internal/Other/AnimationType.swift @@ -12,18 +12,15 @@ import SwiftUI public enum AnimationType { case spring, linear, easeInOut } extension AnimationType { - var entry: Animation { - switch self { - case .spring: return .spring(response: 0.4, dampingFraction: 1, blendDuration: 0.1) - case .linear: return .linear(duration: 0.4) - case .easeInOut: return .easeInOut(duration: 0.4) - } - } - var removal: Animation { - switch self { - case .spring: return .interactiveSpring(response: 0.14, dampingFraction: 1, blendDuration: 1) - case .linear: return .linear(duration: 0.3) - case .easeInOut: return .easeInOut(duration: 0.3) - } - } + var entry: Animation { switch self { + case .spring: return .spring(duration: 0.36, bounce: 0, blendDuration: 0.1) + case .linear: return .linear(duration: 0.4) + case .easeInOut: return .easeInOut(duration: 0.4) + }} + var removal: Animation { switch self { + case .spring: return .spring(duration: 0.32, bounce: 0, blendDuration: 0.1) + case .linear: return .linear(duration: 0.3) + case .easeInOut: return .easeInOut(duration: 0.3) + }} + var dragGesture: Animation { .linear(duration: 0.05) } } diff --git a/Sources/Internal/Protocols/PopupStack.swift b/Sources/Internal/Protocols/PopupStack.swift index 17ef5cba0f..af3ad0317e 100644 --- a/Sources/Internal/Protocols/PopupStack.swift +++ b/Sources/Internal/Protocols/PopupStack.swift @@ -107,13 +107,18 @@ extension PopupStack { // MARK: - Initial Height extension PopupStack { func getInitialHeight() -> CGFloat { - guard let previousView = items.nextToLast else { return 0 } + guard let previousView = items.nextToLast else { return 30 } - let height = heights.filter { $0.key == previousView.id }.first?.value ?? 0 + let height = heights.filter { $0.key == previousView.id }.first?.value ?? 30 return height } } +// MARK: - Item ZIndex +extension PopupStack { + func getZIndex(_ item: AnyPopup) -> Double { .init(items.firstIndex(of: item) ?? 2137) } +} + // MARK: - Animations extension PopupStack { @@ -122,6 +127,7 @@ extension PopupStack { extension PopupStack { var transitionEntryAnimation: Animation { globalConfig.common.animation.entry } var transitionRemovalAnimation: Animation { globalConfig.common.animation.removal } + var dragGestureAnimation: Animation { globalConfig.common.animation.dragGesture } } // MARK: - Configurables diff --git a/Sources/Internal/Views/PopupBottomStackView.swift b/Sources/Internal/Views/PopupBottomStackView.swift index 9f5e4a5042..e750aa514e 100644 --- a/Sources/Internal/Views/PopupBottomStackView.swift +++ b/Sources/Internal/Views/PopupBottomStackView.swift @@ -24,7 +24,7 @@ struct PopupBottomStackView: PopupStack { ZStack(alignment: .top, content: createPopupStack) .background(createTapArea()) .animation(getHeightAnimation(isAnimationDisabled: screenManager.animationsDisabled), value: heights) - .animation(transitionRemovalAnimation, value: gestureTranslation) + .animation(isGestureActive ? dragGestureAnimation : transitionRemovalAnimation, value: gestureTranslation) .onDragGesture($isGestureActive, onChanged: onPopupDragGestureChanged, onEnded: onPopupDragGestureEnded) } } @@ -54,6 +54,7 @@ private extension PopupBottomStackView { .focusSectionIfAvailable() .align(to: .bottom, lastPopupConfig.contentFillsEntireScreen ? 0 : popupBottomPadding) .transition(transition) + .zIndex(getZIndex(item)) } } diff --git a/Sources/Internal/Views/PopupTopStackView.swift b/Sources/Internal/Views/PopupTopStackView.swift index 3e1142d7e3..cf3ee36ed2 100644 --- a/Sources/Internal/Views/PopupTopStackView.swift +++ b/Sources/Internal/Views/PopupTopStackView.swift @@ -23,7 +23,7 @@ struct PopupTopStackView: PopupStack { ZStack(alignment: .bottom, content: createPopupStack) .background(createTapArea()) .animation(getHeightAnimation(isAnimationDisabled: screenManager.animationsDisabled), value: heights) - .animation(transitionRemovalAnimation, value: gestureTranslation) + .animation(isGestureActive ? dragGestureAnimation : transitionRemovalAnimation, value: gestureTranslation) .onDragGesture($isGestureActive, onChanged: onPopupDragGestureChanged, onEnded: onPopupDragGestureEnded) } } @@ -51,6 +51,7 @@ private extension PopupTopStackView { .focusSectionIfAvailable() .align(to: .top, popupTopPadding) .transition(transition) + .zIndex(getZIndex(item)) } } diff --git a/Sources/Public/Extensions/Public+Popup.swift b/Sources/Public/Extensions/Public+Popup.swift index a8d777b083..feb249cc0d 100644 --- a/Sources/Public/Extensions/Public+Popup.swift +++ b/Sources/Public/Extensions/Public+Popup.swift @@ -22,7 +22,7 @@ public extension Popup { // MARK: - Modifiers public extension Popup { /// Closes popup after n seconds - @discardableResult func dismissAfter(_ seconds: Double) -> some Popup { DispatchQueue.main.asyncAfter(deadline: .now() + max(0.5, seconds)) { PopupManager.dismiss(Self.self) }; return self } + @discardableResult func dismissAfter(_ seconds: Double) -> some Popup { PopupManager.dismissPopupAfter(self, seconds); return self } /// Hides the overlay for the selected popup @discardableResult func hideOverlay() -> some Popup { PopupManager.hideOverlay(self); return self }