From 56c06dbb2c42d27aa8335e978a5e6a93148e10f9 Mon Sep 17 00:00:00 2001 From: "Tomasz K." Date: Tue, 30 Jul 2024 18:08:30 +0200 Subject: [PATCH] Patch 2.5.1 fix: - Fixed a problem with native sheets covering popups (#110) - Fixed a problem with popups retaining previous @State object (#101) - Fixed a problem with KeyboardManager publishing its state outside the main thread (#120) - Fixed a problem with PopupView that was changing the position of elements (#122) --- MijickPopupView.podspec | 2 +- README.md | 48 +++++++++++++++++-- .../Extensions/View.ScreenManager++.swift | 5 +- .../Internal/Managers/KeyboardManager.swift | 5 +- Sources/Internal/Managers/PopupManager.swift | 16 +++---- Sources/Internal/Other/AnyPopup.swift | 2 +- Sources/Internal/Other/ID.swift | 43 +++++++++++++++++ Sources/Internal/Protocols/Popup.swift | 6 +-- Sources/Internal/Protocols/PopupStack.swift | 4 +- .../Internal/Views/PopupBottomStackView.swift | 2 +- .../Internal/Views/PopupTopStackView.swift | 2 +- Sources/Internal/Views/PopupView.swift | 2 +- .../Extensions/Public+PopupManager.swift | 4 +- .../Public/Public+PopupSceneDelegate.swift | 44 +++++++++++++++++ 14 files changed, 158 insertions(+), 27 deletions(-) create mode 100644 Sources/Internal/Other/ID.swift create mode 100644 Sources/Public/Public+PopupSceneDelegate.swift 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