Flutter iOS Embedder
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 
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.
15 static 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 
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 =
58  [self findAllSpellCheckSuggestionsForText:text inLanguage:language];
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
FlutterSpellCheckPlugin
Definition: FlutterSpellCheckPlugin.h:11
FlutterSpellCheckResult
Definition: FlutterSpellCheckPlugin.mm:17
FlutterSpellCheckResult::misspelledRange
NSRange misspelledRange
Definition: FlutterSpellCheckPlugin.mm:20
FlutterMethodCall::method
NSString * method
Definition: FlutterCodecs.h:233
-[FlutterSpellCheckResult NS_UNAVAILABLE]
instancetype NS_UNAVAILABLE()
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterSpellCheckPlugin.h
FlutterSpellCheckResult::suggestions
NSArray< NSString * > * suggestions
Definition: FlutterSpellCheckPlugin.mm:19
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
kInitiateSpellCheck
static FLUTTER_ASSERT_ARC NSString *const kInitiateSpellCheck
Definition: FlutterSpellCheckPlugin.mm:15
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
-[FlutterSpellCheckResult toDictionary]
NSDictionary< NSString *, NSObject * > * toDictionary()
Definition: FlutterSpellCheckPlugin.mm:156
FlutterMethodCall::arguments
id arguments
Definition: FlutterCodecs.h:238