Flutter Engine
The Flutter Engine
FlutterSpellCheckPlugin.mm
Go to the documentation of this file.
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
13
14// Method Channel name to start spell check.
15static NSString* const kInitiateSpellCheck = @"SpellCheck.initiateSpellCheck";
16
17@interface FlutterSpellCheckResult : NSObject
18
19@property(nonatomic, copy, readonly) NSArray<NSString*>* suggestions;
20@property(nonatomic, assign, readonly) NSRange misspelledRange;
21
22- (instancetype)init NS_UNAVAILABLE;
23+ (instancetype)new NS_UNAVAILABLE;
24- (instancetype)initWithMisspelledRange:(NSRange)range
25 suggestions:(NSArray<NSString*>*)suggestions NS_DESIGNATED_INITIALIZER;
26- (NSDictionary<NSString*, NSObject*>*)toDictionary;
27
28@end
29
30@interface FlutterSpellCheckPlugin ()
31
32@property(nonatomic) UITextChecker* textChecker;
33
34@end
35
36@implementation FlutterSpellCheckPlugin
37
38- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
39 if (!self.textChecker) {
40 // UITextChecker is an expensive object to initiate, see:
41 // https://github.com/flutter/flutter/issues/104454. Lazily initialate the UITextChecker object
42 // until at first method channel call. We avoid using lazy getter for testing.
43 self.textChecker = [[UITextChecker alloc] init];
44 }
45 NSString* method = call.method;
46 NSArray* args = call.arguments;
47 if ([method isEqualToString:kInitiateSpellCheck]) {
48 FML_DCHECK(args.count == 2);
49 id language = args[0];
50 id text = args[1];
51 if (language == [NSNull null] || text == [NSNull null]) {
52 // Bail if null arguments are passed from dart.
53 result(nil);
54 return;
55 }
56
57 NSArray<NSDictionary<NSString*, id>*>* spellCheckResult =
59 result(spellCheckResult);
60 }
61}
62
63// Get all the misspelled words and suggestions in the entire String.
64//
65// The result will be formatted as an NSArray.
66// Each item of the array is a dictionary representing a misspelled word and suggestions.
67// The format looks like:
68// {
69// startIndex: 0,
70// endIndex: 5,
71// suggestions: [hello, ...]
72// }
73//
74// Returns nil if the language is invalid.
75// Returns an empty array if no spell check suggestions.
76- (NSArray<NSDictionary<NSString*, id>*>*)findAllSpellCheckSuggestionsForText:(NSString*)text
77 inLanguage:(NSString*)language {
78 // Transform Dart Locale format to iOS language format if necessary.
79 if ([language containsString:@"-"]) {
80 NSArray<NSString*>* languageCodes = [language componentsSeparatedByString:@"-"];
81 FML_DCHECK(languageCodes.count == 2);
82 NSString* lastCode = [[languageCodes lastObject] uppercaseString];
83 language = [NSString stringWithFormat:@"%@_%@", [languageCodes firstObject], lastCode];
84 }
85
86 if (![UITextChecker.availableLanguages containsObject:language]) {
87 return nil;
88 }
89
90 NSMutableArray<FlutterSpellCheckResult*>* allSpellSuggestions = [[NSMutableArray alloc] init];
91
92 FlutterSpellCheckResult* nextSpellSuggestion;
93 NSUInteger nextOffset = 0;
94 do {
95 nextSpellSuggestion = [self findSpellCheckSuggestionsForText:text
96 inLanguage:language
97 startingOffset:nextOffset];
98 if (nextSpellSuggestion != nil) {
99 [allSpellSuggestions addObject:nextSpellSuggestion];
100 nextOffset =
101 nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length;
102 }
103 } while (nextSpellSuggestion != nil && nextOffset < text.length);
104
105 NSMutableArray* methodChannelResult =
106 [[NSMutableArray alloc] initWithCapacity:allSpellSuggestions.count];
107
108 for (FlutterSpellCheckResult* result in allSpellSuggestions) {
109 [methodChannelResult addObject:[result toDictionary]];
110 }
111
112 return methodChannelResult;
113}
114
115// Get the misspelled word and suggestions.
116//
117// Returns nil if no spell check suggestions.
118- (FlutterSpellCheckResult*)findSpellCheckSuggestionsForText:(NSString*)text
119 inLanguage:(NSString*)language
120 startingOffset:(NSInteger)startingOffset {
121 FML_DCHECK([UITextChecker.availableLanguages containsObject:language]);
122 NSRange misspelledRange =
123 [self.textChecker rangeOfMisspelledWordInString:text
124 range:NSMakeRange(0, text.length)
125 startingAt:startingOffset
126 wrap:NO
127 language:language];
128 if (misspelledRange.location == NSNotFound) {
129 // No misspelled word found
130 return nil;
131 }
132
133 // If no possible guesses, the API returns an empty array:
134 // https://developer.apple.com/documentation/uikit/uitextchecker/1621037-guessesforwordrange?language=objc
135 NSArray<NSString*>* suggestions = [self.textChecker guessesForWordRange:misspelledRange
136 inString:text
137 language:language];
138 return [[FlutterSpellCheckResult alloc] initWithMisspelledRange:misspelledRange
139 suggestions:suggestions];
140}
141
142@end
143
144@implementation FlutterSpellCheckResult
145
146- (instancetype)initWithMisspelledRange:(NSRange)range
147 suggestions:(NSArray<NSString*>*)suggestions {
148 self = [super init];
149 if (self) {
150 _suggestions = [suggestions copy];
151 _misspelledRange = range;
152 }
153 return self;
154}
155
156- (NSDictionary<NSString*, NSObject*>*)toDictionary {
157 return @{
158 @"startIndex" : @(_misspelledRange.location),
159 // The end index represents the next index after the last character of a misspelled word to
160 // match the behavior of Dart's TextRange:
161 // https://api.flutter.dev/flutter/dart-ui/TextRange/end.html
162 @"endIndex" : @(_misspelledRange.location + _misspelledRange.length),
163 @"suggestions" : _suggestions,
164 };
165}
166
167@end
void(^ FlutterResult)(id _Nullable result)
static FLUTTER_ASSERT_ARC NSString *const kInitiateSpellCheck
static void copy(void *dst, const uint8_t *src, int width, int bpp, int deltaSrc, int offset, const SkPMColor ctable[])
Definition: SkSwizzler.cpp:31
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
GAsyncResult * result
#define FML_DCHECK(condition)
Definition: logging.h:103
NSArray< NSDictionary< NSString *, id > * > * findAllSpellCheckSuggestionsForText:inLanguage:(NSString *text, [inLanguage] NSString *language)
NSArray< NSString * > * suggestions
instancetype NS_UNAVAILABLE()
NSDictionary< NSString *, NSObject * > * toDictionary()
size_t length
std::u16string text
static bool init()
def call(args)
Definition: dom.py:159
const uintptr_t id