Skip to content

Commit f670a8b

Browse files
muukiiryolingo
andauthored
Scene-aware safe area (#193)
Co-authored-by: Ryota matsumoto <ryomatsuya04@gmail.com>
1 parent d427a0d commit f670a8b

4 files changed

Lines changed: 124 additions & 66 deletions

File tree

Sources/FluidCore/SafeAreaFinder.swift

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,32 @@ import UIKit
1212
public final class SafeAreaFinder: NSObject {
1313

1414
public static let notificationName = Notification.Name(rawValue: "app.muukii.fluid.SafeAreaInsetsManager")
15-
16-
@MainActor
17-
public static let shared = SafeAreaFinder()
15+
16+
@available(iOS 13.0, *)
17+
public weak var windowScene: UIWindowScene?
1818

1919
private var currentInsets: UIEdgeInsets? = nil
2020

21-
private var referenceCounter: Int = 0 {
22-
didSet {
23-
if referenceCounter > 0 {
24-
currentDisplayLink.isPaused = false
25-
} else {
26-
currentDisplayLink.isPaused = true
27-
}
28-
}
29-
}
21+
private var isRunning: Bool = false
3022

31-
private nonisolated(unsafe) var currentDisplayLink: CADisplayLink!
23+
private nonisolated(unsafe) var currentDisplayLink: CADisplayLink?
3224

33-
private override init() {
25+
@available(iOS 13.0, *)
26+
public init(windowScene: UIWindowScene?) {
27+
self.windowScene = windowScene
3428

3529
super.init()
30+
}
31+
32+
private func setUpDisplayLink() {
33+
guard currentDisplayLink == nil else {
34+
return
35+
}
3636

3737
currentDisplayLink = .init(target: self, selector: #selector(handle))
38-
currentDisplayLink.preferredFramesPerSecond = 1
39-
currentDisplayLink.add(to: .main, forMode: .default)
40-
currentDisplayLink.isPaused = true
38+
currentDisplayLink?.preferredFramesPerSecond = 1
39+
currentDisplayLink?.add(to: .main, forMode: .default)
40+
currentDisplayLink?.isPaused = false
4141
}
4242

4343
public func request() {
@@ -46,23 +46,47 @@ public final class SafeAreaFinder: NSObject {
4646
}
4747

4848
public func start() {
49-
referenceCounter += 1
49+
guard isRunning == false else {
50+
request()
51+
return
52+
}
53+
54+
isRunning = true
55+
setUpDisplayLink()
5056
request()
5157
}
5258

59+
/// Stops polling and releases the display link so the finder can deallocate when its owner goes away.
60+
public func stop() {
61+
guard isRunning || currentDisplayLink != nil else {
62+
return
63+
}
64+
65+
isRunning = false
66+
currentInsets = nil
67+
currentDisplayLink?.invalidate()
68+
currentDisplayLink = nil
69+
}
70+
71+
/// Stops polling. Kept as a compatibility alias for older callers that used the reference-counted API.
5372
public func pause() {
54-
referenceCounter -= 1
73+
stop()
5574
}
5675

5776
deinit {
58-
currentDisplayLink?.isPaused = true
5977
currentDisplayLink?.invalidate()
6078
}
6179

6280
@objc private dynamic func handle() {
63-
guard let window = UIApplication.shared.delegate?.window ?? nil else {
81+
82+
guard let windowScene else {
6483
return
6584
}
85+
86+
guard let window = windowScene.windows.first(where: \.isKeyWindow) ?? windowScene.windows.first else {
87+
return
88+
}
89+
6690
_handle(in: window)
6791
}
6892

@@ -134,7 +158,7 @@ public final class SafeAreaFinder: NSObject {
134158

135159
if currentInsets != maximumInsets {
136160
currentInsets = maximumInsets
137-
NotificationCenter.default.post(name: Self.notificationName, object: maximumInsets)
161+
NotificationCenter.default.post(name: Self.notificationName, object: maximumInsets, userInfo: ["finder": self])
138162
}
139163
}
140164

Sources/FluidPictureInPicture/FluidPictureInPictureController.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ extension FluidPictureInPictureController {
104104
let containerView: ContainerView = .init()
105105

106106
let sizeForFloating = CGSize(width: 100, height: 140)
107+
let safeAreaFinder: SafeAreaFinder
107108

108109
private(set) var state: State = .init() {
109110
didSet {
@@ -123,6 +124,9 @@ extension FluidPictureInPictureController {
123124
override init(
124125
frame: CGRect
125126
) {
127+
128+
self.safeAreaFinder = .init(windowScene: nil)
129+
126130
super.init(frame: frame)
127131

128132
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
@@ -145,6 +149,7 @@ extension FluidPictureInPictureController {
145149
}
146150

147151
@objc private func handleInsetsUpdate(notification: Notification) {
152+
guard notification.userInfo?["finder"] as? SafeAreaFinder === safeAreaFinder else { return }
148153
let inset = notification.object as! UIEdgeInsets
149154
state.inset = inset
150155
setNeedsLayout()
@@ -229,11 +234,13 @@ extension FluidPictureInPictureController {
229234

230235
override func didMoveToWindow() {
231236
super.didMoveToWindow()
232-
237+
238+
safeAreaFinder.windowScene = window?.windowScene
239+
233240
if window != nil {
234-
SafeAreaFinder.shared.start()
241+
safeAreaFinder.start()
235242
} else {
236-
SafeAreaFinder.shared.pause()
243+
safeAreaFinder.pause()
237244
}
238245
}
239246

Sources/FluidSnackbar/FloatingDisplayController.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ open class FloatingDisplayController {
3333

3434
// MARK: - Initializers
3535

36-
public init(edgeTargetSafeArea: FloatingDisplayTarget.EdgeTargetSafeArea) {
37-
self.displayTarget = .init(edgeTargetSafeArea: edgeTargetSafeArea)
36+
@available(iOS 13.0, *)
37+
public init(
38+
edgeTargetSafeArea: FloatingDisplayTarget.EdgeTargetSafeArea,
39+
windowScene: UIWindowScene
40+
) {
41+
self.displayTarget = .init(
42+
edgeTargetSafeArea: edgeTargetSafeArea,
43+
windowScene: windowScene
44+
)
3845
}
3946

4047
// MARK: - Functions

Sources/FluidSnackbar/FloatingDisplayTarget.swift

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,22 @@ public final class FloatingDisplayTarget {
4040
left: .activeWindow
4141
)
4242
}
43+
44+
fileprivate var containsActiveWindowEdge: Bool {
45+
top == .activeWindow
46+
|| right == .activeWindow
47+
|| bottom == .activeWindow
48+
|| left == .activeWindow
49+
}
4350
}
4451

45-
public enum TargetSafeArea {
52+
public enum TargetSafeArea: Equatable {
4653
case notificationWindow
4754
case activeWindow
4855
}
4956

5057
private let notificationWindow: NotificationWindow
58+
private let safeAreaFinder: SafeAreaFinder
5159
private let notificationViewController: NotificationViewController
5260

5361
public var additionalSafeAreaInsets: UIEdgeInsets {
@@ -69,35 +77,31 @@ public final class FloatingDisplayTarget {
6977
}
7078

7179
public func makeWindowVisible() {
80+
_ = notificationViewController.view
7281
notificationWindow.isHidden = false
82+
83+
if notificationViewController.needsActiveWindowSafeArea {
84+
safeAreaFinder.start()
85+
}
7386
}
7487

7588
public func hideWindow() {
7689
notificationWindow.isHidden = true
90+
safeAreaFinder.stop()
7791
}
7892

79-
public init(edgeTargetSafeArea: EdgeTargetSafeArea) {
80-
81-
self.notificationViewController = .init(edgeTargetSafeArea: edgeTargetSafeArea)
82-
83-
if #available(iOS 13, *) {
84-
85-
let windowScene = UIApplication.shared
86-
.connectedScenes
87-
.lazy
88-
.filter { $0.activationState == .foregroundActive }
89-
.compactMap { $0 as? UIWindowScene }
90-
.first
93+
@available(iOS 13.0, *)
94+
public init(
95+
edgeTargetSafeArea: EdgeTargetSafeArea,
96+
windowScene: UIWindowScene
97+
) {
9198

92-
if let windowScene = windowScene {
93-
notificationWindow = .init(windowScene: windowScene)
94-
} else {
95-
notificationWindow = .init(frame: .zero)
96-
}
97-
98-
} else {
99-
notificationWindow = .init(frame: .zero)
100-
}
99+
self.safeAreaFinder = .init(windowScene: windowScene)
100+
self.notificationViewController = .init(
101+
edgeTargetSafeArea: edgeTargetSafeArea,
102+
safeAreaFinder: safeAreaFinder
103+
)
104+
self.notificationWindow = .init(windowScene: windowScene)
101105

102106
notificationWindow.windowLevel = UIWindow.Level(rawValue: 5)
103107
notificationWindow.isHidden = true
@@ -108,11 +112,11 @@ public final class FloatingDisplayTarget {
108112
notificationViewController.endAppearanceTransition()
109113
}
110114

111-
deinit {
112-
Task { @MainActor [notificationWindow] in
115+
deinit {
116+
Task { @MainActor [safeAreaFinder, notificationWindow] in
117+
safeAreaFinder.stop()
113118
notificationWindow.isHidden = false
114119
}
115-
116120
}
117121

118122
}
@@ -149,9 +153,18 @@ extension FloatingDisplayTarget {
149153
fileprivate final class NotificationViewController: UIViewController {
150154

151155
private let edgeTargetSafeArea: EdgeTargetSafeArea
156+
private let safeAreaFinder: SafeAreaFinder
152157

153-
init(edgeTargetSafeArea: EdgeTargetSafeArea) {
158+
fileprivate var needsActiveWindowSafeArea: Bool {
159+
edgeTargetSafeArea.containsActiveWindowEdge
160+
}
161+
162+
init(
163+
edgeTargetSafeArea: EdgeTargetSafeArea,
164+
safeAreaFinder: SafeAreaFinder
165+
) {
154166
self.edgeTargetSafeArea = edgeTargetSafeArea
167+
self.safeAreaFinder = safeAreaFinder
155168
super.init(nibName: nil, bundle: nil)
156169
}
157170

@@ -160,7 +173,10 @@ extension FloatingDisplayTarget {
160173
}
161174

162175
override fileprivate func loadView() {
163-
view = View(edgeTargetSafeArea: edgeTargetSafeArea)
176+
view = View(
177+
edgeTargetSafeArea: edgeTargetSafeArea,
178+
safeAreaFinder: safeAreaFinder
179+
)
164180
}
165181

166182
override fileprivate func viewDidLoad() {
@@ -172,6 +188,7 @@ extension FloatingDisplayTarget {
172188
fileprivate class View: UIView {
173189

174190
private let edgeTargetSafeArea: EdgeTargetSafeArea
191+
private let safeAreaFinder: SafeAreaFinder
175192

176193
private var _safeAreaLayoutGuide: UILayoutGuide = .init()
177194
private var activeWindowSafeAreaLayoutGuideConstraintLeft: NSLayoutConstraint?
@@ -181,15 +198,19 @@ extension FloatingDisplayTarget {
181198

182199
private var hasSafeAreaFinderActivated: Bool = false
183200

184-
init(edgeTargetSafeArea: EdgeTargetSafeArea) {
201+
init(
202+
edgeTargetSafeArea: EdgeTargetSafeArea,
203+
safeAreaFinder: SafeAreaFinder
204+
) {
185205

186206
self.edgeTargetSafeArea = edgeTargetSafeArea
207+
self.safeAreaFinder = safeAreaFinder
187208

188209
super.init(frame: .zero)
189210

190211
addLayoutGuide(_safeAreaLayoutGuide)
191212

192-
var containsActievWindowSafeAreaEdge: Bool = false
213+
var containsActiveWindowSafeAreaEdge: Bool = false
193214

194215
switch edgeTargetSafeArea.top {
195216
case .notificationWindow:
@@ -198,7 +219,7 @@ extension FloatingDisplayTarget {
198219
case .activeWindow:
199220
activeWindowSafeAreaLayoutGuideConstraintTop = topAnchor.constraint(
200221
equalTo: _safeAreaLayoutGuide.topAnchor)
201-
containsActievWindowSafeAreaEdge = true
222+
containsActiveWindowSafeAreaEdge = true
202223
}
203224

204225
switch edgeTargetSafeArea.right {
@@ -209,7 +230,7 @@ extension FloatingDisplayTarget {
209230
case .activeWindow:
210231
activeWindowSafeAreaLayoutGuideConstraintRight = rightAnchor.constraint(
211232
equalTo: _safeAreaLayoutGuide.rightAnchor)
212-
containsActievWindowSafeAreaEdge = true
233+
containsActiveWindowSafeAreaEdge = true
213234
}
214235

215236
switch edgeTargetSafeArea.bottom {
@@ -220,7 +241,7 @@ extension FloatingDisplayTarget {
220241
case .activeWindow:
221242
activeWindowSafeAreaLayoutGuideConstraintBottom = bottomAnchor.constraint(
222243
equalTo: _safeAreaLayoutGuide.bottomAnchor)
223-
containsActievWindowSafeAreaEdge = true
244+
containsActiveWindowSafeAreaEdge = true
224245
}
225246

226247
switch edgeTargetSafeArea.left {
@@ -230,10 +251,10 @@ extension FloatingDisplayTarget {
230251
case .activeWindow:
231252
activeWindowSafeAreaLayoutGuideConstraintLeft = leftAnchor.constraint(
232253
equalTo: _safeAreaLayoutGuide.leftAnchor)
233-
containsActievWindowSafeAreaEdge = true
254+
containsActiveWindowSafeAreaEdge = true
234255
}
235256

236-
if containsActievWindowSafeAreaEdge {
257+
if containsActiveWindowSafeAreaEdge {
237258

238259
NSLayoutConstraint.activate(
239260
[
@@ -248,13 +269,13 @@ extension FloatingDisplayTarget {
248269
NotificationCenter.default.addObserver(
249270
self, selector: #selector(handleInsetsUpdate), name: SafeAreaFinder.notificationName,
250271
object: nil)
251-
SafeAreaFinder.shared.start()
252272
}
253273
}
254274

255275
@objc private func handleInsetsUpdate(notification: Notification) {
256276

257277
guard hasSafeAreaFinderActivated else { return }
278+
guard notification.userInfo?["finder"] as? SafeAreaFinder === safeAreaFinder else { return }
258279

259280
let insets = notification.object as! UIEdgeInsets
260281
self.activeWindowSafeAreaLayoutGuideConstraintLeft?.constant = insets.left
@@ -287,10 +308,9 @@ extension FloatingDisplayTarget {
287308
}
288309

289310
deinit {
290-
Task { @MainActor [hasSafeAreaFinderActivated] in
291-
if hasSafeAreaFinderActivated {
292-
SafeAreaFinder.shared.pause()
293-
}
311+
NotificationCenter.default.removeObserver(self)
312+
Task { @MainActor [safeAreaFinder] in
313+
safeAreaFinder.stop()
294314
}
295315
}
296316
}

0 commit comments

Comments
 (0)