Skip to content

Commit 43b618e

Browse files
authored
Added keyEvent support for iOS 13.4+ (#20972)
1 parent f854cbb commit 43b618e

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

shell/platform/darwin/ios/framework/Headers/FlutterEngine.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,14 @@ FLUTTER_EXPORT
322322
*/
323323
@property(nonatomic, readonly) FlutterBasicMessageChannel* settingsChannel;
324324

325+
/**
326+
* The `FlutterBasicMessageChannel` used for communicating key events
327+
* from physical keyboards
328+
*
329+
* Can be nil after `destroyContext` is called.
330+
*/
331+
@property(nonatomic, readonly) FlutterBasicMessageChannel* keyEventChannel;
332+
325333
/**
326334
* The `NSURL` of the observatory for the service isolate.
327335
*

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ @implementation FlutterEngine {
8181
fml::scoped_nsobject<FlutterBasicMessageChannel> _lifecycleChannel;
8282
fml::scoped_nsobject<FlutterBasicMessageChannel> _systemChannel;
8383
fml::scoped_nsobject<FlutterBasicMessageChannel> _settingsChannel;
84+
fml::scoped_nsobject<FlutterBasicMessageChannel> _keyEventChannel;
8485

8586
int64_t _nextTextureId;
8687

@@ -350,6 +351,9 @@ - (FlutterBasicMessageChannel*)systemChannel {
350351
- (FlutterBasicMessageChannel*)settingsChannel {
351352
return _settingsChannel.get();
352353
}
354+
- (FlutterBasicMessageChannel*)keyEventChannel {
355+
return _keyEventChannel.get();
356+
}
353357

354358
- (NSURL*)observatoryUrl {
355359
return [_publisher.get() url];
@@ -364,6 +368,7 @@ - (void)resetChannels {
364368
_lifecycleChannel.reset();
365369
_systemChannel.reset();
366370
_settingsChannel.reset();
371+
_keyEventChannel.reset();
367372
}
368373

369374
- (void)startProfiler:(NSString*)threadLabel {
@@ -436,6 +441,11 @@ - (void)setupChannels {
436441
binaryMessenger:self.binaryMessenger
437442
codec:[FlutterJSONMessageCodec sharedInstance]]);
438443

444+
_keyEventChannel.reset([[FlutterBasicMessageChannel alloc]
445+
initWithName:@"flutter/keyevent"
446+
binaryMessenger:self.binaryMessenger
447+
codec:[FlutterJSONMessageCodec sharedInstance]]);
448+
439449
_textInputPlugin.reset([[FlutterTextInputPlugin alloc] init]);
440450
_textInputPlugin.get().textInputDelegate = self;
441451

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,58 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification {
10241024
[self updateViewportMetrics];
10251025
}
10261026

1027+
- (void)dispatchPresses:(NSSet<UIPress*>*)presses API_AVAILABLE(ios(13.4)) {
1028+
if (@available(iOS 13.4, *)) {
1029+
for (UIPress* press in presses) {
1030+
if (press.key == nil || press.phase == UIPressPhaseStationary ||
1031+
press.phase == UIPressPhaseChanged) {
1032+
continue;
1033+
}
1034+
NSMutableDictionary* keyMessage = [@{
1035+
@"keymap" : @"ios",
1036+
@"type" : @"unknown",
1037+
@"keyCode" : @(press.key.keyCode),
1038+
@"modifiers" : @(press.key.modifierFlags),
1039+
@"characters" : press.key.characters,
1040+
@"charactersIgnoringModifiers" : press.key.charactersIgnoringModifiers
1041+
} mutableCopy];
1042+
1043+
if (press.phase == UIPressPhaseBegan) {
1044+
keyMessage[@"type"] = @"keydown";
1045+
} else if (press.phase == UIPressPhaseEnded || press.phase == UIPressPhaseCancelled) {
1046+
keyMessage[@"type"] = @"keyup";
1047+
}
1048+
1049+
[[_engine.get() keyEventChannel] sendMessage:keyMessage];
1050+
}
1051+
}
1052+
}
1053+
1054+
- (void)pressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
1055+
if (@available(iOS 13.4, *)) {
1056+
[self dispatchPresses:presses];
1057+
}
1058+
}
1059+
1060+
- (void)pressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
1061+
if (@available(iOS 13.4, *)) {
1062+
[self dispatchPresses:presses];
1063+
}
1064+
}
1065+
1066+
- (void)pressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
1067+
if (@available(iOS 13.4, *)) {
1068+
[self dispatchPresses:presses];
1069+
}
1070+
}
1071+
1072+
- (void)pressesCancelled:(NSSet<UIPress*>*)presses
1073+
withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
1074+
if (@available(iOS 13.4, *)) {
1075+
[self dispatchPresses:presses];
1076+
}
1077+
}
1078+
10271079
#pragma mark - Orientation updates
10281080

10291081
- (void)onOrientationPreferencesUpdated:(NSNotification*)notification {

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

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ - (UIAccessibilityContrast)accessibilityContrast;
6262
@interface FlutterViewController (Tests)
6363
- (void)surfaceUpdated:(BOOL)appeared;
6464
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
65+
- (void)dispatchPresses:(NSSet<UIPress*>*)presses;
6566
@end
6667

6768
@implementation FlutterViewControllerTest
@@ -549,4 +550,150 @@ - (void)testNotifyLowMemory {
549550
OCMVerify([engine notifyLowMemory]);
550551
}
551552

553+
- (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
554+
if (@available(iOS 13.4, *)) {
555+
// noop
556+
} else {
557+
return;
558+
}
559+
560+
id engine = OCMClassMock([FlutterEngine class]);
561+
562+
id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
563+
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);
564+
565+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
566+
nibName:nil
567+
bundle:nil];
568+
569+
id testSet = [self fakeUiPressSetForPhase:UIPressPhaseBegan
570+
keyCode:UIKeyboardHIDUsageKeyboardA
571+
modifierFlags:UIKeyModifierShift
572+
characters:@"a"
573+
charactersIgnoringModifiers:@"A"];
574+
575+
// Exercise behavior under test.
576+
[vc dispatchPresses:testSet];
577+
578+
// Verify behavior.
579+
OCMVerify([keyEventChannel
580+
sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
581+
return [message[@"keymap"] isEqualToString:@"ios"] &&
582+
[message[@"type"] isEqualToString:@"keydown"] &&
583+
[message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] &&
584+
[message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] &&
585+
[message[@"characters"] isEqualToString:@"a"] &&
586+
[message[@"charactersIgnoringModifiers"] isEqualToString:@"A"];
587+
}]]);
588+
589+
// Clean up mocks
590+
[engine stopMocking];
591+
[keyEventChannel stopMocking];
592+
}
593+
594+
- (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
595+
if (@available(iOS 13.4, *)) {
596+
// noop
597+
} else {
598+
return;
599+
}
600+
601+
id engine = OCMClassMock([FlutterEngine class]);
602+
603+
id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
604+
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);
605+
606+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
607+
nibName:nil
608+
bundle:nil];
609+
610+
id testSet = [self fakeUiPressSetForPhase:UIPressPhaseEnded
611+
keyCode:UIKeyboardHIDUsageKeyboardA
612+
modifierFlags:UIKeyModifierShift
613+
characters:@"a"
614+
charactersIgnoringModifiers:@"A"];
615+
616+
// Exercise behavior under test.
617+
[vc dispatchPresses:testSet];
618+
619+
// Verify behavior.
620+
OCMVerify([keyEventChannel
621+
sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
622+
return [message[@"keymap"] isEqualToString:@"ios"] &&
623+
[message[@"type"] isEqualToString:@"keyup"] &&
624+
[message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] &&
625+
[message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] &&
626+
[message[@"characters"] isEqualToString:@"a"] &&
627+
[message[@"charactersIgnoringModifiers"] isEqualToString:@"A"];
628+
}]]);
629+
630+
// Clean up mocks
631+
[engine stopMocking];
632+
[keyEventChannel stopMocking];
633+
}
634+
635+
- (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
636+
if (@available(iOS 13.4, *)) {
637+
// noop
638+
} else {
639+
return;
640+
}
641+
642+
id engine = OCMClassMock([FlutterEngine class]);
643+
644+
id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
645+
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);
646+
647+
FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
648+
nibName:nil
649+
bundle:nil];
650+
651+
id emptySet = [NSSet set];
652+
id ignoredSet = [self fakeUiPressSetForPhase:UIPressPhaseStationary
653+
keyCode:UIKeyboardHIDUsageKeyboardA
654+
modifierFlags:UIKeyModifierShift
655+
characters:@"a"
656+
charactersIgnoringModifiers:@"A"];
657+
658+
id mockUiPress = OCMClassMock([UIPress class]);
659+
OCMStub([mockUiPress phase]).andReturn(UIPressPhaseBegan);
660+
id emptyKeySet = [NSSet setWithArray:@[ mockUiPress ]];
661+
// Exercise behavior under test.
662+
[vc dispatchPresses:emptySet];
663+
[vc dispatchPresses:ignoredSet];
664+
[vc dispatchPresses:emptyKeySet];
665+
666+
// Verify behavior.
667+
OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
668+
669+
// Clean up mocks
670+
[engine stopMocking];
671+
[keyEventChannel stopMocking];
672+
}
673+
674+
- (NSSet<UIPress*>*)fakeUiPressSetForPhase:(UIPressPhase)phase
675+
keyCode:(UIKeyboardHIDUsage)keyCode
676+
modifierFlags:(UIKeyModifierFlags)modifierFlags
677+
characters:(NSString*)characters
678+
charactersIgnoringModifiers:(NSString*)charactersIgnoringModifiers
679+
API_AVAILABLE(ios(13.4)) {
680+
if (@available(iOS 13.4, *)) {
681+
// noop
682+
} else {
683+
return [NSSet set];
684+
}
685+
id mockUiPress = OCMClassMock([UIPress class]);
686+
OCMStub([mockUiPress phase]).andReturn(phase);
687+
688+
id mockUiKey = OCMClassMock([UIKey class]);
689+
OCMStub([mockUiKey keyCode]).andReturn(keyCode);
690+
OCMStub([mockUiKey modifierFlags]).andReturn(modifierFlags);
691+
OCMStub([mockUiKey characters]).andReturn(characters);
692+
OCMStub([mockUiKey charactersIgnoringModifiers]).andReturn(charactersIgnoringModifiers);
693+
694+
OCMStub([mockUiPress key]).andReturn(mockUiKey);
695+
696+
return [NSSet setWithArray:@[ mockUiPress ]];
697+
}
698+
552699
@end

0 commit comments

Comments
 (0)