diff --git a/MijickPopupView.podspec b/MijickPopupView.podspec
index 8ad2b7ab5e..2d63b25d64 100644
--- a/MijickPopupView.podspec
+++ b/MijickPopupView.podspec
@@ -5,7 +5,7 @@ Pod::Spec.new do |s|
PopupView is a free and open-source library dedicated for SwiftUI that makes the process of presenting popups easier and much cleaner.
DESC
- s.version = '2.5.0'
+ s.version = '2.5.1'
s.ios.deployment_target = '14.0'
s.osx.deployment_target = '12.0'
s.swift_version = '5.0'
diff --git a/README.md b/README.md
index 17b82e81c3..c1bcb8c899 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,10 @@
Try demo we prepared
+ |
+ Roadmap
+ |
+ Propose a new feature
@@ -105,11 +109,49 @@ Installation steps:
# Usage
### 1. Setup library
-Inside your `@main` structure call the `implementPopupView` method. It takes the optional argument - *config*, that can be used to configure some modifiers for all popups in the application.
+The library can be initialised in either of two ways:
+1. **DOES NOT WORK with SwiftUI sheets**
Inside your @main structure call the implementPopupView method. It takes the optional argument - config, that can be used to configure some modifiers for all popups in the application.
```Swift
- var body: some Scene {
+@main struct PopupView_Main: App {
+ var body: some Scene {
WindowGroup(content: ContentView().implementPopupView)
- }
+ }
+}
+```
+2. **WORKS with SwiftUI sheets. Only for iOS**
+Declare an AppDelegate class conforming to UIApplicationDelegate and add it to the @main structure.
+```Swift
+@main struct PopupView_Main: App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+
+ var body: some Scene { WindowGroup(content: ContentView.init) }
+}
+
+class AppDelegate: NSObject, UIApplicationDelegate {
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+ let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
+ sceneConfig.delegateClass = CustomPopupSceneDelegate.self
+ return sceneConfig
+ }
+}
+
+class CustomPopupSceneDelegate: PopupSceneDelegate {
+ override init() {
+ super.init()
+ config = { $0
+ .top { $0
+ .cornerRadius(24)
+ .dragGestureEnabled(true)
+ }
+ .centre { $0
+ .tapOutsideToDismiss(false)
+ }
+ .bottom { $0
+ .stackLimit(5)
+ }
+ }
+ }
+}
```
### 2. Declare a structure of your popup
diff --git a/Sources/Internal/Extensions/View.ScreenManager++.swift b/Sources/Internal/Extensions/View.ScreenManager++.swift
index 4a86f8aa81..6e985e9980 100644
--- a/Sources/Internal/Extensions/View.ScreenManager++.swift
+++ b/Sources/Internal/Extensions/View.ScreenManager++.swift
@@ -13,10 +13,9 @@ import SwiftUI
extension View {
func updateScreenSize() -> some View { GeometryReader { reader in
- frame(maxWidth: .infinity, maxHeight: .infinity)
+ frame(width: reader.size.width, height: reader.size.height).frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { ScreenManager.update(reader) }
- .onChange(of: reader.size) { _ in ScreenManager.update(reader) }
- .onChange(of: reader.safeAreaInsets) { _ in ScreenManager.update(reader) }
+ .onChange(of: reader.frame(in: .global)) { _ in ScreenManager.update(reader) }
}}
}
fileprivate extension ScreenManager {
diff --git a/Sources/Internal/Managers/KeyboardManager.swift b/Sources/Internal/Managers/KeyboardManager.swift
index 6df6adf15c..f076add7c1 100644
--- a/Sources/Internal/Managers/KeyboardManager.swift
+++ b/Sources/Internal/Managers/KeyboardManager.swift
@@ -21,12 +21,15 @@ class KeyboardManager: ObservableObject {
private init() { subscribeToKeyboardEvents() }
}
extension KeyboardManager {
- static func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
+ static func hideKeyboard() { DispatchQueue.main.async {
+ UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+ }}
}
private extension KeyboardManager {
func subscribeToKeyboardEvents() {
Publishers.Merge(getKeyboardWillOpenPublisher(), createKeyboardWillHidePublisher())
+ .receive(on: DispatchQueue.main)
.sink { self.height = $0 }
.store(in: &subscription)
}
diff --git a/Sources/Internal/Managers/PopupManager.swift b/Sources/Internal/Managers/PopupManager.swift
index e670ee36cf..c99e528d9d 100644
--- a/Sources/Internal/Managers/PopupManager.swift
+++ b/Sources/Internal/Managers/PopupManager.swift
@@ -13,8 +13,8 @@ import SwiftUI
public class PopupManager: ObservableObject {
@Published private(set) var views: [any Popup] = []
private(set) var presenting: Bool = true
- private(set) var popupsWithoutOverlay: [String] = []
- private(set) var popupsToBeDismissed: [String: DispatchSourceTimer] = [:]
+ private(set) var popupsWithoutOverlay: [ID] = []
+ private(set) var popupsToBeDismissed: [ID: DispatchSourceTimer] = [:]
static let shared: PopupManager = .init()
private init() {}
@@ -23,7 +23,7 @@ public class PopupManager: ObservableObject {
// MARK: - Operations
enum StackOperation {
case insertAndReplace(any Popup), insertAndStack(any Popup)
- case removeLast, remove(id: String), removeAllUpTo(id: String), removeAll
+ case removeLast, remove(ID), removeAllUpTo(ID), removeAll
}
extension PopupManager {
static func performOperation(_ operation: StackOperation) { DispatchQueue.main.async {
@@ -31,12 +31,12 @@ extension PopupManager {
updateOperationType(operation)
shared.views.perform(operation)
}}
- static func dismissPopupAfter(_ popup: any Popup, _ seconds: Double) { shared.popupsToBeDismissed[popup.id] = DispatchSource.createAction(deadline: seconds) { performOperation(.remove(id: popup.id)) } }
+ static func dismissPopupAfter(_ popup: any Popup, _ seconds: Double) { shared.popupsToBeDismissed[popup.id] = DispatchSource.createAction(deadline: seconds) { performOperation(.remove(popup.id)) } }
static func hideOverlay(_ popup: any Popup) { shared.popupsWithoutOverlay.append(popup.id) }
}
private extension PopupManager {
static func removePopupFromStackToBeDismissed(_ operation: StackOperation) { switch operation {
- case .removeLast: shared.popupsToBeDismissed.removeValue(forKey: shared.views.last?.id ?? "")
+ case .removeLast: shared.popupsToBeDismissed.removeValue(forKey: shared.views.last?.id ?? .init())
case .remove(let id): shared.popupsToBeDismissed.removeValue(forKey: id)
case .removeAllUpTo, .removeAll: shared.popupsToBeDismissed.removeAll()
default: break
@@ -60,12 +60,12 @@ private extension [any Popup] {
case .insertAndReplace(let popup): replaceLast(popup, if: canBeInserted(popup))
case .insertAndStack(let popup): append(popup, if: canBeInserted(popup))
case .removeLast: removeLast()
- case .remove(let id): removeAll(where: { $0.id == id })
- case .removeAllUpTo(let id): removeAllUpToElement(where: { $0.id == id })
+ case .remove(let id): removeAll(where: { $0.id ~= id })
+ case .removeAllUpTo(let id): removeAllUpToElement(where: { $0.id ~= id })
case .removeAll: removeAll()
}
}
}
private extension [any Popup] {
- func canBeInserted(_ popup: some Popup) -> Bool { !contains(where: { $0.id == popup.id }) }
+ func canBeInserted(_ popup: some Popup) -> Bool { !contains(where: { $0.id ~= popup.id }) }
}
diff --git a/Sources/Internal/Other/AnyPopup.swift b/Sources/Internal/Other/AnyPopup.swift
index 0070d48bfe..6ea1918c86 100644
--- a/Sources/Internal/Other/AnyPopup.swift
+++ b/Sources/Internal/Other/AnyPopup.swift
@@ -11,7 +11,7 @@
import SwiftUI
struct AnyPopup: Popup, Hashable {
- let id: String
+ let id: ID
private let _body: AnyView
private let _configBuilder: (Config) -> Config
diff --git a/Sources/Internal/Other/ID.swift b/Sources/Internal/Other/ID.swift
new file mode 100644
index 0000000000..b25f3bd3ed
--- /dev/null
+++ b/Sources/Internal/Other/ID.swift
@@ -0,0 +1,43 @@
+//
+// ID.swift of PopupView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2024 Mijick. Licensed under MIT License.
+
+
+import Foundation
+
+public struct ID {
+ let value: String
+}
+
+// MARK: - Initialisers
+extension ID {
+ init(_ object: P) { self.init(P.self) }
+ init(_ type: P.Type) { self.value = .init(describing: P.self) + Self.separator + .init(describing: Date()) }
+ init() { self.value = "" }
+}
+
+// MARK: - Equatable
+extension ID: Equatable {
+ public static func ==(lhs: Self, rhs: Self) -> Bool { lhs.value == rhs.value }
+ public static func ~=(lhs: Self, rhs: Self) -> Bool { getComponent(lhs) == getComponent(rhs) }
+}
+
+// MARK: - Hashing
+extension ID: Hashable {
+ public func hash(into hasher: inout Hasher) { hasher.combine(Self.getComponent(self)) }
+}
+
+
+// MARK: - Helpers
+private extension ID {
+ static func getComponent(_ object: Self) -> String { object.value.components(separatedBy: separator).first ?? "" }
+}
+private extension ID {
+ static var separator: String { "/{}/" }
+}
diff --git a/Sources/Internal/Protocols/Popup.swift b/Sources/Internal/Protocols/Popup.swift
index 9db184f70e..26c5bb857c 100644
--- a/Sources/Internal/Protocols/Popup.swift
+++ b/Sources/Internal/Protocols/Popup.swift
@@ -14,13 +14,13 @@ public protocol Popup: View {
associatedtype Config: Configurable
associatedtype V: View
- var id: String { get }
+ var id: ID { get }
func createContent() -> V
func configurePopup(popup: Config) -> Config
}
public extension Popup {
- var id: String { .init(describing: Self.self) }
+ var id: ID { .init(self) }
var body: V { createContent() }
func configurePopup(popup: Config) -> Config { popup }
@@ -28,5 +28,5 @@ public extension Popup {
// MARK: - Helpers
extension Popup {
- func remove() { PopupManager.performOperation(.remove(id: id)) }
+ func remove() { PopupManager.performOperation(.remove(id)) }
}
diff --git a/Sources/Internal/Protocols/PopupStack.swift b/Sources/Internal/Protocols/PopupStack.swift
index af3ad0317e..0927bac4b1 100644
--- a/Sources/Internal/Protocols/PopupStack.swift
+++ b/Sources/Internal/Protocols/PopupStack.swift
@@ -14,7 +14,7 @@ protocol PopupStack: View {
associatedtype Config: Configurable
var items: [AnyPopup] { get }
- var heights: [String: CGFloat] { get }
+ var heights: [ID: CGFloat] { get }
var globalConfig: GlobalConfig { get }
var gestureTranslation: CGFloat { get }
var isGestureActive: Bool { get }
@@ -29,7 +29,7 @@ protocol PopupStack: View {
var tapOutsideClosesPopup: Bool { get }
}
extension PopupStack {
- var heights: [String: CGFloat] { [:] }
+ var heights: [ID: CGFloat] { [:] }
var gestureTranslation: CGFloat { 0 }
var isGestureActive: Bool { false }
var translationProgress: CGFloat { 1 }
diff --git a/Sources/Internal/Views/PopupBottomStackView.swift b/Sources/Internal/Views/PopupBottomStackView.swift
index befa5a8dff..bc76948112 100644
--- a/Sources/Internal/Views/PopupBottomStackView.swift
+++ b/Sources/Internal/Views/PopupBottomStackView.swift
@@ -14,7 +14,7 @@ struct PopupBottomStackView: PopupStack {
let items: [AnyPopup]
let globalConfig: GlobalConfig
@State var gestureTranslation: CGFloat = 0
- @State var heights: [String: CGFloat] = [:]
+ @State var heights: [ID: CGFloat] = [:]
@GestureState var isGestureActive: Bool = false
@ObservedObject private var screenManager: ScreenManager = .shared
@ObservedObject private var keyboardManager: KeyboardManager = .shared
diff --git a/Sources/Internal/Views/PopupTopStackView.swift b/Sources/Internal/Views/PopupTopStackView.swift
index 390adf8bda..579b21887e 100644
--- a/Sources/Internal/Views/PopupTopStackView.swift
+++ b/Sources/Internal/Views/PopupTopStackView.swift
@@ -14,7 +14,7 @@ struct PopupTopStackView: PopupStack {
let items: [AnyPopup]
let globalConfig: GlobalConfig
@State var gestureTranslation: CGFloat = 0
- @State var heights: [String: CGFloat] = [:]
+ @State var heights: [ID: CGFloat] = [:]
@GestureState var isGestureActive: Bool = false
@ObservedObject private var screenManager: ScreenManager = .shared
diff --git a/Sources/Internal/Views/PopupView.swift b/Sources/Internal/Views/PopupView.swift
index eb8886f07c..7034493ef1 100644
--- a/Sources/Internal/Views/PopupView.swift
+++ b/Sources/Internal/Views/PopupView.swift
@@ -91,7 +91,7 @@ private extension PopupView {
func isOverlayActive(_ type: P.Type) -> Bool { popupManager.views.last is P && !shouldOverlayBeHiddenForCurrentPopup }
}
private extension PopupView {
- var shouldOverlayBeHiddenForCurrentPopup: Bool { popupManager.popupsWithoutOverlay.contains(popupManager.views.last?.id ?? "") }
+ var shouldOverlayBeHiddenForCurrentPopup: Bool { popupManager.popupsWithoutOverlay.contains(popupManager.views.last?.id ?? .init()) }
}
private extension PopupView {
diff --git a/Sources/Public/Extensions/Public+PopupManager.swift b/Sources/Public/Extensions/Public+PopupManager.swift
index 09f6df2f8d..9d332d1b60 100644
--- a/Sources/Public/Extensions/Public+PopupManager.swift
+++ b/Sources/Public/Extensions/Public+PopupManager.swift
@@ -23,10 +23,10 @@ public extension PopupManager {
static func dismiss() { performOperation(.removeLast) }
/// Dismisses all popups of provided type on the stack.
- static func dismiss(_ popup: P.Type) { performOperation(.remove(id: .init(describing: popup))) }
+ static func dismiss(_ popup: P.Type) { performOperation(.remove(ID(popup))) }
/// Dismisses all popups on the stack up to the popup with the selected type
- static func dismissAll(upTo popup: P.Type) { performOperation(.removeAllUpTo(id: .init(describing: popup))) }
+ static func dismissAll(upTo popup: P.Type) { performOperation(.removeAllUpTo(ID(popup))) }
/// Dismisses all the popups on the stack.
static func dismissAll() { performOperation(.removeAll) }
diff --git a/Sources/Public/Public+PopupSceneDelegate.swift b/Sources/Public/Public+PopupSceneDelegate.swift
new file mode 100644
index 0000000000..e7892825f6
--- /dev/null
+++ b/Sources/Public/Public+PopupSceneDelegate.swift
@@ -0,0 +1,44 @@
+//
+// Public+PopupSceneDelegate.swift of PopupView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2024 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+#if os(iOS)
+open class PopupSceneDelegate: NSObject, UIWindowSceneDelegate {
+ open var window: UIWindow?
+ open var config: (GlobalConfig) -> (GlobalConfig) = { _ in .init() }
+}
+
+// MARK: - Creating Popup Scene
+extension PopupSceneDelegate {
+ open func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene {
+ let hostingController = UIHostingController(rootView: Rectangle()
+ .fill(Color.clear)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .implementPopupView(config: config)
+ )
+ hostingController.view.backgroundColor = .clear
+
+ window = Window(windowScene: windowScene)
+ window?.rootViewController = hostingController
+ window?.isHidden = false
+ }}
+}
+
+
+// MARK: - Helpers
+fileprivate class Window: UIWindow {
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ guard let view = super.hitTest(point, with: event) else { return nil }
+ return rootViewController?.view == view ? nil : view
+ }
+}
+#endif