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

Commit c1c1f2c

Browse files
author
Chris Yang
committed
Revert "Revert "iOS spell-checker ObjC" (#33570)"
This reverts commit b1fafb9. method channel move out of plugin tests cleanup fix fix typo
1 parent ec35c9f commit c1c1f2c

File tree

7 files changed

+554
-0
lines changed

7 files changed

+554
-0
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora
16831683
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
16841684
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
16851685
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
1686+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h
1687+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
1688+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm
16861689
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h
16871690
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h
16881691
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+
- (NSString*)toFormattedString;
29+
30+
@end
31+
32+
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
// Spell check results will be encoded as a string representing the span of that result, with
17+
// the format "start_index.end_index.suggestion_1\nsuggestion_2\nsuggestion_3".
18+
static NSString* FormatSpellCheckResult(NSUInteger startIndex,
19+
NSUInteger endIndex,
20+
NSArray* suggestions) {
21+
NSMutableString* result = [NSMutableString stringWithCapacity:0];
22+
[result setString:[NSString stringWithFormat:@"%@.%@.", @(startIndex), @(endIndex)]];
23+
for (NSUInteger i = 0; i < suggestions.count; i++) {
24+
NSString* suggestion = suggestions[i];
25+
[result appendString:suggestion];
26+
if (i < suggestions.count - 1) {
27+
[result appendString:@"\n"];
28+
}
29+
}
30+
return result;
31+
}
32+
33+
@interface FlutterSpellCheckPlugin ()
34+
35+
@property(nonatomic, retain) UITextChecker* textChecker;
36+
37+
@end
38+
39+
@implementation FlutterSpellCheckPlugin
40+
41+
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
42+
if (!_textChecker) {
43+
// UITextChecker is an expensive object to initiate, see:
44+
// https://github.com/flutter/flutter/issues/104454. Lazily initialate the UITextChecker object
45+
// until at first method channel call. We avoid using lazy getter for testing.
46+
_textChecker = [[UITextChecker alloc] init];
47+
}
48+
NSString* method = call.method;
49+
NSArray* args = call.arguments;
50+
if ([method isEqualToString:kInitiateSpellCheck]) {
51+
FML_DCHECK(args.count == 2);
52+
id language = args[0];
53+
id text = args[1];
54+
if (language == [NSNull null] || text == [NSNull null]) {
55+
// Bail if null arguments are passed from dart.
56+
result(nil);
57+
return;
58+
}
59+
60+
NSArray<NSDictionary<NSString*, id>*>* spellCheckResult =
61+
[self findAllSpellCheckSuggestionsForText:text inLanguage:language];
62+
result(spellCheckResult);
63+
}
64+
}
65+
66+
// Get all the misspelled words and suggestions in the entire String.
67+
//
68+
// The result will be formatted as an NSArray.
69+
// Each item of the array is a representation of a misspelled word and suggestions.
70+
// The format of each item looks like this:
71+
// "start_index.end_index.suggestion_1\nsuggestion_2\nsuggestion_3".
72+
//
73+
// Returns nil if the language is invalid.
74+
// Returns an empty array if no spell check suggestions.
75+
- (NSArray<NSDictionary<NSString*, id>*>*)findAllSpellCheckSuggestionsForText:(NSString*)text
76+
inLanguage:(NSString*)language {
77+
if (![UITextChecker.availableLanguages containsObject:language]) {
78+
return nil;
79+
}
80+
81+
NSMutableArray<FlutterSpellCheckResult*>* allSpellSuggestions = [[NSMutableArray alloc] init];
82+
83+
FlutterSpellCheckResult* nextSpellSuggestion;
84+
NSUInteger nextOffset = 0;
85+
do {
86+
nextSpellSuggestion = [self findSpellCheckSuggestionsForText:text
87+
inLanguage:language
88+
startingOffset:nextOffset];
89+
if (nextSpellSuggestion != nil) {
90+
[allSpellSuggestions addObject:nextSpellSuggestion];
91+
nextOffset =
92+
nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length;
93+
}
94+
} while (nextSpellSuggestion != nil && nextOffset < text.length);
95+
96+
NSMutableArray* methodChannelResult = [[[NSMutableArray alloc] init] autorelease];
97+
98+
for (FlutterSpellCheckResult* result in allSpellSuggestions) {
99+
[methodChannelResult addObject:[result toFormattedString]];
100+
}
101+
102+
[allSpellSuggestions release];
103+
return methodChannelResult;
104+
}
105+
106+
// Get the misspelled word and suggestions.
107+
//
108+
// Returns nil if no spell check suggestions.
109+
- (FlutterSpellCheckResult*)findSpellCheckSuggestionsForText:(NSString*)text
110+
inLanguage:(NSString*)language
111+
startingOffset:(NSInteger)startingOffset {
112+
FML_DCHECK([UITextChecker.availableLanguages containsObject:language]);
113+
NSRange misspelledRange =
114+
[self.textChecker rangeOfMisspelledWordInString:text
115+
range:NSMakeRange(0, text.length)
116+
startingAt:startingOffset
117+
wrap:NO
118+
language:language];
119+
if (misspelledRange.location == NSNotFound) {
120+
// No misspelled word found
121+
return nil;
122+
}
123+
124+
// If no possible guesses, the API returns an empty array:
125+
// https://developer.apple.com/documentation/uikit/uitextchecker/1621037-guessesforwordrange?language=objc
126+
NSArray<NSString*>* suggestions = [self.textChecker guessesForWordRange:misspelledRange
127+
inString:text
128+
language:language];
129+
FlutterSpellCheckResult* result =
130+
[[[FlutterSpellCheckResult alloc] initWithMisspelledRange:misspelledRange
131+
suggestions:suggestions] autorelease];
132+
return result;
133+
}
134+
135+
- (UITextChecker*)textChecker {
136+
return _textChecker;
137+
}
138+
139+
- (void)dealloc {
140+
[_textChecker release];
141+
[super dealloc];
142+
}
143+
144+
@end
145+
146+
@implementation FlutterSpellCheckResult
147+
148+
- (instancetype)initWithMisspelledRange:(NSRange)range
149+
suggestions:(NSArray<NSString*>*)suggestions {
150+
self = [super init];
151+
if (self) {
152+
_suggestions = [suggestions copy];
153+
_misspelledRange = range;
154+
}
155+
return self;
156+
}
157+
158+
- (NSString*)toFormattedString {
159+
return FormatSpellCheckResult(_misspelledRange.location,
160+
_misspelledRange.location + _misspelledRange.length - 1,
161+
_suggestions);
162+
}
163+
164+
- (void)dealloc {
165+
[_suggestions release];
166+
[super dealloc];
167+
}
168+
169+
@end

0 commit comments

Comments
 (0)