diff --git a/MijickPopups.podspec b/MijickPopups.podspec index 5cced8735a..82f2d49049 100644 --- a/MijickPopups.podspec +++ b/MijickPopups.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| MijickPopups solves two seemingly contradictory problems - to allow developers to create fully customizable popup, and to make the process as simple as possible. DESC - s.version = '3.0.2' + s.version = '4.0.0' s.ios.deployment_target = '14.0' s.osx.deployment_target = '12.0' s.tvos.deployment_target = '15.0' diff --git a/Sources/Internal/Configurables/Global/GlobalConfig+Center.swift b/Sources/Internal/Configurables/Global/GlobalConfig+Center.swift new file mode 100644 index 0000000000..2062dbc9f9 --- /dev/null +++ b/Sources/Internal/Configurables/Global/GlobalConfig+Center.swift @@ -0,0 +1,29 @@ +// +// GlobalConfig+Center.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import SwiftUI + +public final class GlobalConfigCenter: GlobalConfig { required public init() {} + // MARK: Active Variables + public var popupPadding: EdgeInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16) + public var cornerRadius: CGFloat = 24 + public var backgroundColor: Color = .white + public var overlayColor: Color = .black.opacity(0.5) + public var isTapOutsideToDismissEnabled: Bool = false + + // MARK: Inactive Variables + public var ignoredSafeAreaEdges: Edge.Set = [] + public var heightMode: HeightMode = .auto + public var dragDetents: [DragDetent] = [] + public var isDragGestureEnabled: Bool = false + public var dragThreshold: CGFloat = 0 + public var isStackingEnabled: Bool = false +} diff --git a/Sources/Internal/Configurables/Global/GlobalConfig+Centre.swift b/Sources/Internal/Configurables/Global/GlobalConfig+Centre.swift deleted file mode 100644 index 5685037481..0000000000 --- a/Sources/Internal/Configurables/Global/GlobalConfig+Centre.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// GlobalConfig+Centre.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import Foundation - -public extension GlobalConfig { class Centre: GlobalConfig { - required init() { super.init() - self.popupPadding = .init(top: 0, leading: 16, bottom: 0, trailing: 16) - self.cornerRadius = 24 - } -}} diff --git a/Sources/Internal/Configurables/Global/GlobalConfig+Vertical.swift b/Sources/Internal/Configurables/Global/GlobalConfig+Vertical.swift index bd2758af18..faf8e529ed 100644 --- a/Sources/Internal/Configurables/Global/GlobalConfig+Vertical.swift +++ b/Sources/Internal/Configurables/Global/GlobalConfig+Vertical.swift @@ -9,16 +9,23 @@ // Copyright ©2024 Mijick. All rights reserved. -import Foundation +import SwiftUI -public extension GlobalConfig { class Vertical: GlobalConfig { - var dragThreshold: CGFloat = 1/3 - var isStackingEnabled: Bool = true - var isDragGestureEnabled: Bool = true +public final class GlobalConfigVertical: GlobalConfig { required public init() {} + // MARK: Content + public var popupPadding: EdgeInsets = .init() + public var cornerRadius: CGFloat = 40 + public var backgroundColor: Color = .white + public var overlayColor: Color = .black.opacity(0.5) + public var isStackingEnabled: Bool = true + // MARK: Gestures + public var isTapOutsideToDismissEnabled: Bool = false + public var isDragGestureEnabled: Bool = true + public var dragThreshold: CGFloat = 1/3 - required init() { super.init() - self.popupPadding = .init() - self.cornerRadius = 40 - } -}} + // MARK: Non-Customizable + public var ignoredSafeAreaEdges: Edge.Set = [] + public var heightMode: HeightMode = .auto + public var dragDetents: [DragDetent] = [] +} diff --git a/Sources/Internal/Configurables/Global/GlobalConfig.swift b/Sources/Internal/Configurables/Global/GlobalConfig.swift index 37bbcc426d..6177de2428 100644 --- a/Sources/Internal/Configurables/Global/GlobalConfig.swift +++ b/Sources/Internal/Configurables/Global/GlobalConfig.swift @@ -11,10 +11,7 @@ import SwiftUI -public class GlobalConfig { required init() {} - var popupPadding: EdgeInsets = .init() - var cornerRadius: CGFloat = 28 - var backgroundColor: Color = .white - var overlayColor: Color = .black.opacity(0.44) - var isTapOutsideToDismissEnabled: Bool = false +public protocol GlobalConfig: LocalConfig { + var dragThreshold: CGFloat { get set } + var isStackingEnabled: Bool { get set } } diff --git a/Sources/Internal/Configurables/Local/LocalConfig+Center.swift b/Sources/Internal/Configurables/Local/LocalConfig+Center.swift new file mode 100644 index 0000000000..74875801b2 --- /dev/null +++ b/Sources/Internal/Configurables/Local/LocalConfig+Center.swift @@ -0,0 +1,27 @@ +// +// LocalConfig+Center.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import SwiftUI + +public class LocalConfigCenter: LocalConfig { required public init() {} + // MARK: Active Variables + public var popupPadding: EdgeInsets = GlobalConfigContainer.center.popupPadding + public var cornerRadius: CGFloat = GlobalConfigContainer.center.cornerRadius + public var backgroundColor: Color = GlobalConfigContainer.center.backgroundColor + public var overlayColor: Color = GlobalConfigContainer.center.overlayColor + public var isTapOutsideToDismissEnabled: Bool = GlobalConfigContainer.center.isTapOutsideToDismissEnabled + + // MARK: Inactive Variables + public var ignoredSafeAreaEdges: Edge.Set = GlobalConfigContainer.center.ignoredSafeAreaEdges + public var heightMode: HeightMode = GlobalConfigContainer.center.heightMode + public var dragDetents: [DragDetent] = GlobalConfigContainer.center.dragDetents + public var isDragGestureEnabled: Bool = GlobalConfigContainer.center.isDragGestureEnabled +} diff --git a/Sources/Internal/Configurables/Local/LocalConfig+Centre.swift b/Sources/Internal/Configurables/Local/LocalConfig+Centre.swift deleted file mode 100644 index d4545bef8e..0000000000 --- a/Sources/Internal/Configurables/Local/LocalConfig+Centre.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// LocalConfig+Centre.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import SwiftUI - -public extension LocalConfig { class Centre: LocalConfig { - required init() { super.init() - self.popupPadding = GlobalConfigContainer.centre.popupPadding - self.cornerRadius = GlobalConfigContainer.centre.cornerRadius - self.backgroundColor = GlobalConfigContainer.centre.backgroundColor - self.overlayColor = GlobalConfigContainer.centre.overlayColor - self.isTapOutsideToDismissEnabled = GlobalConfigContainer.centre.isTapOutsideToDismissEnabled - } -}} - -// MARK: Typealias -/** - Configures the popup. - See the list of available methods in ``LocalConfig``. - -- important: If a certain method is not called here, the popup inherits the configuration from ``GlobalConfigContainer``. - */ -public typealias CentrePopupConfig = LocalConfig.Centre - - - -// MARK: - TESTS -#if DEBUG - - - -extension LocalConfig.Centre { - static func t_createNew(popupPadding: EdgeInsets, cornerRadius: CGFloat) -> LocalConfig.Centre { - let config = LocalConfig.Centre() - config.popupPadding = popupPadding - config.cornerRadius = cornerRadius - return config - } -} -#endif diff --git a/Sources/Internal/Configurables/Local/LocalConfig+Vertical.swift b/Sources/Internal/Configurables/Local/LocalConfig+Vertical.swift index 618cdd0434..d315a42cdf 100644 --- a/Sources/Internal/Configurables/Local/LocalConfig+Vertical.swift +++ b/Sources/Internal/Configurables/Local/LocalConfig+Vertical.swift @@ -11,60 +11,23 @@ import SwiftUI -public extension LocalConfig { class Vertical: LocalConfig { - var ignoredSafeAreaEdges: Edge.Set = [] - var heightMode: HeightMode = .auto - var dragDetents: [DragDetent] = [] - var isDragGestureEnabled: Bool = GlobalConfigContainer.vertical.isDragGestureEnabled - - - required init() { super.init() - self.popupPadding = GlobalConfigContainer.vertical.popupPadding - self.cornerRadius = GlobalConfigContainer.vertical.cornerRadius - self.backgroundColor = GlobalConfigContainer.vertical.backgroundColor - self.overlayColor = GlobalConfigContainer.vertical.overlayColor - self.isTapOutsideToDismissEnabled = GlobalConfigContainer.vertical.isTapOutsideToDismissEnabled - } -}} - -// MARK: Subclasses & Typealiases -/** - Configures the popup. - See the list of available methods in ``LocalConfig`` and ``LocalConfig/Vertical``. - -- important: If a certain method is not called here, the popup inherits the configuration from ``GlobalConfigContainer``. - */ -public typealias TopPopupConfig = LocalConfig.Vertical.Top - -/** - Configures the popup. - See the list of available methods in ``LocalConfig`` and ``LocalConfig/Vertical``. - -- important: If a certain method is not called here, the popup inherits the configuration from ``GlobalConfigContainer``. - */ -public typealias BottomPopupConfig = LocalConfig.Vertical.Bottom -public extension LocalConfig.Vertical { - class Top: LocalConfig.Vertical {} - class Bottom: LocalConfig.Vertical {} +public class LocalConfigVertical: LocalConfig { required public init() {} + // MARK: Content + public var popupPadding: EdgeInsets = GlobalConfigContainer.vertical.popupPadding + public var cornerRadius: CGFloat = GlobalConfigContainer.vertical.cornerRadius + public var ignoredSafeAreaEdges: Edge.Set = GlobalConfigContainer.vertical.ignoredSafeAreaEdges + public var backgroundColor: Color = GlobalConfigContainer.vertical.backgroundColor + public var overlayColor: Color = GlobalConfigContainer.vertical.overlayColor + public var heightMode: HeightMode = GlobalConfigContainer.vertical.heightMode + public var dragDetents: [DragDetent] = GlobalConfigContainer.vertical.dragDetents + + // MARK: Gestures + public var isTapOutsideToDismissEnabled: Bool = GlobalConfigContainer.vertical.isTapOutsideToDismissEnabled + public var isDragGestureEnabled: Bool = GlobalConfigContainer.vertical.isDragGestureEnabled } - - -// MARK: - TESTS -#if DEBUG - - - -extension LocalConfig.Vertical { - static func t_createNew(popupPadding: EdgeInsets, cornerRadius: CGFloat, ignoredSafeAreaEdges: Edge.Set, heightMode: HeightMode, dragDetents: [DragDetent], isDragGestureEnabled: Bool) -> C { - let config = C() - config.popupPadding = popupPadding - config.cornerRadius = cornerRadius - config.ignoredSafeAreaEdges = ignoredSafeAreaEdges - config.heightMode = heightMode - config.dragDetents = dragDetents - config.isDragGestureEnabled = isDragGestureEnabled - return config - } +// MARK: Subclasses +public extension LocalConfigVertical { + class Top: LocalConfigVertical {} + class Bottom: LocalConfigVertical {} } -#endif diff --git a/Sources/Internal/Configurables/Local/LocalConfig.swift b/Sources/Internal/Configurables/Local/LocalConfig.swift index eaf824c455..4b280cd8ff 100644 --- a/Sources/Internal/Configurables/Local/LocalConfig.swift +++ b/Sources/Internal/Configurables/Local/LocalConfig.swift @@ -11,10 +11,17 @@ import SwiftUI -public class LocalConfig { required init() {} - var popupPadding: EdgeInsets = .init() - var cornerRadius: CGFloat = 0 - var backgroundColor: Color = .clear - var overlayColor: Color = .clear - var isTapOutsideToDismissEnabled: Bool = false +public protocol LocalConfig { init() + // MARK: Content + var popupPadding: EdgeInsets { get set } + var cornerRadius: CGFloat { get set } + var ignoredSafeAreaEdges: Edge.Set { get set } + var backgroundColor: Color { get set } + var overlayColor: Color { get set } + var heightMode: HeightMode { get set } + var dragDetents: [DragDetent] { get set } + + // MARK: Gestures + var isTapOutsideToDismissEnabled: Bool { get set } + var isDragGestureEnabled: Bool { get set } } diff --git a/Sources/Internal/Containers/GlobalConfigContainer.swift b/Sources/Internal/Containers/GlobalConfigContainer.swift index 27347b4d27..25aa8d7ff8 100644 --- a/Sources/Internal/Containers/GlobalConfigContainer.swift +++ b/Sources/Internal/Containers/GlobalConfigContainer.swift @@ -9,7 +9,7 @@ // Copyright ©2023 Mijick. All rights reserved. -public class GlobalConfigContainer { - nonisolated(unsafe) static var centre: GlobalConfig.Centre = .init() - nonisolated(unsafe) static var vertical: GlobalConfig.Vertical = .init() +public actor GlobalConfigContainer { + nonisolated(unsafe) static var center: GlobalConfigCenter = .init() + nonisolated(unsafe) static var vertical: GlobalConfigVertical = .init() } diff --git a/Sources/Internal/Containers/PopupManagerContainer.swift b/Sources/Internal/Containers/PopupManagerContainer.swift deleted file mode 100644 index 603c8142c5..0000000000 --- a/Sources/Internal/Containers/PopupManagerContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PopupManagerContainer.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import Foundation - -@MainActor class PopupManagerContainer { - static private(set) var instances: [PopupManager] = [] -} - -// MARK: Register -extension PopupManagerContainer { - static func register(popupManager: PopupManager) -> PopupManager { - if let alreadyRegisteredInstance = instances.first(where: { $0.id == popupManager.id }) { return alreadyRegisteredInstance } - - instances.append(popupManager) - return popupManager - } -} - -// MARK: Clean -extension PopupManagerContainer { - static func clean() { instances = [] } -} diff --git a/Sources/Internal/Containers/PopupStack.swift b/Sources/Internal/Containers/PopupStack.swift new file mode 100644 index 0000000000..3823856658 --- /dev/null +++ b/Sources/Internal/Containers/PopupStack.swift @@ -0,0 +1,128 @@ +// +// PopupStack.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2023 Mijick. All rights reserved. + + +import SwiftUI + +@MainActor public class PopupStack: ObservableObject { + let id: PopupStackID + @Published private(set) var popups: [AnyPopup] = [] + @Published private(set) var priority: StackPriority = .init() + + private init(id: PopupStackID) { self.id = id } +} + +// MARK: Update +extension PopupStack { + func update(popup: AnyPopup) async { if let index = popups.firstIndex(of: popup) { + popups[index] = popup + }} +} + + + +// MARK: - STACK OPERATIONS + + + +// MARK: Modify +extension PopupStack { enum StackOperation { + case insertPopup(AnyPopup) + case removeLastPopup, removePopup(AnyPopup), removeAllPopupsOfType(any Popup.Type), removeAllPopupsWithID(String), removeAllPopups +}} +extension PopupStack { + func modify(_ operation: StackOperation) { Task { + await hideKeyboard() + + let oldPopups = popups, + newPopups = await getNewPopups(operation), + newPriority = await getNewPriority(newPopups) + + await updatePopups(newPopups) + await updatePriority(newPriority, oldPopups.count) + }} +} +private extension PopupStack { + nonisolated func hideKeyboard() async { + await AnyView.hideKeyboard() + } + nonisolated func getNewPopups(_ operation: StackOperation) async -> [AnyPopup] { switch operation { + case .insertPopup(let popup): await insertedPopup(popup) + case .removeLastPopup: await removedLastPopup() + case .removePopup(let popup): await removedPopup(popup) + case .removeAllPopupsOfType(let popupType): await removedAllPopupsOfType(popupType) + case .removeAllPopupsWithID(let id): await removedAllPopupsWithID(id) + case .removeAllPopups: await removedAllPopups() + }} + nonisolated func getNewPriority(_ newPopups: [AnyPopup]) async -> StackPriority { + await priority.reshuffled(newPopups) + } + func updatePopups(_ newPopups: [AnyPopup]) async { + popups = newPopups + } + func updatePriority(_ newPriority: StackPriority, _ oldPopupsCount: Int) async { + let delayDuration = oldPopupsCount > popups.count ? Animation.duration : 0 + await Task.sleep(seconds: delayDuration) + + priority = newPriority + } +} +private extension PopupStack { + nonisolated func insertedPopup(_ erasedPopup: AnyPopup) async -> [AnyPopup] { await popups.modifiedAsync(if: await !popups.contains { $0.id.isSameType(as: erasedPopup.id) }) { + $0.append(await erasedPopup.startDismissTimerIfNeeded(self)) + }} + nonisolated func removedLastPopup() async -> [AnyPopup] { await popups.modifiedAsync(if: !popups.isEmpty) { + $0.removeLast() + }} + nonisolated func removedPopup(_ popup: AnyPopup) async -> [AnyPopup] { await popups.modifiedAsync { + $0.removeAll { $0.id.isSame(as: popup) } + }} + nonisolated func removedAllPopupsOfType(_ popupType: any Popup.Type) async -> [AnyPopup] { await popups.modifiedAsync { + $0.removeAll { $0.id.isSameType(as: popupType) } + }} + nonisolated func removedAllPopupsWithID(_ id: String) async -> [AnyPopup] { await popups.modifiedAsync { + $0.removeAll { $0.id.isSameType(as: id) } + }} + nonisolated func removedAllPopups() async -> [AnyPopup] { + [] + } +} + + + +// MARK: - STACK CONTAINER OPERATIONS + + + +// MARK: Fetch +extension PopupStack { + nonisolated static func fetch(id: PopupStackID) async -> PopupStack? { + let stack = await PopupStackContainer.stacks.first(where: { $0.id == id }) + await logNoStackRegisteredErrorIfNeeded(stack: stack, id: id) + return stack + } +} +private extension PopupStack { + nonisolated static func logNoStackRegisteredErrorIfNeeded(stack: PopupStack?, id: PopupStackID) async { if stack == nil { + Logger.log( + level: .fault, + message: "PopupStack (\(id.rawValue)) must be registered before use. More details can be found in the documentation." + ) + }} +} + +// MARK: Register +extension PopupStack { + static func registerStack(id: PopupStackID) -> PopupStack { + let stackToRegister = PopupStack(id: id) + let registeredStack = PopupStackContainer.register(stack: stackToRegister) + return registeredStack + } +} diff --git a/Sources/Internal/Containers/PopupStackContainer.swift b/Sources/Internal/Containers/PopupStackContainer.swift new file mode 100644 index 0000000000..514b186c61 --- /dev/null +++ b/Sources/Internal/Containers/PopupStackContainer.swift @@ -0,0 +1,31 @@ +// +// PopupStackContainer.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import Foundation + +@MainActor class PopupStackContainer { + static private(set) var stacks: [PopupStack] = [] +} + +// MARK: Register +extension PopupStackContainer { + static func register(stack: PopupStack) -> PopupStack { + if let alreadyRegisteredStack = stacks.first(where: { $0.id == stack.id }) { return alreadyRegisteredStack } + + stacks.append(stack) + return stack + } +} + +// MARK: Clean +extension PopupStackContainer { + static func clean() { stacks = [] } +} diff --git a/Sources/Internal/Extensions/Animation++.swift b/Sources/Internal/Extensions/Animation++.swift index 1b524229c4..053d7796fd 100644 --- a/Sources/Internal/Extensions/Animation++.swift +++ b/Sources/Internal/Extensions/Animation++.swift @@ -15,5 +15,5 @@ extension Animation { static var transition: Animation { .spring(duration: Animation.duration, bounce: 0, blendDuration: 0) } } extension Animation { - static var duration: CGFloat { 0.27 } + static var duration: CGFloat { 0.28 } } diff --git a/Sources/Internal/Extensions/Array++.swift b/Sources/Internal/Extensions/Array++.swift index 49e1dd99f0..989efa87f2 100644 --- a/Sources/Internal/Extensions/Array++.swift +++ b/Sources/Internal/Extensions/Array++.swift @@ -9,6 +9,25 @@ // Copyright ©2024 Mijick. All rights reserved. +// MARK: Modified extension Array { - @inlinable func appending(_ newElement: Element) -> Self { self + [newElement] } + func modifiedAsync(if value: Bool = true, _ builder: (inout [Element]) async -> ()) async -> [Element] { guard value else { return self } + var array = self + await builder(&array) + return array + } + func modified(if value: Bool = true, _ builder: (inout [Element]) -> ()) -> [Element] { guard value else { return self } + var array = self + builder(&array) + return array + } +} + +// MARK: Inverted Index +extension Array where Element: Equatable { + func getInvertedIndex(of element: Element) -> Int { + let index = firstIndex(of: element) ?? 0 + let invertedIndex = count - 1 - index + return invertedIndex + } } diff --git a/Sources/Internal/Extensions/EdgeInsets++.swift b/Sources/Internal/Extensions/EdgeInsets++.swift index ee0178dd7f..521eaa6649 100644 --- a/Sources/Internal/Extensions/EdgeInsets++.swift +++ b/Sources/Internal/Extensions/EdgeInsets++.swift @@ -12,8 +12,9 @@ import SwiftUI extension EdgeInsets { - subscript(_ edge: VerticalEdge) -> CGFloat { switch edge { + subscript(_ edge: PopupAlignment) -> CGFloat { switch edge { case .top: top + case .center: 0 case .bottom: bottom }} } diff --git a/Tests/Extensions/Task++.swift b/Sources/Internal/Extensions/Task++.swift similarity index 90% rename from Tests/Extensions/Task++.swift rename to Sources/Internal/Extensions/Task++.swift index 62f5d9e2ed..7b70c6cf35 100644 --- a/Tests/Extensions/Task++.swift +++ b/Sources/Internal/Extensions/Task++.swift @@ -12,7 +12,7 @@ import SwiftUI extension Task where Success == Never, Failure == Never { - static func sleep(seconds: Double) async { + static func sleep(seconds: CGFloat) async { try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) } } diff --git a/Sources/Internal/Extensions/View+Background.swift b/Sources/Internal/Extensions/View+Background.swift index e05a3be3b2..8302600067 100644 --- a/Sources/Internal/Extensions/View+Background.swift +++ b/Sources/Internal/Extensions/View+Background.swift @@ -12,16 +12,16 @@ import SwiftUI extension View { - func background(backgroundColor: Color, overlayColor: Color, corners: [VerticalEdge: CGFloat]) -> some View { background( - backgroundColor + func background(backgroundColor: Color, overlayColor: Color, corners: [PopupAlignment: CGFloat]) -> some View { + background(backgroundColor) .overlay(overlayColor) .mask(RoundedCorner(corners: corners)) - )} + } } // MARK: Background Shape fileprivate struct RoundedCorner: Shape { - var corners: [VerticalEdge: CGFloat] + var corners: [PopupAlignment: CGFloat] var animatableData: CGFloat { diff --git a/Sources/Internal/Extensions/View+Gestures.swift b/Sources/Internal/Extensions/View+Gestures.swift index 09933debef..11140692be 100644 --- a/Sources/Internal/Extensions/View+Gestures.swift +++ b/Sources/Internal/Extensions/View+Gestures.swift @@ -24,14 +24,14 @@ extension View { // MARK: On Drag Gesture extension View { - func onDragGesture(onChanged actionOnChanged: @escaping (CGFloat) -> (), onEnded actionOnEnded: @escaping (CGFloat) -> (), isEnabled: Bool) -> some View { + func onDragGesture(onChanged actionOnChanged: @escaping (CGFloat) async -> (), onEnded actionOnEnded: @escaping (CGFloat) async -> (), isEnabled: Bool) -> some View { #if os(tvOS) self #else highPriorityGesture( DragGesture() - .onChanged { actionOnChanged($0.translation.height) } - .onEnded { actionOnEnded($0.translation.height) }, + .onChanged { newValue in Task { @MainActor in await actionOnChanged(newValue.translation.height) }} + .onEnded { newValue in Task { @MainActor in await actionOnEnded(newValue.translation.height) }}, isEnabled: isEnabled ) #endif diff --git a/Sources/Internal/Extensions/View+Keyboard.swift b/Sources/Internal/Extensions/View+Keyboard.swift index e57e92d6f7..1a45401cd8 100644 --- a/Sources/Internal/Extensions/View+Keyboard.swift +++ b/Sources/Internal/Extensions/View+Keyboard.swift @@ -41,11 +41,11 @@ fileprivate extension View { // MARK: Hide Keyboard extension AnyView { - @MainActor static func hideKeyboard() { + nonisolated static func hideKeyboard() async { #if os(iOS) - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + await UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) #elseif os(macOS) - NSApp.keyWindow?.makeFirstResponder(nil) + await NSApp.keyWindow?.makeFirstResponder(nil) #endif } } diff --git a/Sources/Internal/Extensions/View+ReadHeight.swift b/Sources/Internal/Extensions/View+ReadHeight.swift index 5b28069781..a68ff26a49 100644 --- a/Sources/Internal/Extensions/View+ReadHeight.swift +++ b/Sources/Internal/Extensions/View+ReadHeight.swift @@ -12,9 +12,11 @@ import SwiftUI extension View { - func onHeightChange(perform action: @escaping (CGFloat) -> ()) -> some View { background( + func onHeightChange(perform action: @escaping (CGFloat) async -> ()) -> some View { background( GeometryReader { proxy in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { action(proxy.size.height) } + Task { @MainActor in + await action(proxy.size.height) + } return Color.clear } )} diff --git a/Sources/Internal/Managers/PopupManager.swift b/Sources/Internal/Managers/PopupManager.swift deleted file mode 100644 index 9a023935a0..0000000000 --- a/Sources/Internal/Managers/PopupManager.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// PopupManager.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2023 Mijick. All rights reserved. - - -import SwiftUI - -@MainActor public class PopupManager: ObservableObject { - let id: PopupManagerID - @Published private(set) var stack: [AnyPopup] = [] - @Published private(set) var stackPriority: StackPriority = .init() - - private init(id: PopupManagerID) { self.id = id } -} - -// MARK: Update -extension PopupManager { - func updateStack(_ popup: AnyPopup) { if let index = stack.firstIndex(of: popup) { - stack[index] = popup - }} -} - - - -// MARK: - STACK OPERATIONS - - - -// MARK: Available Operations -extension PopupManager { enum StackOperation { - case insertPopup(any Popup) - case removeLastPopup, removePopupInstance(AnyPopup), removeAllPopupsOfType(any Popup.Type), removeAllPopupsWithID(String), removeAllPopups -}} - -// MARK: Perform Operation -extension PopupManager { - func stack(_ operation: StackOperation) { let oldStackCount = stack.count - hideKeyboard() - perform(operation) - reshuffleStackPriority(oldStackCount) - } -} -private extension PopupManager { - func hideKeyboard() { - AnyView.hideKeyboard() - } - func perform(_ operation: StackOperation) { switch operation { - case .insertPopup(let popup): insertPopup(popup) - case .removeLastPopup: removeLastPopup() - case .removePopupInstance(let popup): removePopupInstance(popup) - case .removeAllPopupsOfType(let popupType): removeAllPopupsOfType(popupType) - case .removeAllPopupsWithID(let id): removeAllPopupsWithID(id) - case .removeAllPopups: removeAllPopups() - }} - func reshuffleStackPriority(_ oldStackCount: Int) { - let delayDuration = oldStackCount > stack.count ? Animation.duration : 0 - - DispatchQueue.main.asyncAfter(deadline: .now() + delayDuration) { [self] in - stackPriority.reshuffle(newPopups: stack) - } - } -} -private extension PopupManager { - func insertPopup(_ popup: any Popup) { - let erasedPopup = AnyPopup(popup) - let canPopupBeInserted = !stack.contains(where: { $0.id.isSameType(as: erasedPopup.id) }) - - if canPopupBeInserted { stack.append(erasedPopup.startingDismissTimerIfNeeded(self)) } - } - func removeLastPopup() { if !stack.isEmpty { - stack.removeLast() - }} - func removePopupInstance(_ popup: AnyPopup) { - stack.removeAll(where: { $0.id.isSameInstance(as: popup) }) - } - func removeAllPopupsOfType(_ popupType: any Popup.Type) { - stack.removeAll(where: { $0.id.isSameType(as: popupType) }) - } - func removeAllPopupsWithID(_ id: String) { - stack.removeAll(where: { $0.id.isSameType(as: id) }) - } - func removeAllPopups() { - stack.removeAll() - } -} - - - -// MARK: - INSTACE OPERATIONS - - - -// MARK: Fetch -extension PopupManager { - static func fetchInstance(id: PopupManagerID) -> PopupManager? { - let managerObject = PopupManagerContainer.instances.first(where: { $0.id == id }) - logNoInstanceErrorIfNeeded(managerObject: managerObject, popupManagerID: id) - return managerObject - } -} -private extension PopupManager { - static func logNoInstanceErrorIfNeeded(managerObject: PopupManager?, popupManagerID: PopupManagerID) { if managerObject == nil { - Logger.log( - level: .fault, - message: "PopupManager instance (\(popupManagerID.rawValue)) must be registered before use. More details can be found in the documentation." - ) - }} -} - -// MARK: Register -extension PopupManager { - static func registerInstance(id: PopupManagerID) -> PopupManager { - let instanceToRegister = PopupManager(id: id) - let registeredInstance = PopupManagerContainer.register(popupManager: instanceToRegister) - return registeredInstance - } -} diff --git a/Sources/Internal/Models/ActivePopupProperties.swift b/Sources/Internal/Models/ActivePopupProperties.swift new file mode 100644 index 0000000000..3f894bb8c6 --- /dev/null +++ b/Sources/Internal/Models/ActivePopupProperties.swift @@ -0,0 +1,22 @@ +// +// ActivePopupProperties.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import SwiftUI + +struct ActivePopupProperties: Sendable { + var height: CGFloat? = nil + var innerPadding: EdgeInsets = .init() + var outerPadding: EdgeInsets = .init() + var corners: [PopupAlignment: CGFloat] = [.top: 0, .bottom: 0] + var verticalFixedSize: Bool = true + var gestureTranslation: CGFloat = 0 + var translationProgress: CGFloat = 0 +} diff --git a/Sources/Internal/Models/AnyPopup.swift b/Sources/Internal/Models/AnyPopup.swift index 76c1b145da..e21246854b 100644 --- a/Sources/Internal/Models/AnyPopup.swift +++ b/Sources/Internal/Models/AnyPopup.swift @@ -13,11 +13,11 @@ import SwiftUI struct AnyPopup: Popup { private(set) var id: PopupID - private(set) var config: LocalConfig + private(set) var config: AnyPopupConfig private(set) var height: CGFloat? = nil - private(set) var dragHeight: CGFloat? = nil + private(set) var dragHeight: CGFloat = 0 - private var dismissTimer: PopupActionScheduler? = nil + private var _dismissTimer: PopupActionScheduler? = nil private var _body: AnyView private let _onFocus: () -> () private let _onDismiss: () -> () @@ -25,18 +25,18 @@ struct AnyPopup: Popup { -// MARK: - INITIALISE & UPDATE +// MARK: - INITIALIZE & UPDATE -// MARK: Initialise +// MARK: Initialize extension AnyPopup { - init(_ popup: P) { + init(_ popup: P) async { if let popup = popup as? AnyPopup { self = popup } else { - self.id = .create(from: P.self) - self.config = popup.configurePopup(config: .init()) - self._body = AnyView(popup) + self.id = await .init(P.self) + self.config = .init(popup.configurePopup(config: .init())) + self._body = .init(popup) self._onFocus = popup.onFocus self._onDismiss = popup.onDismiss } @@ -45,15 +45,24 @@ extension AnyPopup { // MARK: Update extension AnyPopup { - func settingCustomID(_ customID: String) -> AnyPopup { updatingPopup { $0.id = .create(from: customID) }} - func settingDismissTimer(_ secondsToDismiss: Double) -> AnyPopup { updatingPopup { $0.dismissTimer = .prepare(time: secondsToDismiss) }} - func startingDismissTimerIfNeeded(_ popupManager: PopupManager) -> AnyPopup { updatingPopup { $0.dismissTimer?.schedule { popupManager.stack(.removePopupInstance(self)) }}} - func settingHeight(_ newHeight: CGFloat?) -> AnyPopup { updatingPopup { $0.height = newHeight }} - func settingDragHeight(_ newDragHeight: CGFloat?) -> AnyPopup { updatingPopup { $0.dragHeight = newDragHeight }} - func settingEnvironmentObject(_ environmentObject: some ObservableObject) -> AnyPopup { updatingPopup { $0._body = AnyView(_body.environmentObject(environmentObject)) }} + nonisolated func updatedHeight(_ newHeight: CGFloat?) async -> AnyPopup { await updatedAsync { $0.height = newHeight }} + nonisolated func updatedDragHeight(_ newDragHeight: CGFloat) async -> AnyPopup { await updatedAsync { $0.dragHeight = newDragHeight }} + nonisolated func updatedID(_ customID: String) async -> AnyPopup { await updatedAsync { $0.id = await .init(customID) }} } private extension AnyPopup { - func updatingPopup(_ customBuilder: (inout AnyPopup) -> ()) -> AnyPopup { + nonisolated func updatedAsync(_ customBuilder: (inout AnyPopup) async -> ()) async -> AnyPopup { + var popup = self + await customBuilder(&popup) + return popup + } +} +extension AnyPopup { + func updatedDismissTimer(_ secondsToDismiss: Double) -> AnyPopup { updated { $0._dismissTimer = .prepare(time: secondsToDismiss) }} + func updatedEnvironmentObject(_ environmentObject: some ObservableObject) -> AnyPopup { updated { $0._body = .init(_body.environmentObject(environmentObject)) }} + func startDismissTimerIfNeeded(_ popupStack: PopupStack) -> AnyPopup { updated { $0._dismissTimer?.schedule { popupStack.modify(.removePopup(self)) }}} +} +private extension AnyPopup { + func updated(_ customBuilder: (inout AnyPopup) -> ()) -> AnyPopup { var popup = self customBuilder(&popup) return popup @@ -67,7 +76,7 @@ private extension AnyPopup { // MARK: Popup -extension AnyPopup { typealias Config = LocalConfig +extension AnyPopup { typealias Config = AnyPopupConfig var body: some View { _body } func onFocus() { _onFocus() } @@ -76,28 +85,6 @@ extension AnyPopup { typealias Config = LocalConfig // MARK: Hashable extension AnyPopup: Hashable { - nonisolated static func ==(lhs: AnyPopup, rhs: AnyPopup) -> Bool { lhs.id.isSameInstance(as: rhs) } + nonisolated static func ==(lhs: AnyPopup, rhs: AnyPopup) -> Bool { lhs.id.isSame(as: rhs) } nonisolated func hash(into hasher: inout Hasher) { hasher.combine(id.rawValue) } } - - - -// MARK: - TESTS -#if DEBUG - - - -// MARK: New Object -extension AnyPopup { - static func t_createNew(id: String = UUID().uuidString, config: LocalConfig) -> AnyPopup { .init( - id: .create(from: id), - config: config, - height: nil, - dragHeight: nil, - dismissTimer: nil, - _body: .init(EmptyView()), - _onFocus: {}, - _onDismiss: {} - )} -} -#endif diff --git a/Sources/Internal/Models/AnyPopupConfig.swift b/Sources/Internal/Models/AnyPopupConfig.swift new file mode 100644 index 0000000000..04265deda7 --- /dev/null +++ b/Sources/Internal/Models/AnyPopupConfig.swift @@ -0,0 +1,44 @@ +// +// AnyPopupConfig.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import SwiftUI + +struct AnyPopupConfig: LocalConfig, Sendable { init() {} + // MARK: Content + var alignment: PopupAlignment = .center + var popupPadding: EdgeInsets = .init() + var cornerRadius: CGFloat = 0 + var ignoredSafeAreaEdges: Edge.Set = [] + var backgroundColor: Color = .clear + var overlayColor: Color = .clear + var heightMode: HeightMode = .auto + var dragDetents: [DragDetent] = [] + + // MARK: Gestures + var isTapOutsideToDismissEnabled: Bool = false + var isDragGestureEnabled: Bool = false +} + +// MARK: Initialize +extension AnyPopupConfig { + init(_ config: Config) { + self.alignment = .init(Config.self) + self.popupPadding = config.popupPadding + self.cornerRadius = config.cornerRadius + self.ignoredSafeAreaEdges = config.ignoredSafeAreaEdges + self.backgroundColor = config.backgroundColor + self.overlayColor = config.overlayColor + self.heightMode = config.heightMode + self.dragDetents = config.dragDetents + self.isTapOutsideToDismissEnabled = config.isTapOutsideToDismissEnabled + self.isDragGestureEnabled = config.isDragGestureEnabled + } +} diff --git a/Sources/Internal/Models/ID+Popup.swift b/Sources/Internal/Models/ID+Popup.swift index e01b899ded..866463dbf2 100644 --- a/Sources/Internal/Models/ID+Popup.swift +++ b/Sources/Internal/Models/ID+Popup.swift @@ -11,29 +11,29 @@ import Foundation -struct PopupID { +struct PopupID: Sendable { let rawValue: String } // MARK: Create extension PopupID { - static func create(from id: String) -> Self { + init(_ id: String) async { let firstComponent = id, - secondComponent = separator, + secondComponent = Self.separator, thirdComponent = String(describing: Date()) - return .init(rawValue: firstComponent + secondComponent + thirdComponent) + self.init(rawValue: firstComponent + secondComponent + thirdComponent) } - static func create(from popupType: any Popup.Type) -> Self { - create(from: .init(describing: popupType)) + init(_ popupType: any Popup.Type) async { + await self.init(.init(describing: popupType)) } } // MARK: Comparison extension PopupID { + func isSame(as popup: AnyPopup) -> Bool { rawValue == popup.id.rawValue } func isSameType(as id: String) -> Bool { getFirstComponent(of: self) == id } func isSameType(as popupType: any Popup.Type) -> Bool { getFirstComponent(of: self) == String(describing: popupType) } func isSameType(as popupID: PopupID) -> Bool { getFirstComponent(of: self) == getFirstComponent(of: popupID) } - func isSameInstance(as popup: AnyPopup) -> Bool { rawValue == popup.id.rawValue } } diff --git a/Sources/Internal/Models/Screen.swift b/Sources/Internal/Models/Screen.swift index 5259ed7c99..4ecf8b55f4 100644 --- a/Sources/Internal/Models/Screen.swift +++ b/Sources/Internal/Models/Screen.swift @@ -11,17 +11,8 @@ import SwiftUI -struct Screen { - let height: CGFloat - let safeArea: EdgeInsets - - - init(height: CGFloat = .zero, safeArea: EdgeInsets = .init()) { - self.height = height - self.safeArea = safeArea - } - init(_ reader: GeometryProxy) { - self.height = reader.size.height + reader.safeAreaInsets.top + reader.safeAreaInsets.bottom - self.safeArea = reader.safeAreaInsets - } +struct Screen: Sendable { + var height: CGFloat = .zero + var safeArea: EdgeInsets = .init() + var isKeyboardActive: Bool = false } diff --git a/Sources/Internal/Models/StackPriority.swift b/Sources/Internal/Models/StackPriority.swift index b2fb3b5808..68e67ac80f 100644 --- a/Sources/Internal/Models/StackPriority.swift +++ b/Sources/Internal/Models/StackPriority.swift @@ -11,30 +11,30 @@ import Foundation -struct StackPriority: Equatable { +struct StackPriority: Equatable, Sendable { var top: CGFloat { values[0] } - var centre: CGFloat { values[1] } + var center: CGFloat { values[1] } var bottom: CGFloat { values[2] } var overlay: CGFloat { 1 } private var values: [CGFloat] = [0, 0, 0] } -// MARK: Reshuffle +// MARK: Reshuffled extension StackPriority { - @MainActor mutating func reshuffle(newPopups: [AnyPopup]) { switch newPopups.last { - case .some(let popup) where popup.config is TopPopupConfig: reshuffle(0) - case .some(let popup) where popup.config is CentrePopupConfig: reshuffle(1) - case .some(let popup) where popup.config is BottomPopupConfig: reshuffle(2) - default: return + func reshuffled(_ newPopups: [AnyPopup]) -> StackPriority { switch newPopups.last?.config.alignment { + case .top: reshuffled(0) + case .center: reshuffled(1) + case .bottom: reshuffled(2) + default: self }} } private extension StackPriority { - mutating func reshuffle(_ index: Int) { - guard values[index] != maxPriority else { return } + func reshuffled(_ index: Int) -> StackPriority { + guard values[index] != maxPriority else { return self } let newValues = values.enumerated().map { $0.offset == index ? maxPriority : $0.element - 2 } - values = newValues + return .init(values: newValues) } } private extension StackPriority { diff --git a/Sources/Internal/UI/PopupCentreStackView.swift b/Sources/Internal/UI/PopupCenterStackView.swift similarity index 61% rename from Sources/Internal/UI/PopupCentreStackView.swift rename to Sources/Internal/UI/PopupCenterStackView.swift index aee1e515eb..0b6af71a2d 100644 --- a/Sources/Internal/UI/PopupCentreStackView.swift +++ b/Sources/Internal/UI/PopupCenterStackView.swift @@ -1,5 +1,5 @@ // -// PopupCentreStackView.swift of MijickPopups +// PopupCenterStackView.swift of MijickPopups // // Created by Tomasz Kurylik. Sending ❤️ from Kraków! // - Mail: tomasz.kurylik@mijick.com @@ -11,8 +11,8 @@ import SwiftUI -struct PopupCentreStackView: View { - @ObservedObject var viewModel: VM.CentreStack +struct PopupCenterStackView: View { + @ObservedObject var viewModel: VM.CenterStack var body: some View { @@ -22,29 +22,29 @@ struct PopupCentreStackView: View { .frame(maxWidth: .infinity, maxHeight: viewModel.screen.height) } } -private extension PopupCentreStackView { +private extension PopupCenterStackView { func createPopupStack() -> some View { ForEach(viewModel.popups, id: \.self, content: createPopup) } } -private extension PopupCentreStackView { +private extension PopupCenterStackView { func createPopup(_ popup: AnyPopup) -> some View { popup.body - .fixedSize(horizontal: false, vertical: viewModel.calculateVerticalFixedSize(for: popup)) - .onHeightChange { viewModel.recalculateAndSave(height: $0, for: popup) } - .frame(height: viewModel.activePopupHeight) - .frame(maxWidth: .infinity, maxHeight: viewModel.activePopupHeight) - .background(backgroundColor: getBackgroundColor(for: popup), overlayColor: .clear, corners: viewModel.calculateCornerRadius()) + .compositingGroup() + .fixedSize(horizontal: false, vertical: viewModel.activePopupProperties.verticalFixedSize) + .onHeightChange { await viewModel.updatePopupHeight($0, popup) } + .frame(height: viewModel.activePopupProperties.height) + .frame(maxWidth: .infinity, maxHeight: viewModel.activePopupProperties.height) + .background(backgroundColor: getBackgroundColor(for: popup), overlayColor: .clear, corners: viewModel.activePopupProperties.corners) .opacity(viewModel.calculateOpacity(for: popup)) .focusSection_tvOS() - .padding(viewModel.calculatePopupPadding()) - .compositingGroup() + .padding(viewModel.activePopupProperties.outerPadding) } } -private extension PopupCentreStackView { +private extension PopupCenterStackView { func getBackgroundColor(for popup: AnyPopup) -> Color { popup.config.backgroundColor } } -private extension PopupCentreStackView { +private extension PopupCenterStackView { var transition: AnyTransition { .scale(scale: 1.1).combined(with: .opacity) } } diff --git a/Sources/Internal/UI/PopupVerticalStackView.swift b/Sources/Internal/UI/PopupVerticalStackView.swift index 6cf7245577..cbeaa0685d 100644 --- a/Sources/Internal/UI/PopupVerticalStackView.swift +++ b/Sources/Internal/UI/PopupVerticalStackView.swift @@ -11,8 +11,8 @@ import SwiftUI -struct PopupVerticalStackView: View { - @ObservedObject var viewModel: VM.VerticalStack +struct PopupVerticalStackView: View { + @ObservedObject var viewModel: VM.VerticalStack var body: some View { @@ -27,22 +27,22 @@ private extension PopupVerticalStackView { } } private extension PopupVerticalStackView { - func createPopup(_ popup: AnyPopup) -> some View { + @ViewBuilder func createPopup(_ popup: AnyPopup) -> some View { if viewModel.isPopupActive(popup) { popup.body - .padding(viewModel.calculateBodyPadding(for: popup)) - .fixedSize(horizontal: false, vertical: viewModel.calculateVerticalFixedSize(for: popup)) - .onHeightChange { viewModel.recalculateAndSave(height: $0, for: popup) } - .frame(height: viewModel.activePopupHeight, alignment: (!viewModel.alignment).toAlignment()) - .frame(maxWidth: .infinity, maxHeight: viewModel.activePopupHeight, alignment: (!viewModel.alignment).toAlignment()) - .background(backgroundColor: getBackgroundColor(for: popup), overlayColor: getStackOverlayColor(for: popup), corners: viewModel.calculateCornerRadius()) + .compositingGroup() + .padding(viewModel.activePopupProperties.innerPadding) + .fixedSize(horizontal: false, vertical: viewModel.activePopupProperties.verticalFixedSize) + .onHeightChange { await viewModel.updatePopupHeight($0, popup) } + .frame(height: viewModel.activePopupProperties.height, alignment: (!viewModel.alignment).toAlignment()) + .frame(maxWidth: .infinity, maxHeight: viewModel.activePopupProperties.height, alignment: (!viewModel.alignment).toAlignment()) + .background(backgroundColor: getBackgroundColor(for: popup), overlayColor: getStackOverlayColor(for: popup), corners: viewModel.activePopupProperties.corners) .offset(y: viewModel.calculateOffsetY(for: popup)) .scaleEffect(x: viewModel.calculateScaleX(for: popup)) .focusSection_tvOS() - .padding(viewModel.calculatePopupPadding()) + .padding(viewModel.activePopupProperties.outerPadding) .transition(transition) .zIndex(viewModel.calculateZIndex()) - .compositingGroup() - } + }} } private extension PopupVerticalStackView { diff --git a/Sources/Internal/UI/PopupView.swift b/Sources/Internal/UI/PopupView.swift index d477887a6d..b59e99cff8 100644 --- a/Sources/Internal/UI/PopupView.swift +++ b/Sources/Internal/UI/PopupView.swift @@ -16,16 +16,16 @@ struct PopupView: View { let rootView: any View #endif - @ObservedObject var popupManager: PopupManager - private let topStackViewModel: VM.VerticalStack = .init() - private let centreStackViewModel: VM.CentreStack = .init() - private let bottomStackViewModel: VM.VerticalStack = .init() + @ObservedObject var stack: PopupStack + private let topStackViewModel: VM.VerticalStack = .init(TopPopupConfig.self) + private let centerStackViewModel: VM.CenterStack = .init(CenterPopupConfig.self) + private let bottomStackViewModel: VM.VerticalStack = .init(BottomPopupConfig.self) var body: some View { #if os(tvOS) AnyView(rootView) - .disabled(!popupManager.stack.isEmpty) + .disabled(!stack.popups.isEmpty) .overlay(createBody()) #else createBody() @@ -41,8 +41,8 @@ private extension PopupView { .onChange(of: reader.size) { _ in onScreenChange(reader) } } .onAppear(perform: onAppear) - .onChange(of: popupManager.stack.map { [$0.height, $0.dragHeight] }, perform: onPopupsHeightChange) - .onChange(of: popupManager.stack) { [oldValue = popupManager.stack] newValue in onStackChange(oldValue, newValue) } + .onChange(of: stack.popups.map { [$0.height, $0.dragHeight] }, perform: onPopupsHeightChange) + .onChange(of: stack.popups) { [oldValue = stack.popups] newValue in onStackChange(oldValue, newValue) } .onKeyboardStateChange(perform: onKeyboardStateChange) } } @@ -51,7 +51,7 @@ private extension PopupView { ZStack { createOverlayView() createTopPopupStackView() - createCentrePopupStackView() + createCenterPopupStackView() createBottomPopupStackView() } } @@ -59,38 +59,34 @@ private extension PopupView { private extension PopupView { func createOverlayView() -> some View { getOverlayColor() - .zIndex(popupManager.stackPriority.overlay) - .animation(.linear, value: popupManager.stack) + .zIndex(stack.priority.overlay) + .animation(.linear, value: stack.popups) .onTapGesture(perform: onTap) } func createTopPopupStackView() -> some View { - PopupVerticalStackView(viewModel: topStackViewModel).zIndex(popupManager.stackPriority.top) + PopupVerticalStackView(viewModel: topStackViewModel).zIndex(stack.priority.top) } - func createCentrePopupStackView() -> some View { - PopupCentreStackView(viewModel: centreStackViewModel).zIndex(popupManager.stackPriority.centre) + func createCenterPopupStackView() -> some View { + PopupCenterStackView(viewModel: centerStackViewModel).zIndex(stack.priority.center) } func createBottomPopupStackView() -> some View { - PopupVerticalStackView(viewModel: bottomStackViewModel).zIndex(popupManager.stackPriority.bottom) + PopupVerticalStackView(viewModel: bottomStackViewModel).zIndex(stack.priority.bottom) } } private extension PopupView { - func getOverlayColor() -> Color { switch popupManager.stack.last?.config.overlayColor { - case .some(let color) where color == .clear: .black.opacity(0.0000000000001) - case .some(let color): color - case nil: .clear - }} + func getOverlayColor() -> Color { stack.popups.last?.config.overlayColor ?? .clear } } private extension PopupView { - func onAppear() { - updateViewModels { $0.setup(updatePopupAction: updatePopup, closePopupAction: closePopup) } - } - func onScreenChange(_ reader: GeometryProxy) { - updateViewModels { $0.updateScreenValue(.init(reader)) } - } - func onPopupsHeightChange(_ p: Any) { - updateViewModels { $0.updatePopupsValue(popupManager.stack) } - } + func onAppear() { Task { + await updateViewModels { $0.setup(updatePopupAction: updatePopup, closePopupAction: closePopup) } + }} + func onScreenChange(_ screenReader: GeometryProxy) { Task { + await updateViewModels { await $0.updateScreen(screenHeight: screenReader.size.height + screenReader.safeAreaInsets.top + screenReader.safeAreaInsets.bottom, screenSafeArea: screenReader.safeAreaInsets) } + }} + func onPopupsHeightChange(_ p: Any) { Task { + await updateViewModels { await $0.updatePopups(stack.popups) } + }} func onStackChange(_ oldStack: [AnyPopup], _ newStack: [AnyPopup]) { newStack .difference(from: oldStack) @@ -100,24 +96,24 @@ private extension PopupView { }} newStack.last?.onFocus() } - func onKeyboardStateChange(_ isKeyboardActive: Bool) { - updateViewModels { $0.updateKeyboardValue(isKeyboardActive) } - } + func onKeyboardStateChange(_ isKeyboardActive: Bool) { Task { + await updateViewModels { await $0.updateScreen(isKeyboardActive: isKeyboardActive) } + }} func onTap() { if tapOutsideClosesPopup { - popupManager.stack(.removeLastPopup) + stack.modify(.removeLastPopup) }} } private extension PopupView { - func updatePopup(_ popup: AnyPopup) { - popupManager.updateStack(popup) + nonisolated func updatePopup(_ popup: AnyPopup) async { + await stack.update(popup: popup) } - func closePopup(_ popup: AnyPopup) { - popupManager.stack(.removePopupInstance(popup)) + nonisolated func closePopup(_ popup: AnyPopup) async { + await stack.modify(.removePopup(popup)) } - func updateViewModels(_ updateBuilder: (any ViewModelObject) -> ()) { - [topStackViewModel, centreStackViewModel, bottomStackViewModel].forEach(updateBuilder) + func updateViewModels(_ updateBuilder: @MainActor @escaping (any ViewModel) async -> ()) async { + for viewModel in [topStackViewModel, centerStackViewModel, bottomStackViewModel] { await updateBuilder(viewModel as! any ViewModel) } } } private extension PopupView { - var tapOutsideClosesPopup: Bool { popupManager.stack.last?.config.isTapOutsideToDismissEnabled ?? false } + var tapOutsideClosesPopup: Bool { stack.popups.last?.config.isTapOutsideToDismissEnabled ?? false } } diff --git a/Sources/Internal/Utilities/VerticalEdge.swift b/Sources/Internal/Utilities/PopupAlignment.swift similarity index 72% rename from Sources/Internal/Utilities/VerticalEdge.swift rename to Sources/Internal/Utilities/PopupAlignment.swift index 6aaf2ea879..4047536dbc 100644 --- a/Sources/Internal/Utilities/VerticalEdge.swift +++ b/Sources/Internal/Utilities/PopupAlignment.swift @@ -1,5 +1,5 @@ // -// VerticalEdge.swift of MijickPopups +// PopupAlignment.swift of MijickPopups // // Created by Tomasz Kurylik. Sending ❤️ from Kraków! // - Mail: tomasz.kurylik@mijick.com @@ -11,33 +11,41 @@ import SwiftUI -enum VerticalEdge { +enum PopupAlignment { case top + case center case bottom +} +// MARK: Initialize +extension PopupAlignment { init(_ config: LocalConfig.Type) { switch config.self { case is TopPopupConfig.Type: self = .top + case is CenterPopupConfig.Type: self = .center case is BottomPopupConfig.Type: self = .bottom default: fatalError() }} } // MARK: Negation -extension VerticalEdge { +extension PopupAlignment { static prefix func !(lhs: Self) -> Self { switch lhs { case .top: .bottom + case .center: .center case .bottom: .top }} } // MARK: Type Casting -extension VerticalEdge { +extension PopupAlignment { func toEdge() -> Edge { switch self { case .top: .top + case .center: .bottom case .bottom: .bottom }} func toAlignment() -> Alignment { switch self { case .top: .top + case .center: .center case .bottom: .bottom }} } diff --git a/Sources/Internal/View Models/ViewModel+CentreStack.swift b/Sources/Internal/View Models/ViewModel+CentreStack.swift index c305029f65..db6847549c 100644 --- a/Sources/Internal/View Models/ViewModel+CentreStack.swift +++ b/Sources/Internal/View Models/ViewModel+CentreStack.swift @@ -1,5 +1,5 @@ // -// ViewModel+CentreStack.swift of MijickPopups +// ViewModel+CenterStack.swift of MijickPopups // // Created by Tomasz Kurylik. Sending ❤️ from Kraków! // - Mail: tomasz.kurylik@mijick.com @@ -11,116 +11,107 @@ import SwiftUI -extension VM { class CentreStack: ViewModel { - // MARK: Overridden Methods - override func recalculateAndSave(height: CGFloat, for popup: AnyPopup) { _recalculateAndSave(height: height, for: popup) } - override func calculateHeightForActivePopup() -> CGFloat? { _calculateHeightForActivePopup() } - override func calculatePopupPadding() -> EdgeInsets { _calculatePopupPadding() } - override func calculateCornerRadius() -> [VerticalEdge : CGFloat] { _calculateCornerRadius() } - override func calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { _calculateVerticalFixedSize(for: popup) } +extension VM { class CenterStack: ViewModel { required init() {} + var alignment: PopupAlignment = .center + var popups: [AnyPopup] = [] + var activePopupProperties: ActivePopupProperties = .init() + var screen: Screen = .init() + var updatePopupAction: ((AnyPopup) async -> ())! + var closePopupAction: ((AnyPopup) async -> ())! }} -// MARK: - VIEW METHODS +// MARK: - METHODS / VIEW MODEL / ACTIVE POPUP -// MARK: Recalculate & Update Popup Height -private extension VM.CentreStack { - func _recalculateAndSave(height: CGFloat, for popup: AnyPopup) { - let newHeight = calculateHeight(height) - updateHeight(newHeight, popup) - } -} -private extension VM.CentreStack { - func calculateHeight(_ heightCandidate: CGFloat) -> CGFloat { - min(heightCandidate, calculateLargeScreenHeight()) - } -} -private extension VM.CentreStack { - func calculateLargeScreenHeight() -> CGFloat { - let fullscreenHeight = screen.height, - safeAreaHeight = screen.safeArea.top + screen.safeArea.bottom - return fullscreenHeight - safeAreaHeight +// MARK: Height +extension VM.CenterStack { + func calculateActivePopupHeight() async -> CGFloat? { + popups.last?.height } } -// MARK: Popup Padding -private extension VM.CentreStack { - func _calculatePopupPadding() -> EdgeInsets { .init( +// MARK: Outer Padding +extension VM.CenterStack { + func calculateActivePopupOuterPadding() async -> EdgeInsets { .init( top: calculateVerticalPopupPadding(for: .top), leading: calculateLeadingPopupPadding(), bottom: calculateVerticalPopupPadding(for: .bottom), trailing: calculateTrailingPopupPadding() )} } -private extension VM.CentreStack { - func calculateVerticalPopupPadding(for edge: VerticalEdge) -> CGFloat { - guard let activePopupHeight, - isKeyboardActive && edge == .bottom - else { return 0 } +private extension VM.CenterStack { + func calculateVerticalPopupPadding(for edge: PopupAlignment) -> CGFloat { + guard let activePopupHeight = activePopupProperties.height, screen.isKeyboardActive && edge == .bottom else { return 0 } let remainingHeight = screen.height - activePopupHeight let paddingCandidate = (remainingHeight / 2 - screen.safeArea.bottom) * 2 return abs(min(paddingCandidate, 0)) } func calculateLeadingPopupPadding() -> CGFloat { - getActivePopupConfig().popupPadding.leading + popups.last?.config.popupPadding.leading ?? 0 } func calculateTrailingPopupPadding() -> CGFloat { - getActivePopupConfig().popupPadding.trailing + popups.last?.config.popupPadding.trailing ?? 0 } } -// MARK: Corner Radius -private extension VM.CentreStack { - func _calculateCornerRadius() -> [VerticalEdge : CGFloat] {[ - .top: getActivePopupConfig().cornerRadius, - .bottom: getActivePopupConfig().cornerRadius +// MARK: Inner Padding +extension VM.CenterStack { + func calculateActivePopupInnerPadding() async -> EdgeInsets { .init() } +} + +// MARK: Corners +extension VM.CenterStack { + func calculateActivePopupCorners() async -> [PopupAlignment : CGFloat] { [ + .top: popups.last?.config.cornerRadius ?? 0, + .bottom: popups.last?.config.cornerRadius ?? 0 ]} } -// MARK: Opacity -extension VM.CentreStack { - func calculateOpacity(for popup: AnyPopup) -> CGFloat { - popups.last == popup ? 1 : 0 +// MARK: Vertical Fixed Size +extension VM.CenterStack { + func calculateActivePopupVerticalFixedSize() async -> Bool { + activePopupProperties.height != calculateLargeScreenHeight() } } -// MARK: Fixed Size -private extension VM.CentreStack { - func _calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { - activePopupHeight != calculateLargeScreenHeight() - } +// MARK: Translation Progress +extension VM.CenterStack { + func calculateActivePopupTranslationProgress() async -> CGFloat { 0 } } -// MARK: - HELPERS +// MARK: - METHODS / VIEW MODEL / SELECTED POPUP -// MARK: Active Popup Height -private extension VM.CentreStack { - func _calculateHeightForActivePopup() -> CGFloat? { - popups.last?.height +// MARK: Height +extension VM.CenterStack { + func calculatePopupHeight(_ heightCandidate: CGFloat, _ popup: AnyPopup) async -> CGFloat { + min(heightCandidate, calculateLargeScreenHeight()) + } +} +private extension VM.CenterStack { + func calculateLargeScreenHeight() -> CGFloat { + let fullscreenHeight = screen.height, + safeAreaHeight = screen.safeArea.top + screen.safeArea.bottom + return fullscreenHeight - safeAreaHeight } } -// MARK: - TESTS -#if DEBUG +// MARK: - METHODS / VIEW -// MARK: Methods -extension VM.CentreStack { - func t_calculateHeight(heightCandidate: CGFloat) -> CGFloat { calculateHeight(heightCandidate) } - func t_calculatePopupPadding() -> EdgeInsets { calculatePopupPadding() } - func t_calculateCornerRadius() -> [VerticalEdge: CGFloat] { calculateCornerRadius() } - func t_calculateOpacity(for popup: AnyPopup) -> CGFloat { calculateOpacity(for: popup) } - func t_calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { calculateVerticalFixedSize(for: popup) } +// MARK: Opacity +extension VM.CenterStack { + func calculateOpacity(for popup: AnyPopup) -> CGFloat { + popups.last == popup ? 1 : 0 + } } -#endif diff --git a/Sources/Internal/View Models/ViewModel+VerticalStack.swift b/Sources/Internal/View Models/ViewModel+VerticalStack.swift index 74f336732a..a29ca2e87f 100644 --- a/Sources/Internal/View Models/ViewModel+VerticalStack.swift +++ b/Sources/Internal/View Models/ViewModel+VerticalStack.swift @@ -11,161 +11,221 @@ import SwiftUI -extension VM { class VerticalStack: ViewModel { - // MARK: Attributes - private(set) var alignment: VerticalEdge - private(set) var gestureTranslation: CGFloat = 0 - private(set) var translationProgress: CGFloat = 0 - - // MARK: Overridden Methods - override func recalculateAndSave(height: CGFloat, for popup: AnyPopup) { _recalculateAndSave(height: height, for: popup) } - override func calculateHeightForActivePopup() -> CGFloat? { _calculateHeightForActivePopup() } - override func calculatePopupPadding() -> EdgeInsets { _calculatePopupPadding() } - override func calculateCornerRadius() -> [VerticalEdge : CGFloat] { _calculateCornerRadius() } - override func calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { _calculateVerticalFixedSize(for: popup) } - - // MARK: Initialise - override init() { self.alignment = .init(Config.self) } +extension VM { class VerticalStack: ViewModel { required init() {} + var alignment: PopupAlignment = .center + var popups: [AnyPopup] = [] + var activePopupProperties: ActivePopupProperties = .init() + var screen: Screen = .init() + var updatePopupAction: ((AnyPopup) async -> ())! + var closePopupAction: ((AnyPopup) async -> ())! }} -// MARK: - SETUP & UPDATE +// MARK: - METHODS / VIEW MODEL / ACTIVE POPUP -// MARK: Update -private extension VM.VerticalStack { - func updateGestureTranslation(_ newGestureTranslation: CGFloat) { - gestureTranslation = newGestureTranslation - translationProgress = calculateTranslationProgress() - activePopupHeight = calculateHeightForActivePopup() - - withAnimation(gestureTranslation == 0 ? .transition : nil) { objectWillChange.send() } - } -} - - - -// MARK: - VIEW METHODS - +// MARK: Height +extension VM.VerticalStack { + func calculateActivePopupHeight() async -> CGFloat? { + guard let activePopupHeight = popups.last?.height, let activePopupDragHeight = popups.last?.dragHeight else { return nil } + let popupHeightFromGestureTranslation = activePopupHeight + activePopupDragHeight + activePopupProperties.gestureTranslation * getDragTranslationMultiplier() -// MARK: Recalculate & Update Popup Height -private extension VM.VerticalStack { - func _recalculateAndSave(height: CGFloat, for popup: AnyPopup) { if gestureTranslation.isZero, height != popup.height { - let popupConfig = getConfig(popup) - let newHeight = calculateHeight(height, popupConfig) - updateHeight(newHeight, popup) - }} -} -private extension VM.VerticalStack { - func calculateHeight(_ heightCandidate: CGFloat, _ popupConfig: Config) -> CGFloat { switch popupConfig.heightMode { - case .auto: min(heightCandidate, calculateLargeScreenHeight()) - case .large: calculateLargeScreenHeight() - case .fullscreen: getFullscreenHeight() - }} -} -private extension VM.VerticalStack { - func calculateLargeScreenHeight() -> CGFloat { - let fullscreenHeight = getFullscreenHeight(), - safeAreaHeight = screen.safeArea[!alignment], - stackHeight = calculateStackHeight() - return fullscreenHeight - safeAreaHeight - stackHeight - } - func getFullscreenHeight() -> CGFloat { - screen.height + let newHeightCandidate = max(activePopupHeight, popupHeightFromGestureTranslation) + return min(newHeightCandidate, screen.height) } } private extension VM.VerticalStack { - func calculateStackHeight() -> CGFloat { - let numberOfStackedItems = max(popups.count - 1, 0) - - let stackedItemsHeight = stackOffset * .init(numberOfStackedItems) - return stackedItemsHeight - } + func getDragTranslationMultiplier() -> CGFloat { switch alignment { + case .top: 1 + case .bottom: -1 + case .center: fatalError() + }} } -// MARK: Popup Padding -private extension VM.VerticalStack { - func _calculatePopupPadding() -> EdgeInsets { .init( - top: calculateVerticalPopupPadding(for: .top), - leading: calculateLeadingPopupPadding(), - bottom: calculateVerticalPopupPadding(for: .bottom), - trailing: calculateTrailingPopupPadding() +// MARK: Outer Padding +extension VM.VerticalStack { + func calculateActivePopupOuterPadding() async -> EdgeInsets { guard let activePopupConfig = popups.last?.config else { return .init() }; return .init( + top: calculateVerticalOuterPadding(for: .top, activePopupConfig: activePopupConfig), + leading: calculateLeadingOuterPadding(activePopupConfig: activePopupConfig), + bottom: calculateVerticalOuterPadding(for: .bottom, activePopupConfig: activePopupConfig), + trailing: calculateTrailingOuterPadding(activePopupConfig: activePopupConfig) )} } private extension VM.VerticalStack { - func calculateVerticalPopupPadding(for edge: VerticalEdge) -> CGFloat { - guard let activePopupHeight else { return 0 } - + func calculateVerticalOuterPadding(for edge: PopupAlignment, activePopupConfig: AnyPopupConfig) -> CGFloat { let largeScreenHeight = calculateLargeScreenHeight(), - priorityPopupPaddingValue = calculatePriorityPopupPaddingValue(for: edge), + activePopupHeight = activePopupProperties.height ?? 0, + priorityPopupPaddingValue = calculatePriorityOuterPaddingValue(for: edge, activePopupConfig: activePopupConfig), remainingHeight = largeScreenHeight - activePopupHeight - priorityPopupPaddingValue - let popupPaddingCandidate = min(remainingHeight, getActivePopupConfig().popupPadding[edge]) + let popupPaddingCandidate = min(remainingHeight, activePopupConfig.popupPadding[edge]) return max(popupPaddingCandidate, 0) } - func calculateLeadingPopupPadding() -> CGFloat { - getActivePopupConfig().popupPadding.leading + func calculateLeadingOuterPadding(activePopupConfig: AnyPopupConfig) -> CGFloat { + activePopupConfig.popupPadding.leading } - func calculateTrailingPopupPadding() -> CGFloat { - getActivePopupConfig().popupPadding.trailing + func calculateTrailingOuterPadding(activePopupConfig: AnyPopupConfig) -> CGFloat { + activePopupConfig.popupPadding.trailing } } private extension VM.VerticalStack { - func calculatePriorityPopupPaddingValue(for edge: VerticalEdge) -> CGFloat { switch edge == alignment { + func calculatePriorityOuterPaddingValue(for edge: PopupAlignment, activePopupConfig: AnyPopupConfig) -> CGFloat { switch edge == alignment { case true: 0 - case false: getActivePopupConfig().popupPadding[!edge] + case false: activePopupConfig.popupPadding[!edge] }} } -// MARK: Body Padding +// MARK: Inner Padding extension VM.VerticalStack { - func calculateBodyPadding(for popup: AnyPopup) -> EdgeInsets { let activePopupHeight = activePopupHeight ?? 0, popupConfig = getConfig(popup); return .init( - top: calculateTopBodyPadding(activePopupHeight: activePopupHeight, popupConfig: popupConfig), - leading: calculateLeadingBodyPadding(popupConfig: popupConfig), - bottom: calculateBottomBodyPadding(activePopupHeight: activePopupHeight, popupConfig: popupConfig), - trailing: calculateTrailingBodyPadding(popupConfig: popupConfig) + func calculateActivePopupInnerPadding() async -> EdgeInsets { guard let popup = popups.last else { return .init() }; return .init( + top: calculateTopInnerPadding(popup: popup), + leading: calculateLeadingInnerPadding(popup: popup), + bottom: calculateBottomInnerPadding(popup: popup), + trailing: calculateTrailingInnerPadding(popup: popup) )} } private extension VM.VerticalStack { - func calculateTopBodyPadding(activePopupHeight: CGFloat, popupConfig: Config) -> CGFloat { - if popupConfig.ignoredSafeAreaEdges.contains(.top) { return 0 } + func calculateTopInnerPadding(popup: AnyPopup) -> CGFloat { + if popup.config.ignoredSafeAreaEdges.contains(.top) { return 0 } return switch alignment { - case .top: calculateVerticalPaddingAdhereEdge(safeAreaHeight: screen.safeArea.top, popupPadding: calculatePopupPadding().top) - case .bottom: calculateVerticalPaddingCounterEdge(popupHeight: activePopupHeight, safeArea: screen.safeArea.top) + case .top: calculateVerticalInnerPaddingAdhereEdge(safeAreaHeight: screen.safeArea.top, popupOuterPadding: activePopupProperties.outerPadding.top) + case .bottom: calculateVerticalInnerPaddingCounterEdge(popupHeight: activePopupProperties.height ?? 0, safeArea: screen.safeArea.top) + case .center: fatalError() } } - func calculateBottomBodyPadding(activePopupHeight: CGFloat, popupConfig: Config) -> CGFloat { - if popupConfig.ignoredSafeAreaEdges.contains(.bottom) && !isKeyboardActive { return 0 } + func calculateBottomInnerPadding(popup: AnyPopup) -> CGFloat { + if popup.config.ignoredSafeAreaEdges.contains(.bottom) && !screen.isKeyboardActive { return 0 } return switch alignment { - case .top: calculateVerticalPaddingCounterEdge(popupHeight: activePopupHeight, safeArea: screen.safeArea.bottom) - case .bottom: calculateVerticalPaddingAdhereEdge(safeAreaHeight: screen.safeArea.bottom, popupPadding: calculatePopupPadding().bottom) + case .top: calculateVerticalInnerPaddingCounterEdge(popupHeight: activePopupProperties.height ?? 0, safeArea: screen.safeArea.bottom) + case .bottom: calculateVerticalInnerPaddingAdhereEdge(safeAreaHeight: screen.safeArea.bottom, popupOuterPadding: activePopupProperties.outerPadding.bottom) + case .center: fatalError() } } - func calculateLeadingBodyPadding(popupConfig: Config) -> CGFloat { switch popupConfig.ignoredSafeAreaEdges.contains(.leading) { + func calculateLeadingInnerPadding(popup: AnyPopup) -> CGFloat { switch popup.config.ignoredSafeAreaEdges.contains(.leading) { case true: 0 case false: screen.safeArea.leading }} - func calculateTrailingBodyPadding(popupConfig: Config) -> CGFloat { switch popupConfig.ignoredSafeAreaEdges.contains(.trailing) { + func calculateTrailingInnerPadding(popup: AnyPopup) -> CGFloat { switch popup.config.ignoredSafeAreaEdges.contains(.trailing) { case true: 0 case false: screen.safeArea.trailing }} } private extension VM.VerticalStack { - func calculateVerticalPaddingCounterEdge(popupHeight: CGFloat, safeArea: CGFloat) -> CGFloat { + func calculateVerticalInnerPaddingCounterEdge(popupHeight: CGFloat, safeArea: CGFloat) -> CGFloat { let paddingValueCandidate = safeArea + popupHeight - screen.height return max(paddingValueCandidate, 0) } - func calculateVerticalPaddingAdhereEdge(safeAreaHeight: CGFloat, popupPadding: CGFloat) -> CGFloat { - let paddingValueCandidate = safeAreaHeight - popupPadding + func calculateVerticalInnerPaddingAdhereEdge(safeAreaHeight: CGFloat, popupOuterPadding: CGFloat) -> CGFloat { + let paddingValueCandidate = safeAreaHeight - popupOuterPadding return max(paddingValueCandidate, 0) } } +// MARK: Corners +extension VM.VerticalStack { + func calculateActivePopupCorners() async -> [PopupAlignment: CGFloat] { guard let activePopup = popups.last else { return [.top: 0, .bottom: 0] } + let cornerRadiusValue = calculateCornerRadiusValue(activePopup) + return [ + .top: calculateTopCornerRadius(cornerRadiusValue), + .bottom: calculateBottomCornerRadius(cornerRadiusValue) + ] + } +} +private extension VM.VerticalStack { + func calculateCornerRadiusValue(_ activePopup: AnyPopup) -> CGFloat { switch activePopup.config.heightMode { + case .auto, .large: activePopup.config.cornerRadius + case .fullscreen: 0 + }} + func calculateTopCornerRadius(_ cornerRadiusValue: CGFloat) -> CGFloat { switch alignment { + case .top: activePopupProperties.outerPadding.top != 0 ? cornerRadiusValue : 0 + case .bottom: cornerRadiusValue + case .center: fatalError() + }} + func calculateBottomCornerRadius(_ cornerRadiusValue: CGFloat) -> CGFloat { switch alignment { + case .top: cornerRadiusValue + case .bottom: activePopupProperties.outerPadding.bottom != 0 ? cornerRadiusValue : 0 + case .center: fatalError() + }} +} + +// MARK: Vertical Fixed Size +extension VM.VerticalStack { + func calculateActivePopupVerticalFixedSize() async -> Bool { guard let popup = popups.last else { return true }; return switch popup.config.heightMode { + case .fullscreen, .large: false + case .auto: activePopupProperties.height != calculateLargeScreenHeight() + }} +} + +// MARK: Translation Progress +extension VM.VerticalStack { + func calculateActivePopupTranslationProgress() async -> CGFloat { guard let activePopupHeight = popups.last?.height else { return 0 }; return switch alignment { + case .top: abs(min(activePopupProperties.gestureTranslation + (popups.last?.dragHeight ?? 0), 0)) / activePopupHeight + case .bottom: max(activePopupProperties.gestureTranslation - (popups.last?.dragHeight ?? 0), 0) / activePopupHeight + case .center: fatalError() + }} +} + + + +// MARK: - METHODS / VIEW MODEL / SELECTED POPUP + + + +// MARK: Height +extension VM.VerticalStack { + func calculatePopupHeight(_ heightCandidate: CGFloat, _ popup: AnyPopup) async -> CGFloat { + guard activePopupProperties.gestureTranslation.isZero else { return popup.height ?? 0 } + + let popupHeight = calculateNewPopupHeight(heightCandidate, popup.config) + return popupHeight + } +} +private extension VM.VerticalStack { + func calculateNewPopupHeight(_ heightCandidate: CGFloat, _ popupConfig: AnyPopupConfig) -> CGFloat { switch popupConfig.heightMode { + case .auto: min(heightCandidate, calculateLargeScreenHeight()) + case .large: calculateLargeScreenHeight() + case .fullscreen: getFullscreenHeight() + }} +} +private extension VM.VerticalStack { + func calculateLargeScreenHeight() -> CGFloat { + let fullscreenHeight = getFullscreenHeight(), + safeAreaHeight = screen.safeArea[!alignment], + stackHeight = calculateStackHeight() + return fullscreenHeight - safeAreaHeight - stackHeight + } + func getFullscreenHeight() -> CGFloat { + screen.height + } +} +private extension VM.VerticalStack { + func calculateStackHeight() -> CGFloat { + let numberOfStackedItems = max(popups.count - 1, 0), + numberOfVisibleItems = min(numberOfStackedItems, maxNumberOfVisiblePopups) + + let stackedItemsHeight = stackOffset * .init(numberOfVisibleItems) + return stackedItemsHeight + } +} + + + +// MARK: - METHODS / VIEW + + + +// MARK: Is Popup Active +extension VM.VerticalStack { + func isPopupActive(_ popup: AnyPopup) -> Bool { + popups.getInvertedIndex(of: popup) < maxNumberOfVisiblePopups + } +} + // MARK: Offset Y extension VM.VerticalStack { func calculateOffsetY(for popup: AnyPopup) -> CGFloat { switch popup == popups.last { @@ -178,16 +238,18 @@ private extension VM.VerticalStack { let lastPopupDragHeight = popups.last?.dragHeight ?? 0 return switch alignment { - case .top: min(gestureTranslation + lastPopupDragHeight, 0) - case .bottom: max(gestureTranslation - lastPopupDragHeight, 0) + case .top: min(activePopupProperties.gestureTranslation + lastPopupDragHeight, 0) + case .bottom: max(activePopupProperties.gestureTranslation - lastPopupDragHeight, 0) + case .center: fatalError() } } func calculateOffsetForStackedPopup(_ popup: AnyPopup) -> CGFloat { - let invertedIndex = getInvertedIndex(of: popup) + let invertedIndex = popups.getInvertedIndex(of: popup) let offsetValue = stackOffset * .init(invertedIndex) let alignmentMultiplier = switch alignment { case .top: 1.0 case .bottom: -1.0 + case .center: fatalError() } return offsetValue * alignmentMultiplier @@ -199,50 +261,14 @@ extension VM.VerticalStack { func calculateScaleX(for popup: AnyPopup) -> CGFloat { guard popup != popups.last else { return 1 } - let invertedIndex = getInvertedIndex(of: popup), - remainingTranslationProgress = 1 - translationProgress + let invertedIndex = popups.getInvertedIndex(of: popup), + remainingTranslationProgress = 1 - activePopupProperties.translationProgress let progressMultiplier = invertedIndex == 1 ? remainingTranslationProgress : max(minScaleProgressMultiplier, remainingTranslationProgress) let scaleValue = .init(invertedIndex) * stackScaleFactor * progressMultiplier return 1 - scaleValue } } -private extension VM.VerticalStack { - var minScaleProgressMultiplier: CGFloat { 0.7 } -} - -// MARK: Corner Radius -private extension VM.VerticalStack { - func _calculateCornerRadius() -> [VerticalEdge: CGFloat] { - let cornerRadiusValue = calculateCornerRadiusValue(getActivePopupConfig()) - return [ - .top: calculateTopCornerRadius(cornerRadiusValue), - .bottom: calculateBottomCornerRadius(cornerRadiusValue) - ] - } -} -private extension VM.VerticalStack { - func calculateCornerRadiusValue(_ activePopupConfig: Config) -> CGFloat { switch activePopupConfig.heightMode { - case .auto, .large: activePopupConfig.cornerRadius - case .fullscreen: 0 - }} - func calculateTopCornerRadius(_ cornerRadiusValue: CGFloat) -> CGFloat { switch alignment { - case .top: calculatePopupPadding().top != 0 ? cornerRadiusValue : 0 - case .bottom: cornerRadiusValue - }} - func calculateBottomCornerRadius(_ cornerRadiusValue: CGFloat) -> CGFloat { switch alignment { - case .top: cornerRadiusValue - case .bottom: calculatePopupPadding().bottom != 0 ? cornerRadiusValue : 0 - }} -} - -// MARK: Fixed Size -private extension VM.VerticalStack { - func _calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { switch getConfig(popup).heightMode { - case .fullscreen, .large: false - case .auto: activePopupHeight != calculateLargeScreenHeight() - }} -} // MARK: Z Index extension VM.VerticalStack { @@ -253,74 +279,31 @@ extension VM.VerticalStack { // MARK: - Stack Overlay Opacity extension VM.VerticalStack { - func calculateStackOverlayOpacity(for popup: AnyPopup) -> Double { + func calculateStackOverlayOpacity(for popup: AnyPopup) -> CGFloat { guard popup != popups.last else { return 0 } - let invertedIndex = getInvertedIndex(of: popup), - remainingTranslationProgress = 1 - translationProgress + let invertedIndex = popups.getInvertedIndex(of: popup), + remainingTranslationProgress = 1 - activePopupProperties.translationProgress let progressMultiplier = invertedIndex == 1 ? remainingTranslationProgress : max(minStackOverlayProgressMultiplier, remainingTranslationProgress) - let overlayValue = min(stackOverlayFactor * .init(invertedIndex), maxStackOverlayFactor) + let overlayValue = stackOverlayFactor * .init(invertedIndex) let opacity = overlayValue * progressMultiplier return max(opacity, 0) } } -private extension VM.VerticalStack { - var minStackOverlayProgressMultiplier: CGFloat { 0.6 } -} - - - -// MARK: - HELPERS - - - -// MARK: Active Popup Height -private extension VM.VerticalStack { - func _calculateHeightForActivePopup() -> CGFloat? { - guard let activePopupHeight = popups.last?.height else { return nil } - - let activePopupDragHeight = popups.last?.dragHeight ?? 0 - let popupHeightFromGestureTranslation = activePopupHeight + activePopupDragHeight + gestureTranslation * getDragTranslationMultiplier() - - let newHeightCandidate1 = max(activePopupHeight, popupHeightFromGestureTranslation), - newHeightCanditate2 = screen.height - return min(newHeightCandidate1, newHeightCanditate2) - } -} -private extension VM.VerticalStack { - func getDragTranslationMultiplier() -> CGFloat { switch alignment { - case .top: 1 - case .bottom: -1 - }} -} - -// MARK: Translation Progress -private extension VM.VerticalStack { - func calculateTranslationProgress() -> CGFloat { guard let activePopupHeight = popups.last?.height else { return 0 }; return switch alignment { - case .top: abs(min(gestureTranslation + (popups.last?.dragHeight ?? 0), 0)) / activePopupHeight - case .bottom: max(gestureTranslation - (popups.last?.dragHeight ?? 0), 0) / activePopupHeight - }} -} - -// MARK: Others -private extension VM.VerticalStack { - func getInvertedIndex(of popup: AnyPopup) -> Int { - let index = popups.firstIndex(of: popup) ?? 0 - let invertedIndex = popups.count - 1 - index - return invertedIndex - } -} // MARK: Attributes extension VM.VerticalStack { var stackScaleFactor: CGFloat { 0.025 } - var stackOverlayFactor: CGFloat { 0.1 } - var maxStackOverlayFactor: CGFloat { 0.48 } + var stackOverlayFactor: CGFloat { 0.2 } var stackOffset: CGFloat { GlobalConfigContainer.vertical.isStackingEnabled ? 8 : 0 } var dragThreshold: CGFloat { GlobalConfigContainer.vertical.dragThreshold } - var dragGestureEnabled: Bool { getActivePopupConfig().isDragGestureEnabled } + var dragGestureEnabled: Bool { popups.last?.config.isDragGestureEnabled ?? false } + var dragTranslationThreshold: CGFloat { 32 } + var minScaleProgressMultiplier: CGFloat { 0.7 } + var minStackOverlayProgressMultiplier: CGFloat { 0.6 } + var maxNumberOfVisiblePopups: Int { 3 } } @@ -331,13 +314,15 @@ extension VM.VerticalStack { // MARK: On Changed extension VM.VerticalStack { - func onPopupDragGestureChanged(_ value: CGFloat) { if dragGestureEnabled { - let newGestureTranslation = calculateGestureTranslation(value) - updateGestureTranslation(newGestureTranslation) - }} + func onPopupDragGestureChanged(_ value: CGFloat) async { + guard dragGestureEnabled else { return } + + let newGestureTranslation = await calculateGestureTranslation(value) + await updateGestureTranslation(newGestureTranslation) + } } private extension VM.VerticalStack { - func calculateGestureTranslation(_ value: CGFloat) -> CGFloat { switch getActivePopupConfig().dragDetents.isEmpty { + func calculateGestureTranslation(_ value: CGFloat) async -> CGFloat { switch popups.last?.config.dragDetents.isEmpty ?? true { case true: calculateGestureTranslationWhenNoDragDetents(value) case false: calculateGestureTranslationWhenDragDetents(value) }} @@ -346,85 +331,88 @@ private extension VM.VerticalStack { func calculateGestureTranslationWhenNoDragDetents(_ value: CGFloat) -> CGFloat { calculateDragExtremeValue(value, 0) } - func calculateGestureTranslationWhenDragDetents(_ value: CGFloat) -> CGFloat { guard value * getDragTranslationMultiplier() > 0, let activePopupHeight = popups.last?.height else { return value } - let maxHeight = calculateMaxHeightForDragGesture(activePopupHeight) + func calculateGestureTranslationWhenDragDetents(_ value: CGFloat) -> CGFloat { + guard value * getDragTranslationMultiplier() > 0, let activePopup = popups.last, let activePopupHeight = activePopup.height else { return value } + + let maxHeight = calculateMaxHeightForDragGesture(activePopupHeight, activePopup.config) let dragTranslation = calculateDragTranslation(maxHeight, activePopupHeight) return calculateDragExtremeValue(dragTranslation, value) } } private extension VM.VerticalStack { - func calculateMaxHeightForDragGesture(_ activePopupHeight: CGFloat) -> CGFloat { - let maxHeight1 = (calculatePopupTargetHeightsFromDragDetents(activePopupHeight).max() ?? 0) + dragTranslationThreshold - let maxHeight2 = screen.height - return min(maxHeight1, maxHeight2) + func calculateMaxHeightForDragGesture(_ activePopupHeight: CGFloat, _ activePopupConfig: AnyPopupConfig) -> CGFloat { + let maxDragDetent = calculatePopupTargetHeightsFromDragDetents(activePopupHeight, activePopupConfig).max() ?? 0 + let maxHeightCandidate = maxDragDetent + dragTranslationThreshold + return min(maxHeightCandidate, screen.height) } func calculateDragTranslation(_ maxHeight: CGFloat, _ activePopupHeight: CGFloat) -> CGFloat { - let translation = maxHeight - activePopupHeight - (popups.last?.dragHeight ?? 0) + let activePopupDragHeight = popups.last?.dragHeight ?? 0 + let translation = maxHeight - activePopupHeight - activePopupDragHeight return translation * getDragTranslationMultiplier() } func calculateDragExtremeValue(_ value1: CGFloat, _ value2: CGFloat) -> CGFloat { switch alignment { case .top: min(value1, value2) case .bottom: max(value1, value2) + case .center: fatalError() }} } -private extension VM.VerticalStack { - var dragTranslationThreshold: CGFloat { 8 } -} // MARK: On Ended extension VM.VerticalStack { - func onPopupDragGestureEnded(_ value: CGFloat) { if value != 0 { - dismissLastItemIfNeeded() - updateTranslationValues() - }} + func onPopupDragGestureEnded(_ value: CGFloat) async { + guard value != 0, let activePopup = popups.last else { return } + + await dismissLastPopupIfNeeded(activePopup) + + let targetDragHeight = await calculateTargetDragHeight(activePopup) + await updateGestureTranslation(0) + await updatePopupDragHeight(targetDragHeight, activePopup) + } } private extension VM.VerticalStack { - func dismissLastItemIfNeeded() { if shouldDismissPopup() { if let popup = popups.last { - closePopupAction(popup) - }}} - func updateTranslationValues() { if let activePopupHeight = popups.last?.height { - let currentPopupHeight = calculateCurrentPopupHeight(activePopupHeight) - let popupTargetHeights = calculatePopupTargetHeightsFromDragDetents(activePopupHeight) - let targetHeight = calculateTargetPopupHeight(currentPopupHeight, popupTargetHeights) - let targetDragHeight = calculateTargetDragHeight(targetHeight, activePopupHeight) - - resetGestureTranslation() - updateDragHeight(targetDragHeight) + func dismissLastPopupIfNeeded(_ popup: AnyPopup) async { switch activePopupProperties.translationProgress >= dragThreshold { + case true: await closePopupAction(popup) + case false: return }} + func calculateTargetDragHeight(_ activePopup: AnyPopup) async -> CGFloat { + guard let activePopupHeight = activePopup.height else { return 0 } + + let currentPopupHeight = calculateCurrentPopupHeight(activePopupHeight: activePopupHeight, activePopupDragHeight: activePopup.dragHeight) + let popupTargetHeights = calculatePopupTargetHeightsFromDragDetents(activePopupHeight, activePopup.config) + let targetHeight = calculateTargetPopupHeight(activePopupHeight: activePopupHeight, activePopupDragHeight: activePopup.dragHeight, currentPopupHeight: currentPopupHeight, popupTargetHeights: popupTargetHeights) + let targetDragHeight = calculateTargetDragHeight(targetHeight, activePopupHeight) + return targetDragHeight + } } private extension VM.VerticalStack { - func calculateCurrentPopupHeight(_ activePopupHeight: CGFloat) -> CGFloat { - let activePopupDragHeight = popups.last?.dragHeight ?? 0 - let currentDragHeight = activePopupDragHeight + gestureTranslation * getDragTranslationMultiplier() - + func calculateCurrentPopupHeight(activePopupHeight: CGFloat, activePopupDragHeight: CGFloat) -> CGFloat { + let currentDragHeight = activePopupDragHeight + activePopupProperties.gestureTranslation * getDragTranslationMultiplier() let currentPopupHeight = activePopupHeight + currentDragHeight return currentPopupHeight } - func calculatePopupTargetHeightsFromDragDetents(_ activePopupHeight: CGFloat) -> [CGFloat] { - getActivePopupConfig().dragDetents + func calculatePopupTargetHeightsFromDragDetents(_ activePopupHeight: CGFloat, _ activePopupConfig: AnyPopupConfig) -> [CGFloat] { + activePopupConfig.dragDetents .map { switch $0 { case .height(let targetHeight): min(targetHeight, calculateLargeScreenHeight()) case .fraction(let fraction): min(fraction * activePopupHeight, calculateLargeScreenHeight()) case .large: calculateLargeScreenHeight() case .fullscreen: screen.height }} - .appending(activePopupHeight) + .modified { $0.append(activePopupHeight) } .sorted(by: <) } - func calculateTargetPopupHeight(_ currentPopupHeight: CGFloat, _ popupTargetHeights: [CGFloat]) -> CGFloat { - guard let activePopupHeight = popups.last?.height, - currentPopupHeight < screen.height - else { return popupTargetHeights.last ?? 0 } - - let initialIndex = popupTargetHeights.firstIndex(where: { $0 >= currentPopupHeight }) ?? popupTargetHeights.count - 1, - targetIndex = gestureTranslation * getDragTranslationMultiplier() > 0 ? initialIndex : max(0, initialIndex - 1) - let previousPopupHeight = (popups.last?.dragHeight ?? 0) + activePopupHeight, + func calculateTargetPopupHeight(activePopupHeight: CGFloat, activePopupDragHeight: CGFloat, currentPopupHeight: CGFloat, popupTargetHeights: [CGFloat]) -> CGFloat { + guard currentPopupHeight < screen.height else { return popupTargetHeights.last ?? 0 } + + let initialIndex = popupTargetHeights.firstIndex { $0 >= currentPopupHeight } ?? popupTargetHeights.count - 1, + targetIndex = activePopupProperties.gestureTranslation * getDragTranslationMultiplier() > 0 ? initialIndex : max(0, initialIndex - 1) + let previousPopupHeight = activePopupDragHeight + activePopupHeight, popupTargetHeight = popupTargetHeights[targetIndex], deltaHeight = abs(previousPopupHeight - popupTargetHeight) let progress = abs(currentPopupHeight - previousPopupHeight) / deltaHeight if progress < dragThreshold { - let index = gestureTranslation * getDragTranslationMultiplier() > 0 ? max(0, initialIndex - 1) : initialIndex + let index = activePopupProperties.gestureTranslation * getDragTranslationMultiplier() > 0 ? max(0, initialIndex - 1) : initialIndex return popupTargetHeights[index] } return popupTargetHeights[targetIndex] @@ -432,53 +420,4 @@ private extension VM.VerticalStack { func calculateTargetDragHeight(_ targetHeight: CGFloat, _ activePopupHeight: CGFloat) -> CGFloat { targetHeight - activePopupHeight } - func updateDragHeight(_ targetDragHeight: CGFloat) { if let activePopup = popups.last { - updatePopupAction(activePopup.settingDragHeight(targetDragHeight)) - }} - func resetGestureTranslation() { - updateGestureTranslation(0) - } - func shouldDismissPopup() -> Bool { - translationProgress >= dragThreshold - } -} - - - -// MARK: - TESTS -#if DEBUG - - - -// MARK: Methods -extension VM.VerticalStack { - func t_calculatePopupPadding() -> EdgeInsets { calculatePopupPadding() } - func t_calculateBodyPadding(for popup: AnyPopup) -> EdgeInsets { calculateBodyPadding(for: popup) } - func t_calculateHeight(heightCandidate: CGFloat, popupConfig: Config) -> CGFloat { calculateHeight(heightCandidate, popupConfig) } - func t_calculateOffsetY(for popup: AnyPopup) -> CGFloat { calculateOffsetY(for: popup) } - func t_calculateScaleX(for popup: AnyPopup) -> CGFloat { calculateScaleX(for: popup) } - func t_calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { calculateVerticalFixedSize(for: popup) } - func t_calculateStackOverlayOpacity(for popup: AnyPopup) -> CGFloat { calculateStackOverlayOpacity(for: popup) } - func t_calculateCornerRadius() -> [VerticalEdge: CGFloat] { calculateCornerRadius() } - func t_calculateTranslationProgress() -> CGFloat { calculateTranslationProgress() } - func t_getInvertedIndex(of popup: AnyPopup) -> Int { getInvertedIndex(of: popup) } - - func t_calculateAndUpdateTranslationProgress() { translationProgress = calculateTranslationProgress() } - func t_updateGestureTranslation(_ newGestureTranslation: CGFloat) { updateGestureTranslation(newGestureTranslation) } - - func t_onPopupDragGestureChanged(_ value: CGFloat) { onPopupDragGestureChanged(value) } - func t_onPopupDragGestureEnded(_ value: CGFloat) { onPopupDragGestureEnded(value) } -} - -// MARK: Variables -extension VM.VerticalStack { - var t_stackOffset: CGFloat { stackOffset } - var t_stackScaleFactor: CGFloat { stackScaleFactor } - var t_stackOverlayFactor: CGFloat { stackOverlayFactor } - var t_minScaleProgressMultiplier: CGFloat { minScaleProgressMultiplier } - var t_minStackOverlayProgressMultiplier: CGFloat { minStackOverlayProgressMultiplier } - var t_maxStackOverlayFactor: CGFloat { maxStackOverlayFactor } - var t_dragTranslationThreshold: CGFloat { dragTranslationThreshold } - var t_gestureTranslation: CGFloat { gestureTranslation } } -#endif diff --git a/Sources/Internal/View Models/ViewModel.swift b/Sources/Internal/View Models/ViewModel.swift index c8650c81b4..4f852add0d 100644 --- a/Sources/Internal/View Models/ViewModel.swift +++ b/Sources/Internal/View Models/ViewModel.swift @@ -10,91 +10,125 @@ import SwiftUI +import Combine enum VM {} -class ViewModel: ViewModelObject { +@MainActor protocol ViewModel: ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { init() // MARK: Attributes - private(set) var popups: [AnyPopup] = [] - private(set) var updatePopupAction: ((AnyPopup) -> ())! - private(set) var closePopupAction: ((AnyPopup) -> ())! - - // MARK: Subclass Attributes - var activePopupHeight: CGFloat? = nil - var screen: Screen = .init() - var isKeyboardActive: Bool = false - - // MARK: Methods to Override - func recalculateAndSave(height: CGFloat, for popup: AnyPopup) { fatalError() } - func calculateHeightForActivePopup() -> CGFloat? { fatalError() } - func calculatePopupPadding() -> EdgeInsets { fatalError() } - func calculateCornerRadius() -> [VerticalEdge: CGFloat] { fatalError() } - func calculateVerticalFixedSize(for popup: AnyPopup) -> Bool { fatalError() } + var alignment: PopupAlignment { get set } + var popups: [AnyPopup] { get set } + var activePopupProperties: ActivePopupProperties { get set } + var screen: Screen { get set } + + // MARK: Actions + var updatePopupAction: ((AnyPopup) async -> ())! { get set } + var closePopupAction: ((AnyPopup) async -> ())! { get set } + + // MARK: Methods + func calculateActivePopupHeight() async -> CGFloat? + func calculateActivePopupOuterPadding() async -> EdgeInsets + func calculateActivePopupInnerPadding() async -> EdgeInsets + func calculateActivePopupCorners() async -> [PopupAlignment: CGFloat] + func calculateActivePopupVerticalFixedSize() async -> Bool + func calculateActivePopupTranslationProgress() async -> CGFloat + func calculatePopupHeight(_ heightCandidate: CGFloat, _ popup: AnyPopup) async -> CGFloat +} + + + +// MARK: - INITIALIZE & SETUP + + + +// MARK: Initialize +extension ViewModel { + init(_ config: Config.Type) { self.init(); self.alignment = .init(Config.self) } } // MARK: Setup extension ViewModel { - func setup(updatePopupAction: @escaping (AnyPopup) -> (), closePopupAction: @escaping (AnyPopup) -> ()) { + func setup(updatePopupAction: @escaping (AnyPopup) async -> (), closePopupAction: @escaping (AnyPopup) async -> ()) { self.updatePopupAction = updatePopupAction self.closePopupAction = closePopupAction } } -// MARK: Update + + +// MARK: UPDATE + + + +// MARK: Popups extension ViewModel { - func updatePopupsValue(_ newPopups: [AnyPopup]) { - popups = newPopups.filter { $0.config is Config } - activePopupHeight = calculateHeightForActivePopup() + func updatePopups(_ newPopups: [AnyPopup]) async { + popups = await filteredPopups(newPopups) + await updateActivePopupProperties() withAnimation(.transition) { objectWillChange.send() } } - func updateScreenValue(_ newScreen: Screen) { - screen = newScreen +} - withAnimation(.transition) { objectWillChange.send() } - } - func updateKeyboardValue(_ isActive: Bool) { - isKeyboardActive = isActive +// MARK: Screen +extension ViewModel { + func updateScreen(screenHeight: CGFloat? = nil, screenSafeArea: EdgeInsets? = nil, isKeyboardActive: Bool? = nil) async { + screen = await updatedScreenProperties(screenHeight, screenSafeArea, isKeyboardActive) + await updateActivePopupProperties() withAnimation(.transition) { objectWillChange.send() } } } -// MARK: Helpers +// MARK: Gesture Translation extension ViewModel { - func updateHeight(_ newHeight: CGFloat, _ popup: AnyPopup) { if popup.height != newHeight { - updatePopupAction(popup.settingHeight(newHeight)) - }} -} -extension ViewModel { - func getConfig(_ item: AnyPopup?) -> Config { - let config = item?.config as? Config - return config ?? .init() - } - func getActivePopupConfig() -> Config { - getConfig(popups.last) + func updateGestureTranslation(_ newGestureTranslation: CGFloat) async { + await updateActivePopupPropertiesOnGestureTranslationChange(newGestureTranslation) + + withAnimation(activePopupProperties.gestureTranslation == 0 ? .transition : nil) { objectWillChange.send() } } } +// MARK: Popup Height +extension ViewModel { + func updatePopupHeight(_ heightCandidate: CGFloat, _ popup: AnyPopup) async { + guard activePopupProperties.gestureTranslation == 0 else { return } + let newHeight = await calculatePopupHeight(heightCandidate, popup) + if newHeight != popup.height { + await updatePopupAction(popup.updatedHeight(newHeight)) + } + } +} -// MARK: - TESTS -#if DEBUG - - - -// MARK: Methods +// MARK: Popup Drag Height extension ViewModel { - func t_setup(updatePopupAction: @escaping (AnyPopup) -> (), closePopupAction: @escaping (AnyPopup) -> ()) { setup(updatePopupAction: updatePopupAction, closePopupAction: closePopupAction) } - func t_updatePopupsValue(_ newPopups: [AnyPopup]) { updatePopupsValue(newPopups) } - func t_updateScreenValue(_ newScreen: Screen) { updateScreenValue(newScreen) } - func t_updateKeyboardValue(_ isActive: Bool) { updateKeyboardValue(isActive) } - func t_updatePopup(_ popup: AnyPopup) { updatePopupAction(popup) } - func t_calculateAndUpdateActivePopupHeight() { activePopupHeight = calculateHeightForActivePopup() } + func updatePopupDragHeight(_ targetDragHeight: CGFloat, _ popup: AnyPopup) async { + await updatePopupAction(popup.updatedDragHeight(targetDragHeight)) + } } -// MARK: Variables -extension ViewModel { - var t_popups: [AnyPopup] { popups } - var t_activePopupHeight: CGFloat? { activePopupHeight } +// MARK: Helpers +private extension ViewModel { + func filteredPopups(_ popups: [AnyPopup]) async -> [AnyPopup] { + popups.filter { $0.config.alignment == alignment } + } + func updatedScreenProperties(_ screenHeight: CGFloat?, _ screenSafeArea: EdgeInsets?, _ isKeyboardActive: Bool?) async -> Screen { .init( + height: screenHeight ?? screen.height, + safeArea: screenSafeArea ?? screen.safeArea, + isKeyboardActive: isKeyboardActive ?? screen.isKeyboardActive + )} +} +private extension ViewModel { + func updateActivePopupProperties() async { + activePopupProperties.height = await calculateActivePopupHeight() + activePopupProperties.outerPadding = await calculateActivePopupOuterPadding() + activePopupProperties.innerPadding = await calculateActivePopupInnerPadding() + activePopupProperties.corners = await calculateActivePopupCorners() + activePopupProperties.verticalFixedSize = await calculateActivePopupVerticalFixedSize() + } + func updateActivePopupPropertiesOnGestureTranslationChange(_ newGestureTranslation: CGFloat) async { + activePopupProperties.gestureTranslation = newGestureTranslation + activePopupProperties.translationProgress = await calculateActivePopupTranslationProgress() + activePopupProperties.height = await calculateActivePopupHeight() + } } -#endif diff --git a/Sources/Internal/View Models/ViewModelObject.swift b/Sources/Internal/View Models/ViewModelObject.swift deleted file mode 100644 index d917d39a60..0000000000 --- a/Sources/Internal/View Models/ViewModelObject.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ViewModelObject.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import SwiftUI - -@MainActor protocol ViewModelObject: ObservableObject { - associatedtype Config = LocalConfig - - func setup(updatePopupAction: @escaping (AnyPopup) -> (), closePopupAction: @escaping (AnyPopup) -> ()) - func updatePopupsValue(_ newPopups: [AnyPopup]) - func updateScreenValue(_ newScreen: Screen) - func updateKeyboardValue(_ isActive: Bool) - func getConfig(_ item: AnyPopup?) -> Config - func getActivePopupConfig() -> Config -} diff --git a/Sources/Public/Dismiss/Public+Dismiss+PopupManager.swift b/Sources/Public/Dismiss/Public+Dismiss+PopupManager.swift deleted file mode 100644 index ba9b81a243..0000000000 --- a/Sources/Public/Dismiss/Public+Dismiss+PopupManager.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Public+Dismiss+PopupManager.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import Foundation - -public extension PopupManager { - /** - Removes the currently active popup from the stack. - Makes the next popup in the stack the new active popup. - - - Parameters: - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. - - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. - */ - static func dismissLastPopup(popupManagerID: PopupManagerID = .shared) { fetchInstance(id: popupManagerID)?.stack(.removeLastPopup) } - - /** - Removes all popups with the specified identifier from the stack. - - - Parameters: - - id: Identifier of the popup located on the stack. - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. - - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. - */ - static func dismissPopup(_ id: String, popupManagerID: PopupManagerID = .shared) { fetchInstance(id: popupManagerID)?.stack(.removeAllPopupsWithID(id)) } - - /** - Removes all popups of the provided type from the stack. - - - Parameters: - - type: Type of the popup located on the stack. - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. - - - Important: If a custom ID (``Popup/setCustomID(_:)``) is set for the popup, use the ``dismissPopup(_:popupManagerID:)-1atvy`` method instead. - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. - */ - static func dismissPopup(_ type: P.Type, popupManagerID: PopupManagerID = .shared) { fetchInstance(id: popupManagerID)?.stack(.removeAllPopupsOfType(type)) } - - /** - Removes all popups from the stack. - - - Parameters: - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. - - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popups. - */ - static func dismissAllPopups(popupManagerID: PopupManagerID = .shared) { fetchInstance(id: popupManagerID)?.stack(.removeAllPopups) } -} diff --git a/Sources/Public/Dismiss/Public+Dismiss+PopupStack.swift b/Sources/Public/Dismiss/Public+Dismiss+PopupStack.swift new file mode 100644 index 0000000000..b89ed73c33 --- /dev/null +++ b/Sources/Public/Dismiss/Public+Dismiss+PopupStack.swift @@ -0,0 +1,57 @@ +// +// Public+Dismiss+PopupStack.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import Foundation + +public extension PopupStack { + /** + Dismisses the currently active popup. + + - Parameters: + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. + + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. + */ + @MainActor static func dismissLastPopup(popupStackID: PopupStackID = .shared) async { await fetch(id: popupStackID)?.modify(.removeLastPopup) } + + /** + Dismisses all popups with the specified identifier. + + - Parameters: + - id: Identifier of the popup located on the stack. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. + + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. + */ + @MainActor static func dismissPopup(_ id: String, popupStackID: PopupStackID = .shared) async { await fetch(id: popupStackID)?.modify(.removeAllPopupsWithID(id)) } + + /** + Dismisses all popups of the provided type. + + - Parameters: + - type: Type of the popup located on the stack. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. + + - Important: If a custom ID (``Popup/setCustomID(_:)``) is set for the popup, use the ``dismissPopup(_:popupStackID:)-1atvy`` method instead. + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. + */ + @MainActor static func dismissPopup(_ type: P.Type, popupStackID: PopupStackID = .shared) async { await fetch(id: popupStackID)?.modify(.removeAllPopupsOfType(type)) } + + /** + Dismisses all the popups. + + - Parameters: + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. + + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popups. + */ + @MainActor static func dismissAllPopups(popupStackID: PopupStackID = .shared) async { await fetch(id: popupStackID)?.modify(.removeAllPopups) } +} diff --git a/Sources/Public/Dismiss/Public+Dismiss+View.swift b/Sources/Public/Dismiss/Public+Dismiss+View.swift index c3b99e4687..da812635bd 100644 --- a/Sources/Public/Dismiss/Public+Dismiss+View.swift +++ b/Sources/Public/Dismiss/Public+Dismiss+View.swift @@ -13,46 +13,45 @@ import SwiftUI public extension View { /** - Removes the currently active popup from the stack. - Makes the next popup in the stack the new active popup. + Dismisses the currently active popup. - Parameters: - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. */ - func dismissLastPopup(popupManagerID: PopupManagerID = .shared) { PopupManager.dismissLastPopup(popupManagerID: popupManagerID) } + @MainActor func dismissLastPopup(popupStackID: PopupStackID = .shared) async { await PopupStack.dismissLastPopup(popupStackID: popupStackID) } /** - Removes all popups with the specified identifier from the stack. + Dismisses all popups with the specified identifier. - Parameters: - id: Identifier of the popup located on the stack. - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. */ - func dismissPopup(_ id: String, popupManagerID: PopupManagerID = .shared) { PopupManager.dismissPopup(id, popupManagerID: popupManagerID) } + @MainActor func dismissPopup(_ id: String, popupStackID: PopupStackID = .shared) async { await PopupStack.dismissPopup(id, popupStackID: popupStackID) } /** - Removes all popups of the provided type from the stack. + Dismisses all popups of the provided type. - Parameters: - type: Type of the popup located on the stack. - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. - - Important: If a custom ID (see ``Popup/setCustomID(_:)`` method for reference) is set for the popup, use the ``SwiftUICore/View/dismissPopup(_:popupManagerID:)-55ubm`` method instead. - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popup. + - Important: If a custom ID (see ``Popup/setCustomID(_:)`` method for reference) is set for the popup, use the ``SwiftUICore/View/dismissPopup(_:popupStackID:)-55ubm`` method instead. + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popup. */ - func dismissPopup(_ type: P.Type, popupManagerID: PopupManagerID = .shared) { PopupManager.dismissPopup(type, popupManagerID: popupManagerID) } + @MainActor func dismissPopup(_ type: P.Type, popupStackID: PopupStackID = .shared) async { await PopupStack.dismissPopup(type, popupStackID: popupStackID) } /** - Removes all popups from the stack. + Dismisses all the popups. - Parameters: - - popupManagerID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupManagerID:)``. + - popupStackID: The identifier for which the popup was presented. For more information, see ``Popup/present(popupStackID:)``. - - Important: Make sure you use the correct **popupManagerID** from which you want to remove the popups. + - Important: Make sure you use the correct **popupStackID** from which you want to remove the popups. */ - func dismissAllPopups(popupManagerID: PopupManagerID = .shared) { PopupManager.dismissAllPopups(popupManagerID: popupManagerID) } + @MainActor func dismissAllPopups(popupStackID: PopupStackID = .shared) async { await PopupStack.dismissAllPopups(popupStackID: popupStackID) } } diff --git a/Sources/Public/Popup/Public+Popup+Config.swift b/Sources/Public/Popup/Public+Popup+Config.swift index 02a75adc12..203ed96733 100644 --- a/Sources/Public/Popup/Public+Popup+Config.swift +++ b/Sources/Public/Popup/Public+Popup+Config.swift @@ -11,8 +11,8 @@ import SwiftUI -// MARK: Vertical & Centre -public extension LocalConfig { +// MARK: Center +public extension LocalConfigCenter { /** Distance of the entire popup (including its background) from the horizontal edges of the screen. @@ -56,8 +56,8 @@ public extension LocalConfig { func tapOutsideToDismissPopup(_ value: Bool) -> Self { self.isTapOutsideToDismissEnabled = value; return self } } -// MARK: Only Vertical -public extension LocalConfig.Vertical { +// MARK: Vertical +public extension LocalConfigVertical { /** Distance of the entire popup (including its background) from the top edge of the screen. @@ -74,17 +74,51 @@ public extension LocalConfig.Vertical { */ func popupBottomPadding(_ value: CGFloat) -> Self { self.popupPadding = .init(top: popupPadding.top, leading: popupPadding.leading, bottom: value, trailing: popupPadding.trailing); return self } + /** + Distance of the entire popup (including its background) from the horizontal edges of the screen. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/horizontal-padding.png?raw=true) + */ + func popupHorizontalPadding(_ value: CGFloat) -> Self { self.popupPadding = .init(top: popupPadding.top, leading: value, bottom: popupPadding.bottom, trailing: value); return self } + + /** + Corner radius of the background of the active popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/corner-radius.png?raw=true) + */ + func cornerRadius(_ value: CGFloat) -> Self { self.cornerRadius = value; return self } + /** Expands the safe area of a popup. - Parameters: - - edges: The regions to expand the popup’s safe area into. + - edges: The regions to expand the popup’s safe area into. ## Visualisation ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/ignore-safe-area.png?raw=true) */ func ignoreSafeArea(edges: Edge.Set) -> Self { self.ignoredSafeAreaEdges = edges; return self } + /** + Background color of the popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/background-color.png?raw=true) + */ + func backgroundColor(_ color: Color) -> Self { self.backgroundColor = color; return self } + + /** + The color of the overlay covering the view behind the popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/overlay-color.png?raw=true) + + - tip: Use .clear to hide the overlay. + */ + func overlayColor(_ color: Color) -> Self { self.overlayColor = color; return self } + /** Sets the height for the popup. By default, the height of the popup is calculated based on its content. @@ -101,6 +135,14 @@ public extension LocalConfig.Vertical { */ func dragDetents(_ value: [DragDetent]) -> Self { self.dragDetents = value; return self } + /** + If enabled, dismisses the active popup when touched outside its area. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/tap-to-close.png?raw=true) + */ + func tapOutsideToDismissPopup(_ value: Bool) -> Self { self.isTapOutsideToDismissEnabled = value; return self } + /** Determines whether it's possible to interact with popups using a drag gesture. diff --git a/Sources/Public/Popup/Public+Popup+Main.swift b/Sources/Public/Popup/Public+Popup+Main.swift index d69ccb5215..a0aff7a9db 100644 --- a/Sources/Public/Popup/Public+Popup+Main.swift +++ b/Sources/Public/Popup/Public+Popup+Main.swift @@ -12,70 +12,18 @@ import SwiftUI /** - The view to be displayed as a popup. It may appear in one of three positions (see **Usage Examples** section). - # Optional Methods + The view to be displayed as a popup. It may appear in one of three positions (see **Usage** section). + ## Optional Methods - ``configurePopup(config:)-3ze4`` - ``onFocus()-6krqs`` - ``onDismiss()-254h8`` - - # Usage Examples - - ## TopPopup - ```swift - struct TopPopupExample: TopPopup { - func onFocus() { print("Popup is now active") } - func onDismiss() { print("Popup was dismissed") } - func configurePopup(config: TopPopupConfig) -> TopPopupConfig { config - .heightMode(.auto) - .cornerRadius(44) - .dragDetents([.fraction(1.2), .fraction(1.4), .large]) - } - var body: some View { - Text("Hello Kitty") - } - } - ``` - ![TopPopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/top-popup.png?raw=true) - - ## CentrePopup - ```swift - struct CentrePopupExample: CentrePopup { - func onFocus() { print("Popup is now active") } - func onDismiss() { print("Popup was dismissed") } - func configurePopup(config: CentrePopupConfig) -> CentrePopupConfig { config - .cornerRadius(44) - .tapOutsideToDismissPopup(true) - } - var body: some View { - Text("Hello Kitty") - } - } - ``` - ![CentrePopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/centre-popup.png?raw=true) - - ## BottomPopup - ```swift - struct BottomPopupExample: BottomPopup { - func onFocus() { print("Popup is now active") } - func onDismiss() { print("Popup was dismissed") } - func configurePopup(config: BottomPopupConfig) -> BottomPopupConfig { config - .heightMode(.auto) - .cornerRadius(44) - .dragDetents([.fraction(1.2), .fraction(1.4), .large]) - } - var body: some View { - Text("Hello Kitty") - } - } - ``` - ![BottomPopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/bottom-popup.png?raw=true) */ public protocol Popup: View { associatedtype Config: LocalConfig /** Configures the popup. - See the list of available methods in ``LocalConfig`` and ``LocalConfig/Vertical``. + See the list of available methods in ``LocalConfigCenter`` and ``LocalConfigVertical``. - important: If a certain method is not called here, the popup inherits the configuration from ``GlobalConfigContainer``. */ @@ -103,14 +51,12 @@ public extension Popup { /** The view to be displayed as a Top popup. - # Optional Methods + ## Optional Methods - ``Popup/configurePopup(config:)-98ha0`` - ``Popup/onFocus()-6krqs`` - ``Popup/onDismiss()-3bufs`` - # Usage Examples - - ## TopPopup + ## Usage ```swift struct TopPopupExample: TopPopup { func onFocus() { print("Popup is now active") } @@ -128,23 +74,22 @@ public extension Popup { ![TopPopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/top-popup.png?raw=true) */ public protocol TopPopup: Popup { associatedtype Config = TopPopupConfig } +public typealias TopPopupConfig = LocalConfigVertical.Top /** - The view to be displayed as a Centre popup. + The view to be displayed as a Center popup. - # Optional Methods + ## Optional Methods - ``Popup/configurePopup(config:)-3ze4`` - ``Popup/onFocus()-loq5`` - ``Popup/onDismiss()-3bufs`` - # Usage Examples - - ## CentrePopup + ## Usage ```swift - struct CentrePopupExample: CentrePopup { + struct CenterPopupExample: CenterPopup { func onFocus() { print("Popup is now active") } func onDismiss() { print("Popup was dismissed") } - func configurePopup(config: CentrePopupConfig) -> CentrePopupConfig { config + func configurePopup(config: CenterPopupConfig) -> CenterPopupConfig { config .cornerRadius(44) .tapOutsideToDismissPopup(true) } @@ -153,9 +98,10 @@ public protocol TopPopup: Popup { associatedtype Config = TopPopupConfig } } } ``` - ![CentrePopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/centre-popup.png?raw=true) + ![CenterPopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/centre-popup.png?raw=true) */ -public protocol CentrePopup: Popup { associatedtype Config = CentrePopupConfig } +public protocol CenterPopup: Popup { associatedtype Config = CenterPopupConfig } +public typealias CenterPopupConfig = LocalConfigCenter /** The view to be displayed as a Bottom popup. @@ -165,9 +111,7 @@ public protocol CentrePopup: Popup { associatedtype Config = CentrePopupConfig } - ``Popup/onFocus()-loq5`` - ``Popup/onDismiss()-254h8`` - # Usage Examples - - ## BottomPopup + ## Usage ```swift struct BottomPopupExample: BottomPopup { func onFocus() { print("Popup is now active") } @@ -185,3 +129,4 @@ public protocol CentrePopup: Popup { associatedtype Config = CentrePopupConfig } ![BottomPopup](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/bottom-popup.png?raw=true) */ public protocol BottomPopup: Popup { associatedtype Config = BottomPopupConfig } +public typealias BottomPopupConfig = LocalConfigVertical.Bottom diff --git a/Sources/Public/Popup/Public+Popup+Utilities.swift b/Sources/Public/Popup/Public+Popup+Utilities.swift index 9209854162..39f80ff36e 100644 --- a/Sources/Public/Popup/Public+Popup+Utilities.swift +++ b/Sources/Public/Popup/Public+Popup+Utilities.swift @@ -12,7 +12,7 @@ import Foundation // MARK: Height Mode -public enum HeightMode { +public enum HeightMode: Sendable { /** Popup height is calculated based on its content. @@ -41,7 +41,7 @@ public enum HeightMode { } // MARK: Drag Detent -public enum DragDetent { +public enum DragDetent: Sendable { /** A detent with the specified height. diff --git a/Sources/Public/Present/Public+Present+Popup.swift b/Sources/Public/Present/Public+Present+Popup.swift index fb3894be26..5904e3b0e6 100644 --- a/Sources/Public/Present/Public+Present+Popup.swift +++ b/Sources/Public/Present/Public+Present+Popup.swift @@ -16,23 +16,23 @@ public extension Popup { Presents the popup. - Parameters: - - popupManagerID: The identifier registered in one of the application windows in which the popup is to be displayed. + - popupStackID: The identifier registered in one of the application windows in which the popup is to be displayed. - - Important: The **popupManagerID** must be registered prior to use. For more information see ``SwiftUICore/View/registerPopups(id:configBuilder:)``. + - Important: The **popupStackID** must be registered prior to use. For more information see ``SwiftUICore/View/registerPopups(id:configBuilder:)``. - Important: The methods - ``PopupManager/dismissLastPopup(popupManagerID:)``, - ``PopupManager/dismissPopup(_:popupManagerID:)-1atvy``, - ``PopupManager/dismissPopup(_:popupManagerID:)-6l2c2``, - ``PopupManager/dismissAllPopups(popupManagerID:)``, - ``SwiftUICore/View/dismissLastPopup(popupManagerID:)``, - ``SwiftUICore/View/dismissPopup(_:popupManagerID:)-55ubm``, - ``SwiftUICore/View/dismissPopup(_:popupManagerID:)-9mkd5``, - ``SwiftUICore/View/dismissAllPopups(popupManagerID:)`` - should be called with the same **popupManagerID** as the one used here. + ``PopupStack/dismissLastPopup(popupStackID:)``, + ``PopupStack/dismissPopup(_:popupStackID:)-1atvy``, + ``PopupStack/dismissPopup(_:popupStackID:)-6l2c2``, + ``PopupStack/dismissAllPopups(popupStackID:)``, + ``SwiftUICore/View/dismissLastPopup(popupStackID:)``, + ``SwiftUICore/View/dismissPopup(_:popupStackID:)-55ubm``, + ``SwiftUICore/View/dismissPopup(_:popupStackID:)-9mkd5``, + ``SwiftUICore/View/dismissAllPopups(popupStackID:)`` + should be called with the same **popupStackID** as the one used here. - Warning: To present multiple popups of the same type, set a unique identifier using the method ``Popup/setCustomID(_:)``. */ - func present(popupManagerID: PopupManagerID = .shared) { PopupManager.fetchInstance(id: popupManagerID)?.stack(.insertPopup(self)) } + @MainActor func present(popupStackID: PopupStackID = .shared) async { await PopupStack.fetch(id: popupStackID)?.modify(.insertPopup(.init(self))) } } // MARK: Configure Popup @@ -40,15 +40,15 @@ public extension Popup { /** Sets the custom ID for the selected popup. - - important: To dismiss a popup with a custom ID set, use methods ``PopupManager/dismissPopup(_:popupManagerID:)-1atvy`` or ``SwiftUICore/View/dismissPopup(_:popupManagerID:)-55ubm`` + - important: To dismiss a popup with a custom ID set, use methods ``PopupStack/dismissPopup(_:popupStackID:)-1atvy`` or ``SwiftUICore/View/dismissPopup(_:popupStackID:)-55ubm`` - tip: Useful if you want to display several different popups of the same type. */ - func setCustomID(_ id: String) -> some Popup { AnyPopup(self).settingCustomID(id) } + @MainActor func setCustomID(_ id: String) async -> some Popup { await AnyPopup(self).updatedID(id) } /** Supplies an observable object to a popup's hierarchy. */ - func setEnvironmentObject(_ object: T) -> some Popup { AnyPopup(self).settingEnvironmentObject(object) } + @MainActor func setEnvironmentObject(_ object: T) async -> some Popup { await AnyPopup(self).updatedEnvironmentObject(object) } /** Dismisses the popup after a specified period of time. @@ -56,5 +56,5 @@ public extension Popup { - Parameters: - seconds: Time in seconds after which the popup will be closed. */ - func dismissAfter(_ seconds: Double) -> some Popup { AnyPopup(self).settingDismissTimer(seconds) } + @MainActor func dismissAfter(_ seconds: Double) async -> some Popup { await AnyPopup(self).updatedDismissTimer(seconds) } } diff --git a/Sources/Public/Setup/Public+Setup+Config.swift b/Sources/Public/Setup/Public+Setup+Config.swift index 4c1485233d..3bb73fc2ac 100644 --- a/Sources/Public/Setup/Public+Setup+Config.swift +++ b/Sources/Public/Setup/Public+Setup+Config.swift @@ -11,8 +11,8 @@ import SwiftUI -// MARK: Vertical & Centre -public extension GlobalConfig { +// MARK: Center +public extension GlobalConfigCenter { /** Distance of the entire popup (including its background) from the horizontal edges of the screen. @@ -56,8 +56,8 @@ public extension GlobalConfig { func tapOutsideToDismissPopup(_ value: Bool) -> Self { self.isTapOutsideToDismissEnabled = value; return self } } -// MARK: Only Vertical -public extension GlobalConfig.Vertical { +// MARK: Vertical +public extension GlobalConfigVertical { /** Distance of the entire popup (including its background) from the top edge of the screen. @@ -75,14 +75,38 @@ public extension GlobalConfig.Vertical { func popupBottomPadding(_ value: CGFloat) -> Self { self.popupPadding = .init(top: popupPadding.top, leading: popupPadding.leading, bottom: value, trailing: popupPadding.trailing); return self } /** - The drag progress value above which the popup will either be dismissed or move to the next drag detent value. + Distance of the entire popup (including its background) from the horizontal edges of the screen. ## Visualisation - ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/drag-threshold.png?raw=true) + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/horizontal-padding.png?raw=true) + */ + func popupHorizontalPadding(_ value: CGFloat) -> Self { self.popupPadding = .init(top: popupPadding.top, leading: value, bottom: popupPadding.bottom, trailing: value); return self } - - important: Drag progress is calculated as **dragTranslation** / **popupHeight**, therefore drag threshold value is expected to be between 0 and 1. + /** + Corner radius of the background of the active popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/corner-radius.png?raw=true) */ - func dragThreshold(_ value: CGFloat) -> Self { self.dragThreshold = value; return self } + func cornerRadius(_ value: CGFloat) -> Self { self.cornerRadius = value; return self } + + /** + Background color of the popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/background-color.png?raw=true) + */ + func backgroundColor(_ color: Color) -> Self { self.backgroundColor = color; return self } + + /** + The color of the overlay covering the view behind the popup. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/overlay-color.png?raw=true) + + - tip: Use .clear to hide the overlay. + */ + func overlayColor(_ color: Color) -> Self { self.overlayColor = color; return self } /** Indicates whether stacked popups should be visible in the view. @@ -92,6 +116,14 @@ public extension GlobalConfig.Vertical { */ func enableStacking(_ value: Bool) -> Self { self.isStackingEnabled = value; return self } + /** + If enabled, dismisses the active popup when touched outside its area. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/tap-to-close.png?raw=true) + */ + func tapOutsideToDismissPopup(_ value: Bool) -> Self { self.isTapOutsideToDismissEnabled = value; return self } + /** Determines whether it's possible to interact with popups using a drag gesture. @@ -99,4 +131,14 @@ public extension GlobalConfig.Vertical { ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/enable-drag-gesture.png?raw=true) */ func enableDragGesture(_ value: Bool) -> Self { self.isDragGestureEnabled = value; return self } + + /** + The drag progress value above which the popup will either be dismissed or move to the next drag detent value. + + ## Visualisation + ![image](https://github.com/Mijick/Assets/blob/main/Framework%20Docs/Popups/drag-threshold.png?raw=true) + + - important: Drag progress is calculated as **dragTranslation** / **popupHeight**, therefore drag threshold value is expected to be between 0 and 1. + */ + func dragThreshold(_ value: CGFloat) -> Self { self.dragThreshold = value; return self } } diff --git a/Sources/Public/Setup/Public+Setup+ConfigContainer.swift b/Sources/Public/Setup/Public+Setup+ConfigContainer.swift index cbf5d77940..a11773c152 100644 --- a/Sources/Public/Setup/Public+Setup+ConfigContainer.swift +++ b/Sources/Public/Setup/Public+Setup+ConfigContainer.swift @@ -11,16 +11,16 @@ public extension GlobalConfigContainer { /** - Default configuration for all centre popups. + Default configuration for all center popups. Use the ``Popup/configurePopup(config:)-98ha0`` method to change the configuration for a specific popup. See the list of available methods in ``GlobalConfig``. */ - func centre(_ builder: (GlobalConfig.Centre) -> GlobalConfig.Centre) -> Self { Self.centre = builder(.init()); return self } + nonisolated func center(_ builder: (GlobalConfigCenter) -> GlobalConfigCenter) -> Self { Self.center = builder(.init()); return self } /** Default configuration for all top and bottom popups. Use the ``Popup/configurePopup(config:)-98ha0`` method to change the configuration for a specific popup. See the list of available methods in ``GlobalConfig`` and ``GlobalConfig/Vertical``. */ - func vertical(_ builder: (GlobalConfig.Vertical) -> GlobalConfig.Vertical) -> Self { Self.vertical = builder(.init()); return self } + nonisolated func vertical(_ builder: (GlobalConfigVertical) -> GlobalConfigVertical) -> Self { Self.vertical = builder(.init()); return self } } diff --git a/Sources/Public/Setup/Popup+Setup+PopupManagerID.swift b/Sources/Public/Setup/Public+Setup+PopupStackID.swift similarity index 70% rename from Sources/Public/Setup/Popup+Setup+PopupManagerID.swift rename to Sources/Public/Setup/Public+Setup+PopupStackID.swift index e6ec71f860..ce41ba8758 100644 --- a/Sources/Public/Setup/Popup+Setup+PopupManagerID.swift +++ b/Sources/Public/Setup/Public+Setup+PopupStackID.swift @@ -1,5 +1,5 @@ // -// Popup+Setup+PopupManagerID.swift of MijickPopups +// Public+Setup+PopupStackID.swift of MijickPopups // // Created by Tomasz Kurylik. Sending ❤️ from Kraków! // - Mail: tomasz.kurylik@mijick.com @@ -12,7 +12,7 @@ /** A set of identifiers to be registered. - # Usage Example + ## Usage ```swift @main struct App_Main: App { var body: some Scene { @@ -25,22 +25,22 @@ } } - extension PopupManagerID { + extension PopupStackID { static let custom1: Self = .init(rawValue: "custom1") static let custom2: Self = .init(rawValue: "custom2") } ``` - - important: Use methods like ``SwiftUICore/View/dismissLastPopup(popupManagerID:)`` or ``Popup/present(popupManagerID:)`` only with a registered PopupManagerID. - - tip: The main use case where you might need to register a different PopupManagerID is when your application has multiple windows - for example, on macOS, iPad or visionOS. + - important: Use methods like ``SwiftUICore/View/dismissLastPopup(popupStackID:)`` or ``Popup/present(popupStackID:)`` only with a registered PopupStackID. + - tip: The main use case where you might need to register a different PopupStackID is when your application has multiple windows - for example, on macOS, iPad or visionOS. */ -public struct PopupManagerID: Equatable, Sendable { +public struct PopupStackID: Equatable, Sendable { let rawValue: String public init(rawValue: String) { self.rawValue = rawValue } } -// MARK: Default Instance -public extension PopupManagerID { +// MARK: Default ID +public extension PopupStackID { static let shared: Self = .init(rawValue: "shared") } diff --git a/Sources/Public/Setup/Public+Setup+SceneDelegate.swift b/Sources/Public/Setup/Public+Setup+SceneDelegate.swift index 8ab5a7db80..3e391b15f7 100644 --- a/Sources/Public/Setup/Public+Setup+SceneDelegate.swift +++ b/Sources/Public/Setup/Public+Setup+SceneDelegate.swift @@ -15,9 +15,9 @@ import SwiftUI /** Registers the framework to work in your application. Works on iOS only. - - tip: Recommended initialisation way when using the framework with standard Apple sheets. + - tip: Recommended initialization way when using the framework with standard Apple sheets. - ## Usage Example + ## Usage ```swift @main struct App_Main: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @@ -41,7 +41,7 @@ import SwiftUI .tapOutsideToDismissPopup(true) .cornerRadius(32) } - .centre { $0 + .center { $0 .tapOutsideToDismissPopup(false) .backgroundColor(.white) } diff --git a/Sources/Public/Setup/Public+Setup+View.swift b/Sources/Public/Setup/Public+Setup+View.swift index b4e01dfe8a..676d277a4d 100644 --- a/Sources/Public/Setup/Public+Setup+View.swift +++ b/Sources/Public/Setup/Public+Setup+View.swift @@ -16,11 +16,11 @@ public extension View { Registers the framework to work in your application. - Parameters: - - id: It is possible to register multiple managers (for different windows); especially useful in a macOS or iPad implementation. Read more in ``PopupManagerID``. + - id: It is possible to register multiple stacks (for different windows); especially useful in a macOS or iPad implementation. Read more in ``PopupStackID``. - configBuilder: Default configuration for all popups. Use the ``Popup/configurePopup(config:)-98ha0`` method to change the configuration for a specific popup. See the list of available methods in ``GlobalConfig``. - ## Usage Example + ## Usage ```swift @main struct App_Main: App { var body: some Scene { WindowGroup { @@ -31,7 +31,7 @@ public extension View { .tapOutsideToDismissPopup(true) .cornerRadius(32) } - .centre { $0 + .center { $0 .tapOutsideToDismissPopup(false) .backgroundColor(.white) } @@ -42,13 +42,13 @@ public extension View { - seealso: It's also possible to register the framework with ``PopupSceneDelegate``; useful if you want to use the library with Apple's default sheets. */ - func registerPopups(id: PopupManagerID = .shared, configBuilder: @escaping (GlobalConfigContainer) -> GlobalConfigContainer = { $0 }) -> some View { + func registerPopups(id: PopupStackID = .shared, configBuilder: @escaping (GlobalConfigContainer) -> GlobalConfigContainer = { $0 }) -> some View { #if os(tvOS) - PopupView(rootView: self, popupManager: .registerInstance(id: id)).onAppear { _ = configBuilder(.init()) } + PopupView(rootView: self, stack: .registerStack(id: id)).onAppear { _ = configBuilder(.init()) } #else self .frame(maxWidth: .infinity, maxHeight: .infinity) - .overlay(PopupView(popupManager: .registerInstance(id: id)), alignment: .top) + .overlay(PopupView(stack: .registerStack(id: id)), alignment: .top) .onAppear { _ = configBuilder(.init()) } #endif } diff --git a/Tests/Tests+PopupID.swift b/Tests/Tests+PopupID.swift index 585ec52b99..c30460e79f 100644 --- a/Tests/Tests+PopupID.swift +++ b/Tests/Tests+PopupID.swift @@ -23,90 +23,97 @@ import SwiftUI // MARK: Create ID extension PopupIDTests { - func test_createPopupID_1() { + func test_createPopupID_1() async { let dateString = String(describing: Date()) - let popupID = PopupID.create(from: TestTopPopup.self) + let popupID = await PopupID(TestTopPopup.self) let idComponents = popupID.rawValue.components(separatedBy: "/{}/") XCTAssertEqual(idComponents.count, 2) XCTAssertEqual(idComponents[0], "TestTopPopup") XCTAssertEqual(idComponents[1], dateString) } - func test_createPopupID_2() { + func test_createPopupID_2() async { let dateString = String(describing: Date()) - let popupID = PopupID.create(from: TestCentrePopup.self) + let popupID = await PopupID(TestCenterPopup.self) let idComponents = popupID.rawValue.components(separatedBy: "/{}/") XCTAssertEqual(idComponents.count, 2) - XCTAssertEqual(idComponents[0], "TestCentrePopup") + XCTAssertEqual(idComponents[0], "TestCenterPopup") XCTAssertEqual(idComponents[1], dateString) } } // MARK: Is Same Type extension PopupIDTests { - func test_isSameType_1() { - let popupID1 = PopupID.create(from: TestTopPopup.self), - popupID2 = PopupID.create(from: TestBottomPopup.self) + func test_isSameType_1() async { + let popupID1 = await PopupID(TestTopPopup.self), + popupID2 = await PopupID(TestBottomPopup.self) let result = popupID1.isSameType(as: popupID2) XCTAssertEqual(result, false) } - func test_isSameType_2() { - let popupID1 = PopupID.create(from: TestTopPopup.self), + func test_isSameType_2() async { + let popupID1 = await PopupID(TestTopPopup.self), popupID2 = "TestTopPopup" let result = popupID1.isSameType(as: popupID2) XCTAssertEqual(result, true) } - func test_isSameType_3() { - let popupID1 = PopupID.create(from: "2137"), + func test_isSameType_3() async { + let popupID1 = await PopupID("2137"), popupID2 = "2137" let result = popupID1.isSameType(as: popupID2) XCTAssertEqual(result, true) } - func test_isSameType_4() { - let popupID1 = AnyPopup(TestTopPopup().setCustomID("2137")).id, - popupID2 = AnyPopup(TestTopPopup()).id + func test_isSameType_4() async { + let popupID1 = await AnyPopup(TestTopPopup().setCustomID("2137")).id, + popupID2 = "2137" let result = popupID1.isSameType(as: popupID2) - XCTAssertEqual(result, false) + XCTAssertEqual(result, true) } func test_isSameType_5() async { - let popupID1 = PopupID.create(from: TestTopPopup.self) + let popupID1 = await AnyPopup(TestTopPopup().setCustomID("2137")).id, + popupID2 = await AnyPopup(TestTopPopup()).id + + let result = popupID1.isSameType(as: popupID2) + XCTAssertEqual(result, false) + } + func test_isSameType_6() async { + let popupID1 = await PopupID(TestTopPopup.self) await Task.sleep(seconds: 1) - let popupID2 = PopupID.create(from: TestTopPopup.self) + let popupID2 = await PopupID(TestTopPopup.self) let result = popupID1.isSameType(as: popupID2) XCTAssertEqual(result, true) } } -// MARK: Is Same Instance +// MARK: Is Same extension PopupIDTests { - func test_isSameInstance_1() { - let popupID = PopupID.create(from: TestTopPopup.self), - popup = AnyPopup(TestCentrePopup()) + func test_isSame_1() async { + let popupID = await PopupID(TestTopPopup.self), + popup = await AnyPopup(TestCenterPopup()) - let result = popupID.isSameInstance(as: popup) + let result = popupID.isSame(as: popup) XCTAssertEqual(result, false) } - func test_isSameInstance_2() { - let popupID = PopupID.create(from: TestTopPopup.self), - popup = AnyPopup(TestTopPopup()) + func test_isSame_2() async { + let popupID = await PopupID(TestTopPopup.self), + popup = await AnyPopup(TestTopPopup()) - let result = popupID.isSameInstance(as: popup) + let result = popupID.isSame(as: popup) XCTAssertEqual(result, true) } - func test_isSameInstance_3() async { - let popupID = PopupID.create(from: TestTopPopup.self) + func test_isSame_3() async { + let popupID = await PopupID(TestTopPopup.self) await Task.sleep(seconds: 1) - let popup = AnyPopup(TestTopPopup()) + let popup = await AnyPopup(TestTopPopup()) - let result = popupID.isSameInstance(as: popup) + let result = popupID.isSame(as: popup) XCTAssertEqual(result, false) } } @@ -121,7 +128,7 @@ extension PopupIDTests { private struct TestTopPopup: TopPopup { var body: some View { EmptyView() } } -private struct TestCentrePopup: CentrePopup { +private struct TestCenterPopup: CenterPopup { var body: some View { EmptyView() } } private struct TestBottomPopup: BottomPopup { diff --git a/Tests/Tests+PopupManager.swift b/Tests/Tests+PopupManager.swift deleted file mode 100644 index f172a83983..0000000000 --- a/Tests/Tests+PopupManager.swift +++ /dev/null @@ -1,294 +0,0 @@ -// -// Tests+PopupManager.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import XCTest -import SwiftUI -@testable import MijickPopups - -@MainActor final class PopupManagerTests: XCTestCase { - override func setUp() async throws { - PopupManagerContainer.clean() - } -} - - - -// MARK: - TEST CASES - - - -// MARK: Register New Instance -extension PopupManagerTests { - func test_registerNewInstance_withNoInstancesToRegister() { - let popupManagerIds: [PopupManagerID] = [] - - registerNewInstances(popupManagerIds: popupManagerIds) - XCTAssertEqual(popupManagerIds, getRegisteredInstances()) - } - func test_registerNewInstance_withUniqueInstancesToRegister() { - let popupManagerIds: [PopupManagerID] = [ - .staremiasto, - .grzegorzki, - .krowodrza, - .bronowice - ] - - registerNewInstances(popupManagerIds: popupManagerIds) - XCTAssertEqual(popupManagerIds, getRegisteredInstances()) - } - func test_registerNewInstance_withRepeatingInstancesToRegister() { - let popupManagerIds: [PopupManagerID] = [ - .staremiasto, - .grzegorzki, - .krowodrza, - .bronowice, - .bronowice, - .pradnikbialy, - .pradnikczerwony, - .krowodrza - ] - - registerNewInstances(popupManagerIds: popupManagerIds) - XCTAssertNotEqual(popupManagerIds, getRegisteredInstances()) - XCTAssertEqual(getRegisteredInstances().count, 6) - } -} -private extension PopupManagerTests { - func registerNewInstances(popupManagerIds: [PopupManagerID]) { - popupManagerIds.forEach { _ = PopupManager.registerInstance(id: $0) } - } - func getRegisteredInstances() -> [PopupManagerID] { - PopupManagerContainer.instances.map(\.id) - } -} - -// MARK: Get Instance -extension PopupManagerTests { - func test_getInstance_whenNoInstancesAreRegistered() { - let managerInstance = PopupManager.fetchInstance(id: .bronowice) - XCTAssertNil(managerInstance) - } - func test_getInstance_whenInstanceIsNotRegistered() { - registerNewInstances(popupManagerIds: [ - .krowodrza, - .staremiasto, - .pradnikczerwony, - .pradnikbialy, - .grzegorzki - ]) - - let managerInstance = PopupManager.fetchInstance(id: .bronowice) - XCTAssertNil(managerInstance) - } - func test_getInstance_whenInstanceIsRegistered() { - registerNewInstances(popupManagerIds: [ - .krowodrza, - .staremiasto, - .grzegorzki - ]) - - let managerInstance = PopupManager.fetchInstance(id: .grzegorzki) - XCTAssertNotNil(managerInstance) - } -} - -// MARK: Present Popup -extension PopupManagerTests { - func test_presentPopup_withThreePopupsToBePresented() { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()) - ]) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 3) - } - func test_presentPopup_withPopupsWithSameID() { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(id: "2137", config: .init()), - AnyPopup.t_createNew(id: "2137", config: .init()), - AnyPopup.t_createNew(id: "2331", config: .init()) - ]) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 2) - } - func test_presentPopup_withCustomID() { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(id: "2137", config: .init()).setCustomID("1"), - AnyPopup.t_createNew(id: "2137", config: .init()), - AnyPopup.t_createNew(id: "2137", config: .init()).setCustomID("3") - ]) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 3) - } - func test_presentPopup_withDismissAfter() async { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(config: .init()).dismissAfter(0.7), - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()).dismissAfter(1.5) - ]) - - let popupsOnStack1 = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack1.count, 3) - - await Task.sleep(seconds: 1) - - let popupsOnStack2 = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack2.count, 2) - - await Task.sleep(seconds: 1) - - let popupsOnStack3 = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack3.count, 1) - } -} - -// MARK: Dismiss Popup -extension PopupManagerTests { - func test_dismissLastPopup_withNoPopupsOnStack() { - registerNewInstanceAndPresentPopups(popups: []) - PopupManager.dismissLastPopup(popupManagerID: defaultPopupManagerID) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 0) - } - func test_dismissLastPopup_withThreePopupsOnStack() { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()) - ]) - PopupManager.dismissLastPopup(popupManagerID: defaultPopupManagerID) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 2) - } - func test_dismissPopupWithType_whenPopupOnStack() { - let popups: [AnyPopup] = [ - .init(TestTopPopup()), - .init(TestCentrePopup()), - .init(TestBottomPopup()) - ] - registerNewInstanceAndPresentPopups(popups: popups) - - let popupsOnStackBefore = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackBefore) - - PopupManager.dismissPopup(TestBottomPopup.self, popupManagerID: defaultPopupManagerID) - - let popupsOnStackAfter = getPopupsForActiveInstance() - XCTAssertEqual([popups[0], popups[1]], popupsOnStackAfter) - } - func test_dismissPopupWithType_whenPopupNotOnStack() { - let popups: [AnyPopup] = [ - .init(TestTopPopup()), - .init(TestBottomPopup()) - ] - registerNewInstanceAndPresentPopups(popups: popups) - - let popupsOnStackBefore = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackBefore) - - PopupManager.dismissPopup(TestCentrePopup.self, popupManagerID: defaultPopupManagerID) - - let popupsOnStackAfter = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackAfter) - } - func test_dismissPopupWithType_whenPopupHasCustomID() { - let popups: [AnyPopup] = [ - .init(TestTopPopup().setCustomID("2137")), - .init(TestBottomPopup()) - ] - registerNewInstanceAndPresentPopups(popups: popups) - - let popupsOnStackBefore = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackBefore) - - PopupManager.dismissPopup(TestTopPopup.self, popupManagerID: defaultPopupManagerID) - - let popupsOnStackAfter = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackAfter) - } - func test_dismissPopupWithID_whenPopupHasCustomID() { - let popups: [AnyPopup] = [ - .init(TestTopPopup().setCustomID("2137")), - .init(TestBottomPopup()) - ] - registerNewInstanceAndPresentPopups(popups: popups) - - let popupsOnStackBefore = getPopupsForActiveInstance() - XCTAssertEqual(popups, popupsOnStackBefore) - - PopupManager.dismissPopup("2137", popupManagerID: defaultPopupManagerID) - - let popupsOnStackAfter = getPopupsForActiveInstance() - XCTAssertEqual([popups[1]], popupsOnStackAfter) - } - func test_dismissAllPopups() { - registerNewInstanceAndPresentPopups(popups: [ - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()), - AnyPopup.t_createNew(config: .init()) - ]) - PopupManager.dismissAllPopups(popupManagerID: defaultPopupManagerID) - - let popupsOnStack = getPopupsForActiveInstance() - XCTAssertEqual(popupsOnStack.count, 0) - } -} - - - -// MARK: - HELPERS - - - -// MARK: Methods -private extension PopupManagerTests { - func registerNewInstanceAndPresentPopups(popups: [any Popup]) { - registerNewInstances(popupManagerIds: [defaultPopupManagerID]) - popups.forEach { $0.present(popupManagerID: defaultPopupManagerID) } - } - func getPopupsForActiveInstance() -> [AnyPopup] { - PopupManager - .fetchInstance(id: defaultPopupManagerID)? - .stack ?? [] - } -} - -// MARK: Variables -private extension PopupManagerTests { - var defaultPopupManagerID: PopupManagerID { .staremiasto } -} - -// MARK: Popup Manager Identifiers -private extension PopupManagerID { - static let staremiasto: Self = .init(rawValue: "staremiasto") - static let grzegorzki: Self = .init(rawValue: "grzegorzki") - static let pradnikczerwony: Self = .init(rawValue: "pradnikczerwony") - static let pradnikbialy: Self = .init(rawValue: "pradnikbialy") - static let krowodrza: Self = .init(rawValue: "krowodrza") - static let bronowice: Self = .init(rawValue: "bronowice") -} - -// MARK: Test Popups -private struct TestTopPopup: TopPopup { - var body: some View { EmptyView() } -} -private struct TestCentrePopup: CentrePopup { - var body: some View { EmptyView() } -} -private struct TestBottomPopup: BottomPopup { - var body: some View { EmptyView() } -} diff --git a/Tests/Tests+PopupStack.swift b/Tests/Tests+PopupStack.swift new file mode 100644 index 0000000000..148bae864d --- /dev/null +++ b/Tests/Tests+PopupStack.swift @@ -0,0 +1,304 @@ +// +// Tests+PopupStack.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import XCTest +import SwiftUI +@testable import MijickPopups + +@MainActor final class PopupStackTests: XCTestCase { + override func setUp() async throws { + PopupStackContainer.clean() + } +} + + + +// MARK: - TEST CASES + + + +// MARK: Register +extension PopupStackTests { + func test_registerStack_withNoStacksToRegister() { + let popupStacksIDs: [PopupStackID] = [] + + registerStacks(ids: popupStacksIDs) + XCTAssertEqual(popupStacksIDs, getRegisteredStacks()) + } + func test_registerStack_withUniqueStacksToRegister() { + let popupStacksIDs: [PopupStackID] = [ + .staremiasto, + .grzegorzki, + .krowodrza, + .bronowice + ] + + registerStacks(ids: popupStacksIDs) + XCTAssertEqual(popupStacksIDs, getRegisteredStacks()) + } + func test_registerStack_withRepeatingStacksToRegister() { + let popupStacksIDs: [PopupStackID] = [ + .staremiasto, + .grzegorzki, + .krowodrza, + .bronowice, + .bronowice, + .pradnikbialy, + .pradnikczerwony, + .krowodrza + ] + + registerStacks(ids: popupStacksIDs) + XCTAssertNotEqual(popupStacksIDs, getRegisteredStacks()) + XCTAssertEqual(getRegisteredStacks().count, 6) + } +} + +// MARK: Fetch +extension PopupStackTests { + func test_fetchStack_whenNoStacksAreRegistered() async { + let stack = await PopupStack.fetch(id: .bronowice) + XCTAssertNil(stack) + } + func test_fetchStack_whenStackIsNotRegistered() async { + registerStacks(ids: [ + .krowodrza, + .staremiasto, + .pradnikczerwony, + .pradnikbialy, + .grzegorzki + ]) + + let stack = await PopupStack.fetch(id: .bronowice) + XCTAssertNil(stack) + } + func test_fetchStack_whenStackIsRegistered() async { + registerStacks(ids: [ + .krowodrza, + .staremiasto, + .grzegorzki + ]) + + let stack = await PopupStack.fetch(id: .grzegorzki) + XCTAssertNotNil(stack) + } +} + +// MARK: Present Popup +extension PopupStackTests { + func test_presentPopup_withThreePopupsToBePresented() async { + await registerNewStackAndPresent(popups: [ + await AnyPopup(TestBottomPopup1()), + await AnyPopup(TestBottomPopup2()), + await AnyPopup(TestBottomPopup3()) + ]) + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 3) + } + func test_presentPopup_withPopupsWithSameID() async { + await registerNewStackAndPresent(popups: [ + await AnyPopup(TestBottomPopup1()), + await AnyPopup(TestBottomPopup1()), + await AnyPopup(TestBottomPopup1()), + ]) + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 1) + } + func test_presentPopup_withCustomID() async { + await registerNewStackAndPresent(popups: [ + await AnyPopup(TestBottomPopup1()), + await AnyPopup(TestBottomPopup1().setCustomID("2137")), + await AnyPopup(TestBottomPopup1().setCustomID("I Pan Paweł oczywiście")), + ]) + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 3) + } + func test_presentPopup_withDismissAfter() async { + await registerNewStackAndPresent(popups: [ + await AnyPopup(TestBottomPopup1()), + await AnyPopup(TestBottomPopup2()).dismissAfter(0.7), + await AnyPopup(TestBottomPopup3()).dismissAfter(1.5), + ]) + + let popupsOnStack1 = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack1.count, 3) + + await Task.sleep(seconds: 1) + + let popupsOnStack2 = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack2.count, 2) + + await Task.sleep(seconds: 1) + + let popupsOnStack3 = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack3.count, 1) + } +} + +// MARK: Dismiss Popup +extension PopupStackTests { + func test_dismissLastPopup_withNoPopupsOnStack() async { + await registerNewStackAndPresent(popups: []) + await performAction { await PopupStack.dismissLastPopup(popupStackID: defaultPopupStackID) } + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 0) + } + func test_dismissLastPopup_withThreePopupsOnStack() async { + await registerNewStackAndPresent(popups: [ + AnyPopup(TestBottomPopup1()), + AnyPopup(TestBottomPopup2()), + AnyPopup(TestBottomPopup3()) + ]) + await performAction { await PopupStack.dismissLastPopup(popupStackID: defaultPopupStackID) } + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 2) + } + func test_dismissPopupWithType_whenPopupOnStack() async { + let popups: [AnyPopup] = await [ + .init(TestTopPopup()), + .init(TestCenterPopup()), + .init(TestBottomPopup1()) + ] + await registerNewStackAndPresent(popups: popups) + + let popupsOnStackBefore = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackBefore) + + await performAction { await PopupStack.dismissPopup(TestBottomPopup1.self, popupStackID: defaultPopupStackID) } + + let popupsOnStackAfter = await getPopupsForDefaultStack() + XCTAssertEqual([popups[0], popups[1]], popupsOnStackAfter) + } + func test_dismissPopupWithType_whenPopupNotOnStack() async { + let popups: [AnyPopup] = await [ + .init(TestTopPopup()), + .init(TestBottomPopup1()) + ] + await registerNewStackAndPresent(popups: popups) + + let popupsOnStackBefore = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackBefore) + + await performAction { await PopupStack.dismissPopup(TestCenterPopup.self, popupStackID: defaultPopupStackID) } + + let popupsOnStackAfter = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackAfter) + } + func test_dismissPopupWithType_whenPopupHasCustomID() async { + let popups: [AnyPopup] = await [ + .init(TestTopPopup().setCustomID("2137")), + .init(TestBottomPopup1()) + ] + await registerNewStackAndPresent(popups: popups) + + let popupsOnStackBefore = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackBefore) + + await performAction { await PopupStack.dismissPopup(TestTopPopup.self, popupStackID: defaultPopupStackID) } + + let popupsOnStackAfter = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackAfter) + } + func test_dismissPopupWithID_whenPopupHasCustomID() async { + let popups: [AnyPopup] = await [ + .init(TestTopPopup().setCustomID("2137")), + .init(TestBottomPopup1()) + ] + await registerNewStackAndPresent(popups: popups) + + let popupsOnStackBefore = await getPopupsForDefaultStack() + XCTAssertEqual(popups, popupsOnStackBefore) + + await performAction { await PopupStack.dismissPopup("2137", popupStackID: defaultPopupStackID) } + + let popupsOnStackAfter = await getPopupsForDefaultStack() + XCTAssertEqual([popups[1]], popupsOnStackAfter) + } + func test_dismissAllPopups() async { + await registerNewStackAndPresent(popups: [ + AnyPopup(TestBottomPopup1()), + AnyPopup(TestBottomPopup2()), + AnyPopup(TestBottomPopup3()) + ]) + await performAction { await PopupStack.dismissAllPopups(popupStackID: defaultPopupStackID) } + + let popupsOnStack = await getPopupsForDefaultStack() + XCTAssertEqual(popupsOnStack.count, 0) + } +} + + + +// MARK: - HELPERS + + + +// MARK: Methods +private extension PopupStackTests { + func registerNewStackAndPresent(popups: [any Popup]) async { + registerStacks(ids: [defaultPopupStackID]) + for popup in popups { await performAction { await popup.present(popupStackID: defaultPopupStackID) } } + } + func performAction(_ action: () async -> ()) async { + await action() + await Task.sleep(seconds: 0.06) + } + func getPopupsForDefaultStack() async -> [AnyPopup] { + await PopupStack + .fetch(id: defaultPopupStackID)? + .popups ?? [] + } +} +private extension PopupStackTests { + func registerStacks(ids: [PopupStackID]) { + ids.forEach { _ = PopupStack.registerStack(id: $0) } + } + func getRegisteredStacks() -> [PopupStackID] { + PopupStackContainer.stacks.map(\.id) + } +} + +// MARK: Variables +private extension PopupStackTests { + var defaultPopupStackID: PopupStackID { .staremiasto } +} + +// MARK: Popup Stack Identifiers +private extension PopupStackID { + static let staremiasto: Self = .init(rawValue: "staremiasto") + static let grzegorzki: Self = .init(rawValue: "grzegorzki") + static let pradnikczerwony: Self = .init(rawValue: "pradnikczerwony") + static let pradnikbialy: Self = .init(rawValue: "pradnikbialy") + static let krowodrza: Self = .init(rawValue: "krowodrza") + static let bronowice: Self = .init(rawValue: "bronowice") +} + +// MARK: Test Popups +private struct TestTopPopup: TopPopup { + var body: some View { EmptyView() } +} +private struct TestCenterPopup: CenterPopup { + var body: some View { EmptyView() } +} +private struct TestBottomPopup1: BottomPopup { + var body: some View { EmptyView() } +} +private struct TestBottomPopup2: BottomPopup { + var body: some View { EmptyView() } +} +private struct TestBottomPopup3: BottomPopup { + var body: some View { EmptyView() } +} diff --git a/Tests/Tests+ViewModel+PopupCenterStack.swift b/Tests/Tests+ViewModel+PopupCenterStack.swift new file mode 100644 index 0000000000..47887c6aaf --- /dev/null +++ b/Tests/Tests+ViewModel+PopupCenterStack.swift @@ -0,0 +1,283 @@ +// +// Tests+ViewModel+PopupCenterStack.swift of MijickPopups +// +// Created by Tomasz Kurylik. Sending ❤️ from Kraków! +// - Mail: tomasz.kurylik@mijick.com +// - GitHub: https://github.com/FulcrumOne +// - Medium: https://medium.com/@mijick +// +// Copyright ©2024 Mijick. All rights reserved. + + +import XCTest +import SwiftUI +@testable import MijickPopups + +@MainActor final class PopupCenterStackViewModelTests: XCTestCase { + @ObservedObject private var viewModel: ViewModel = .init(CenterPopupConfig.self) + + override func setUp() async throws { + await viewModel.updateScreen(screenHeight: screen.height, screenSafeArea: screen.safeArea) + viewModel.setup(updatePopupAction: { [self] in await updatePopupAction(viewModel, $0) }, closePopupAction: { [self] in await closePopupAction(viewModel, $0) }) + } +} +private extension PopupCenterStackViewModelTests { + func updatePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) async { if let index = viewModel.popups.firstIndex(of: popup) { + var popups = viewModel.popups + popups[index] = popup + + await viewModel.updatePopups(popups) + }} + func closePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) async { if let index = viewModel.popups.firstIndex(of: popup) { + var popups = viewModel.popups + popups.remove(at: index) + + await viewModel.updatePopups(popups) + }} +} + + + +// MARK: - TEST CASES + + + +// MARK: Outer Padding +extension PopupCenterStackViewModelTests { + func test_calculateOuterPadding_withKeyboardHidden_whenCustomPaddingNotSet() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72), + createPopupInstanceForPopupHeightTests(popupHeight: 400) + ] + + await appendPopupsAndCheckOuterHorizontalPadding( + popups: popups, + isKeyboardActive: false, + expectedValue: 0 + ) + } + func test_calculateOuterPadding_withKeyboardHidden_whenCustomPaddingSet() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72, horizontalPadding: 11), + createPopupInstanceForPopupHeightTests(popupHeight: 400, horizontalPadding: 16) + ] + + await appendPopupsAndCheckOuterHorizontalPadding( + popups: popups, + isKeyboardActive: false, + expectedValue: 16 + ) + } + func test_calculateOuterPadding_withKeyboardShown_whenKeyboardNotOverlapingPopup() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72, horizontalPadding: 11), + createPopupInstanceForPopupHeightTests(popupHeight: 400, horizontalPadding: 16) + ] + + await appendPopupsAndCheckOuterHorizontalPadding( + popups: popups, + isKeyboardActive: true, + expectedValue: 16 + ) + } + func test_calculateOuterPadding_withKeyboardShown_whenKeyboardOverlapingPopup() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72, horizontalPadding: 11), + createPopupInstanceForPopupHeightTests(popupHeight: 1000, horizontalPadding: 16) + ] + + await appendPopupsAndCheckOuterHorizontalPadding( + popups: popups, + isKeyboardActive: true, + expectedValue: 16 + ) + } +} +private extension PopupCenterStackViewModelTests { + func appendPopupsAndCheckOuterHorizontalPadding(popups: [AnyPopup], isKeyboardActive: Bool, expectedValue: CGFloat) async { + await appendPopupsAndPerformChecks( + popups: popups, + isKeyboardActive: isKeyboardActive, + calculatedValue: { await $0.calculateActivePopupOuterPadding().leading }, + expectedValueBuilder: { _ in expectedValue } + ) + } +} + +// MARK: Corners +extension PopupCenterStackViewModelTests { + func test_calculateCorners_withCornerRadiusZero() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 20), + createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 0), + ] + + await appendPopupsAndCheckCorners( + popups: popups, + expectedValue: [.top: 0, .bottom: 0] + ) + } + func test_calculateCorners_withCornerRadiusNonZero() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 20), + createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 24), + ] + + await appendPopupsAndCheckCorners( + popups: popups, + expectedValue: [.top: 24, .bottom: 24] + ) + } +} +private extension PopupCenterStackViewModelTests { + func appendPopupsAndCheckCorners(popups: [AnyPopup], expectedValue: [MijickPopups.PopupAlignment: CGFloat]) async { + await appendPopupsAndPerformChecks( + popups: popups, + isKeyboardActive: false, + calculatedValue: { await $0.calculateActivePopupCorners() }, + expectedValueBuilder: { _ in expectedValue } + ) + } +} + +// MARK: Opacity +extension PopupCenterStackViewModelTests { + func test_calculatePopupOpacity_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72), + createPopupInstanceForPopupHeightTests(popupHeight: 400) + ] + + await appendPopupsAndCheckOpacity( + popups: popups, + calculateForIndex: 1, + expectedValue: 0 + ) + } + func test_calculatePopupOpacity_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72), + createPopupInstanceForPopupHeightTests(popupHeight: 400) + ] + + await appendPopupsAndCheckOpacity( + popups: popups, + calculateForIndex: 2, + expectedValue: 1 + ) + } +} +private extension PopupCenterStackViewModelTests { + func appendPopupsAndCheckOpacity(popups: [AnyPopup], calculateForIndex index: Int, expectedValue: CGFloat) async { + await appendPopupsAndPerformChecks( + popups: popups, + isKeyboardActive: false, + calculatedValue: { [self] in $0.calculateOpacity(for: viewModel.popups[index]) }, + expectedValueBuilder: { _ in expectedValue } + ) + } +} + +// MARK: Vertical Fixed Size +extension PopupCenterStackViewModelTests { + func test_calculateVerticalFixedSize_withHeightSmallerThanScreen() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 913), + createPopupInstanceForPopupHeightTests(popupHeight: 400) + ] + + await appendPopupsAndCheckVerticalFixedSize( + popups: popups, + expectedValue: true + ) + } + func test_calculateVerticalFixedSize_withHeightLargerThanScreen() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(popupHeight: 350), + createPopupInstanceForPopupHeightTests(popupHeight: 72), + createPopupInstanceForPopupHeightTests(popupHeight: 913) + ] + + await appendPopupsAndCheckVerticalFixedSize( + popups: popups, + expectedValue: false + ) + } +} +private extension PopupCenterStackViewModelTests { + func appendPopupsAndCheckVerticalFixedSize(popups: [AnyPopup], expectedValue: Bool) async { + await appendPopupsAndPerformChecks( + popups: popups, + isKeyboardActive: false, + calculatedValue: { await $0.calculateActivePopupVerticalFixedSize() }, + expectedValueBuilder: { _ in expectedValue } + ) + } +} + + + +// MARK: - HELPERS + + + +// MARK: Methods +private extension PopupCenterStackViewModelTests { + func createPopupInstanceForPopupHeightTests(popupHeight: CGFloat, horizontalPadding: CGFloat = 0, cornerRadius: CGFloat = 0) async -> AnyPopup { + let popup = await TestPopup(horizontalPadding: horizontalPadding, cornerRadius: cornerRadius).setCustomID(UUID().uuidString) + return await AnyPopup(popup).updatedHeight(popupHeight) + } + func appendPopupsAndPerformChecks(popups: [AnyPopup], isKeyboardActive: Bool, calculatedValue: @escaping (ViewModel) async -> Value, expectedValueBuilder: @escaping (ViewModel) async -> Value) async { + await viewModel.updatePopups(popups) + await updatePopups() + await viewModel.updateScreen(screenHeight: isKeyboardActive ? screenWithKeyboard.height : screen.height, screenSafeArea: isKeyboardActive ? screenWithKeyboard.safeArea : screen.safeArea, isKeyboardActive: isKeyboardActive) + + let calculatedValue = await calculatedValue(viewModel) + let expectedValue = await expectedValueBuilder(viewModel) + XCTAssertEqual(calculatedValue, expectedValue) + } +} +private extension PopupCenterStackViewModelTests { + func updatePopups() async { + for popup in viewModel.popups { await viewModel.updatePopupHeight(popup.height!, popup) } + } +} + +// MARK: Screen +private extension PopupCenterStackViewModelTests { + var screen: Screen { .init( + height: 1000, + safeArea: .init(top: 100, leading: 20, bottom: 50, trailing: 30) + )} + var screenWithKeyboard: Screen { .init( + height: 1000, + safeArea: .init(top: 100, leading: 20, bottom: 200, trailing: 30) + )} +} + +// MARK: Typealiases +private extension PopupCenterStackViewModelTests { + typealias ViewModel = VM.CenterStack +} + +// MARK: Test Popup +private struct TestPopup: CenterPopup { + let horizontalPadding: CGFloat + let cornerRadius: CGFloat + + func configurePopup(config: LocalConfigCenter) -> LocalConfigCenter { config + .popupHorizontalPadding(horizontalPadding) + .cornerRadius(cornerRadius) + } + var body: some View { EmptyView() } +} + +// MARK: Others +extension VM.CenterStack: @unchecked Sendable {} diff --git a/Tests/Tests+ViewModel+PopupCentreStack.swift b/Tests/Tests+ViewModel+PopupCentreStack.swift deleted file mode 100644 index 4ddbc39b32..0000000000 --- a/Tests/Tests+ViewModel+PopupCentreStack.swift +++ /dev/null @@ -1,275 +0,0 @@ -// -// Tests+ViewModel+PopupCentreStack.swift of MijickPopups -// -// Created by Tomasz Kurylik. Sending ❤️ from Kraków! -// - Mail: tomasz.kurylik@mijick.com -// - GitHub: https://github.com/FulcrumOne -// - Medium: https://medium.com/@mijick -// -// Copyright ©2024 Mijick. All rights reserved. - - -import XCTest -import SwiftUI -@testable import MijickPopups - -@MainActor final class PopupCentreStackViewModelTests: XCTestCase { - @ObservedObject private var viewModel: ViewModel = .init() - - override func setUp() async throws { - viewModel.t_updateScreenValue(screen) - viewModel.t_setup(updatePopupAction: { [self] in updatePopupAction(viewModel, $0) }, closePopupAction: { [self] in closePopupAction(viewModel, $0) }) - } -} -private extension PopupCentreStackViewModelTests { - func updatePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) { if let index = viewModel.t_popups.firstIndex(of: popup) { - var popups = viewModel.t_popups - popups[index] = popup - - viewModel.t_updatePopupsValue(popups) - viewModel.t_calculateAndUpdateActivePopupHeight() - }} - func closePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) { if let index = viewModel.t_popups.firstIndex(of: popup) { - var popups = viewModel.t_popups - popups.remove(at: index) - - viewModel.t_updatePopupsValue(popups) - }} -} - - - -// MARK: - TEST CASES - - - -// MARK: Popup Padding -extension PopupCentreStackViewModelTests { - func test_calculatePopupPadding_withKeyboardHidden_whenCustomPaddingNotSet() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72), - createPopupInstanceForPopupHeightTests(popupHeight: 400) - ] - - appendPopupsAndCheckPopupPadding( - popups: popups, - isKeyboardActive: false, - expectedValue: .init() - ) - } - func test_calculatePopupPadding_withKeyboardHidden_whenCustomPaddingSet() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72, popupPadding: .init(top: 0, leading: 11, bottom: 0, trailing: 11)), - createPopupInstanceForPopupHeightTests(popupHeight: 400, popupPadding: .init(top: 0, leading: 16, bottom: 0, trailing: 16)) - ] - - appendPopupsAndCheckPopupPadding( - popups: popups, - isKeyboardActive: false, - expectedValue: .init(top: 0, leading: 16, bottom: 0, trailing: 16) - ) - } - func test_calculatePopupPadding_withKeyboardShown_whenKeyboardNotOverlapingPopup() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72, popupPadding: .init(top: 0, leading: 11, bottom: 0, trailing: 11)), - createPopupInstanceForPopupHeightTests(popupHeight: 400, popupPadding: .init(top: 0, leading: 16, bottom: 0, trailing: 16)) - ] - - appendPopupsAndCheckPopupPadding( - popups: popups, - isKeyboardActive: true, - expectedValue: .init(top: 0, leading: 16, bottom: 0, trailing: 16) - ) - } - func test_calculatePopupPadding_withKeyboardShown_whenKeyboardOverlapingPopup() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72, popupPadding: .init(top: 0, leading: 11, bottom: 0, trailing: 11)), - createPopupInstanceForPopupHeightTests(popupHeight: 1000, popupPadding: .init(top: 0, leading: 16, bottom: 0, trailing: 16)) - ] - - appendPopupsAndCheckPopupPadding( - popups: popups, - isKeyboardActive: true, - expectedValue: .init(top: 0, leading: 16, bottom: 250, trailing: 16) - ) - } -} -private extension PopupCentreStackViewModelTests { - func appendPopupsAndCheckPopupPadding(popups: [AnyPopup], isKeyboardActive: Bool, expectedValue: EdgeInsets) { - appendPopupsAndPerformChecks( - popups: popups, - isKeyboardActive: isKeyboardActive, - calculatedValue: { $0.t_calculatePopupPadding() }, - expectedValueBuilder: { _ in expectedValue } - ) - } -} - -// MARK: Corner Radius -extension PopupCentreStackViewModelTests { - func test_calculateCornerRadius_withCornerRadiusZero() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 20), - createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 0), - ] - - appendPopupsAndCheckCornerRadius( - popups: popups, - expectedValue: [.top: 0, .bottom: 0] - ) - } - func test_calculateCornerRadius_withCornerRadiusNonZero() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 20), - createPopupInstanceForPopupHeightTests(popupHeight: 234, cornerRadius: 24), - ] - - appendPopupsAndCheckCornerRadius( - popups: popups, - expectedValue: [.top: 24, .bottom: 24] - ) - } -} -private extension PopupCentreStackViewModelTests { - func appendPopupsAndCheckCornerRadius(popups: [AnyPopup], expectedValue: [MijickPopups.VerticalEdge: CGFloat]) { - appendPopupsAndPerformChecks( - popups: popups, - isKeyboardActive: false, - calculatedValue: { $0.t_calculateCornerRadius() }, - expectedValueBuilder: { _ in expectedValue } - ) - } -} - -// MARK: Opacity -extension PopupCentreStackViewModelTests { - func test_calculatePopupOpacity_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72), - createPopupInstanceForPopupHeightTests(popupHeight: 400) - ] - - appendPopupsAndCheckOpacity( - popups: popups, - calculateForIndex: 1, - expectedValue: 0 - ) - } - func test_calculatePopupOpacity_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72), - createPopupInstanceForPopupHeightTests(popupHeight: 400) - ] - - appendPopupsAndCheckOpacity( - popups: popups, - calculateForIndex: 2, - expectedValue: 1 - ) - } -} -private extension PopupCentreStackViewModelTests { - func appendPopupsAndCheckOpacity(popups: [AnyPopup], calculateForIndex index: Int, expectedValue: CGFloat) { - appendPopupsAndPerformChecks( - popups: popups, - isKeyboardActive: false, - calculatedValue: { [self] in $0.t_calculateOpacity(for: viewModel.t_popups[index]) }, - expectedValueBuilder: { _ in expectedValue } - ) - } -} - -// MARK: Vertical Fixed Size -extension PopupCentreStackViewModelTests { - func test_calculateVerticalFixedSize_withHeightSmallerThanScreen() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 913), - createPopupInstanceForPopupHeightTests(popupHeight: 400) - ] - - appendPopupsAndCheckVerticalFixedSize( - popups: popups, - calculateForIndex: 2, - expectedValue: true - ) - } - func test_calculateVerticalFixedSize_withHeightLargerThanScreen() { - let popups = [ - createPopupInstanceForPopupHeightTests(popupHeight: 350), - createPopupInstanceForPopupHeightTests(popupHeight: 72), - createPopupInstanceForPopupHeightTests(popupHeight: 913) - ] - - appendPopupsAndCheckVerticalFixedSize( - popups: popups, - calculateForIndex: 2, - expectedValue: false - ) - } -} -private extension PopupCentreStackViewModelTests { - func appendPopupsAndCheckVerticalFixedSize(popups: [AnyPopup], calculateForIndex index: Int, expectedValue: Bool) { - appendPopupsAndPerformChecks( - popups: popups, - isKeyboardActive: false, - calculatedValue: { $0.t_calculateVerticalFixedSize(for: $0.t_popups[index]) }, - expectedValueBuilder: { _ in expectedValue } - ) - } -} - - - -// MARK: - HELPERS - - - -// MARK: Methods -private extension PopupCentreStackViewModelTests { - func createPopupInstanceForPopupHeightTests(popupHeight: CGFloat, popupPadding: EdgeInsets = .init(), cornerRadius: CGFloat = 0) -> AnyPopup { - let config = getConfigForPopupHeightTests(cornerRadius: cornerRadius, popupPadding: popupPadding) - return AnyPopup.t_createNew(config: config).settingHeight(popupHeight) - } - func appendPopupsAndPerformChecks(popups: [AnyPopup], isKeyboardActive: Bool, calculatedValue: @escaping (ViewModel) -> (Value), expectedValueBuilder: @escaping (ViewModel) -> Value) { - viewModel.t_updatePopupsValue(popups) - viewModel.t_updatePopupsValue(recalculatePopupHeights(viewModel)) - viewModel.t_updateKeyboardValue(isKeyboardActive) - viewModel.t_updateScreenValue(isKeyboardActive ? screenWithKeyboard : screen) - - XCTAssertEqual(calculatedValue(viewModel), expectedValueBuilder(viewModel)) - } -} -private extension PopupCentreStackViewModelTests { - func getConfigForPopupHeightTests(cornerRadius: CGFloat, popupPadding: EdgeInsets) -> Config { .t_createNew( - popupPadding: popupPadding, - cornerRadius: cornerRadius - )} - func recalculatePopupHeights(_ viewModel: ViewModel) -> [AnyPopup] { viewModel.t_popups.map { - $0.settingHeight(viewModel.t_calculateHeight(heightCandidate: $0.height!)) - }} -} - -// MARK: Screen -private extension PopupCentreStackViewModelTests { - var screen: Screen { .init( - height: 1000, - safeArea: .init(top: 100, leading: 20, bottom: 50, trailing: 30) - )} - var screenWithKeyboard: Screen { .init( - height: 1000, - safeArea: .init(top: 100, leading: 20, bottom: 200, trailing: 30) - )} -} - -// MARK: Typealiases -private extension PopupCentreStackViewModelTests { - typealias Config = LocalConfig.Centre - typealias ViewModel = VM.CentreStack -} diff --git a/Tests/Tests+ViewModel+PopupVerticalStack.swift b/Tests/Tests+ViewModel+PopupVerticalStack.swift index 3961fc31b0..2ca6c82671 100644 --- a/Tests/Tests+ViewModel+PopupVerticalStack.swift +++ b/Tests/Tests+ViewModel+PopupVerticalStack.swift @@ -14,33 +14,32 @@ import SwiftUI @testable import MijickPopups @MainActor final class PopupVerticalStackViewModelTests: XCTestCase { - @ObservedObject private var topViewModel: ViewModel = .init() - @ObservedObject private var bottomViewModel: ViewModel = .init() + @ObservedObject private var topViewModel: ViewModel = .init(TopPopupConfig.self) + @ObservedObject private var bottomViewModel: ViewModel = .init(BottomPopupConfig.self) override func setUp() async throws { - setup(topViewModel) - setup(bottomViewModel) + await setup(topViewModel) + await setup(bottomViewModel) } } private extension PopupVerticalStackViewModelTests { - func setup(_ viewModel: ViewModel) { - viewModel.t_updateScreenValue(screen) - viewModel.t_setup(updatePopupAction: { self.updatePopupAction(viewModel, $0) }, closePopupAction: { self.closePopupAction(viewModel, $0) }) + func setup(_ viewModel: ViewModel) async { + await viewModel.updateScreen(screenHeight: screen.height, screenSafeArea: screen.safeArea) + viewModel.setup(updatePopupAction: { await self.updatePopupAction(viewModel, $0) }, closePopupAction: { await self.closePopupAction(viewModel, $0) }) } } private extension PopupVerticalStackViewModelTests { - func updatePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) { if let index = viewModel.t_popups.firstIndex(of: popup) { - var popups = viewModel.t_popups + func updatePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) async { if let index = viewModel.popups.firstIndex(of: popup) { + var popups = viewModel.popups popups[index] = popup - viewModel.t_updatePopupsValue(popups) - viewModel.t_calculateAndUpdateActivePopupHeight() + await viewModel.updatePopups(popups) }} - func closePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) { if let index = viewModel.t_popups.firstIndex(of: popup) { - var popups = viewModel.t_popups + func closePopupAction(_ viewModel: ViewModel, _ popup: AnyPopup) async { if let index = viewModel.popups.firstIndex(of: popup) { + var popups = viewModel.popups popups.remove(at: index) - viewModel.t_updatePopupsValue(popups) + await viewModel.updatePopups(popups) }} } @@ -52,27 +51,27 @@ private extension PopupVerticalStackViewModelTests { // MARK: Inverted Index extension PopupVerticalStackViewModelTests { - func test_getInvertedIndex_1() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150) + func test_getInvertedIndex_1() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150) ]) XCTAssertEqual( - bottomViewModel.t_getInvertedIndex(of: bottomViewModel.t_popups[0]), + bottomViewModel.popups.getInvertedIndex(of: bottomViewModel.popups[0]), 0 ) } - func test_getInvertedIndex_2() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150) + func test_getInvertedIndex_2() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150) ]) XCTAssertEqual( - bottomViewModel.t_getInvertedIndex(of: bottomViewModel.t_popups[3]), + bottomViewModel.popups.getInvertedIndex(of: bottomViewModel.popups[3]), 1 ) } @@ -80,340 +79,368 @@ extension PopupVerticalStackViewModelTests { // MARK: Update Popup extension PopupVerticalStackViewModelTests { - func test_updatePopup_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 0) + func test_updatePopup_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 0) ] - let updatedPopup = popups[0] - .settingHeight(100) - .settingDragHeight(100) + let updatedPopup = await popups[0] + .updatedHeight(100) + .updatedDragHeight(100) - appendPopupsAndCheckPopups( + await appendPopupsAndCheckPopups( viewModel: bottomViewModel, popups: popups, updatedPopup: updatedPopup, expectedValue: (height: 100, dragHeight: 100) ) } - func test_updatePopup_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 50), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 25), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 15), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2137) + func test_updatePopup_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 50), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 25), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 15), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2137) ] - let updatedPopup = popups[2].settingHeight(1371) + let updatedPopup = await popups[2].updatedHeight(1371) - appendPopupsAndCheckPopups( + await appendPopupsAndCheckPopups( viewModel: bottomViewModel, popups: popups, updatedPopup: updatedPopup, - expectedValue: (height: 1371, dragHeight: nil) + expectedValue: (height: 1371, dragHeight: 0) ) } - func test_updatePopup_3() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 50), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 25), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 15), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2137), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 77) + func test_updatePopup_3() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 50), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 25), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 15), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2137), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 77) ] - let updatedPopup = popups[4].settingHeight(nil) + let updatedPopup = await popups[4].updatedHeight(nil) - appendPopupsAndCheckPopups( + await appendPopupsAndCheckPopups( viewModel: bottomViewModel, popups: popups, updatedPopup: updatedPopup, - expectedValue: (height: nil, dragHeight: nil) + expectedValue: (height: nil, dragHeight: 0) ) } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckPopups(viewModel: ViewModel, popups: [AnyPopup], updatedPopup: AnyPopup, expectedValue: (height: CGFloat?, dragHeight: CGFloat?)) { - viewModel.t_updatePopupsValue(popups) - viewModel.t_updatePopup(updatedPopup) + func appendPopupsAndCheckPopups(viewModel: ViewModel, popups: [AnyPopup], updatedPopup: AnyPopup, expectedValue: (height: CGFloat?, dragHeight: CGFloat)) async { + await viewModel.updatePopups(popups) + await viewModel.updatePopupAction(updatedPopup) - if let index = viewModel.t_popups.firstIndex(of: updatedPopup) { - XCTAssertEqual(viewModel.t_popups[index].height, expectedValue.height) - XCTAssertEqual(viewModel.t_popups[index].dragHeight, expectedValue.dragHeight) + if let index = viewModel.popups.firstIndex(of: updatedPopup) { + XCTAssertEqual(viewModel.popups[index].height, expectedValue.height) + XCTAssertEqual(viewModel.popups[index].dragHeight, expectedValue.dragHeight) } } } // MARK: Popup Height extension PopupVerticalStackViewModelTests { - func test_calculatePopupHeight_withAutoHeightMode_whenLessThanScreen_onePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150) - ]) + func test_calculatePopupHeight_withAutoHeightMode_whenLessThanScreen_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - 150.0 + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 0, + expectedValue: 150 ) } - func test_calculatePopupHeight_withAutoHeightMode_whenLessThanScreen_fourPopupsStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 200), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100) - ]) + func test_calculatePopupHeight_withAutoHeightMode_whenLessThanScreen_fourPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 200), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - 100.0 + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 3, + expectedValue: 100 ) } - func test_calculatePopupHeight_withAutoHeightMode_whenBiggerThanScreen_onePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2000) - ]) + func test_calculatePopupHeight_withAutoHeightMode_whenBiggerThanScreen_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2000) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height - screen.safeArea.top + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 0, + expectedValue: screen.height - screen.safeArea.top ) } - func test_calculatePopupHeight_withAutoHeightMode_whenBiggerThanScreen_fivePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 150), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 200), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2000) - ]) + func test_calculatePopupHeight_withAutoHeightMode_whenBiggerThanScreen_fivePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 150), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 200), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2000) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height - screen.safeArea.top - bottomViewModel.t_stackOffset * 4 + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 4, + expectedValue: screen.height - screen.safeArea.top - bottomViewModel.stackOffset * 4 ) } - func test_calculatePopupHeight_withLargeHeightMode_whenOnePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 100) - ]) + func test_calculatePopupHeight_withLargeHeightMode_whenOnePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 100) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height - screen.safeArea.top + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 0, + expectedValue: screen.height - screen.safeArea.top ) } - func test_calculatePopupHeight_withLargeHeightMode_whenThreePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 700), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 1000) - ]) + func test_calculatePopupHeight_withLargeHeightMode_whenThreePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 700), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 1000) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height - screen.safeArea.top - bottomViewModel.t_stackOffset * 2 + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 2, + expectedValue: screen.height - screen.safeArea.top - bottomViewModel.stackOffset * 2 ) } - func test_calculatePopupHeight_withFullscreenHeightMode_whenOnePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 100) - ]) + func test_calculatePopupHeight_withFullscreenHeightMode_whenOnePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 100) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 0, + expectedValue: screen.height ) } - func test_calculatePopupHeight_withFullscreenHeightMode_whenThreePopupsStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 2000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 3000) - ]) + func test_calculatePopupHeight_withFullscreenHeightMode_whenThreePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 2000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 3000) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 2, + expectedValue: screen.height ) } - func test_calculatePopupHeight_withLargeHeightMode_whenThreePopupsStacked_popupPadding() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 2000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 3000, popupPadding: .init(top: 33, leading: 15, bottom: 21, trailing: 15)) - ]) + func test_calculatePopupHeight_withLargeHeightMode_whenThreePopupsStacked_popupPadding() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 2000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 3000, popupPadding: .init(top: 33, leading: 15, bottom: 21, trailing: 15)) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height - screen.safeArea.top - 2 * bottomViewModel.t_stackOffset + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 2, + expectedValue: screen.height - screen.safeArea.top - 2 * bottomViewModel.stackOffset ) } - func test_calculatePopupHeight_withFullscreenHeightMode_whenThreePopupsStacked_popupPadding() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 2000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 3000, popupPadding: .init(top: 33, leading: 15, bottom: 21, trailing: 15)) - ]) + func test_calculatePopupHeight_withFullscreenHeightMode_whenThreePopupsStacked_popupPadding() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 2000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 3000, popupPadding: .init(top: 33, leading: 15, bottom: 21, trailing: 15)) + ] - XCTAssertEqual( - calculateLastPopupHeight(bottomViewModel), - screen.height + await appendPopupsAndCheckPopupHeight( + viewModel: bottomViewModel, + popups: popups, + calculateForIndex: 2, + expectedValue: screen.height ) } - func test_calculatePopupHeight_withLargeHeightMode_whenPopupsHaveTopAlignment() { - topViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .large, popupHeight: 100) - ]) + func test_calculatePopupHeight_withLargeHeightMode_whenPopupsHaveTopAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .large, popupHeight: 100) + ] - XCTAssertEqual( - calculateLastPopupHeight(topViewModel), - screen.height - screen.safeArea.bottom + await appendPopupsAndCheckPopupHeight( + viewModel: topViewModel, + popups: popups, + calculateForIndex: 0, + expectedValue: screen.height - screen.safeArea.bottom ) } } private extension PopupVerticalStackViewModelTests { - func calculateLastPopupHeight(_ viewModel: ViewModel) -> CGFloat { - viewModel.t_calculateHeight(heightCandidate: viewModel.t_popups.last!.height!, popupConfig: viewModel.t_popups.last!.config as! C) + func appendPopupsAndCheckPopupHeight(viewModel: ViewModel, popups: [AnyPopup], calculateForIndex index: Int, expectedValue: CGFloat) async { + await appendPopupsAndPerformChecks( + viewModel: viewModel, + popups: popups, + gestureTranslation: 0, + calculatedValue: { $0.popups[index].height }, + expectedValueBuilder: { _ in expectedValue } + ) } } // MARK: Active Popup Height extension PopupVerticalStackViewModelTests { - func test_calculateActivePopupHeight_withAutoHeightMode_whenLessThanScreen_onePopupStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100) + func test_calculateActivePopupHeight_withAutoHeightMode_whenLessThanScreen_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: 100 ) } - func test_calculateActivePopupHeight_withAutoHeightMode_whenBiggerThanScreen_threePopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 3000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2000) + func test_calculateActivePopupHeight_withAutoHeightMode_whenBiggerThanScreen_threePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 3000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2000) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - expectedValue: screen.height - screen.safeArea.top - 2 * bottomViewModel.t_stackOffset + expectedValue: screen.height - screen.safeArea.top - 2 * bottomViewModel.stackOffset ) } - func test_calculateActivePopupHeight_withLargeHeightMode_whenThreePopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 2000) + func test_calculateActivePopupHeight_withLargeHeightMode_whenThreePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 2000) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - expectedValue: screen.height - screen.safeArea.top - 2 * bottomViewModel.t_stackOffset + expectedValue: screen.height - screen.safeArea.top - 2 * bottomViewModel.stackOffset ) } - func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsNegative_twoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 2000) + func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsNegative_twoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 2000) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: -51, - expectedValue: screen.height - screen.safeArea.top - bottomViewModel.t_stackOffset * 1 + 51 + expectedValue: screen.height - screen.safeArea.top - bottomViewModel.stackOffset * 1 + 51 ) } - func test_calculateActivePopupHeight_withLargeHeightMode_whenGestureIsNegative_onePopupStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 350) + func test_calculateActivePopupHeight_withLargeHeightMode_whenGestureIsNegative_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 350) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: -99, expectedValue: screen.height - screen.safeArea.top + 99 ) } - func test_calculateActivePopupHeight_withFullscreenHeightMode_whenGestureIsNegative_twoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 100), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 250) + func test_calculateActivePopupHeight_withFullscreenHeightMode_whenGestureIsNegative_twoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 100), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 250) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: -21, expectedValue: screen.height ) } - func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsPositive_threePopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1000), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 850) + func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsPositive_threePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1000), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 850) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 100, expectedValue: 850 ) } - func test_calculateActivePopupHeight_withFullscreenHeightMode_whenGestureIsPositive_onePopupStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 350) + func test_calculateActivePopupHeight_withFullscreenHeightMode_whenGestureIsPositive_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 350) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 31, expectedValue: screen.height ) } - func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsNegative_hasDragHeightStored_twoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 500, popupDragHeight: 100) + func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsNegative_hasDragHeightStored_twoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 500, popupDragHeight: 100) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: -93, expectedValue: 500 + 100 + 93 ) } - func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsPositive_hasDragHeightStored_onePopupStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1300, popupDragHeight: 100) + func test_calculateActivePopupHeight_withAutoHeightMode_whenGestureIsPositive_hasDragHeightStored_onePopupStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1300, popupDragHeight: 100) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: bottomViewModel, popups: popups, gestureTranslation: 350, expectedValue: screen.height - screen.safeArea.top ) } - func test_calculateActivePopupHeight_withPopupsHaveTopAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .large, popupHeight: 1300) + func test_calculateActivePopupHeight_withPopupsHaveTopAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .large, popupHeight: 1300) ] - appendPopupsAndCheckActivePopupHeight( + await appendPopupsAndCheckActivePopupHeight( viewModel: topViewModel, popups: popups, gestureTranslation: 0, @@ -422,12 +449,12 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckActivePopupHeight(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: CGFloat) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckActivePopupHeight(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: CGFloat) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_activePopupHeight }, + calculatedValue: { $0.activePopupProperties.height }, expectedValueBuilder: { _ in expectedValue } ) } @@ -435,186 +462,188 @@ private extension PopupVerticalStackViewModelTests { // MARK: Offset extension PopupVerticalStackViewModelTests { - func test_calculateOffsetY_withZeroGestureTranslation_fivePopupsStacked_thirdElement() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 240), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 670), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 310) + func test_calculateOffsetY_withZeroGestureTranslation_fivePopupsStacked_thirdElement() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 240), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 670), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 310) ]) XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[2]), - -bottomViewModel.t_stackOffset * 2 + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[2]), + -bottomViewModel.stackOffset * 2 ) } - func test_calculateOffsetY_withZeroGestureTranslation_fivePopupsStacked_lastElement() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 240), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 670), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 310) + func test_calculateOffsetY_withZeroGestureTranslation_fivePopupsStacked_lastElement() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 240), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 670), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 310) ]) + await bottomViewModel.updateGestureTranslation(0) XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[4]), + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[4]), 0 ) } - func test_calculateOffsetY_withNegativeGestureTranslation_dragHeight_onePopupStacked() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 100) + func test_calculateOffsetY_withNegativeGestureTranslation_dragHeight_onePopupStacked() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350, popupDragHeight: 100) ]) - bottomViewModel.t_updateGestureTranslation(-100) + await bottomViewModel.updateGestureTranslation(-100) XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[0]), + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[0]), 0 ) } - func test_calculateOffsetY_withPositiveGestureTranslation_dragHeight_twoPopupsStacked_firstElement() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) + func test_calculateOffsetY_withPositiveGestureTranslation_dragHeight_twoPopupsStacked_firstElement() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) ]) - bottomViewModel.t_updateGestureTranslation(100) + await bottomViewModel.updateGestureTranslation(100) XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[0]), - -bottomViewModel.t_stackOffset + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[0]), + -bottomViewModel.stackOffset ) } - func test_calculateOffsetY_withPositiveGestureTranslation_dragHeight_twoPopupsStacked_lastElement() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) + func test_calculateOffsetY_withPositiveGestureTranslation_dragHeight_twoPopupsStacked_lastElement() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) ]) - bottomViewModel.t_updateGestureTranslation(100) + await bottomViewModel.updateGestureTranslation(100) XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[1]), + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[1]), 100 - 21 ) } - func test_calculateOffsetY_withStackingDisabled() { - bottomViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) + func test_calculateOffsetY_withStackingDisabled() async { + await bottomViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) ]) GlobalConfigContainer.vertical.isStackingEnabled = false XCTAssertEqual( - bottomViewModel.t_calculateOffsetY(for: bottomViewModel.t_popups[0]), + bottomViewModel.calculateOffsetY(for: bottomViewModel.popups[0]), 0 ) } - func test_calculateOffsetY_withPopupsHaveTopAlignment_1() { - topViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) + func test_calculateOffsetY_withPopupsHaveTopAlignment_1() async { + await topViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) ]) + await topViewModel.updateGestureTranslation(0) XCTAssertEqual( - topViewModel.t_calculateOffsetY(for: topViewModel.t_popups[0]), - topViewModel.t_stackOffset + topViewModel.calculateOffsetY(for: topViewModel.popups[0]), + topViewModel.stackOffset ) } - func test_calculateOffsetY_withPopupsHaveTopAlignment_2() { - topViewModel.t_updatePopupsValue([ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) + func test_calculateOffsetY_withPopupsHaveTopAlignment_2() async { + await topViewModel.updatePopups([ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 350, popupDragHeight: 249), + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 133, popupDragHeight: 21) ]) - topViewModel.t_updateGestureTranslation(-100) + await topViewModel.updateGestureTranslation(-100) XCTAssertEqual( - topViewModel.t_calculateOffsetY(for: topViewModel.t_popups[1]), + topViewModel.calculateOffsetY(for: topViewModel.popups[1]), 21 - 100 ) } } -// MARK: Popup Padding +// MARK: Outer Padding extension PopupVerticalStackViewModelTests { - func test_calculatePopupPadding_withAutoHeightMode_whenLessThanScreen() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withAutoHeightMode_whenLessThanScreen() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 12, leading: 17, bottom: 33, trailing: 17) ) } - func test_calculatePopupPadding_withAutoHeightMode_almostLikeScreen_onlyOnePaddingShouldBeNonZero() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 877, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withAutoHeightMode_almostLikeScreen_onlyOnePaddingShouldBeNonZero() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 877, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: 17, bottom: 23, trailing: 17) ) } - func test_calculatePopupPadding_withAutoHeightMode_almostLikeScreen_bothPaddingsShouldBeNonZero() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 861, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withAutoHeightMode_almostLikeScreen_bothPaddingsShouldBeNonZero() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 861, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 6, leading: 17, bottom: 33, trailing: 17) ) } - func test_calculatePopupPadding_withAutoHeightMode_almostLikeScreen_topPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 911, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withAutoHeightMode_almostLikeScreen_topPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 911, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: topViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 12, leading: 17, bottom: 27, trailing: 17) ) } - func test_calculatePopupPadding_withAutoHeightMode_whenBiggerThanScreen() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1100, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withAutoHeightMode_whenBiggerThanScreen() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1100, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: 17, bottom: 0, trailing: 17) ) } - func test_calculatePopupPadding_withLargeHeightMode() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withLargeHeightMode() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: 17, bottom: 0, trailing: 17) ) } - func test_calculatePopupPadding_withFullscreenHeightMode() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) + func test_calculateOuterPadding_withFullscreenHeightMode() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 344, popupPadding: .init(top: 12, leading: 17, bottom: 33, trailing: 17)) ] - appendPopupsAndCheckPopupPadding( + await appendPopupsAndCheckOuterPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, @@ -623,109 +652,109 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckPopupPadding(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: EdgeInsets) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckOuterPadding(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: EdgeInsets) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculatePopupPadding() }, + calculatedValue: { await $0.calculateActivePopupOuterPadding() }, expectedValueBuilder: { _ in expectedValue } ) } } -// MARK: Body Padding +// MARK: Inner Padding extension PopupVerticalStackViewModelTests { - func test_calculateBodyPadding_withDefaultSettings() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 350) + func test_calculateInnerPadding_withDefaultSettings() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 350) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: screen.safeArea.top, leading: screen.safeArea.leading, bottom: screen.safeArea.bottom, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withIgnoringSafeArea_bottom() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 200, ignoredSafeAreaEdges: .bottom) + func test_calculateInnerPadding_withIgnoringSafeArea_bottom() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 200, ignoredSafeAreaEdges: .bottom) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: screen.safeArea.leading, bottom: 0, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withIgnoringSafeArea_all() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1200, ignoredSafeAreaEdges: .all) + func test_calculateInnerPadding_withIgnoringSafeArea_all() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1200, ignoredSafeAreaEdges: .all) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: 0, bottom: 0, trailing: 0) ) } - func test_calculateBodyPadding_withPopupPadding() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1200, popupPadding: .init(top: 21, leading: 12, bottom: 37, trailing: 12)) + func test_calculateInnerPadding_withPopupPadding() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1200, popupPadding: .init(top: 21, leading: 12, bottom: 37, trailing: 12)) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: screen.safeArea.leading, bottom: screen.safeArea.bottom, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withFullscreenHeightMode_ignoringSafeArea_top() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 100, ignoredSafeAreaEdges: .top) + func test_calculateInnerPadding_withFullscreenHeightMode_ignoringSafeArea_top() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 100, ignoredSafeAreaEdges: .top) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: .init(top: 0, leading: screen.safeArea.leading, bottom: screen.safeArea.bottom, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withGestureTranslation() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 800) + func test_calculateInnerPadding_withGestureTranslation() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 800) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: -300, expectedValue: .init(top: screen.safeArea.top, leading: screen.safeArea.leading, bottom: screen.safeArea.bottom, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withGestureTranslation_dragHeight() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, popupDragHeight: 700) + func test_calculateInnerPadding_withGestureTranslation_dragHeight() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, popupDragHeight: 700) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: bottomViewModel, popups: popups, gestureTranslation: 21, expectedValue: .init(top: screen.safeArea.top - 21, leading: screen.safeArea.leading, bottom: screen.safeArea.bottom, trailing: screen.safeArea.trailing) ) } - func test_calculateBodyPadding_withGestureTranslation_dragHeight_topPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 300, popupDragHeight: 700) + func test_calculateInnerPadding_withGestureTranslation_dragHeight_topPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 300, popupDragHeight: 700) ] - appendPopupsAndCheckBodyPadding( + await appendPopupsAndCheckInnerPadding( viewModel: topViewModel, popups: popups, gestureTranslation: -21, @@ -734,12 +763,12 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckBodyPadding(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: EdgeInsets) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckInnerPadding(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: EdgeInsets) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateBodyPadding(for: popups.last!) }, + calculatedValue: { await $0.calculateActivePopupInnerPadding() }, expectedValueBuilder: { _ in expectedValue } ) } @@ -747,60 +776,60 @@ private extension PopupVerticalStackViewModelTests { // MARK: Translation Progress extension PopupVerticalStackViewModelTests { - func test_calculateTranslationProgress_withNoGestureTranslation() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300) + func test_calculateTranslationProgress_withNoGestureTranslation() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300) ] - appendPopupsAndCheckTranslationProgress( + await appendPopupsAndCheckTranslationProgress( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: 0 ) } - func test_calculateTranslationProgress_withPositiveGestureTranslation() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300) + func test_calculateTranslationProgress_withPositiveGestureTranslation() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300) ] - appendPopupsAndCheckTranslationProgress( + await appendPopupsAndCheckTranslationProgress( viewModel: bottomViewModel, popups: popups, gestureTranslation: 250, expectedValue: 250 / 300 ) } - func test_calculateTranslationProgress_withPositiveGestureTranslation_dragHeight() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, popupDragHeight: 120) + func test_calculateTranslationProgress_withPositiveGestureTranslation_dragHeight() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, popupDragHeight: 120) ] - appendPopupsAndCheckTranslationProgress( + await appendPopupsAndCheckTranslationProgress( viewModel: bottomViewModel, popups: popups, gestureTranslation: 250, expectedValue: (250 - 120) / 300 ) } - func test_calculateTranslationProgress_withNegativeGestureTranslation() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300) + func test_calculateTranslationProgress_withNegativeGestureTranslation() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300) ] - appendPopupsAndCheckTranslationProgress( + await appendPopupsAndCheckTranslationProgress( viewModel: bottomViewModel, popups: popups, gestureTranslation: -175, expectedValue: 0 ) } - func test_calculateTranslationProgress_withNegativeGestureTranslation_whenTopPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 300) + func test_calculateTranslationProgress_withNegativeGestureTranslation_whenTopPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 300) ] - appendPopupsAndCheckTranslationProgress( + await appendPopupsAndCheckTranslationProgress( viewModel: topViewModel, popups: popups, gestureTranslation: -175, @@ -809,12 +838,12 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckTranslationProgress(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: CGFloat) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckTranslationProgress(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: CGFloat) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateTranslationProgress() }, + calculatedValue: { await $0.calculateActivePopupTranslationProgress() }, expectedValueBuilder: { _ in expectedValue } ) } @@ -822,76 +851,76 @@ private extension PopupVerticalStackViewModelTests { // MARK: Corner Radius extension PopupVerticalStackViewModelTests { - func test_calculateCornerRadius_withTwoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 1), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 12) + func test_calculateCornerRadius_withTwoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, cornerRadius: 1), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, cornerRadius: 12) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: [.top: 12, .bottom: 0] ) } - func test_calculateCornerRadius_withPopupPadding_bottom_first() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 0, leading: 0, bottom: 12, trailing: 0), cornerRadius: 1), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 12) + func test_calculateCornerRadius_withPopupPadding_bottom_first() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 0, leading: 0, bottom: 12, trailing: 0), cornerRadius: 1), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, cornerRadius: 12) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: [.top: 12, .bottom: 0] ) } - func test_calculateCornerRadius_withPopupPadding_bottom_last() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 1), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 0, leading: 0, bottom: 12, trailing: 0), cornerRadius: 12) + func test_calculateCornerRadius_withPopupPadding_bottom_last() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, cornerRadius: 1), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 0, leading: 0, bottom: 12, trailing: 0), cornerRadius: 12) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: [.top: 12, .bottom: 12] ) } - func test_calculateCornerRadius_withPopupPadding_all() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 1), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 12, leading: 24, bottom: 12, trailing: 24), cornerRadius: 12) + func test_calculateCornerRadius_withPopupPadding_all() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, cornerRadius: 1), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300, popupPadding: .init(top: 12, leading: 24, bottom: 12, trailing: 24), cornerRadius: 12) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: [.top: 12, .bottom: 12] ) } - func test_calculateCornerRadius_fullscreen() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 300, cornerRadius: 13) + func test_calculateCornerRadius_fullscreen() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 300, cornerRadius: 13) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, expectedValue: [.top: 0, .bottom: 0] ) } - func test_calculateCornerRadius_whenPopupsHaveTopAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 300, cornerRadius: 12) + func test_calculateCornerRadius_whenPopupsHaveTopAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 300, cornerRadius: 12) ] - appendPopupsAndCheckCornerRadius( + await appendPopupsAndCheckCornerRadius( viewModel: topViewModel, popups: popups, gestureTranslation: 0, @@ -900,12 +929,12 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckCornerRadius(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: [MijickPopups.VerticalEdge: CGFloat]) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckCornerRadius(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: [MijickPopups.PopupAlignment: CGFloat]) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateCornerRadius() }, + calculatedValue: { await $0.calculateActivePopupCorners() }, expectedValueBuilder: { _ in expectedValue } ) } @@ -913,14 +942,14 @@ private extension PopupVerticalStackViewModelTests { // MARK: Scale X extension PopupVerticalStackViewModelTests { - func test_calculateScaleX_withNoGestureTranslation_threePopupsStacked_last() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 360) + func test_calculateScaleX_withNoGestureTranslation_threePopupsStacked_last() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 360) ] - appendPopupsAndCheckScaleX( + await appendPopupsAndCheckScaleX( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, @@ -928,63 +957,63 @@ extension PopupVerticalStackViewModelTests { expectedValueBuilder: {_ in 1 } ) } - func test_calculateScaleX_withNoGestureTranslation_fourPopupsStacked_second() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1360) + func test_calculateScaleX_withNoGestureTranslation_fourPopupsStacked_second() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1360) ] - appendPopupsAndCheckScaleX( + await appendPopupsAndCheckScaleX( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, calculateForIndex: 1, - expectedValueBuilder: { 1 - $0.t_stackScaleFactor * 2 } + expectedValueBuilder: { 1 - $0.stackScaleFactor * 2 } ) } - func test_calculateScaleX_withNegativeGestureTranslation_fourPopupsStacked_third() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1360) + func test_calculateScaleX_withNegativeGestureTranslation_fourPopupsStacked_third() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1360) ] - appendPopupsAndCheckScaleX( + await appendPopupsAndCheckScaleX( viewModel: bottomViewModel, popups: popups, gestureTranslation: -100, calculateForIndex: 2, - expectedValueBuilder: { 1 - $0.t_stackScaleFactor * 1 } + expectedValueBuilder: { 1 - $0.stackScaleFactor * 1 } ) } - func test_calculateScaleX_withPositiveGestureTranslation_fivePopupsStacked_second() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 300), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 120), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 123) + func test_calculateScaleX_withPositiveGestureTranslation_fivePopupsStacked_second() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 300), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 120), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 123) ] - appendPopupsAndCheckScaleX( + await appendPopupsAndCheckScaleX( viewModel: bottomViewModel, popups: popups, gestureTranslation: 100, calculateForIndex: 1, - expectedValueBuilder: { 1 - $0.t_stackScaleFactor * 3 * max(1 - $0.t_calculateTranslationProgress(), $0.t_minScaleProgressMultiplier) } + expectedValueBuilder: { await 1 - $0.stackScaleFactor * 3 * max(1 - $0.calculateActivePopupTranslationProgress(), $0.minScaleProgressMultiplier) } ) } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckScaleX(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculateForIndex index: Int, expectedValueBuilder: @escaping (ViewModel) -> CGFloat) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckScaleX(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculateForIndex index: Int, expectedValueBuilder: @escaping (ViewModel) async -> CGFloat) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateScaleX(for: $0.t_popups[index]) }, + calculatedValue: { $0.calculateScaleX(for: $0.popups[index]) }, expectedValueBuilder: expectedValueBuilder ) } @@ -992,74 +1021,70 @@ private extension PopupVerticalStackViewModelTests { // MARK: Fixed Size extension PopupVerticalStackViewModelTests { - func test_calculateFixedSize_withAutoHeightMode_whenLessThanScreen_twoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 123) + func test_calculateFixedSize_withAutoHeightMode_whenLessThanScreen_twoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 123) ] - appendPopupsAndCheckVerticalFixedSize( + await appendPopupsAndCheckVerticalFixedSize( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - calculateForIndex: 1, expectedValue: true ) } - func test_calculateFixedSize_withAutoHeightMode_whenBiggerThanScreen_twoPopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1223) + func test_calculateFixedSize_withAutoHeightMode_whenBiggerThanScreen_twoPopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1223) ] - appendPopupsAndCheckVerticalFixedSize( + await appendPopupsAndCheckVerticalFixedSize( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - calculateForIndex: 1, expectedValue: false ) } - func test_calculateFixedSize_withLargeHeightMode_threePopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1223), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 1223) + func test_calculateFixedSize_withLargeHeightMode_threePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1223), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 1223) ] - appendPopupsAndCheckVerticalFixedSize( + await appendPopupsAndCheckVerticalFixedSize( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - calculateForIndex: 2, expectedValue: false ) } - func test_calculateFixedSize_withFullscreenHeightMode_fivePopupsStacked() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1223), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .large, popupHeight: 1223), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1223), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1223) + func test_calculateFixedSize_withFullscreenHeightMode_fivePopupsStacked() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1223), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .large, popupHeight: 1223), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1223), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1223) ] - appendPopupsAndCheckVerticalFixedSize( + await appendPopupsAndCheckVerticalFixedSize( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, - calculateForIndex: 4, expectedValue: false ) } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckVerticalFixedSize(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculateForIndex index: Int, expectedValue: Bool) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckVerticalFixedSize(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, expectedValue: Bool) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateVerticalFixedSize(for: $0.t_popups[index]) }, + calculatedValue: { await $0.calculateActivePopupVerticalFixedSize() }, expectedValueBuilder: { _ in expectedValue } ) } @@ -1067,14 +1092,14 @@ private extension PopupVerticalStackViewModelTests { // MARK: Stack Overlay Opacity extension PopupVerticalStackViewModelTests { - func test_calculateStackOverlayOpacity_withThreePopupsStacked_whenNoGestureTranslation_last() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512) + func test_calculateStackOverlayOpacity_withThreePopupsStacked_whenNoGestureTranslation_last() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, @@ -1082,31 +1107,31 @@ extension PopupVerticalStackViewModelTests { expectedValueBuilder: { _ in 0 } ) } - func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenNoGestureTranslation_second() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 812) + func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenNoGestureTranslation_second() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 812) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: 0, calculateForIndex: 1, - expectedValueBuilder: { $0.t_stackOverlayFactor * 2 } + expectedValueBuilder: { $0.stackOverlayFactor * 2 } ) } - func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenGestureTranslationIsNegative_last() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 812) + func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenGestureTranslationIsNegative_last() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 812) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: -123, @@ -1114,36 +1139,36 @@ extension PopupVerticalStackViewModelTests { expectedValueBuilder: { _ in 0 } ) } - func test_calculateStackOverlayOpacity_withTenPopupsStacked_whenGestureTranslationIsNegative_first() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 55), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 812), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 34), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 664), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 754), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 357), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 1234), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 356) + func test_calculateStackOverlayOpacity_withTenPopupsStacked_whenGestureTranslationIsNegative_first() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 55), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 812), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 34), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 664), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 754), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 357), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 1234), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 356) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: -123, calculateForIndex: 0, - expectedValueBuilder: { min($0.t_stackOverlayFactor * 9, $0.t_maxStackOverlayFactor) } + expectedValueBuilder: { $0.stackOverlayFactor * 9 } ) } - func test_calculateStackOverlayOpacity_withThreePopupsStacked_whenGestureTranslationIsPositive_last() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512) + func test_calculateStackOverlayOpacity_withThreePopupsStacked_whenGestureTranslationIsPositive_last() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: 494, @@ -1151,30 +1176,30 @@ extension PopupVerticalStackViewModelTests { expectedValueBuilder: { _ in 0 } ) } - func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenGestureTranslationIsPositive_nextToLast() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .fullscreen, popupHeight: 1360), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 233), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 512), - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 343) + func test_calculateStackOverlayOpacity_withFourPopupsStacked_whenGestureTranslationIsPositive_nextToLast() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .fullscreen, popupHeight: 1360), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 233), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 512), + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 343) ] - appendPopupsAndCheckStackOverlayOpacity( + await appendPopupsAndCheckStackOverlayOpacity( viewModel: bottomViewModel, popups: popups, gestureTranslation: 241, calculateForIndex: 2, - expectedValueBuilder: { (1 - $0.t_calculateTranslationProgress()) * $0.t_stackOverlayFactor } + expectedValueBuilder: { await (1 - $0.calculateActivePopupTranslationProgress()) * $0.stackOverlayFactor } ) } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckStackOverlayOpacity(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculateForIndex index: Int, expectedValueBuilder: @escaping (ViewModel) -> CGFloat) { - appendPopupsAndPerformChecks( + func appendPopupsAndCheckStackOverlayOpacity(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculateForIndex index: Int, expectedValueBuilder: @escaping (ViewModel) async -> CGFloat) async { + await appendPopupsAndPerformChecks( viewModel: viewModel, popups: popups, gestureTranslation: gestureTranslation, - calculatedValue: { $0.t_calculateStackOverlayOpacity(for: $0.t_popups[index]) }, + calculatedValue: { $0.calculateStackOverlayOpacity(for: $0.popups[index]) }, expectedValueBuilder: expectedValueBuilder ) } @@ -1182,84 +1207,84 @@ private extension PopupVerticalStackViewModelTests { // MARK: On Drag Gesture Changed extension PopupVerticalStackViewModelTests { - func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureDisabled() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragGestureEnabled: false) + func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureDisabled() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragGestureEnabled: false) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: bottomViewModel, popups: popups, gestureValue: 11, expectedValues: (popupHeight: 344, gestureTranslation: 0) ) } - func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureEnabled_bottomPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344) + func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureEnabled_bottomPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: bottomViewModel, popups: popups, gestureValue: 11, expectedValues: (popupHeight: 344, gestureTranslation: 11) ) } - func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureEnabled_topPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 344) + func test_calculateValuesOnDragGestureChanged_withPositiveDragValue_whenDragGestureEnabled_topPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 344) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: topViewModel, popups: popups, gestureValue: 11, expectedValues: (popupHeight: 344, gestureTranslation: 0) ) } - func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenNoDragDetents() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: []) + func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenNoDragDetents() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: []) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: bottomViewModel, popups: popups, gestureValue: -133, expectedValues: (popupHeight: 344, gestureTranslation: 0) ) } - func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetents() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(450)]) + func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetents() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(450)]) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: bottomViewModel, popups: popups, gestureValue: -40, expectedValues: (popupHeight: 384, gestureTranslation: -40) ) } - func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetentsLessThanDragValue_bottomPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(370)]) + func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetentsLessThanDragValue_bottomPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(370)]) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: bottomViewModel, popups: popups, gestureValue: -133, - expectedValues: (popupHeight: 370 + bottomViewModel.t_dragTranslationThreshold, gestureTranslation: 344 - 370 - bottomViewModel.t_dragTranslationThreshold) + expectedValues: (popupHeight: 370 + bottomViewModel.dragTranslationThreshold, gestureTranslation: 344 - 370 - bottomViewModel.dragTranslationThreshold) ) } - func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetentsLessThanDragValue_topPopupsAlignment() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(370)]) + func test_calculateValuesOnDragGestureChanged_withNegativeDragValue_whenDragDetentsLessThanDragValue_topPopupsAlignment() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 344, dragDetents: [.height(370)]) ] - appendPopupsAndCheckGestureTranslationOnChange( + await appendPopupsAndCheckGestureTranslationOnChange( viewModel: topViewModel, popups: popups, gestureValue: -133, @@ -1268,168 +1293,168 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckGestureTranslationOnChange(viewModel: ViewModel, popups: [AnyPopup], gestureValue: CGFloat, expectedValues: (popupHeight: CGFloat, gestureTranslation: CGFloat)) { - viewModel.t_updatePopupsValue(popups) - viewModel.t_updatePopupsValue(recalculatePopupHeights(viewModel)) - viewModel.t_onPopupDragGestureChanged(gestureValue) + func appendPopupsAndCheckGestureTranslationOnChange(viewModel: ViewModel, popups: [AnyPopup], gestureValue: CGFloat, expectedValues: (popupHeight: CGFloat, gestureTranslation: CGFloat)) async { + await viewModel.updatePopups(popups) + await updatePopups(viewModel) + await viewModel.onPopupDragGestureChanged(gestureValue) - XCTAssertEqual(viewModel.t_activePopupHeight, expectedValues.popupHeight) - XCTAssertEqual(viewModel.t_gestureTranslation, expectedValues.gestureTranslation) + XCTAssertEqual(viewModel.activePopupProperties.height, expectedValues.popupHeight) + XCTAssertEqual(viewModel.activePopupProperties.gestureTranslation, expectedValues.gestureTranslation) } } // MARK: On Drag Gesture Ended extension PopupVerticalStackViewModelTests { - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenNoDragDetents() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenNoDragDetents() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -200, expectedValues: (popupHeight: 344, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440)]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440)]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -200, expectedValues: (popupHeight: 440, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520)]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520)]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -120, expectedValues: (popupHeight: 520, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_3() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520)]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_3() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520)]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -42, expectedValues: (popupHeight: 440, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_4() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_4() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -300, expectedValues: (popupHeight: screen.height - screen.safeArea.top, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_5() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_bottomPopupsAlignment_5() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: -600, expectedValues: (popupHeight: screen.height, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_topPopupsAlignment_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_topPopupsAlignment_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: topViewModel, popups: popups, gestureValue: -300, expectedValues: (popupHeight: nil, shouldPopupBeDismissed: true) ) } - func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_topPopupsAlignment_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) + func test_calculateValuesOnDragGestureEnded_withNegativeDragValue_whenDragDetentsSet_topPopupsAlignment_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 344, dragDetents: [.height(440), .height(520), .large, .fullscreen]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: topViewModel, popups: popups, gestureValue: -15, expectedValues: (popupHeight: 344, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_bottomPopupsAlignment_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 400) + func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_bottomPopupsAlignment_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 400) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: 50, expectedValues: (popupHeight: 400, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_bottomPopupsAlignment_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: BottomPopupConfig.self, heightMode: .auto, popupHeight: 400) + func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_bottomPopupsAlignment_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .bottom, heightMode: .auto, popupHeight: 400) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: bottomViewModel, popups: popups, gestureValue: 300, expectedValues: (popupHeight: nil, shouldPopupBeDismissed: true) ) } - func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_1() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 400) + func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_1() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 400) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: topViewModel, popups: popups, gestureValue: 400, expectedValues: (popupHeight: 400, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_2() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 400, dragDetents: [.large]) + func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_2() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 400, dragDetents: [.large]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: topViewModel, popups: popups, gestureValue: 100, expectedValues: (popupHeight: 400, shouldPopupBeDismissed: false) ) } - func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_3() { - let popups = [ - createPopupInstanceForPopupHeightTests(type: TopPopupConfig.self, heightMode: .auto, popupHeight: 400, dragDetents: [.large]) + func test_calculateValuesOnDragGestureEnded_withPositiveDragValue_topPopupsAlignment_3() async { + let popups = await [ + createPopupInstanceForPopupHeightTests(alignment: .top, heightMode: .auto, popupHeight: 400, dragDetents: [.large]) ] - appendPopupsAndCheckGestureTranslationOnEnd( + await appendPopupsAndCheckGestureTranslationOnEnd( viewModel: topViewModel, popups: popups, gestureValue: 400, @@ -1438,15 +1463,14 @@ extension PopupVerticalStackViewModelTests { } } private extension PopupVerticalStackViewModelTests { - func appendPopupsAndCheckGestureTranslationOnEnd(viewModel: ViewModel, popups: [AnyPopup], gestureValue: CGFloat, expectedValues: (popupHeight: CGFloat?, shouldPopupBeDismissed: Bool)) { - viewModel.t_updatePopupsValue(popups) - viewModel.t_updatePopupsValue(recalculatePopupHeights(viewModel)) - viewModel.t_updateGestureTranslation(gestureValue) - viewModel.t_calculateAndUpdateTranslationProgress() - viewModel.t_onPopupDragGestureEnded(gestureValue) + func appendPopupsAndCheckGestureTranslationOnEnd(viewModel: ViewModel, popups: [AnyPopup], gestureValue: CGFloat, expectedValues: (popupHeight: CGFloat?, shouldPopupBeDismissed: Bool)) async { + await viewModel.updatePopups(popups) + await updatePopups(viewModel) + await viewModel.updateGestureTranslation(gestureValue) + await viewModel.onPopupDragGestureEnded(gestureValue) - XCTAssertEqual(viewModel.t_popups.count, expectedValues.shouldPopupBeDismissed ? 0 : 1) - XCTAssertEqual(viewModel.t_activePopupHeight, expectedValues.popupHeight) + XCTAssertEqual(viewModel.popups.count, expectedValues.shouldPopupBeDismissed ? 0 : 1) + XCTAssertEqual(viewModel.activePopupProperties.height, expectedValues.popupHeight) } } @@ -1458,33 +1482,32 @@ private extension PopupVerticalStackViewModelTests { // MARK: Methods private extension PopupVerticalStackViewModelTests { - func createPopupInstanceForPopupHeightTests(type: C.Type, heightMode: HeightMode, popupHeight: CGFloat, popupDragHeight: CGFloat? = nil, ignoredSafeAreaEdges: Edge.Set = [], popupPadding: EdgeInsets = .init(), cornerRadius: CGFloat = 0, dragGestureEnabled: Bool = true, dragDetents: [DragDetent] = []) -> AnyPopup { - let config = getConfigForPopupHeightTests(type: type, heightMode: heightMode, ignoredSafeAreaEdges: ignoredSafeAreaEdges, popupPadding: popupPadding, cornerRadius: cornerRadius, dragGestureEnabled: dragGestureEnabled, dragDetents: dragDetents) - - return AnyPopup.t_createNew(config: config) - .settingHeight(popupHeight) - .settingDragHeight(popupDragHeight) - } - func appendPopupsAndPerformChecks(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculatedValue: @escaping (ViewModel) -> (Value), expectedValueBuilder: @escaping (ViewModel) -> Value) { - viewModel.t_updatePopupsValue(popups) - viewModel.t_updatePopupsValue(recalculatePopupHeights(viewModel)) - viewModel.t_updateGestureTranslation(gestureTranslation) - - XCTAssertEqual(calculatedValue(viewModel), expectedValueBuilder(viewModel)) + func createPopupInstanceForPopupHeightTests(alignment: PopupAlignment, heightMode: HeightMode, popupHeight: CGFloat, popupDragHeight: CGFloat = 0, ignoredSafeAreaEdges: Edge.Set = [], popupPadding: EdgeInsets = .init(), cornerRadius: CGFloat = 0, dragGestureEnabled: Bool = true, dragDetents: [DragDetent] = []) async -> AnyPopup { + let popup = getTestPopup(alignment, heightMode, popupHeight, popupDragHeight, ignoredSafeAreaEdges, popupPadding, cornerRadius, dragGestureEnabled, dragDetents) + return await AnyPopup(popup) + .updatedID(UUID().uuidString) + .updatedHeight(popupHeight) + .updatedDragHeight(popupDragHeight) + } + func appendPopupsAndPerformChecks(viewModel: ViewModel, popups: [AnyPopup], gestureTranslation: CGFloat, calculatedValue: @escaping (ViewModel) async -> Value, expectedValueBuilder: @escaping (ViewModel) async -> Value) async { + await viewModel.updatePopups(popups) + await updatePopups(viewModel) + await viewModel.updateGestureTranslation(gestureTranslation) + + let calculatedValue = await calculatedValue(viewModel) + let expectedValue = await expectedValueBuilder(viewModel) + XCTAssertEqual(calculatedValue, expectedValue) } } private extension PopupVerticalStackViewModelTests { - func getConfigForPopupHeightTests(type: C.Type, heightMode: HeightMode, ignoredSafeAreaEdges: Edge.Set, popupPadding: EdgeInsets, cornerRadius: CGFloat, dragGestureEnabled: Bool, dragDetents: [DragDetent]) -> C { .t_createNew( - popupPadding: popupPadding, - cornerRadius: cornerRadius, - ignoredSafeAreaEdges: ignoredSafeAreaEdges, - heightMode: heightMode, - dragDetents: dragDetents, - isDragGestureEnabled: dragGestureEnabled - )} - func recalculatePopupHeights(_ viewModel: ViewModel) -> [AnyPopup] { viewModel.t_popups.map { - $0.settingHeight(viewModel.t_calculateHeight(heightCandidate: $0.height!, popupConfig: $0.config as! C)) + func getTestPopup(_ alignment: PopupAlignment, _ heightMode: HeightMode, _ popupHeight: CGFloat, _ popupDragHeight: CGFloat, _ ignoredSafeAreaEdges: Edge.Set, _ popupPadding: EdgeInsets, _ cornerRadius: CGFloat, _ dragGestureEnabled: Bool, _ dragDetents: [DragDetent]) -> any Popup { switch alignment { + case .top: TestTopPopup(popupPadding: popupPadding, cornerRadius: cornerRadius, ignoredSafeAreaEdges: ignoredSafeAreaEdges, heightMode: heightMode, dragDetents: dragDetents, dragGestureEnabled: dragGestureEnabled) + case .bottom: TestBottomPopup(popupPadding: popupPadding, cornerRadius: cornerRadius, ignoredSafeAreaEdges: ignoredSafeAreaEdges, heightMode: heightMode, dragDetents: dragDetents, dragGestureEnabled: dragGestureEnabled) + case .center: fatalError() }} + func updatePopups(_ viewModel: ViewModel) async { + for popup in viewModel.popups { await viewModel.updatePopupHeight(popup.height!, popup) } + } } // MARK: Screen @@ -1497,6 +1520,52 @@ private extension PopupVerticalStackViewModelTests { // MARK: Typealiases private extension PopupVerticalStackViewModelTests { - typealias Config = LocalConfig.Vertical typealias ViewModel = VM.VerticalStack } + +// MARK: Test Popups +private struct TestTopPopup: TopPopup { + let popupPadding: EdgeInsets + let cornerRadius: CGFloat + let ignoredSafeAreaEdges: Edge.Set + let heightMode: HeightMode + let dragDetents: [DragDetent] + let dragGestureEnabled: Bool + + + func configurePopup(config: TopPopupConfig) -> TopPopupConfig { config + .popupTopPadding(popupPadding.top) + .popupBottomPadding(popupPadding.bottom) + .popupHorizontalPadding(popupPadding.leading) + .cornerRadius(cornerRadius) + .ignoreSafeArea(edges: ignoredSafeAreaEdges) + .heightMode(heightMode) + .dragDetents(dragDetents) + .enableDragGesture(dragGestureEnabled) + } + var body: some View { EmptyView() } +} +private struct TestBottomPopup: BottomPopup { + let popupPadding: EdgeInsets + let cornerRadius: CGFloat + let ignoredSafeAreaEdges: Edge.Set + let heightMode: HeightMode + let dragDetents: [DragDetent] + let dragGestureEnabled: Bool + + + func configurePopup(config: BottomPopupConfig) -> BottomPopupConfig { config + .popupTopPadding(popupPadding.top) + .popupBottomPadding(popupPadding.bottom) + .popupHorizontalPadding(popupPadding.leading) + .cornerRadius(cornerRadius) + .ignoreSafeArea(edges: ignoredSafeAreaEdges) + .heightMode(heightMode) + .dragDetents(dragDetents) + .enableDragGesture(dragGestureEnabled) + } + var body: some View { EmptyView() } +} + +// MARK: Others +extension VM.VerticalStack: @unchecked Sendable {}