Skip to content

Commit ed8992d

Browse files
Merge pull request #420 from Countly/mainscreen_dpc
refactor: screen size functions to accomadate ios 26
2 parents 9e14e7a + 055c7a7 commit ed8992d

6 files changed

Lines changed: 207 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## XX.XX.XX
2+
* Updated resolution extraction to accommodate iOS 26 deprecations.
3+
24
* Mitigated an issue where non-queued requests were affected from request timeout settings.
3-
* Mitigated a race condition in the request queue that could drop or duplicate requests.
5+
* Mitigated a race condition for tests in the request queue that could drop or duplicate requests.
46
* Mitigated an issue where server config defaults overrode user-provided SDK limits.
57
* Mitigated an issue where invalid or unknown `sdkBehaviorSettings` keys were persisted.
68
* Mitigated an issue where listing-filter conflicts cleared keys across unrelated categories.

Countly.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
9673567F2EC60CD400C742D8 /* TestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */; };
9595
968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; };
9696
96CB10A12F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */; };
97+
AB10000011112222333344A1 /* CountlyScreenMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */; };
9798
9691B7652F1FA35500A6ADCB /* CountlyOverlayWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 9691B7642F1FA35500A6ADCB /* CountlyOverlayWindow.h */; };
9899
9691B7672F1FA35C00A6ADCB /* CountlyOverlayWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 9691B7662F1FA35C00A6ADCB /* CountlyOverlayWindow.m */; };
99100
969E5BCE2ECC4D3200AB406A /* CountlyConsentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */; };
@@ -220,6 +221,7 @@
220221
9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLProtocol.swift; sourceTree = "<group>"; };
221222
968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = "<group>"; };
222223
96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyContentBuilderTests.swift; sourceTree = "<group>"; };
224+
AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyScreenMetricsTests.swift; sourceTree = "<group>"; };
223225
9691B7642F1FA35500A6ADCB /* CountlyOverlayWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyOverlayWindow.h; sourceTree = "<group>"; };
224226
9691B7662F1FA35C00A6ADCB /* CountlyOverlayWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyOverlayWindow.m; sourceTree = "<group>"; };
225227
969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConsentManagerTests.swift; sourceTree = "<group>"; };
@@ -276,6 +278,7 @@
276278
1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */,
277279
968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */,
278280
96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */,
281+
AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */,
279282
96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */,
280283
EBD1642D826B471A80175BE3 /* CountlyWebViewManagerTests.swift */,
281284
265156CB5F59417EA14BAC2F /* CountlyWebViewManager+Tests.h */,
@@ -558,6 +561,7 @@
558561
3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */,
559562
968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */,
560563
96CB10A12F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift in Sources */,
564+
AB10000011112222333344A1 /* CountlyScreenMetricsTests.swift in Sources */,
561565
96329DE42D952F1500BFD641 /* MockURLProtocol.swift in Sources */,
562566
96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */,
563567
ABCDE001000000000000ABCD /* CountlyServerConfigValidationTests.swift in Sources */,

CountlyCommon.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ - (CGSize)getWindowSize{
377377
}
378378
}
379379
} else {
380-
window = UIApplication.sharedApplication.delegate.window;
380+
window = [[UIApplication sharedApplication].delegate window];
381381
}
382382

383383
if (!window) return CGSizeZero;

CountlyDeviceInfo.m

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,29 @@ + (NSString *)carrier
261261

262262
+ (NSString *)resolution
263263
{
264-
CGRect bounds;
265-
CGFloat scale;
264+
CGRect bounds = CGRectZero;
265+
CGFloat scale = 0;
266266
#if (TARGET_OS_IOS || TARGET_OS_TV)
267-
bounds = UIScreen.mainScreen.bounds;
268-
scale = UIScreen.mainScreen.scale;
267+
UIWindow *window = nil;
268+
#if (TARGET_OS_IOS)
269+
if (@available(iOS 13.0, *)) {
270+
#elif (TARGET_OS_TV)
271+
if (@available(tvOS 13.0, *)) {
272+
#endif
273+
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
274+
if ([scene isKindOfClass:[UIWindowScene class]]) {
275+
window = ((UIWindowScene *)scene).windows.firstObject;
276+
break;
277+
}
278+
}
279+
if(window){
280+
bounds = window.bounds;
281+
scale = window.traitCollection.displayScale;
282+
}
283+
} else {
284+
bounds = UIScreen.mainScreen.bounds;
285+
scale = UIScreen.mainScreen.scale;
286+
}
269287
#elif (TARGET_OS_WATCH)
270288
bounds = WKInterfaceDevice.currentDevice.screenBounds;
271289
scale = WKInterfaceDevice.currentDevice.screenScale;
@@ -519,3 +537,4 @@ - (void)resetInstance
519537
}
520538

521539
@end
540+

CountlyFeedbacksInternal.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ - (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSStri
301301
{
302302
__block CLYInternalViewController* webVC = CLYInternalViewController.new;
303303
webVC.view.backgroundColor = UIColor.whiteColor;
304-
webVC.view.bounds = UIScreen.mainScreen.bounds;
304+
CGSize windowSize = [CountlyCommon.sharedInstance getWindowSize];
305+
webVC.view.bounds = CGRectMake(0, 0, windowSize.width, windowSize.height);
305306
webVC.modalPresentationStyle = UIModalPresentationCustom;
306307

307308
WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds];
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//
2+
// CountlyScreenMetricsTests.swift
3+
// CountlyTests
4+
//
5+
// Covers screen-metric helpers refactored on the `mainscreen_dpc` branch:
6+
// - CountlyCommon.getWindowSize() — used for content/feedback view sizing.
7+
// - CountlyDeviceInfo.resolution() — used for the `_resolution` metric.
8+
//
9+
// These tests are written to work both in a hosted XCTest bundle (where
10+
// UIApplication has connected window scenes) and in a logic-test bundle
11+
// (where no scene is attached). They assert *property* invariants rather
12+
// than concrete sizes so they hold across hosts and devices.
13+
//
14+
15+
import XCTest
16+
@testable import Countly
17+
18+
#if os(iOS)
19+
class CountlyScreenMetricsTests: CountlyBaseTestCase {
20+
21+
override func setUp() {
22+
super.setUp()
23+
Countly.sharedInstance().halt(true)
24+
}
25+
26+
override func tearDown() {
27+
Countly.sharedInstance().halt(true)
28+
super.tearDown()
29+
}
30+
31+
// MARK: - getWindowSize
32+
33+
/**
34+
* Regression guard: getWindowSize must not crash and must return a
35+
* finite, non-negative CGSize. The `mainscreen_dpc` refactor previously
36+
* broke the build by referencing `size` after removing its declaration;
37+
* this test exists primarily so the build/runtime path stays exercised.
38+
*/
39+
func test_getWindowSize_returnsFiniteNonNegativeSize() {
40+
let size = CountlyCommon.sharedInstance().getWindowSize()
41+
42+
XCTAssertFalse(size.width.isNaN, "width should not be NaN")
43+
XCTAssertFalse(size.height.isNaN, "height should not be NaN")
44+
XCTAssertGreaterThanOrEqual(size.width, 0, "width should be >= 0")
45+
XCTAssertGreaterThanOrEqual(size.height, 0, "height should be >= 0")
46+
}
47+
48+
/**
49+
* getWindowSize must be deterministic — repeated calls without any
50+
* window-scene churn should return the same value.
51+
*/
52+
func test_getWindowSize_isStableAcrossCalls() {
53+
let first = CountlyCommon.sharedInstance().getWindowSize()
54+
let second = CountlyCommon.sharedInstance().getWindowSize()
55+
let third = CountlyCommon.sharedInstance().getWindowSize()
56+
57+
XCTAssertEqual(first.width, second.width, accuracy: 0.0001)
58+
XCTAssertEqual(first.height, second.height, accuracy: 0.0001)
59+
XCTAssertEqual(second.width, third.width, accuracy: 0.0001)
60+
XCTAssertEqual(second.height, third.height, accuracy: 0.0001)
61+
}
62+
63+
/**
64+
* If a UIWindowScene is present (hosted test), getWindowSize should not
65+
* exceed the underlying window's bounds — safe-area adjustments may
66+
* shrink the size but never grow it. If no scene is present, the
67+
* function returns CGSizeZero, which satisfies the same invariant.
68+
*/
69+
func test_getWindowSize_doesNotExceedWindowBounds() {
70+
let size = CountlyCommon.sharedInstance().getWindowSize()
71+
72+
if let window = firstWindow() {
73+
XCTAssertLessThanOrEqual(size.width, window.bounds.width,
74+
"Reported width should not exceed window width")
75+
XCTAssertLessThanOrEqual(size.height, window.bounds.height,
76+
"Reported height should not exceed window height")
77+
} else {
78+
XCTAssertEqual(size.width, 0, accuracy: 0.0001,
79+
"Expected CGSizeZero when no window scene is attached")
80+
XCTAssertEqual(size.height, 0, accuracy: 0.0001,
81+
"Expected CGSizeZero when no window scene is attached")
82+
}
83+
}
84+
85+
// MARK: - resolution
86+
87+
/**
88+
* resolution() must return a "WIDTHxHEIGHT" formatted string with two
89+
* non-negative numeric components. This is the contract consumed by the
90+
* `_resolution` metric and by the content-builder query parameters.
91+
*/
92+
func test_resolution_isWellFormedString() {
93+
guard let resolution = CountlyDeviceInfo.resolution() else {
94+
XCTFail("resolution() returned nil on iOS")
95+
return
96+
}
97+
98+
let parts = resolution.components(separatedBy: "x")
99+
XCTAssertEqual(parts.count, 2,
100+
"Expected WIDTHxHEIGHT format, got: \(resolution)")
101+
guard parts.count == 2 else { return }
102+
103+
guard let width = Double(parts[0]), let height = Double(parts[1]) else {
104+
XCTFail("resolution() components should be numeric, got: \(resolution)")
105+
return
106+
}
107+
XCTAssertGreaterThanOrEqual(width, 0, "resolution width should be >= 0 (got: \(resolution))")
108+
XCTAssertGreaterThanOrEqual(height, 0, "resolution height should be >= 0 (got: \(resolution))")
109+
}
110+
111+
/**
112+
* On iOS 13+ the refactor reads bounds/scale from the first
113+
* UIWindowScene's window. When a scene is connected, the returned
114+
* string must match `bounds.size * displayScale` for that window.
115+
* When no scene is connected, the implementation falls through with
116+
* zero values and emits "0x0" — locks in the iOS 13+ no-fallback
117+
* behavior so regressions are visible.
118+
*/
119+
func test_resolution_matchesFirstSceneOrIsZero() {
120+
guard let resolution = CountlyDeviceInfo.resolution() else {
121+
XCTFail("resolution() returned nil on iOS")
122+
return
123+
}
124+
125+
if let window = firstWindow() {
126+
let scale = window.traitCollection.displayScale
127+
let expected = "\(percentG(window.bounds.width * scale))x\(percentG(window.bounds.height * scale))"
128+
XCTAssertEqual(resolution, expected,
129+
"resolution should reflect first window scene's pixel size")
130+
} else {
131+
XCTAssertEqual(resolution, "0x0",
132+
"iOS 13+ without a window scene should report 0x0 (no UIScreen fallback)")
133+
}
134+
}
135+
136+
/**
137+
* The metrics dictionary returned by CountlyDeviceInfo.metrics()
138+
* must include the `_resolution` key — this is the SDK-facing
139+
* contract that ends up in `/i` requests.
140+
*/
141+
func test_resolution_appearsInMetricsString() {
142+
guard let metricsString = CountlyDeviceInfo.metrics() else {
143+
XCTFail("metrics() returned nil")
144+
return
145+
}
146+
XCTAssertTrue(metricsString.contains("_resolution"),
147+
"metrics() should include the _resolution key, got: \(metricsString)")
148+
}
149+
150+
// MARK: - Helpers
151+
152+
private func firstWindow() -> UIWindow? {
153+
if #available(iOS 13.0, *) {
154+
for scene in UIApplication.shared.connectedScenes {
155+
if let windowScene = scene as? UIWindowScene,
156+
let window = windowScene.windows.first {
157+
return window
158+
}
159+
}
160+
return nil
161+
} else {
162+
return UIApplication.shared.delegate?.window ?? nil
163+
}
164+
}
165+
166+
/// Mirrors Objective-C `%g` formatting used by `CountlyDeviceInfo.resolution`.
167+
/// `%g` uses the shorter of `%e` or `%f`, trims trailing zeros, and drops
168+
/// the decimal point when not needed. `String(format:)` with `%g` matches
169+
/// this behavior on Apple platforms.
170+
private func percentG(_ value: CGFloat) -> String {
171+
return String(format: "%g", Double(value))
172+
}
173+
}
174+
#endif

0 commit comments

Comments
 (0)