From 9966d1012025864b0a1acbf95e752dc9650ef8bd Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 12 Nov 2024 12:43:05 +0200 Subject: [PATCH] feat: get current window screen size (#247) * feat: get current window screen size * fix: wrap in compiler directive * fix: lint * fix: build for tvOS and watchOS * fix: filter foregrounded scenes by default * feat: search for UIWindowSceneDelegate when getting current window * feat: get device size for watchOS * fix: iterate over window scenes once * fix: iOS15 fallback * chore: update CHANGELOG --- CHANGELOG.md | 2 + PostHog.xcodeproj/project.pbxproj | 4 ++ .../PostHogAutocaptureEventTracker.swift | 11 +---- PostHog/PostHogContext.swift | 36 ++++++++++----- PostHog/Replay/PostHogReplayIntegration.swift | 26 +---------- PostHog/Replay/UIApplicationTracker.swift | 2 +- PostHog/UIViewController.swift | 12 +---- PostHog/Utils/UIApplication+.swift | 45 +++++++++++++++++++ 8 files changed, 80 insertions(+), 58 deletions(-) create mode 100644 PostHog/Utils/UIApplication+.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a197e3a..a37957320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- fix: accessing UI APIs off main thread to get screen size ([#247](https://github.com/PostHog/posthog-ios/pull/247)) + ## 3.15.0 - 2024-11-11 - add autocapture support for UIKit ([#224](https://github.com/PostHog/posthog-ios/pull/224)) diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index fd3966a7a..25eed903c 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -120,6 +120,7 @@ 69F518382BB2BA0100F52C14 /* PostHogSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */; }; 69F5183A2BB2BA8300F52C14 /* UIApplicationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */; }; DA26419C2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */; }; + DA5AA7192CE245D2004EFB99 /* UIApplication+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5AA7132CE245CD004EFB99 /* UIApplication+.swift */; }; DA5B85882CD21CBB00686389 /* AutocaptureEventProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */; }; DA979D7B2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */; }; DAC699D62CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC699D52CC790D9000D1D6B /* PostHogAutocaptureIntegration.swift */; }; @@ -390,6 +391,7 @@ 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwizzler.swift; sourceTree = ""; }; 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationTracker.swift; sourceTree = ""; }; DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureEventTracker.swift; sourceTree = ""; }; + DA5AA7132CE245CD004EFB99 /* UIApplication+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+.swift"; sourceTree = ""; }; DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocaptureEventProcessing.swift; sourceTree = ""; }; DA8D37242CBEAC02005EBD27 /* PostHogExampleAutocapture.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleAutocapture.xcodeproj; path = PostHogExampleAutocapture/PostHogExampleAutocapture.xcodeproj; sourceTree = ""; }; DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureEventTrackerSpec.swift; sourceTree = ""; }; @@ -506,6 +508,7 @@ 3AA4C09B2988315D006C4731 /* Utils */ = { isa = PBXGroup; children = ( + DA5AA7132CE245CD004EFB99 /* UIApplication+.swift */, 3AE3FB422992985A00AFFC18 /* Reachability.swift */, 3AE3FB462992AB0000AFFC18 /* Hedgelog.swift */, 3A0F108429C9ABB6002C0084 /* ReadWriteLock.swift */, @@ -1176,6 +1179,7 @@ 69F23A7A2BB309F3001194F6 /* MethodSwizzler.swift in Sources */, 69261D1B2AD9678C00232EC7 /* PostHogEvent.swift in Sources */, 69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */, + DA5AA7192CE245D2004EFB99 /* UIApplication+.swift in Sources */, 69EE82CE2BAAC76000EB9542 /* ViewTreeSnapshotStatus.swift in Sources */, 69ED1AD42C90A0F100FE7A91 /* URLSessionExtension.swift in Sources */, 69ED1A9F2C8F451B00FE7A91 /* PostHogPersonProfiles.swift in Sources */, diff --git a/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift b/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift index fc70a5cac..be53264ba 100644 --- a/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift +++ b/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift @@ -342,17 +342,8 @@ } } - extension UIApplication { - static var ph_currentWindow: UIWindow? { - Array(UIApplication.shared.connectedScenes) - .compactMap { $0 as? UIWindowScene } - .flatMap(\.windows) - .first { $0.windowLevel != .statusBar } - } - } - extension UIViewController { - class func ph_topViewController(base: UIViewController? = UIApplication.ph_currentWindow?.rootViewController) -> UIViewController? { + class func ph_topViewController(base: UIViewController? = UIApplication.getCurrentWindow()?.rootViewController) -> UIViewController? { if let nav = base as? UINavigationController { return ph_topViewController(base: nav.visibleViewController) diff --git a/PostHog/PostHogContext.swift b/PostHog/PostHogContext.swift index 54fd1d4b7..8f98e8b7d 100644 --- a/PostHog/PostHogContext.swift +++ b/PostHog/PostHogContext.swift @@ -11,6 +11,8 @@ import Foundation import UIKit #elseif os(macOS) import AppKit +#elseif os(watchOS) + import WatchKit #endif class PostHogContext { @@ -18,6 +20,24 @@ class PostHogContext { 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] = [:] @@ -122,18 +142,10 @@ class PostHogContext { func dynamicContext() -> [String: Any] { var properties: [String: Any] = [:] - #if os(iOS) || os(tvOS) - if let screen = UIApplication.shared.windows.first?.screen { - properties["$screen_width"] = Float(screen.bounds.width) - properties["$screen_height"] = Float(screen.bounds.height) - } - #elseif os(macOS) - if let mainScreen = NSScreen.main { - let screenFrame = mainScreen.visibleFrame - properties["$screen_width"] = Float(screenFrame.size.width) - properties["$screen_height"] = Float(screenFrame.size.height) - } - #endif + if let screenSize { + properties["$screen_width"] = Float(screenSize.width) + properties["$screen_height"] = Float(screenSize.height) + } if Locale.current.languageCode != nil { properties["$locale"] = Locale.current.languageCode diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index ecd34da0f..a2b6e7757 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -509,30 +509,6 @@ return wireframe } - static func getCurrentWindow() -> UIWindow? { - // TODO: support multi windows - - // UIApplication.shared.windows is deprecated - for scene in UIApplication.shared.connectedScenes { - if scene is UIWindowScene, - scene.activationState == .foregroundActive, - let windowScene = scene as? UIWindowScene - { - if #available(iOS 15.0, *) { - if let keyWindow = windowScene.keyWindow { - return keyWindow - } - } - - for window in windowScene.windows where window.isKeyWindow { - return window - } - } - } - - return nil - } - @objc private func snapshot() { if !PostHogSDK.shared.isSessionReplayActive() { return @@ -543,7 +519,7 @@ } ViewLayoutTracker.clear() - guard let window = PostHogReplayIntegration.getCurrentWindow() else { + guard let window = UIApplication.getCurrentWindow() else { return } diff --git a/PostHog/Replay/UIApplicationTracker.swift b/PostHog/Replay/UIApplicationTracker.swift index ba1a2dead..1ec20a41c 100644 --- a/PostHog/Replay/UIApplicationTracker.swift +++ b/PostHog/Replay/UIApplicationTracker.swift @@ -44,7 +44,7 @@ guard event.type == .touches else { return } - guard let window = PostHogReplayIntegration.getCurrentWindow() else { + guard let window = UIApplication.getCurrentWindow() else { return } guard let touches = event.touches(for: window) else { diff --git a/PostHog/UIViewController.swift b/PostHog/UIViewController.swift index 0b2b72bbd..14774635a 100644 --- a/PostHog/UIViewController.swift +++ b/PostHog/UIViewController.swift @@ -29,17 +29,9 @@ // if a view is being dismissed, this will return nil if let root = viewIfLoaded?.window?.rootViewController { return root - } else { - // preferred way to get active controller in ios 13+ - for scene in UIApplication.shared.connectedScenes where scene.activationState == .foregroundActive { - let windowScene = scene as? UIWindowScene - let sceneDelegate = windowScene?.delegate as? UIWindowSceneDelegate - if let target = sceneDelegate, let window = target.window { - return window?.rootViewController - } - } } - return nil + // TODO: handle container controllers (see ph_topViewController) + return UIApplication.getCurrentWindow()?.rootViewController } static func getViewControllerName(_ viewController: UIViewController) -> String? { diff --git a/PostHog/Utils/UIApplication+.swift b/PostHog/Utils/UIApplication+.swift new file mode 100644 index 000000000..440a57d01 --- /dev/null +++ b/PostHog/Utils/UIApplication+.swift @@ -0,0 +1,45 @@ +// +// UIApplication+.swift +// PostHog +// +// Created by Yiannis Josephides on 11/11/2024. +// + +#if os(iOS) || os(tvOS) + import UIKit + + extension UIApplication { + static func getCurrentWindow(filterForegrounded: Bool = true) -> UIWindow? { + let windowScenes = UIApplication.shared + .connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { + !filterForegrounded || $0.activationState == .foregroundActive + } + + for scene in windowScenes { + // attempt to retrieve directly from UIWindowScene + if #available(iOS 15.0, tvOS 15.0, *) { + if let keyWindow = scene.keyWindow { + return keyWindow + } + } else { + // check scene.windows.isKeyWindow + for window in scene.windows { + if window.isKeyWindow { + return window + } + } + } + + // check scene.delegate.window property + let sceneDelegate = scene.delegate as? UIWindowSceneDelegate + if let target = sceneDelegate, let window = target.window { + return window + } + } + + return nil + } + } +#endif