From f52e491183084c5c3b3f80df10429492ff3a0690 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 23 Mar 2022 17:57:07 +0100 Subject: [PATCH 1/2] Support for multiple SwiftEntryKit instances working in parallel --- Source/Infra/EKContentView.swift | 3 +- Source/Infra/EKEntryView.swift | 13 +- Source/Infra/EKRootViewController.swift | 6 +- Source/Infra/EKWindow.swift | 4 +- Source/Infra/EKWindowProvider.swift | 19 ++- Source/SwiftEntryKit.swift | 151 ++++++++++++++++++++---- 6 files changed, 158 insertions(+), 38 deletions(-) diff --git a/Source/Infra/EKContentView.swift b/Source/Infra/EKContentView.swift index 9ca94961..ffc8b6b3 100644 --- a/Source/Infra/EKContentView.swift +++ b/Source/Infra/EKContentView.swift @@ -12,6 +12,7 @@ protocol EntryContentViewDelegate: AnyObject { func changeToActive(withAttributes attributes: EKAttributes) func changeToInactive(withAttributes attributes: EKAttributes, pushOut: Bool) func didFinishDisplaying(entry: EKEntryView, keepWindowActive: Bool, dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?) + var safeAreaInsets: UIEdgeInsets { get } } class EKContentView: UIView { @@ -119,7 +120,7 @@ class EKContentView: UIView { // Define a spacer to catch top / bottom offsets var spacerView: UIView! - let safeAreaInsets = EKWindowProvider.safeAreaInsets + let safeAreaInsets = entryDelegate.safeAreaInsets let overrideSafeArea = attributes.positionConstraints.safeArea.isOverridden if !overrideSafeArea && safeAreaInsets.hasVerticalInsets && !attributes.position.isCenter { diff --git a/Source/Infra/EKEntryView.swift b/Source/Infra/EKEntryView.swift index 0687b6e7..cc3c0405 100644 --- a/Source/Infra/EKEntryView.swift +++ b/Source/Infra/EKEntryView.swift @@ -8,6 +8,10 @@ import UIKit +protocol EntryViewDelegate: AnyObject { + var safeAreaInsets: UIEdgeInsets { get } +} + class EKEntryView: EKStyleView { struct Content { @@ -28,6 +32,8 @@ class EKEntryView: EKStyleView { } // MARK: Props + + private weak var delegate: EntryViewDelegate! /** Background view */ private var backgroundView: EKBackgroundView! @@ -53,8 +59,9 @@ class EKEntryView: EKStyleView { }() // MARK: Setup - init(newEntry content: Content) { + init(newEntry content: Content, delegate: EntryViewDelegate) { self.content = content + self.delegate = delegate super.init(frame: UIScreen.main.bounds) setupContentView() applyDropShadow() @@ -161,9 +168,9 @@ class EKEntryView: EKStyleView { var bottomInset: CGFloat = 0 switch attributes.position { case .top: - topInset = -EKWindowProvider.safeAreaInsets.top + topInset = -delegate.safeAreaInsets.top case .bottom, .center: - bottomInset = EKWindowProvider.safeAreaInsets.bottom + bottomInset = delegate.safeAreaInsets.bottom } backgroundView.layoutToSuperview(.top, offset: topInset) diff --git a/Source/Infra/EKRootViewController.swift b/Source/Infra/EKRootViewController.swift index 738ad09c..e629d106 100644 --- a/Source/Infra/EKRootViewController.swift +++ b/Source/Infra/EKRootViewController.swift @@ -11,6 +11,7 @@ import UIKit protocol EntryPresenterDelegate: AnyObject { var isResponsiveToTouches: Bool { set get } func displayPendingEntryOrRollbackWindow(dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?) + var safeAreaInsets: UIEdgeInsets { get } } class EKRootViewController: UIViewController { @@ -213,7 +214,10 @@ extension EKRootViewController { // MARK: - EntryScrollViewDelegate extension EKRootViewController: EntryContentViewDelegate { - + var safeAreaInsets: UIEdgeInsets { + delegate.safeAreaInsets + } + func didFinishDisplaying(entry: EKEntryView, keepWindowActive: Bool, dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?) { guard !isDisplaying else { return diff --git a/Source/Infra/EKWindow.swift b/Source/Infra/EKWindow.swift index d7cd1280..23d33652 100644 --- a/Source/Infra/EKWindow.swift +++ b/Source/Infra/EKWindow.swift @@ -9,7 +9,7 @@ import UIKit class EKWindow: UIWindow { - + var isAbleToReceiveTouches = false init(with rootVC: UIViewController) { @@ -37,7 +37,7 @@ class EKWindow: UIWindow { return super.hitTest(point, with: event) } - guard let rootVC = EKWindowProvider.shared.rootVC else { + guard let rootVC = rootViewController as? EKRootViewController else { return nil } diff --git a/Source/Infra/EKWindowProvider.swift b/Source/Infra/EKWindowProvider.swift index 5cd4e00a..8598c513 100644 --- a/Source/Infra/EKWindowProvider.swift +++ b/Source/Infra/EKWindowProvider.swift @@ -8,21 +8,18 @@ import UIKit -final class EKWindowProvider: EntryPresenterDelegate { - +final class EKWindowProvider: EntryPresenterDelegate, EntryViewDelegate { + /** The artificial safe area insets */ - static var safeAreaInsets: UIEdgeInsets { + var safeAreaInsets: UIEdgeInsets { if #available(iOS 11.0, *) { - return EKWindowProvider.shared.entryWindow?.rootViewController?.view?.safeAreaInsets ?? UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets ?? .zero + return entryWindow?.rootViewController?.view?.safeAreaInsets ?? UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets ?? .zero } else { let statusBarMaxY = UIApplication.shared.statusBarFrame.maxY return UIEdgeInsets(top: statusBarMaxY, left: 0, bottom: 10, right: 0) } } - - /** Single access point */ - static let shared = EKWindowProvider() - + /** Current entry window */ var entryWindow: EKWindow! @@ -43,7 +40,7 @@ final class EKWindowProvider: EntryPresenterDelegate { private weak var entryView: EKEntryView! /** Cannot be instantiated, customized, inherited */ - private init() {} + init() {} var isResponsiveToTouches: Bool { set { @@ -139,13 +136,13 @@ final class EKWindowProvider: EntryPresenterDelegate { /** Display a view using attributes */ func display(view: UIView, using attributes: EKAttributes, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) { - let entryView = EKEntryView(newEntry: .init(view: view, attributes: attributes)) + let entryView = EKEntryView(newEntry: .init(view: view, attributes: attributes), delegate: self) display(entryView: entryView, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) } /** Display a view controller using attributes */ func display(viewController: UIViewController, using attributes: EKAttributes, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) { - let entryView = EKEntryView(newEntry: .init(viewController: viewController, attributes: attributes)) + let entryView = EKEntryView(newEntry: .init(viewController: viewController, attributes: attributes), delegate: self) display(entryView: entryView, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) } diff --git a/Source/SwiftEntryKit.swift b/Source/SwiftEntryKit.swift index 4d57291b..3517dc5d 100644 --- a/Source/SwiftEntryKit.swift +++ b/Source/SwiftEntryKit.swift @@ -43,10 +43,24 @@ public final class SwiftEntryKit { /** Completion handler for the dismissal method */ public typealias DismissCompletionHandler = () -> Void + + /// Shared instance, used by class functions + public static let shared = SwiftEntryKit() + + let windowProvider = EKWindowProvider() - /** Cannot be instantiated, customized, inherited. */ - private init() {} - + public init() {} + + /** + Returns the window that displays the entry. + **Warning**: the returned `UIWindow` instance is `nil` in case + no entry is currently displayed. + This can be used + */ + public var window: UIWindow? { + return windowProvider.entryWindow + } + /** Returns the window that displays the entry. **Warning**: the returned `UIWindow` instance is `nil` in case @@ -54,7 +68,16 @@ public final class SwiftEntryKit { This can be used */ public class var window: UIWindow? { - return EKWindowProvider.shared.entryWindow + return shared.window + } + + /** + Returns true if **any** entry is currently displayed. + - Not thread safe - should be called from the main queue only in order to receive a reliable result. + - Convenience computed variable. Using it is the same as invoking **isCurrentlyDisplaying() -> Bool** (witohut the name of the entry). + */ + public var isCurrentlyDisplaying: Bool { + return isCurrentlyDisplaying() } /** @@ -63,7 +86,18 @@ public final class SwiftEntryKit { - Convenience computed variable. Using it is the same as invoking **isCurrentlyDisplaying() -> Bool** (witohut the name of the entry). */ public class var isCurrentlyDisplaying: Bool { - return isCurrentlyDisplaying() + return Self.isCurrentlyDisplaying() + } + + /** + Returns true if an entry with a given name is currently displayed. + - Not thread safe - should be called from the main queue only in order to receive a reliable result. + - If invoked with *name* = *nil* or without the parameter value, it will return *true* if **any** entry is currently displayed. + - Returns a *false* value for currently enqueued entries. + - parameter name: The name of the entry. Its default value is *nil*. + */ + public func isCurrentlyDisplaying(entryNamed name: String? = nil) -> Bool { + return windowProvider.isCurrentlyDisplaying(entryNamed: name) } /** @@ -74,9 +108,18 @@ public final class SwiftEntryKit { - parameter name: The name of the entry. Its default value is *nil*. */ public class func isCurrentlyDisplaying(entryNamed name: String? = nil) -> Bool { - return EKWindowProvider.shared.isCurrentlyDisplaying(entryNamed: name) + return shared.isCurrentlyDisplaying(entryNamed: name) } - + + /** + Returns true if **any** entry is currently enqueued and waiting to be displayed. + - Not thread safe - should be called from the main queue only in order to receive a reliable result. + - Convenience computed variable. Using it is the same as invoking **~queueContains() -> Bool** (witohut the name of the entry) + */ + public var isQueueEmpty: Bool { + return !queueContains() + } + /** Returns true if **any** entry is currently enqueued and waiting to be displayed. - Not thread safe - should be called from the main queue only in order to receive a reliable result. @@ -85,7 +128,17 @@ public final class SwiftEntryKit { public class var isQueueEmpty: Bool { return !queueContains() } - + + /** + Returns true if an entry with a given name is currently enqueued and waiting to be displayed. + - Not thread safe - should be called from the main queue only in order to receive a reliable result. + - If invoked with *name* = *nil* or without the parameter value, it will return *true* if **any** entry is currently displayed, meaning, the queue is not currently empty. + - parameter name: The name of the entry. Its default value is *nil*. + */ + public func queueContains(entryNamed name: String? = nil) -> Bool { + return windowProvider.queueContains(entryNamed: name) + } + /** Returns true if an entry with a given name is currently enqueued and waiting to be displayed. - Not thread safe - should be called from the main queue only in order to receive a reliable result. @@ -93,7 +146,22 @@ public final class SwiftEntryKit { - parameter name: The name of the entry. Its default value is *nil*. */ public class func queueContains(entryNamed name: String? = nil) -> Bool { - return EKWindowProvider.shared.queueContains(entryNamed: name) + return shared.queueContains(entryNamed: name) + } + + /** + Displays a given entry view using an attributes struct. + - A thread-safe method - Can be invokes from any thread + - A class method - Should be called on the class + - parameter view: Custom view that is to be displayed + - parameter attributes: Display properties + - parameter presentInsideKeyWindow: Indicates whether the entry window should become the key window. + - parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* which is the app main window + */ + public func display(entry view: UIView, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) { + DispatchQueue.main.async { + self.windowProvider.display(view: view, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) + } } /** @@ -106,11 +174,24 @@ public final class SwiftEntryKit { - parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* which is the app main window */ public class func display(entry view: UIView, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) { + shared.display(entry: view, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) + } + + /** + Displays a given entry view controller using an attributes struct. + - A thread-safe method - Can be invokes from any thread + - A class method - Should be called on the class + - parameter view: Custom view that is to be displayed + - parameter attributes: Display properties + - parameter presentInsideKeyWindow: Indicates whether the entry window should become the key window. + - parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* - which is the app main window + */ + public func display(entry viewController: UIViewController, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) { DispatchQueue.main.async { - EKWindowProvider.shared.display(view: view, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) + self.windowProvider.display(viewController: viewController, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) } } - + /** Displays a given entry view controller using an attributes struct. - A thread-safe method - Can be invokes from any thread @@ -121,8 +202,19 @@ public final class SwiftEntryKit { - parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* - which is the app main window */ public class func display(entry viewController: UIViewController, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) { + shared.display(entry: viewController, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) + } + + /** + ALPHA FEATURE: Transform the previous entry to the current one using the previous attributes struct. + - A thread-safe method - Can be invoked from any thread. + - A class method - Should be called on the class. + - This feature hasn't been fully tested. Use with caution. + - parameter view: Custom view that is to be displayed instead of the currently displayed entry + */ + public func transform(to view: UIView) { DispatchQueue.main.async { - EKWindowProvider.shared.display(viewController: viewController, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow) + self.windowProvider.transform(to: view) } } @@ -134,8 +226,19 @@ public final class SwiftEntryKit { - parameter view: Custom view that is to be displayed instead of the currently displayed entry */ public class func transform(to view: UIView) { + shared.transform(to: view) + } + + /** + Dismisses the currently presented entry and removes the presented window instance after the exit animation is concluded. + - A thread-safe method - Can be invoked from any thread. + - A class method - Should be called on the class. + - parameter descriptor: A descriptor for the entries that are to be dismissed. The default value is *.displayed*. + - parameter completion: A completion handler that is to be called right after the entry is dismissed (After the animation is concluded). + */ + public func dismiss(_ descriptor: EntryDismissalDescriptor = .displayed, with completion: DismissCompletionHandler? = nil) { DispatchQueue.main.async { - EKWindowProvider.shared.transform(to: view) + self.windowProvider.dismiss(descriptor, with: completion) } } @@ -147,24 +250,32 @@ public final class SwiftEntryKit { - parameter completion: A completion handler that is to be called right after the entry is dismissed (After the animation is concluded). */ public class func dismiss(_ descriptor: EntryDismissalDescriptor = .displayed, with completion: DismissCompletionHandler? = nil) { - DispatchQueue.main.async { - EKWindowProvider.shared.dismiss(descriptor, with: completion) - } + shared.dismiss(descriptor, with: completion) } - + /** Layout the view hierarchy that is rooted in the window. - In case you use complex animations, you can call it to refresh the AutoLayout mechanism on the entire view hierarchy. - A thread-safe method - Can be invoked from any thread. - A class method - Should be called on the class. */ - public class func layoutIfNeeded() { + public func layoutIfNeeded() { if Thread.isMainThread { - EKWindowProvider.shared.layoutIfNeeded() + windowProvider.layoutIfNeeded() } else { DispatchQueue.main.async { - EKWindowProvider.shared.layoutIfNeeded() + self.windowProvider.layoutIfNeeded() } } } + + /** + Layout the view hierarchy that is rooted in the window. + - In case you use complex animations, you can call it to refresh the AutoLayout mechanism on the entire view hierarchy. + - A thread-safe method - Can be invoked from any thread. + - A class method - Should be called on the class. + */ + public class func layoutIfNeeded() { + shared.layoutIfNeeded() + } } From 6154da0ff6a24ca05c890b6523fa1fff99238d5d Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Tue, 31 May 2022 10:15:53 +0200 Subject: [PATCH 2/2] Fix flickering when switching between windows --- Source/Infra/EKWindowProvider.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Infra/EKWindowProvider.swift b/Source/Infra/EKWindowProvider.swift index 8598c513..61a39a17 100644 --- a/Source/Infra/EKWindowProvider.swift +++ b/Source/Infra/EKWindowProvider.swift @@ -156,12 +156,10 @@ final class EKWindowProvider: EntryPresenterDelegate, EntryViewDelegate { switch rollbackWindow! { case .main: if let mainRollbackWindow = mainRollbackWindow { - mainRollbackWindow.makeKeyAndVisible() - } else { - UIApplication.shared.keyWindow?.makeKeyAndVisible() + mainRollbackWindow.becomeKey() } case .custom(window: let window): - window.makeKeyAndVisible() + window.becomeKey() } }