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

Commit 45d0ae7

Browse files
author
Chris Yang
authored
Reland "iOS spell-checker ObjC #32941" (#34356)
1 parent abe7b2c commit 45d0ae7

File tree

7 files changed

+558
-0
lines changed

7 files changed

+558
-0
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora
16981698
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
16991699
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
17001700
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
1701+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h
1702+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
1703+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm
17011704
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h
17021705
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h
17031706
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

shell/platform/darwin/ios/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ source_set("flutter_framework_source") {
7979
"framework/Source/FlutterRestorationPlugin.mm",
8080
"framework/Source/FlutterSemanticsScrollView.h",
8181
"framework/Source/FlutterSemanticsScrollView.mm",
82+
"framework/Source/FlutterSpellCheckPlugin.h",
83+
"framework/Source/FlutterSpellCheckPlugin.mm",
8284
"framework/Source/FlutterTextInputDelegate.h",
8385
"framework/Source/FlutterTextInputPlugin.h",
8486
"framework/Source/FlutterTextInputPlugin.mm",
@@ -264,6 +266,7 @@ shared_library("ios_test_flutter") {
264266
"framework/Source/FlutterKeyboardManagerTest.mm",
265267
"framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm",
266268
"framework/Source/FlutterRestorationPluginTest.mm",
269+
"framework/Source/FlutterSpellCheckPluginTest.mm",
267270
"framework/Source/FlutterTextInputPluginTest.mm",
268271
"framework/Source/FlutterUndoManagerPluginTest.mm",
269272
"framework/Source/FlutterViewControllerTest.mm",

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h"
2525
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h"
2626
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
27+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
2728
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
2829
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerDelegate.h"
2930
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.h"
@@ -116,6 +117,7 @@ @implementation FlutterEngine {
116117
fml::scoped_nsobject<FlutterPlatformPlugin> _platformPlugin;
117118
fml::scoped_nsobject<FlutterTextInputPlugin> _textInputPlugin;
118119
fml::scoped_nsobject<FlutterUndoManagerPlugin> _undoManagerPlugin;
120+
fml::scoped_nsobject<FlutterSpellCheckPlugin> _spellCheckPlugin;
119121
fml::scoped_nsobject<FlutterRestorationPlugin> _restorationPlugin;
120122
fml::scoped_nsobject<FlutterMethodChannel> _localizationChannel;
121123
fml::scoped_nsobject<FlutterMethodChannel> _navigationChannel;
@@ -124,6 +126,7 @@ @implementation FlutterEngine {
124126
fml::scoped_nsobject<FlutterMethodChannel> _platformViewsChannel;
125127
fml::scoped_nsobject<FlutterMethodChannel> _textInputChannel;
126128
fml::scoped_nsobject<FlutterMethodChannel> _undoManagerChannel;
129+
fml::scoped_nsobject<FlutterMethodChannel> _spellCheckChannel;
127130
fml::scoped_nsobject<FlutterBasicMessageChannel> _lifecycleChannel;
128131
fml::scoped_nsobject<FlutterBasicMessageChannel> _systemChannel;
129132
fml::scoped_nsobject<FlutterBasicMessageChannel> _settingsChannel;
@@ -469,6 +472,9 @@ - (FlutterMethodChannel*)textInputChannel {
469472
- (FlutterMethodChannel*)undoManagerChannel {
470473
return _undoManagerChannel.get();
471474
}
475+
- (FlutterMethodChannel*)spellCheckChannel {
476+
return _spellCheckChannel.get();
477+
}
472478
- (FlutterBasicMessageChannel*)lifecycleChannel {
473479
return _lifecycleChannel.get();
474480
}
@@ -498,6 +504,7 @@ - (void)resetChannels {
498504
_systemChannel.reset();
499505
_settingsChannel.reset();
500506
_keyEventChannel.reset();
507+
_spellCheckChannel.reset();
501508
}
502509

503510
- (void)startProfiler {
@@ -566,6 +573,11 @@ - (void)setupChannels {
566573
binaryMessenger:self.binaryMessenger
567574
codec:[FlutterJSONMethodCodec sharedInstance]]);
568575

576+
_spellCheckChannel.reset([[FlutterMethodChannel alloc]
577+
initWithName:@"flutter/spellcheck"
578+
binaryMessenger:self.binaryMessenger
579+
codec:[FlutterStandardMethodCodec sharedInstance]]);
580+
569581
_lifecycleChannel.reset([[FlutterBasicMessageChannel alloc]
570582
initWithName:@"flutter/lifecycle"
571583
binaryMessenger:self.binaryMessenger
@@ -600,6 +612,7 @@ - (void)setupChannels {
600612
_restorationPlugin.reset([[FlutterRestorationPlugin alloc]
601613
initWithChannel:_restorationChannel.get()
602614
restorationEnabled:_restorationEnabled]);
615+
_spellCheckPlugin.reset([[FlutterSpellCheckPlugin alloc] init]);
603616
}
604617

605618
- (void)maybeSetupPlatformViewChannels {
@@ -627,6 +640,12 @@ - (void)maybeSetupPlatformViewChannels {
627640
setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
628641
[undoManagerPlugin handleMethodCall:call result:result];
629642
}];
643+
644+
FlutterSpellCheckPlugin* spellCheckPlugin = _spellCheckPlugin.get();
645+
[_spellCheckChannel.get()
646+
setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
647+
[spellCheckPlugin handleMethodCall:call result:result];
648+
}];
630649
}
631650
}
632651

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
6+
#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
7+
8+
#include "flutter/fml/memory/weak_ptr.h"
9+
10+
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
11+
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"
12+
13+
@interface FlutterSpellCheckPlugin : NSObject
14+
15+
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
16+
17+
@end
18+
19+
@interface FlutterSpellCheckResult : NSObject
20+
21+
@property(nonatomic, copy, readonly) NSArray<NSString*>* suggestions;
22+
@property(nonatomic, assign, readonly) NSRange misspelledRange;
23+
24+
- (instancetype)init NS_UNAVAILABLE;
25+
+ (instancetype)new NS_UNAVAILABLE;
26+
- (instancetype)initWithMisspelledRange:(NSRange)range
27+
suggestions:(NSArray<NSString*>*)suggestions NS_DESIGNATED_INITIALIZER;
28+
- (NSDictionary<NSString*, NSObject*>*)toDictionary;
29+
30+
@end
31+
32+
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
6+
7+
#import <Foundation/Foundation.h>
8+
#import <UIKit/UIKit.h>
9+
10+
#import "flutter/fml/logging.h"
11+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
12+
13+
// Method Channel name to start spell check.
14+
static NSString* const kInitiateSpellCheck = @"SpellCheck.initiateSpellCheck";
15+
16+
@interface FlutterSpellCheckPlugin ()
17+
18+
@property(nonatomic, retain) UITextChecker* textChecker;
19+
20+
@end
21+
22+
@implementation FlutterSpellCheckPlugin
23+
24+
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
25+
if (!_textChecker) {
26+
// UITextChecker is an expensive object to initiate, see:
27+
// https://github.com/flutter/flutter/issues/104454. Lazily initialate the UITextChecker object
28+
// until at first method channel call. We avoid using lazy getter for testing.
29+
_textChecker = [[UITextChecker alloc] init];
30+
}
31+
NSString* method = call.method;
32+
NSArray* args = call.arguments;
33+
if ([method isEqualToString:kInitiateSpellCheck]) {
34+
FML_DCHECK(args.count == 2);
35+
id language = args[0];
36+
id text = args[1];
37+
if (language == [NSNull null] || text == [NSNull null]) {
38+
// Bail if null arguments are passed from dart.
39+
result(nil);
40+
return;
41+
}
42+
43+
NSArray<NSDictionary<NSString*, id>*>* spellCheckResult =
44+
[self findAllSpellCheckSuggestionsForText:text inLanguage:language];
45+
result(spellCheckResult);
46+
}
47+
}
48+
49+
// Get all the misspelled words and suggestions in the entire String.
50+
//
51+
// The result will be formatted as an NSArray.
52+
// Each item of the array is a dictionary representing a misspelled word and suggestions.
53+
// The format looks like:
54+
// {
55+
// startIndex: 0,
56+
// endIndex: 5,
57+
// suggestions: [hello, ...]
58+
// }
59+
//
60+
// Returns nil if the language is invalid.
61+
// Returns an empty array if no spell check suggestions.
62+
- (NSArray<NSDictionary<NSString*, id>*>*)findAllSpellCheckSuggestionsForText:(NSString*)text
63+
inLanguage:(NSString*)language {
64+
if (![UITextChecker.availableLanguages containsObject:language]) {
65+
return nil;
66+
}
67+
68+
NSMutableArray<FlutterSpellCheckResult*>* allSpellSuggestions = [[NSMutableArray alloc] init];
69+
70+
FlutterSpellCheckResult* nextSpellSuggestion;
71+
NSUInteger nextOffset = 0;
72+
do {
73+
nextSpellSuggestion = [self findSpellCheckSuggestionsForText:text
74+
inLanguage:language
75+
startingOffset:nextOffset];
76+
if (nextSpellSuggestion != nil) {
77+
[allSpellSuggestions addObject:nextSpellSuggestion];
78+
nextOffset =
79+
nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length;
80+
}
81+
} while (nextSpellSuggestion != nil && nextOffset < text.length);
82+
83+
NSMutableArray* methodChannelResult = [[[NSMutableArray alloc] init] autorelease];
84+
85+
for (FlutterSpellCheckResult* result in allSpellSuggestions) {
86+
[methodChannelResult addObject:[result toDictionary]];
87+
}
88+
89+
[allSpellSuggestions release];
90+
return methodChannelResult;
91+
}
92+
93+
// Get the misspelled word and suggestions.
94+
//
95+
// Returns nil if no spell check suggestions.
96+
- (FlutterSpellCheckResult*)findSpellCheckSuggestionsForText:(NSString*)text
97+
inLanguage:(NSString*)language
98+
startingOffset:(NSInteger)startingOffset {
99+
FML_DCHECK([UITextChecker.availableLanguages containsObject:language]);
100+
NSRange misspelledRange =
101+
[self.textChecker rangeOfMisspelledWordInString:text
102+
range:NSMakeRange(0, text.length)
103+
startingAt:startingOffset
104+
wrap:NO
105+
language:language];
106+
if (misspelledRange.location == NSNotFound) {
107+
// No misspelled word found
108+
return nil;
109+
}
110+
111+
// If no possible guesses, the API returns an empty array:
112+
// https://developer.apple.com/documentation/uikit/uitextchecker/1621037-guessesforwordrange?language=objc
113+
NSArray<NSString*>* suggestions = [self.textChecker guessesForWordRange:misspelledRange
114+
inString:text
115+
language:language];
116+
FlutterSpellCheckResult* result =
117+
[[[FlutterSpellCheckResult alloc] initWithMisspelledRange:misspelledRange
118+
suggestions:suggestions] autorelease];
119+
return result;
120+
}
121+
122+
- (UITextChecker*)textChecker {
123+
return _textChecker;
124+
}
125+
126+
- (void)dealloc {
127+
[_textChecker release];
128+
[super dealloc];
129+
}
130+
131+
@end
132+
133+
@implementation FlutterSpellCheckResult
134+
135+
- (instancetype)initWithMisspelledRange:(NSRange)range
136+
suggestions:(NSArray<NSString*>*)suggestions {
137+
self = [super init];
138+
if (self) {
139+
_suggestions = [suggestions copy];
140+
_misspelledRange = range;
141+
}
142+
return self;
143+
}
144+
145+
- (NSDictionary<NSString*, NSObject*>*)toDictionary {
146+
NSMutableDictionary* result = [[[NSMutableDictionary alloc] initWithCapacity:3] autorelease];
147+
result[@"startIndex"] = @(_misspelledRange.location);
148+
result[@"endIndex"] = @(_misspelledRange.location + _misspelledRange.length - 1);
149+
result[@"suggestions"] = _suggestions;
150+
return result;
151+
}
152+
153+
- (void)dealloc {
154+
[_suggestions release];
155+
[super dealloc];
156+
}
157+
158+
@end

0 commit comments

Comments
 (0)