1#import <UIKit/UIKit.h>
2
3#import "RNSSearchBar.h"
4
5#import <React/RCTBridge.h>
6#import <React/RCTComponent.h>
7#import <React/RCTUIManager.h>
8
9#ifdef RCT_NEW_ARCH_ENABLED
10#import <React/RCTConversions.h>
11#import <React/RCTFabricComponentsPlugins.h>
12#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
13#import <react/renderer/components/rnscreens/EventEmitters.h>
14#import <react/renderer/components/rnscreens/Props.h>
15#import "RNSConvert.h"
16#endif
17
18@implementation RNSSearchBar {
19  __weak RCTBridge *_bridge;
20  UISearchController *_controller;
21  UIColor *_textColor;
22}
23
24@synthesize controller = _controller;
25
26- (instancetype)initWithBridge:(RCTBridge *)bridge
27{
28  if (self = [super init]) {
29    _bridge = bridge;
30    [self initCommonProps];
31  }
32  return self;
33}
34
35#ifdef RCT_NEW_ARCH_ENABLED
36- (instancetype)init
37{
38  if (self = [super init]) {
39    static const auto defaultProps = std::make_shared<const facebook::react::RNSSearchBarProps>();
40    _props = defaultProps;
41    [self initCommonProps];
42  }
43  return self;
44}
45#endif
46
47- (void)initCommonProps
48{
49  _controller = [[UISearchController alloc] initWithSearchResultsController:nil];
50  _controller.searchBar.delegate = self;
51  _hideWhenScrolling = YES;
52}
53
54- (void)emitOnFocusEvent
55{
56#ifdef RCT_NEW_ARCH_ENABLED
57  if (_eventEmitter != nullptr) {
58    std::dynamic_pointer_cast<const facebook::react::RNSSearchBarEventEmitter>(_eventEmitter)
59        ->onFocus(facebook::react::RNSSearchBarEventEmitter::OnFocus{});
60  }
61#else
62  if (self.onFocus) {
63    self.onFocus(@{});
64  }
65#endif
66}
67
68- (void)emitOnBlurEvent
69{
70#ifdef RCT_NEW_ARCH_ENABLED
71  if (_eventEmitter != nullptr) {
72    std::dynamic_pointer_cast<const facebook::react::RNSSearchBarEventEmitter>(_eventEmitter)
73        ->onBlur(facebook::react::RNSSearchBarEventEmitter::OnBlur{});
74  }
75#else
76  if (self.onBlur) {
77    self.onBlur(@{});
78  }
79#endif
80}
81
82- (void)emitOnSearchButtonPressEventWithText:(NSString *)text
83{
84#ifdef RCT_NEW_ARCH_ENABLED
85  if (_eventEmitter != nullptr) {
86    std::dynamic_pointer_cast<const facebook::react::RNSSearchBarEventEmitter>(_eventEmitter)
87        ->onSearchButtonPress(
88            facebook::react::RNSSearchBarEventEmitter::OnSearchButtonPress{.text = RCTStringFromNSString(text)});
89  }
90#else
91  if (self.onSearchButtonPress) {
92    self.onSearchButtonPress(@{
93      @"text" : text,
94    });
95  }
96#endif
97}
98
99- (void)emitOnCancelButtonPressEvent
100{
101#ifdef RCT_NEW_ARCH_ENABLED
102  if (_eventEmitter != nullptr) {
103    std::dynamic_pointer_cast<const facebook::react::RNSSearchBarEventEmitter>(_eventEmitter)
104        ->onCancelButtonPress(facebook::react::RNSSearchBarEventEmitter::OnCancelButtonPress{});
105  }
106#else
107  if (self.onCancelButtonPress) {
108    self.onCancelButtonPress(@{});
109  }
110#endif
111}
112
113- (void)emitOnChangeTextEventWithText:(NSString *)text
114{
115#ifdef RCT_NEW_ARCH_ENABLED
116  if (_eventEmitter != nullptr) {
117    std::dynamic_pointer_cast<const facebook::react::RNSSearchBarEventEmitter>(_eventEmitter)
118        ->onChangeText(facebook::react::RNSSearchBarEventEmitter::OnChangeText{.text = RCTStringFromNSString(text)});
119  }
120#else
121  if (self.onChangeText) {
122    self.onChangeText(@{
123      @"text" : text,
124    });
125  }
126#endif
127}
128
129- (void)setObscureBackground:(BOOL)obscureBackground
130{
131  if (@available(iOS 9.1, *)) {
132    [_controller setObscuresBackgroundDuringPresentation:obscureBackground];
133  }
134}
135
136- (void)setHideNavigationBar:(BOOL)hideNavigationBar
137{
138  [_controller setHidesNavigationBarDuringPresentation:hideNavigationBar];
139}
140
141- (void)setHideWhenScrolling:(BOOL)hideWhenScrolling
142{
143  _hideWhenScrolling = hideWhenScrolling;
144}
145
146- (void)setAutoCapitalize:(UITextAutocapitalizationType)autoCapitalize
147{
148  [_controller.searchBar setAutocapitalizationType:autoCapitalize];
149}
150
151- (void)setPlaceholder:(NSString *)placeholder
152{
153  [_controller.searchBar setPlaceholder:placeholder];
154}
155
156- (void)setBarTintColor:(UIColor *)barTintColor
157{
158#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
159    __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 && !TARGET_OS_TV
160  if (@available(iOS 13.0, *)) {
161    [_controller.searchBar.searchTextField setBackgroundColor:barTintColor];
162  }
163#endif
164}
165
166- (void)setTintColor:(UIColor *)tintColor
167{
168  [_controller.searchBar setTintColor:tintColor];
169}
170
171- (void)setTextColor:(UIColor *)textColor
172{
173#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
174    __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 && !TARGET_OS_TV
175  _textColor = textColor;
176  if (@available(iOS 13.0, *)) {
177    [_controller.searchBar.searchTextField setTextColor:_textColor];
178  }
179#endif
180}
181
182- (void)setCancelButtonText:(NSString *)text
183{
184  [_controller.searchBar setValue:text forKey:@"cancelButtonText"];
185}
186
187- (void)hideCancelButton
188{
189#if !TARGET_OS_TV
190  if (@available(iOS 13, *)) {
191    // On iOS 13+ UISearchController automatically shows/hides cancel button
192    // https://developer.apple.com/documentation/uikit/uisearchcontroller/3152926-automaticallyshowscancelbutton?language=objc
193  } else {
194    [_controller.searchBar setShowsCancelButton:NO animated:YES];
195  }
196#endif
197}
198
199- (void)showCancelButton
200{
201#if !TARGET_OS_TV
202  if (@available(iOS 13, *)) {
203    // On iOS 13+ UISearchController automatically shows/hides cancel button
204    // https://developer.apple.com/documentation/uikit/uisearchcontroller/3152926-automaticallyshowscancelbutton?language=objc
205  } else {
206    [_controller.searchBar setShowsCancelButton:YES animated:YES];
207  }
208#endif
209}
210
211#pragma mark delegate methods
212
213- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
214{
215#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
216    __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 && !TARGET_OS_TV
217  if (@available(iOS 13.0, *)) {
218    // for some reason, the color does not change when set at the beginning,
219    // so we apply it again here
220    if (_textColor != nil) {
221      [_controller.searchBar.searchTextField setTextColor:_textColor];
222    }
223  }
224#endif
225
226  [self showCancelButton];
227  [self becomeFirstResponder];
228  [self emitOnFocusEvent];
229}
230
231- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
232{
233  [self emitOnBlurEvent];
234  [self hideCancelButton];
235}
236
237- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
238{
239  [self emitOnChangeTextEventWithText:_controller.searchBar.text];
240}
241
242- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
243{
244  [self emitOnSearchButtonPressEventWithText:_controller.searchBar.text];
245}
246
247#if !TARGET_OS_TV
248- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
249{
250  _controller.searchBar.text = @"";
251  [self resignFirstResponder];
252  [self hideCancelButton];
253
254  [self emitOnCancelButtonPressEvent];
255  [self emitOnChangeTextEventWithText:_controller.searchBar.text];
256}
257#endif // !TARGET_OS_TV
258
259- (void)blur
260{
261  [_controller.searchBar resignFirstResponder];
262}
263
264- (void)focus
265{
266  [_controller.searchBar becomeFirstResponder];
267}
268
269- (void)clearText
270{
271  [_controller.searchBar setText:@""];
272}
273
274- (void)toggleCancelButton:(BOOL)flag
275{
276#if !TARGET_OS_TV
277  [_controller.searchBar setShowsCancelButton:flag animated:YES];
278#endif
279}
280
281- (void)setText:(NSString *)text
282{
283  [_controller.searchBar setText:text];
284}
285
286#pragma mark-- Fabric specific
287
288#ifdef RCT_NEW_ARCH_ENABLED
289- (void)updateProps:(facebook::react::Props::Shared const &)props
290           oldProps:(facebook::react::Props::Shared const &)oldProps
291{
292  const auto &oldScreenProps = *std::static_pointer_cast<const facebook::react::RNSSearchBarProps>(_props);
293  const auto &newScreenProps = *std::static_pointer_cast<const facebook::react::RNSSearchBarProps>(props);
294
295  [self setHideWhenScrolling:newScreenProps.hideWhenScrolling];
296
297  if (oldScreenProps.cancelButtonText != newScreenProps.cancelButtonText) {
298    [self setCancelButtonText:RCTNSStringFromStringNilIfEmpty(newScreenProps.cancelButtonText)];
299  }
300
301  if (oldScreenProps.obscureBackground != newScreenProps.obscureBackground) {
302    [self setObscureBackground:newScreenProps.obscureBackground];
303  }
304
305  if (oldScreenProps.hideNavigationBar != newScreenProps.hideNavigationBar) {
306    [self setHideNavigationBar:newScreenProps.hideNavigationBar];
307  }
308
309  if (oldScreenProps.placeholder != newScreenProps.placeholder) {
310    [self setPlaceholder:RCTNSStringFromStringNilIfEmpty(newScreenProps.placeholder)];
311  }
312
313  if (oldScreenProps.autoCapitalize != newScreenProps.autoCapitalize) {
314    [self setAutoCapitalize:[RNSConvert UITextAutocapitalizationTypeFromCppEquivalent:newScreenProps.autoCapitalize]];
315  }
316
317  if (oldScreenProps.tintColor != newScreenProps.tintColor) {
318    [self setTintColor:RCTUIColorFromSharedColor(newScreenProps.tintColor)];
319  }
320
321  if (oldScreenProps.barTintColor != newScreenProps.barTintColor) {
322    [self setBarTintColor:RCTUIColorFromSharedColor(newScreenProps.barTintColor)];
323  }
324
325  if (oldScreenProps.textColor != newScreenProps.textColor) {
326    [self setTextColor:RCTUIColorFromSharedColor(newScreenProps.textColor)];
327  }
328
329  [super updateProps:props oldProps:oldProps];
330}
331
332+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider
333{
334  return facebook::react::concreteComponentDescriptorProvider<facebook::react::RNSSearchBarComponentDescriptor>();
335}
336
337- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
338{
339  RCTRNSSearchBarHandleCommand(self, commandName, args);
340}
341
342#else
343#endif // RCT_NEW_ARCH_ENABLED
344
345@end
346
347#ifdef RCT_NEW_ARCH_ENABLED
348Class<RCTComponentViewProtocol> RNSSearchBarCls(void)
349{
350  return RNSSearchBar.class;
351}
352#endif
353
354@implementation RNSSearchBarManager
355
356RCT_EXPORT_MODULE()
357
358#ifdef RCT_NEW_ARCH_ENABLED
359#else
360- (UIView *)view
361{
362  return [[RNSSearchBar alloc] initWithBridge:self.bridge];
363}
364#endif
365
366RCT_EXPORT_VIEW_PROPERTY(obscureBackground, BOOL)
367RCT_EXPORT_VIEW_PROPERTY(hideNavigationBar, BOOL)
368RCT_EXPORT_VIEW_PROPERTY(hideWhenScrolling, BOOL)
369RCT_EXPORT_VIEW_PROPERTY(autoCapitalize, UITextAutocapitalizationType)
370RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
371RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor)
372RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
373RCT_EXPORT_VIEW_PROPERTY(textColor, UIColor)
374RCT_EXPORT_VIEW_PROPERTY(cancelButtonText, NSString)
375
376RCT_EXPORT_VIEW_PROPERTY(onChangeText, RCTBubblingEventBlock)
377RCT_EXPORT_VIEW_PROPERTY(onCancelButtonPress, RCTBubblingEventBlock)
378RCT_EXPORT_VIEW_PROPERTY(onSearchButtonPress, RCTBubblingEventBlock)
379RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock)
380RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock)
381
382#ifndef RCT_NEW_ARCH_ENABLED
383
384RCT_EXPORT_METHOD(focus : (NSNumber *_Nonnull)reactTag)
385{
386  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
387    RNSSearchBar *searchBar = viewRegistry[reactTag];
388    [searchBar focus];
389  }];
390}
391
392RCT_EXPORT_METHOD(blur : (NSNumber *_Nonnull)reactTag)
393{
394  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
395    RNSSearchBar *searchBar = viewRegistry[reactTag];
396    [searchBar blur];
397  }];
398}
399
400RCT_EXPORT_METHOD(clearText : (NSNumber *_Nonnull)reactTag)
401{
402  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
403    RNSSearchBar *searchBar = viewRegistry[reactTag];
404    [searchBar clearText];
405  }];
406}
407
408RCT_EXPORT_METHOD(toggleCancelButton : (NSNumber *_Nonnull)reactTag flag : (BOOL *)flag)
409{
410  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
411    RNSSearchBar *searchBar = viewRegistry[reactTag];
412    [searchBar toggleCancelButton:flag];
413  }];
414}
415
416RCT_EXPORT_METHOD(setText : (NSNumber *_Nonnull)reactTag text : (NSString *)text)
417{
418  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
419    RNSSearchBar *searchBar = viewRegistry[reactTag];
420    [searchBar setText:text];
421  }];
422}
423
424#endif /* !RCT_NEW_ARCH_ENABLED */
425
426@end
427