Skip to content

Commit

Permalink
Re-lands platform brightness support on iOS, plus platform contrast (f…
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll authored Aug 15, 2019
1 parent 219816d commit 5bb144c
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ @interface FlutterViewController () <FlutterBinaryMessenger>
@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
@end

// The following conditional compilation defines an API 13 concept on earlier API targets so that
// a compiler compiling against API 12 or below does not blow up due to non-existent members.
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
typedef enum UIAccessibilityContrast : NSInteger {
UIAccessibilityContrastUnspecified = 0,
UIAccessibilityContrastNormal = 1,
UIAccessibilityContrastHigh = 2
} UIAccessibilityContrast;

@interface UITraitCollection (MethodsFromNewerSDK)
- (UIAccessibilityContrast)accessibilityContrast;
@end
#endif

@implementation FlutterViewController {
std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
fml::scoped_nsobject<FlutterEngine> _engine;
Expand Down Expand Up @@ -434,6 +448,9 @@ - (void)viewWillAppear:(BOOL)animated {
_engineNeedsLaunch = NO;
}

// Send platform settings to Flutter, e.g., platform brightness.
[self onUserSettingsChanged:nil];

// Only recreate surface on subsequent appearances when viewport metrics are known.
// First time surface creation is done on viewDidLayoutSubviews.
if (_viewportMetrics.physical_width) {
Expand Down Expand Up @@ -885,10 +902,17 @@ - (void)onLocaleUpdated:(NSNotification*)notification {

#pragma mark - Set user settings

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self onUserSettingsChanged:nil];
}

- (void)onUserSettingsChanged:(NSNotification*)notification {
[[_engine.get() settingsChannel] sendMessage:@{
@"textScaleFactor" : @([self textScaleFactor]),
@"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]),
@"platformBrightness" : [self brightnessMode],
@"platformContrast" : [self contrastMode]
}];
}

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

// The brightness mode of the platform, e.g., light or dark, expressed as a string that
// is understood by the Flutter framework. See the settings system channel for more
// information.
- (NSString*)brightnessMode {
if (@available(iOS 13, *)) {
UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;

if (style == UIUserInterfaceStyleDark) {
return @"dark";
} else {
return @"light";
}
} else {
return @"light";
}
}

// The contrast mode of the platform, e.g., normal or high, expressed as a string that is
// understood by the Flutter framework. See the settings system channel for more
// information.
- (NSString*)contrastMode {
if (@available(iOS 13, *)) {
UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;

if (contrast == UIAccessibilityContrastHigh) {
return @"high";
} else {
return @"normal";
}
} else {
return @"normal";
}
}

#pragma mark - Status Bar touch event handling

// Standard iOS status bar height in pixels.
Expand Down
221 changes: 221 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,29 @@
#import <XCTest/XCTest.h>
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"

#include "FlutterBinaryMessenger.h"

#if !__has_feature(objc_arc)
#error ARC must be enabled!
#endif

@interface FlutterViewControllerTest : XCTestCase
@end

// The following conditional compilation defines an API 13 concept on earlier API targets so that
// a compiler compiling against API 12 or below does not blow up due to non-existent members.
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
typedef enum UIAccessibilityContrast : NSInteger {
UIAccessibilityContrastUnspecified = 0,
UIAccessibilityContrastNormal = 1,
UIAccessibilityContrastHigh = 2
} UIAccessibilityContrast;

@interface UITraitCollection (MethodsFromNewerSDK)
- (UIAccessibilityContrast)accessibilityContrast;
@end
#endif

@implementation FlutterViewControllerTest

- (void)testBinaryMessenger {
Expand All @@ -23,4 +43,205 @@ - (void)testBinaryMessenger {
OCMVerify([engine binaryMessenger]);
}

#pragma mark - Platform Brightness

- (void)testItReportsLightPlatformBrightnessByDefault {
// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

// Exercise behavior under test.
[vc traitCollectionDidChange:nil];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformBrightness"] isEqualToString:@"light"];
}]]);

// Clean up mocks
[engine stopMocking];
[settingsChannel stopMocking];
}

- (void)testItReportsPlatformBrightnessWhenViewWillAppear {
// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

// Exercise behavior under test.
[vc viewWillAppear:false];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformBrightness"] isEqualToString:@"light"];
}]]);

// Clean up mocks
[engine stopMocking];
[settingsChannel stopMocking];
}

- (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
if (!@available(iOS 13, *)) {
return;
}

// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
id mockTraitCollection =
[self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];

// We partially mock the real FlutterViewController to act as the OS and report
// the UITraitCollection of our choice. Mocking the object under test is not
// desirable, but given that the OS does not offer a DI approach to providing
// our own UITraitCollection, this seems to be the least bad option.
id partialMockVC = OCMPartialMock(realVC);
OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);

// Exercise behavior under test.
[partialMockVC traitCollectionDidChange:nil];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformBrightness"] isEqualToString:@"dark"];
}]]);

// Clean up mocks
[partialMockVC stopMocking];
[engine stopMocking];
[settingsChannel stopMocking];
[mockTraitCollection stopMocking];
}

// Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
// which is set to the given "style".
- (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
id mockTraitCollection = OCMClassMock([UITraitCollection class]);
OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
return mockTraitCollection;
}

#pragma mark - Platform Contrast

- (void)testItReportsNormalPlatformContrastByDefault {
if (!@available(iOS 13, *)) {
return;
}

// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

// Exercise behavior under test.
[vc traitCollectionDidChange:nil];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformContrast"] isEqualToString:@"normal"];
}]]);

// Clean up mocks
[engine stopMocking];
[settingsChannel stopMocking];
}

- (void)testItReportsPlatformContrastWhenViewWillAppear {
if (!@available(iOS 13, *)) {
return;
}

// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

// Exercise behavior under test.
[vc viewWillAppear:false];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformContrast"] isEqualToString:@"normal"];
}]]);

// Clean up mocks
[engine stopMocking];
[settingsChannel stopMocking];
}

- (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
if (!@available(iOS 13, *)) {
return;
}

// Setup test.
id engine = OCMClassMock([FlutterEngine class]);

id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine settingsChannel]).andReturn(settingsChannel);

FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];

// We partially mock the real FlutterViewController to act as the OS and report
// the UITraitCollection of our choice. Mocking the object under test is not
// desirable, but given that the OS does not offer a DI approach to providing
// our own UITraitCollection, this seems to be the least bad option.
id partialMockVC = OCMPartialMock(realVC);
OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);

// Exercise behavior under test.
[partialMockVC traitCollectionDidChange:mockTraitCollection];

// Verify behavior.
OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"platformContrast"] isEqualToString:@"high"];
}]]);

// Clean up mocks
[partialMockVC stopMocking];
[engine stopMocking];
[settingsChannel stopMocking];
[mockTraitCollection stopMocking];
}

// Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
// which is set to the given "contrast".
- (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
id mockTraitCollection = OCMClassMock([UITraitCollection class]);
OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
return mockTraitCollection;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
objects = {

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

0 comments on commit 5bb144c

Please sign in to comment.