diff --git a/Sources/FluidCore/SafeAreaFinder.swift b/Sources/FluidCore/SafeAreaFinder.swift index 2c14d725e..60b352d13 100644 --- a/Sources/FluidCore/SafeAreaFinder.swift +++ b/Sources/FluidCore/SafeAreaFinder.swift @@ -12,32 +12,32 @@ import UIKit public final class SafeAreaFinder: NSObject { public static let notificationName = Notification.Name(rawValue: "app.muukii.fluid.SafeAreaInsetsManager") - - @MainActor - public static let shared = SafeAreaFinder() + + @available(iOS 13.0, *) + public weak var windowScene: UIWindowScene? private var currentInsets: UIEdgeInsets? = nil - private var referenceCounter: Int = 0 { - didSet { - if referenceCounter > 0 { - currentDisplayLink.isPaused = false - } else { - currentDisplayLink.isPaused = true - } - } - } + private var isRunning: Bool = false - private nonisolated(unsafe) var currentDisplayLink: CADisplayLink! + private nonisolated(unsafe) var currentDisplayLink: CADisplayLink? - private override init() { + @available(iOS 13.0, *) + public init(windowScene: UIWindowScene?) { + self.windowScene = windowScene super.init() + } + + private func setUpDisplayLink() { + guard currentDisplayLink == nil else { + return + } currentDisplayLink = .init(target: self, selector: #selector(handle)) - currentDisplayLink.preferredFramesPerSecond = 1 - currentDisplayLink.add(to: .main, forMode: .default) - currentDisplayLink.isPaused = true + currentDisplayLink?.preferredFramesPerSecond = 1 + currentDisplayLink?.add(to: .main, forMode: .default) + currentDisplayLink?.isPaused = false } public func request() { @@ -46,23 +46,47 @@ public final class SafeAreaFinder: NSObject { } public func start() { - referenceCounter += 1 + guard isRunning == false else { + request() + return + } + + isRunning = true + setUpDisplayLink() request() } + /// Stops polling and releases the display link so the finder can deallocate when its owner goes away. + public func stop() { + guard isRunning || currentDisplayLink != nil else { + return + } + + isRunning = false + currentInsets = nil + currentDisplayLink?.invalidate() + currentDisplayLink = nil + } + + /// Stops polling. Kept as a compatibility alias for older callers that used the reference-counted API. public func pause() { - referenceCounter -= 1 + stop() } deinit { - currentDisplayLink?.isPaused = true currentDisplayLink?.invalidate() } @objc private dynamic func handle() { - guard let window = UIApplication.shared.delegate?.window ?? nil else { + + guard let windowScene else { return } + + guard let window = windowScene.windows.first(where: \.isKeyWindow) ?? windowScene.windows.first else { + return + } + _handle(in: window) } @@ -134,7 +158,7 @@ public final class SafeAreaFinder: NSObject { if currentInsets != maximumInsets { currentInsets = maximumInsets - NotificationCenter.default.post(name: Self.notificationName, object: maximumInsets) + NotificationCenter.default.post(name: Self.notificationName, object: maximumInsets, userInfo: ["finder": self]) } } diff --git a/Sources/FluidPictureInPicture/FluidPictureInPictureController.swift b/Sources/FluidPictureInPicture/FluidPictureInPictureController.swift index d8e87850c..10c618f24 100644 --- a/Sources/FluidPictureInPicture/FluidPictureInPictureController.swift +++ b/Sources/FluidPictureInPicture/FluidPictureInPictureController.swift @@ -104,6 +104,7 @@ extension FluidPictureInPictureController { let containerView: ContainerView = .init() let sizeForFloating = CGSize(width: 100, height: 140) + let safeAreaFinder: SafeAreaFinder private(set) var state: State = .init() { didSet { @@ -123,6 +124,9 @@ extension FluidPictureInPictureController { override init( frame: CGRect ) { + + self.safeAreaFinder = .init(windowScene: nil) + super.init(frame: frame) let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)) @@ -145,6 +149,7 @@ extension FluidPictureInPictureController { } @objc private func handleInsetsUpdate(notification: Notification) { + guard notification.userInfo?["finder"] as? SafeAreaFinder === safeAreaFinder else { return } let inset = notification.object as! UIEdgeInsets state.inset = inset setNeedsLayout() @@ -229,11 +234,13 @@ extension FluidPictureInPictureController { override func didMoveToWindow() { super.didMoveToWindow() - + + safeAreaFinder.windowScene = window?.windowScene + if window != nil { - SafeAreaFinder.shared.start() + safeAreaFinder.start() } else { - SafeAreaFinder.shared.pause() + safeAreaFinder.pause() } } diff --git a/Sources/FluidSnackbar/FloatingDisplayController.swift b/Sources/FluidSnackbar/FloatingDisplayController.swift index 536234a5e..5e4841922 100644 --- a/Sources/FluidSnackbar/FloatingDisplayController.swift +++ b/Sources/FluidSnackbar/FloatingDisplayController.swift @@ -33,8 +33,15 @@ open class FloatingDisplayController { // MARK: - Initializers - public init(edgeTargetSafeArea: FloatingDisplayTarget.EdgeTargetSafeArea) { - self.displayTarget = .init(edgeTargetSafeArea: edgeTargetSafeArea) + @available(iOS 13.0, *) + public init( + edgeTargetSafeArea: FloatingDisplayTarget.EdgeTargetSafeArea, + windowScene: UIWindowScene + ) { + self.displayTarget = .init( + edgeTargetSafeArea: edgeTargetSafeArea, + windowScene: windowScene + ) } // MARK: - Functions diff --git a/Sources/FluidSnackbar/FloatingDisplayTarget.swift b/Sources/FluidSnackbar/FloatingDisplayTarget.swift index 03a4fe0fc..f08169d22 100644 --- a/Sources/FluidSnackbar/FloatingDisplayTarget.swift +++ b/Sources/FluidSnackbar/FloatingDisplayTarget.swift @@ -40,14 +40,22 @@ public final class FloatingDisplayTarget { left: .activeWindow ) } + + fileprivate var containsActiveWindowEdge: Bool { + top == .activeWindow + || right == .activeWindow + || bottom == .activeWindow + || left == .activeWindow + } } - public enum TargetSafeArea { + public enum TargetSafeArea: Equatable { case notificationWindow case activeWindow } private let notificationWindow: NotificationWindow + private let safeAreaFinder: SafeAreaFinder private let notificationViewController: NotificationViewController public var additionalSafeAreaInsets: UIEdgeInsets { @@ -69,35 +77,31 @@ public final class FloatingDisplayTarget { } public func makeWindowVisible() { + _ = notificationViewController.view notificationWindow.isHidden = false + + if notificationViewController.needsActiveWindowSafeArea { + safeAreaFinder.start() + } } public func hideWindow() { notificationWindow.isHidden = true + safeAreaFinder.stop() } - public init(edgeTargetSafeArea: EdgeTargetSafeArea) { - - self.notificationViewController = .init(edgeTargetSafeArea: edgeTargetSafeArea) - - if #available(iOS 13, *) { - - let windowScene = UIApplication.shared - .connectedScenes - .lazy - .filter { $0.activationState == .foregroundActive } - .compactMap { $0 as? UIWindowScene } - .first + @available(iOS 13.0, *) + public init( + edgeTargetSafeArea: EdgeTargetSafeArea, + windowScene: UIWindowScene + ) { - if let windowScene = windowScene { - notificationWindow = .init(windowScene: windowScene) - } else { - notificationWindow = .init(frame: .zero) - } - - } else { - notificationWindow = .init(frame: .zero) - } + self.safeAreaFinder = .init(windowScene: windowScene) + self.notificationViewController = .init( + edgeTargetSafeArea: edgeTargetSafeArea, + safeAreaFinder: safeAreaFinder + ) + self.notificationWindow = .init(windowScene: windowScene) notificationWindow.windowLevel = UIWindow.Level(rawValue: 5) notificationWindow.isHidden = true @@ -108,11 +112,11 @@ public final class FloatingDisplayTarget { notificationViewController.endAppearanceTransition() } - deinit { - Task { @MainActor [notificationWindow] in + deinit { + Task { @MainActor [safeAreaFinder, notificationWindow] in + safeAreaFinder.stop() notificationWindow.isHidden = false } - } } @@ -149,9 +153,18 @@ extension FloatingDisplayTarget { fileprivate final class NotificationViewController: UIViewController { private let edgeTargetSafeArea: EdgeTargetSafeArea + private let safeAreaFinder: SafeAreaFinder - init(edgeTargetSafeArea: EdgeTargetSafeArea) { + fileprivate var needsActiveWindowSafeArea: Bool { + edgeTargetSafeArea.containsActiveWindowEdge + } + + init( + edgeTargetSafeArea: EdgeTargetSafeArea, + safeAreaFinder: SafeAreaFinder + ) { self.edgeTargetSafeArea = edgeTargetSafeArea + self.safeAreaFinder = safeAreaFinder super.init(nibName: nil, bundle: nil) } @@ -160,7 +173,10 @@ extension FloatingDisplayTarget { } override fileprivate func loadView() { - view = View(edgeTargetSafeArea: edgeTargetSafeArea) + view = View( + edgeTargetSafeArea: edgeTargetSafeArea, + safeAreaFinder: safeAreaFinder + ) } override fileprivate func viewDidLoad() { @@ -172,6 +188,7 @@ extension FloatingDisplayTarget { fileprivate class View: UIView { private let edgeTargetSafeArea: EdgeTargetSafeArea + private let safeAreaFinder: SafeAreaFinder private var _safeAreaLayoutGuide: UILayoutGuide = .init() private var activeWindowSafeAreaLayoutGuideConstraintLeft: NSLayoutConstraint? @@ -181,15 +198,19 @@ extension FloatingDisplayTarget { private var hasSafeAreaFinderActivated: Bool = false - init(edgeTargetSafeArea: EdgeTargetSafeArea) { + init( + edgeTargetSafeArea: EdgeTargetSafeArea, + safeAreaFinder: SafeAreaFinder + ) { self.edgeTargetSafeArea = edgeTargetSafeArea + self.safeAreaFinder = safeAreaFinder super.init(frame: .zero) addLayoutGuide(_safeAreaLayoutGuide) - var containsActievWindowSafeAreaEdge: Bool = false + var containsActiveWindowSafeAreaEdge: Bool = false switch edgeTargetSafeArea.top { case .notificationWindow: @@ -198,7 +219,7 @@ extension FloatingDisplayTarget { case .activeWindow: activeWindowSafeAreaLayoutGuideConstraintTop = topAnchor.constraint( equalTo: _safeAreaLayoutGuide.topAnchor) - containsActievWindowSafeAreaEdge = true + containsActiveWindowSafeAreaEdge = true } switch edgeTargetSafeArea.right { @@ -209,7 +230,7 @@ extension FloatingDisplayTarget { case .activeWindow: activeWindowSafeAreaLayoutGuideConstraintRight = rightAnchor.constraint( equalTo: _safeAreaLayoutGuide.rightAnchor) - containsActievWindowSafeAreaEdge = true + containsActiveWindowSafeAreaEdge = true } switch edgeTargetSafeArea.bottom { @@ -220,7 +241,7 @@ extension FloatingDisplayTarget { case .activeWindow: activeWindowSafeAreaLayoutGuideConstraintBottom = bottomAnchor.constraint( equalTo: _safeAreaLayoutGuide.bottomAnchor) - containsActievWindowSafeAreaEdge = true + containsActiveWindowSafeAreaEdge = true } switch edgeTargetSafeArea.left { @@ -230,10 +251,10 @@ extension FloatingDisplayTarget { case .activeWindow: activeWindowSafeAreaLayoutGuideConstraintLeft = leftAnchor.constraint( equalTo: _safeAreaLayoutGuide.leftAnchor) - containsActievWindowSafeAreaEdge = true + containsActiveWindowSafeAreaEdge = true } - if containsActievWindowSafeAreaEdge { + if containsActiveWindowSafeAreaEdge { NSLayoutConstraint.activate( [ @@ -248,13 +269,13 @@ extension FloatingDisplayTarget { NotificationCenter.default.addObserver( self, selector: #selector(handleInsetsUpdate), name: SafeAreaFinder.notificationName, object: nil) - SafeAreaFinder.shared.start() } } @objc private func handleInsetsUpdate(notification: Notification) { guard hasSafeAreaFinderActivated else { return } + guard notification.userInfo?["finder"] as? SafeAreaFinder === safeAreaFinder else { return } let insets = notification.object as! UIEdgeInsets self.activeWindowSafeAreaLayoutGuideConstraintLeft?.constant = insets.left @@ -287,10 +308,9 @@ extension FloatingDisplayTarget { } deinit { - Task { @MainActor [hasSafeAreaFinderActivated] in - if hasSafeAreaFinderActivated { - SafeAreaFinder.shared.pause() - } + NotificationCenter.default.removeObserver(self) + Task { @MainActor [safeAreaFinder] in + safeAreaFinder.stop() } } }