Skip to content

Commit

Permalink
feat: observe key window changes and cache screen size (#252)
Browse files Browse the repository at this point in the history
* feat: observe key window changes and cache screen size

* fix: tvOS build

* feat: add didBecomeActiveNotification notifications for watchOS

* fix: failing build

* fix: avoid unecessary code runs in next run-loops

* fix: remove unsupported notifications for watchOS
  • Loading branch information
ioannisj authored Nov 19, 2024
1 parent 79dd9dc commit cccf985
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- fix: reading screen size could sometimes lead to a deadlock ([#252](https://github.com/PostHog/posthog-ios/pull/252))

## 3.15.3 - 2024-11-18

- fix: mangled wireframe layouts ([#250](https://github.com/PostHog/posthog-ios/pull/250))
Expand Down
151 changes: 132 additions & 19 deletions PostHog/PostHogContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,13 @@ import Foundation
#endif

class PostHogContext {
@ReadWriteLock
private var screenSize: CGSize?

#if !os(watchOS)
private let reachability: Reachability?
#endif

private var screenSize: CGSize? {
let getWindowSize: () -> CGSize? = {
#if os(iOS) || os(tvOS)
return UIApplication.getCurrentWindow(filterForegrounded: false)?.bounds.size
#elseif os(macOS)
return NSScreen.main?.visibleFrame.size
#elseif os(watchOS)
return WKInterfaceDevice.current().screenBounds.size
#else
return nil
#endif
}

return Thread.isMainThread
? getWindowSize()
: DispatchQueue.main.sync { getWindowSize() }
}

private lazy var theStaticContext: [String: Any] = {
// Properties that do not change over the lifecycle of an application
var properties: [String: Any] = [:]
Expand Down Expand Up @@ -111,11 +96,28 @@ class PostHogContext {
#if !os(watchOS)
init(_ reachability: Reachability?) {
self.reachability = reachability
registerNotifications()
}
#else
init() {}
init() {
if #available(watchOS 7.0, *) {
registerNotifications()
} else {
onShouldUpdateScreenSize()
}
}
#endif

deinit {
#if !os(watchOS)
unregisterNotifications()
#else
if #available(watchOS 7.0, *) {
unregisterNotifications()
}
#endif
}

private lazy var theSdkInfo: [String: Any] = {
var sdkInfo: [String: Any] = [:]
sdkInfo["$lib"] = postHogSdkName
Expand Down Expand Up @@ -161,4 +163,115 @@ class PostHogContext {

return properties
}

private func registerNotifications() {
#if os(iOS) || os(tvOS)
#if os(iOS)
NotificationCenter.default.addObserver(self,
selector: #selector(onOrientationDidChange),
name: UIDevice.orientationDidChangeNotification,
object: nil)
#endif
NotificationCenter.default.addObserver(self,
selector: #selector(onShouldUpdateScreenSize),
name: UIWindow.didBecomeKeyNotification,
object: nil)
#elseif os(macOS)
NotificationCenter.default.addObserver(self,
selector: #selector(onShouldUpdateScreenSize),
name: NSWindow.didBecomeKeyNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(onShouldUpdateScreenSize),
name: NSWindow.didChangeScreenNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(onShouldUpdateScreenSize),
name: NSApplication.didBecomeActiveNotification,
object: nil)
#elseif os(watchOS)
if #available(watchOS 7.0, *) {
NotificationCenter.default.addObserver(self,
selector: #selector(onShouldUpdateScreenSize),
name: WKApplication.didBecomeActiveNotification,
object: nil)
}
#endif
}

private func unregisterNotifications() {
#if os(iOS) || os(tvOS)
#if os(iOS)
NotificationCenter.default.removeObserver(self,
name: UIDevice.orientationDidChangeNotification,
object: nil)
#endif
NotificationCenter.default.removeObserver(self,
name: UIWindow.didBecomeKeyNotification,
object: nil)

#elseif os(macOS)
NotificationCenter.default.removeObserver(self,
name: NSWindow.didBecomeKeyNotification,
object: nil)
NotificationCenter.default.removeObserver(self,
name: NSWindow.didChangeScreenNotification,
object: nil)
NotificationCenter.default.removeObserver(self,
name: NSApplication.didBecomeActiveNotification,
object: nil)
#elseif os(watchOS)
if #available(watchOS 7.0, *) {
NotificationCenter.default.removeObserver(self,
name: WKApplication.didBecomeActiveNotification,
object: nil)
}
#endif
}

/// Retrieves the current screen size of the application window based on platform
private func getScreenSize() -> CGSize? {
#if os(iOS) || os(tvOS)
return UIApplication.getCurrentWindow(filterForegrounded: false)?.bounds.size
#elseif os(macOS)
// NSScreen.frame represents the full screen rectangle and includes any space occupied by menu, dock or camera bezel
return NSApplication.shared.windows.first { $0.isKeyWindow }?.screen?.frame.size
#elseif os(watchOS)
return WKInterfaceDevice.current().screenBounds.size
#else
return nil
#endif
}

#if os(iOS)
// Special treatment for `orientationDidChangeNotification` since the notification seems to be _sometimes_ called early, before screen bounds are flipped
@objc private func onOrientationDidChange() {
updateScreenSize {
self.getScreenSize().map { size in
// manually set width and height based on device orientation. (Needed for fast orientation changes)
if UIDevice.current.orientation.isLandscape {
CGSize(width: max(size.width, size.height), height: min(size.height, size.width))
} else {
CGSize(width: min(size.width, size.height), height: max(size.height, size.width))
}
}
}
}
#endif

@objc private func onShouldUpdateScreenSize() {
updateScreenSize(getScreenSize)
}

private func updateScreenSize(_ getSize: @escaping () -> CGSize?) {
let block = {
self.screenSize = getSize()
}
// ensure block is executed on `main` since closure accesses non thread-safe UI objects like UIApplication
if Thread.isMainThread {
block()
} else {
DispatchQueue.main.async(execute: block)
}
}
}
20 changes: 20 additions & 0 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Foundation
import UIKit
#elseif os(macOS)
import AppKit
#elseif os(watchOS)
import WatchKit
#endif

let retryDelay = 5.0
Expand Down Expand Up @@ -1042,6 +1044,12 @@ let maxRetryDelay = 30.0
defaultCenter.removeObserver(self, name: NSApplication.didFinishLaunchingNotification, object: nil)
defaultCenter.removeObserver(self, name: NSApplication.didResignActiveNotification, object: nil)
defaultCenter.removeObserver(self, name: NSApplication.didBecomeActiveNotification, object: nil)
#elseif os(watchOS)
if #available(watchOS 7.0, *) {
NotificationCenter.default.removeObserver(self,
name: WKApplication.didBecomeActiveNotification,
object: nil)
}
#endif
}

Expand Down Expand Up @@ -1075,6 +1083,18 @@ let maxRetryDelay = 30.0
selector: #selector(handleAppDidBecomeActive),
name: NSApplication.didBecomeActiveNotification,
object: nil)
#elseif os(watchOS)
if #available(watchOS 7.0, *) {
NotificationCenter.default.addObserver(self,
selector: #selector(handleAppDidBecomeActive),
name: WKApplication.didBecomeActiveNotification,
object: nil)
} else {
NotificationCenter.default.addObserver(self,
selector: #selector(handleAppDidBecomeActive),
name: .init("UIApplicationDidBecomeActiveNotification"),
object: nil)
}
#endif
}

Expand Down

0 comments on commit cccf985

Please sign in to comment.