diff --git a/CHANGELOG.md b/CHANGELOG.md index e93ffe51d..e9520f5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/PostHog/PostHogContext.swift b/PostHog/PostHogContext.swift index 8f98e8b7d..b80258134 100644 --- a/PostHog/PostHogContext.swift +++ b/PostHog/PostHogContext.swift @@ -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] = [:] @@ -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 @@ -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) + } + } } diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index e3be74d32..613280930 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -13,6 +13,8 @@ import Foundation import UIKit #elseif os(macOS) import AppKit +#elseif os(watchOS) + import WatchKit #endif let retryDelay = 5.0 @@ -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 } @@ -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 }