diff --git a/Hakawai/Core/Default/HKWDefaultChooserView.m b/Hakawai/Core/Default/HKWDefaultChooserView.m index 3206ccf..0fd1ba7 100644 --- a/Hakawai/Core/Default/HKWDefaultChooserView.m +++ b/Hakawai/Core/Default/HKWDefaultChooserView.m @@ -43,6 +43,12 @@ @implementation HKWDefaultChooserView @synthesize borderMode = _borderMode; ++ (id)chooserViewWithFrame:(CGRect)frame { + HKWDefaultChooserView *chooserView = [[[self class] alloc] initWithFrame:frame]; + [chooserView initialSetupForFrame:frame]; + return chooserView; +} + + (instancetype)chooserViewWithFrame:(CGRect)frame delegate:(id)delegate dataSource:(id)dataSource { diff --git a/Hakawai/Core/HKWChooserViewProtocol.h b/Hakawai/Core/HKWChooserViewProtocol.h index e0ec948..7c48e4e 100644 --- a/Hakawai/Core/HKWChooserViewProtocol.h +++ b/Hakawai/Core/HKWChooserViewProtocol.h @@ -43,13 +43,19 @@ typedef NS_ENUM(NSInteger, HKWChooserBorderMode) { @optional +/*! + Return an instance of the chooser view with a given frame with no delegate. This method is intended for + use with completely custom chooser views. + */ ++ (id)chooserViewWithFrame:(CGRect)frame; + /*! Return an instance of the chooser view with a given frame, properly setting the delegate. This method is intended for use with chooser views that are not backed by a \c UITableView instance, or wish to completely control the process of displaying chooser options, although it can be used for table view-backed views. \warning At least one of the two methods: this method, or \c chooserViewWithFrame:delegate:dataSource must be - implemented. + implemented, unless you are using a custom chooser view. */ + (id)chooserViewWithFrame:(CGRect)frame delegate:(id)delegate; @@ -58,7 +64,7 @@ typedef NS_ENUM(NSInteger, HKWChooserBorderMode) { Return an instance of the chooser view with a given frame, properly setting the delegate and data source. This method is intended for use with chooser views that are backed by a \c UITableView instance. - \warning At least one of the two methods: this method, or \c chooserViewWithFrame:delegate: must be implemented. + \warning At least one of the two methods: this method, or \c chooserViewWithFrame:delegate: must be implemented, unless you are using a custom chooser view. */ + (id)chooserViewWithFrame:(CGRect)frame delegate:(id)delegate diff --git a/Hakawai/Core/HKWTextView.h b/Hakawai/Core/HKWTextView.h index a077c4e..dc745c7 100644 --- a/Hakawai/Core/HKWTextView.h +++ b/Hakawai/Core/HKWTextView.h @@ -43,8 +43,10 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL) enableMentionsPluginV2; + (BOOL) enableMentionsCreationStateMachineV2; ++ (BOOL) directlyUpdateQueryWithCustomDelegate; + (void) setEnableMentionsPluginV2:(BOOL)enabled; + (void) setEnableMentionsCreationStateMachineV2:(BOOL)enabled; ++ (void) setDirectlyUpdateQueryWithCustomDelegate:(BOOL)enabled; #pragma mark - Initialization diff --git a/Hakawai/Core/HKWTextView.m b/Hakawai/Core/HKWTextView.m index dfbe2c1..9c2b08a 100644 --- a/Hakawai/Core/HKWTextView.m +++ b/Hakawai/Core/HKWTextView.m @@ -42,6 +42,7 @@ @interface HKWTextView () static BOOL enableMentionsPluginV2 = NO; static BOOL enableMentionsCreationStateMachineV2 = NO; +static BOOL directlyUpdateQueryWithCustomDelegate = NO; @implementation HKWTextView @@ -61,6 +62,14 @@ + (void)setEnableMentionsCreationStateMachineV2:(BOOL)enabled { enableMentionsCreationStateMachineV2 = enabled; } ++ (BOOL)directlyUpdateQueryWithCustomDelegate { + return directlyUpdateQueryWithCustomDelegate; +} + ++ (void)setDirectlyUpdateQueryWithCustomDelegate:(BOOL)enabled { + directlyUpdateQueryWithCustomDelegate = enabled; +} + #pragma mark - Lifecycle - (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(nullable __unused NSTextContainer *)textContainer { diff --git a/Hakawai/Mentions/HKWMentionsCreationStateMachineV2.m b/Hakawai/Mentions/HKWMentionsCreationStateMachineV2.m index fc8d759..423eca8 100644 --- a/Hakawai/Mentions/HKWMentionsCreationStateMachineV2.m +++ b/Hakawai/Mentions/HKWMentionsCreationStateMachineV2.m @@ -65,7 +65,7 @@ @interface HKWMentionsCreationStateMachineV2 () @property (nonatomic, weak) id delegate; -@property (nonatomic) HKWMentionDataProvider *dataProvider; +@property (nonatomic, nullable) HKWMentionDataProvider *dataProvider; @property (nonatomic) HKWMentionsCreationState state; @property (nonatomic) HKWMentionsCreationResultsState resultsState; @property (nonatomic) HKWMentionsCreationChooserState chooserState; @@ -101,14 +101,17 @@ @implementation HKWMentionsCreationStateMachineV2 #pragma mark - API -+ (instancetype)stateMachineWithDelegate:(id)delegate { ++ (instancetype)stateMachineWithDelegate:(id)delegate isUsingCustomChooserView:(BOOL)isUsingCustomChooserView { NSAssert(delegate != nil, @"Cannot create state machine with nil delegate."); HKWMentionsCreationStateMachineV2 *sm = [[self class] new]; sm.chooserViewClass = [HKWDefaultChooserView class]; sm.delegate = delegate; sm.state = HKWMentionsCreationStateQuiescent; sm.chooserViewEdgeInsets = UIEdgeInsetsZero; - sm.dataProvider = [[HKWMentionDataProvider alloc] initWithStateMachine:sm delegate:delegate]; + // We only need a data provider if we are not using a custom chooser view + if (!isUsingCustomChooserView) { + sm.dataProvider = [[HKWMentionDataProvider alloc] initWithStateMachine:sm delegate:delegate]; + } return sm; } @@ -165,11 +168,17 @@ - (void)stringInserted:(NSString *)string isWhitespace:(BOOL)isWhitespace isNewl ? HKWMentionsCreationActionWhitespaceCharacterInserted : HKWMentionsCreationActionNormalCharacterInserted); [self.stringBuffer appendString:string]; - // Fire off the request and start the timer - [self.dataProvider queryUpdatedWithKeyString:[self.stringBuffer copy] - searchType:self.searchType - isWhitespace:isWhitespace - controlCharacter:self.explicitSearchControlCharacter]; + if (self.dataProvider) { + // Fire off the request and start the timer + [self.dataProvider queryUpdatedWithKeyString:[self.stringBuffer copy] + searchType:self.searchType + isWhitespace:isWhitespace + controlCharacter:self.explicitSearchControlCharacter]; + } else { + // If we do not have a data provider, just pass the updated query directly to the mention plugin + [delegate didUpdateKeyString:[self.stringBuffer copy] + controlCharacter:self.explicitSearchControlCharacter]; + } } break; } @@ -265,11 +274,17 @@ the string buffer will not be empty (it will have "John" in it), but mentions ha // The user hasn't completely backed out of mentions creation, so we can continue firing requests. // Remove a character from the buffer and immediately fire a request [self.stringBuffer deleteCharactersInRange:toDeleteRange]; - // Fire off the request and start the timer - [self.dataProvider queryUpdatedWithKeyString:[self.stringBuffer copy] - searchType:self.searchType - isWhitespace:NO - controlCharacter:self.explicitSearchControlCharacter]; + if (self.dataProvider) { + // Fire off the request and start the timer + [self.dataProvider queryUpdatedWithKeyString:[self.stringBuffer copy] + searchType:self.searchType + isWhitespace:NO + controlCharacter:self.explicitSearchControlCharacter]; + } else { + // If we do not have a data provider, just pass the updated query directly to the mention plugin + [delegate didUpdateKeyString:[self.stringBuffer copy] + controlCharacter:self.explicitSearchControlCharacter]; + } break; } @@ -310,11 +325,17 @@ - (void)mentionCreationStartedWithPrefix:(NSString *)prefix // Prepare state self.resultsState = HKWMentionsCreationResultsStateAwaitingFirstResult; - // Start the timer and fire off a request - [self.dataProvider queryUpdatedWithKeyString:prefix - searchType:self.searchType - isWhitespace:NO - controlCharacter:self.explicitSearchControlCharacter]; + if (self.dataProvider) { + // Start the timer and fire off a request + [self.dataProvider queryUpdatedWithKeyString:prefix + searchType:self.searchType + isWhitespace:NO + controlCharacter:self.explicitSearchControlCharacter]; + } else { + // If we do not have a data provider, just pass the updated query directly to the mention plugin + [self.delegate didUpdateKeyString:prefix + controlCharacter:self.explicitSearchControlCharacter]; + } } - (void)cancelMentionCreation { @@ -346,10 +367,16 @@ - (void)hideChooserArrow { - (void)fetchInitialMentions { self.searchType = HKWMentionsSearchTypeInitial; - [self.dataProvider queryUpdatedWithKeyString:@"" - searchType:self.searchType - isWhitespace:NO - controlCharacter:self.explicitSearchControlCharacter]; + if (self.dataProvider) { + [self.dataProvider queryUpdatedWithKeyString:@"" + searchType:self.searchType + isWhitespace:NO + controlCharacter:self.explicitSearchControlCharacter]; + } else { + // If we do not have a data provider, just pass the updated query directly to the mention plugin + [self.delegate didUpdateKeyString:@"" + controlCharacter:self.explicitSearchControlCharacter]; + } } #pragma mark - Chooser View Frame @@ -469,18 +496,25 @@ - (void)hideChooserView { // Instantiate the chooser view UIView *chooserView = nil; - if ([(id)self.chooserViewClass respondsToSelector:@selector(chooserViewWithFrame:delegate:)]) { - chooserView = [self.chooserViewClass chooserViewWithFrame:chooserFrame - delegate:self.dataProvider]; - } - else if ([(id)self.chooserViewClass respondsToSelector:@selector(chooserViewWithFrame:delegate:dataSource:)]) { - chooserView = [self.chooserViewClass chooserViewWithFrame:chooserFrame - delegate:self.dataProvider - dataSource:self.dataProvider]; - } - else { - NSAssert(NO, @"Chooser view class must support one or both of the following methods: \ - chooserViewWithFrame:delegate: or chooserViewWithFrame:delegate:dataSource:"); + if (self.dataProvider) { + if ([(id)self.chooserViewClass respondsToSelector:@selector(chooserViewWithFrame:delegate:)]) { + chooserView = [self.chooserViewClass chooserViewWithFrame:chooserFrame + delegate:self.dataProvider]; + } + else if ([(id)self.chooserViewClass respondsToSelector:@selector(chooserViewWithFrame:delegate:dataSource:)]) { + chooserView = [self.chooserViewClass chooserViewWithFrame:chooserFrame + delegate:self.dataProvider + dataSource:self.dataProvider]; + } + else { + NSAssert(NO, @"If there is a dataprovider, chooser view class must support one or both of the following methods: \ + chooserViewWithFrame:delegate: or chooserViewWithFrame:delegate:dataSource:"); + } + } else { + // If we are not using a data provider, just create the chooser view without one + if ([(id)self.chooserViewClass respondsToSelector:@selector(chooserViewWithFrame:)]) { + chooserView = [self.chooserViewClass chooserViewWithFrame:chooserFrame]; + } } if ([chooserView respondsToSelector:@selector(setBorderMode:)]) { diff --git a/Hakawai/Mentions/HKWMentionsPluginV1.m b/Hakawai/Mentions/HKWMentionsPluginV1.m index 2d7a969..11116b4 100644 --- a/Hakawai/Mentions/HKWMentionsPluginV1.m +++ b/Hakawai/Mentions/HKWMentionsPluginV1.m @@ -2123,7 +2123,7 @@ - (HKWMentionsStartDetectionStateMachine *)startDetectionStateMachine { - (id)creationStateMachine { if (HKWTextView.enableMentionsCreationStateMachineV2) { if (!_creationStateMachine) { - _creationStateMachine = [HKWMentionsCreationStateMachineV2 stateMachineWithDelegate:self]; + _creationStateMachine = [HKWMentionsCreationStateMachineV2 stateMachineWithDelegate:self isUsingCustomChooserView:(self.customChooserViewDelegate != nil && HKWTextView.directlyUpdateQueryWithCustomDelegate)]; } return _creationStateMachine; } else { @@ -2210,6 +2210,16 @@ - (void)textView:(__unused UITextView *)textView willCustomPasteTextInRange:(__u return; } +- (void)didUpdateKeyString:(nonnull NSString *)keyString + controlCharacter:(unichar)character { + // set up the chooser view prior to data request in order to support fully customized view + [self.creationStateMachine setupChooserViewIfNeeded]; + __strong __auto_type strongCustomChooserViewDelegate = self.customChooserViewDelegate; + NSAssert(strongCustomChooserViewDelegate != nil, @"Must have a custom chooser view if the query is being updated directly via this method"); + [strongCustomChooserViewDelegate didUpdateKeyString:keyString + controlCharacter:character]; +} + #pragma mark - Developer NSString * _Nonnull nameForMentionsState(HKWMentionsState s) { diff --git a/Hakawai/Mentions/HKWMentionsPluginV2.m b/Hakawai/Mentions/HKWMentionsPluginV2.m index 04cfb2b..3ec420a 100644 --- a/Hakawai/Mentions/HKWMentionsPluginV2.m +++ b/Hakawai/Mentions/HKWMentionsPluginV2.m @@ -1115,6 +1115,7 @@ - (void)asyncRetrieveEntitiesForKeyString:(NSString *)keyString completion:(void (^)(NSArray *, BOOL, BOOL))completionBlock { // set up the chooser view prior to data request in order to support fully customized view [self.creationStateMachine setupChooserViewIfNeeded]; + // Remove this after directlyUpdateQueryWithCustomDelegate is ramped, because async vs. didUpdate should be totally separate __strong __auto_type strongCustomChooserViewDelegate = self.customChooserViewDelegate; if (strongCustomChooserViewDelegate) { [strongCustomChooserViewDelegate didUpdateKeyString:keyString @@ -1127,6 +1128,16 @@ - (void)asyncRetrieveEntitiesForKeyString:(NSString *)keyString } } +- (void)didUpdateKeyString:(nonnull NSString *)keyString + controlCharacter:(unichar)character { + // set up the chooser view prior to data request in order to support fully customized view + [self.creationStateMachine setupChooserViewIfNeeded]; + __strong __auto_type strongCustomChooserViewDelegate = self.customChooserViewDelegate; + NSAssert(strongCustomChooserViewDelegate != nil, @"Must have a custom chooser view if the query is being updated directly via this method"); + [strongCustomChooserViewDelegate didUpdateKeyString:keyString + controlCharacter:character]; +} + - (UITableViewCell *)cellForMentionsEntity:(id)entity withMatchString:(NSString *)matchString tableView:(UITableView *)tableView @@ -1421,7 +1432,7 @@ - (BOOL)loadingCellSupported { - (id)creationStateMachine { if (HKWTextView.enableMentionsCreationStateMachineV2) { if (!_creationStateMachine) { - _creationStateMachine = [HKWMentionsCreationStateMachineV2 stateMachineWithDelegate:self]; + _creationStateMachine = [HKWMentionsCreationStateMachineV2 stateMachineWithDelegate:self isUsingCustomChooserView:(self.customChooserViewDelegate != nil && HKWTextView.directlyUpdateQueryWithCustomDelegate)]; } return _creationStateMachine; } else { diff --git a/Hakawai/Mentions/_HKWMentionsCreationStateMachine.h b/Hakawai/Mentions/_HKWMentionsCreationStateMachine.h index f1599b1..975d145 100644 --- a/Hakawai/Mentions/_HKWMentionsCreationStateMachine.h +++ b/Hakawai/Mentions/_HKWMentionsCreationStateMachine.h @@ -4,7 +4,7 @@ #import "HKWMentionsDefaultChooserViewDelegate.h" #import "HKWMentionsPlugin.h" -@protocol HKWMentionsCreationStateMachineProtocol +@protocol HKWMentionsCreationStateMachineProtocol /*! Get whether or not the host app supports displaying a loading cell. @@ -96,11 +96,6 @@ */ @property (nonatomic) unichar explicitSearchControlCharacter; -/*! - Return a new, initialized state machine instance. - */ -+ (instancetype)stateMachineWithDelegate:(id)delegate; - /** Informs the state machine typeahead results are returned, so it can update its internal state accordingly. */ diff --git a/Hakawai/Mentions/_HKWMentionsCreationStateMachineV1.h b/Hakawai/Mentions/_HKWMentionsCreationStateMachineV1.h index 26cf338..f244d35 100644 --- a/Hakawai/Mentions/_HKWMentionsCreationStateMachineV1.h +++ b/Hakawai/Mentions/_HKWMentionsCreationStateMachineV1.h @@ -23,6 +23,12 @@ NS_ASSUME_NONNULL_BEGIN */ @interface HKWMentionsCreationStateMachineV1 : NSObject + +/*! + Return a new, initialized state machine instance. + */ ++ (instancetype)stateMachineWithDelegate:(id)delegate; + @end NS_ASSUME_NONNULL_END diff --git a/Hakawai/Mentions/_HKWMentionsCreationStateMachineV2.h b/Hakawai/Mentions/_HKWMentionsCreationStateMachineV2.h index aedd61f..ba0d02b 100644 --- a/Hakawai/Mentions/_HKWMentionsCreationStateMachineV2.h +++ b/Hakawai/Mentions/_HKWMentionsCreationStateMachineV2.h @@ -23,6 +23,12 @@ NS_ASSUME_NONNULL_BEGIN */ @interface HKWMentionsCreationStateMachineV2 : NSObject + +/*! + Return a new, initialized state machine instance, and let it know whether we are using a custom chooser view or not + */ ++ (instancetype)stateMachineWithDelegate:(id)delegate isUsingCustomChooserView:(BOOL)isUsingCustomChooserView; + @end NS_ASSUME_NONNULL_END