Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 5bb144c

Browse files
Re-lands platform brightness support on iOS, plus platform contrast (#10791)
1 parent 219816d commit 5bb144c

File tree

3 files changed

+280
-1
lines changed

3 files changed

+280
-1
lines changed

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ @interface FlutterViewController () <FlutterBinaryMessenger>
3232
@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
3333
@end
3434

35+
// The following conditional compilation defines an API 13 concept on earlier API targets so that
36+
// a compiler compiling against API 12 or below does not blow up due to non-existent members.
37+
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
38+
typedef enum UIAccessibilityContrast : NSInteger {
39+
UIAccessibilityContrastUnspecified = 0,
40+
UIAccessibilityContrastNormal = 1,
41+
UIAccessibilityContrastHigh = 2
42+
} UIAccessibilityContrast;
43+
44+
@interface UITraitCollection (MethodsFromNewerSDK)
45+
- (UIAccessibilityContrast)accessibilityContrast;
46+
@end
47+
#endif
48+
3549
@implementation FlutterViewController {
3650
std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
3751
fml::scoped_nsobject<FlutterEngine> _engine;
@@ -434,6 +448,9 @@ - (void)viewWillAppear:(BOOL)animated {
434448
_engineNeedsLaunch = NO;
435449
}
436450

451+
// Send platform settings to Flutter, e.g., platform brightness.
452+
[self onUserSettingsChanged:nil];
453+
437454
// Only recreate surface on subsequent appearances when viewport metrics are known.
438455
// First time surface creation is done on viewDidLayoutSubviews.
439456
if (_viewportMetrics.physical_width) {
@@ -885,10 +902,17 @@ - (void)onLocaleUpdated:(NSNotification*)notification {
885902

886903
#pragma mark - Set user settings
887904

905+
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
906+
[super traitCollectionDidChange:previousTraitCollection];
907+
[self onUserSettingsChanged:nil];
908+
}
909+
888910
- (void)onUserSettingsChanged:(NSNotification*)notification {
889911
[[_engine.get() settingsChannel] sendMessage:@{
890912
@"textScaleFactor" : @([self textScaleFactor]),
891913
@"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]),
914+
@"platformBrightness" : [self brightnessMode],
915+
@"platformContrast" : [self contrastMode]
892916
}];
893917
}
894918

@@ -962,6 +986,40 @@ - (BOOL)isAlwaysUse24HourFormat {
962986
return [dateFormat rangeOfString:@"a"].location == NSNotFound;
963987
}
964988

989+
// The brightness mode of the platform, e.g., light or dark, expressed as a string that
990+
// is understood by the Flutter framework. See the settings system channel for more
991+
// information.
992+
- (NSString*)brightnessMode {
993+
if (@available(iOS 13, *)) {
994+
UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
995+
996+
if (style == UIUserInterfaceStyleDark) {
997+
return @"dark";
998+
} else {
999+
return @"light";
1000+
}
1001+
} else {
1002+
return @"light";
1003+
}
1004+
}
1005+
1006+
// The contrast mode of the platform, e.g., normal or high, expressed as a string that is
1007+
// understood by the Flutter framework. See the settings system channel for more
1008+
// information.
1009+
- (NSString*)contrastMode {
1010+
if (@available(iOS 13, *)) {
1011+
UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
1012+
1013+
if (contrast == UIAccessibilityContrastHigh) {
1014+
return @"high";
1015+
} else {
1016+
return @"normal";
1017+
}
1018+
} else {
1019+
return @"normal";
1020+
}
1021+
}
1022+
9651023
#pragma mark - Status Bar touch event handling
9661024

9671025
// Standard iOS status bar height in pixels.

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,29 @@
66
#import <XCTest/XCTest.h>
77
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
88

9+
#include "FlutterBinaryMessenger.h"
10+
11+
#if !__has_feature(objc_arc)
12+
#error ARC must be enabled!
13+
#endif
14+
915
@interface FlutterViewControllerTest : XCTestCase
1016
@end
1117

18+
// The following conditional compilation defines an API 13 concept on earlier API targets so that
19+
// a compiler compiling against API 12 or below does not blow up due to non-existent members.
20+
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
21+
typedef enum UIAccessibilityContrast : NSInteger {
22+
UIAccessibilityContrastUnspecified = 0,
23+
UIAccessibilityContrastNormal = 1,
24+
UIAccessibilityContrastHigh = 2
25+
} UIAccessibilityContrast;
26+
27+
@interface UITraitCollection (MethodsFromNewerSDK)
28+
- (UIAccessibilityContrast)accessibilityContrast;
29+
@end
30+
#endif
31+
1232
@implementation FlutterViewControllerTest
1333

1434
- (void)testBinaryMessenger {
@@ -23,4 +43,205 @@ - (void)testBinaryMessenger {
2343
OCMVerify([engine binaryMessenger]);
2444
}
2545

46+
#pragma mark - Platform Brightness
47+
48+
- (void)testItReportsLightPlatformBrightnessByDefault {
49+
// Setup test.
50+
id engine = OCMClassMock([FlutterEngine class]);
51+
52+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
53+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
54+
55+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
56+
nibName:nil
57+
bundle:nil];
58+
59+
// Exercise behavior under test.
60+
[vc traitCollectionDidChange:nil];
61+
62+
// Verify behavior.
63+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
64+
return [message[@"platformBrightness"] isEqualToString:@"light"];
65+
}]]);
66+
67+
// Clean up mocks
68+
[engine stopMocking];
69+
[settingsChannel stopMocking];
70+
}
71+
72+
- (void)testItReportsPlatformBrightnessWhenViewWillAppear {
73+
// Setup test.
74+
id engine = OCMClassMock([FlutterEngine class]);
75+
76+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
77+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
78+
79+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
80+
nibName:nil
81+
bundle:nil];
82+
83+
// Exercise behavior under test.
84+
[vc viewWillAppear:false];
85+
86+
// Verify behavior.
87+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
88+
return [message[@"platformBrightness"] isEqualToString:@"light"];
89+
}]]);
90+
91+
// Clean up mocks
92+
[engine stopMocking];
93+
[settingsChannel stopMocking];
94+
}
95+
96+
- (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
97+
if (!@available(iOS 13, *)) {
98+
return;
99+
}
100+
101+
// Setup test.
102+
id engine = OCMClassMock([FlutterEngine class]);
103+
104+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
105+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
106+
107+
FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
108+
nibName:nil
109+
bundle:nil];
110+
id mockTraitCollection =
111+
[self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
112+
113+
// We partially mock the real FlutterViewController to act as the OS and report
114+
// the UITraitCollection of our choice. Mocking the object under test is not
115+
// desirable, but given that the OS does not offer a DI approach to providing
116+
// our own UITraitCollection, this seems to be the least bad option.
117+
id partialMockVC = OCMPartialMock(realVC);
118+
OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
119+
120+
// Exercise behavior under test.
121+
[partialMockVC traitCollectionDidChange:nil];
122+
123+
// Verify behavior.
124+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
125+
return [message[@"platformBrightness"] isEqualToString:@"dark"];
126+
}]]);
127+
128+
// Clean up mocks
129+
[partialMockVC stopMocking];
130+
[engine stopMocking];
131+
[settingsChannel stopMocking];
132+
[mockTraitCollection stopMocking];
133+
}
134+
135+
// Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
136+
// which is set to the given "style".
137+
- (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
138+
id mockTraitCollection = OCMClassMock([UITraitCollection class]);
139+
OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
140+
return mockTraitCollection;
141+
}
142+
143+
#pragma mark - Platform Contrast
144+
145+
- (void)testItReportsNormalPlatformContrastByDefault {
146+
if (!@available(iOS 13, *)) {
147+
return;
148+
}
149+
150+
// Setup test.
151+
id engine = OCMClassMock([FlutterEngine class]);
152+
153+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
154+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
155+
156+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
157+
nibName:nil
158+
bundle:nil];
159+
160+
// Exercise behavior under test.
161+
[vc traitCollectionDidChange:nil];
162+
163+
// Verify behavior.
164+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
165+
return [message[@"platformContrast"] isEqualToString:@"normal"];
166+
}]]);
167+
168+
// Clean up mocks
169+
[engine stopMocking];
170+
[settingsChannel stopMocking];
171+
}
172+
173+
- (void)testItReportsPlatformContrastWhenViewWillAppear {
174+
if (!@available(iOS 13, *)) {
175+
return;
176+
}
177+
178+
// Setup test.
179+
id engine = OCMClassMock([FlutterEngine class]);
180+
181+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
182+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
183+
184+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
185+
nibName:nil
186+
bundle:nil];
187+
188+
// Exercise behavior under test.
189+
[vc viewWillAppear:false];
190+
191+
// Verify behavior.
192+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
193+
return [message[@"platformContrast"] isEqualToString:@"normal"];
194+
}]]);
195+
196+
// Clean up mocks
197+
[engine stopMocking];
198+
[settingsChannel stopMocking];
199+
}
200+
201+
- (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
202+
if (!@available(iOS 13, *)) {
203+
return;
204+
}
205+
206+
// Setup test.
207+
id engine = OCMClassMock([FlutterEngine class]);
208+
209+
id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
210+
OCMStub([engine settingsChannel]).andReturn(settingsChannel);
211+
212+
FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
213+
nibName:nil
214+
bundle:nil];
215+
id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];
216+
217+
// We partially mock the real FlutterViewController to act as the OS and report
218+
// the UITraitCollection of our choice. Mocking the object under test is not
219+
// desirable, but given that the OS does not offer a DI approach to providing
220+
// our own UITraitCollection, this seems to be the least bad option.
221+
id partialMockVC = OCMPartialMock(realVC);
222+
OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
223+
224+
// Exercise behavior under test.
225+
[partialMockVC traitCollectionDidChange:mockTraitCollection];
226+
227+
// Verify behavior.
228+
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
229+
return [message[@"platformContrast"] isEqualToString:@"high"];
230+
}]]);
231+
232+
// Clean up mocks
233+
[partialMockVC stopMocking];
234+
[engine stopMocking];
235+
[settingsChannel stopMocking];
236+
[mockTraitCollection stopMocking];
237+
}
238+
239+
// Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
240+
// which is set to the given "contrast".
241+
- (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
242+
id mockTraitCollection = OCMClassMock([UITraitCollection class]);
243+
OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
244+
return mockTraitCollection;
245+
}
246+
26247
@end

testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10-
0D17A5C022D78FCD0057279F /* FlutterViewControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D17A5BF22D78FCD0057279F /* FlutterViewControllerTest.m */; };
10+
0D17A5C022D78FCD0057279F /* FlutterViewControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D17A5BF22D78FCD0057279F /* FlutterViewControllerTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; };
1111
0D4C3FB022DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D4C3FAF22DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; };
1212
0D52D3BD22C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0D52D3B622C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; };
1313
0D6AB6B622BB05E100EEE540 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */; };

0 commit comments

Comments
 (0)