diff --git a/ReactiveObjC.xcodeproj/project.pbxproj b/ReactiveObjC.xcodeproj/project.pbxproj index 1417bb70f..49c6cd7fb 100644 --- a/ReactiveObjC.xcodeproj/project.pbxproj +++ b/ReactiveObjC.xcodeproj/project.pbxproj @@ -148,6 +148,14 @@ 57DC89A61C50675F00E367B7 /* UITextView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = D03764E619EDA41200A782A9 /* UITextView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57DC89A71C50679700E367B7 /* UIButton+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = D03764C819EDA41200A782A9 /* UIButton+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57DC89A81C50679E00E367B7 /* UICollectionReusableView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = D03764CA19EDA41200A782A9 /* UICollectionReusableView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5C6AE5C61F67CFA50053EA0C /* UITextField+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C6AE5C41F67CFA50053EA0C /* UITextField+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5C6AE5C71F67CFA50053EA0C /* UITextField+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C6AE5C41F67CFA50053EA0C /* UITextField+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5C6AE5C81F67CFA50053EA0C /* UITextField+RACCommandSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5C51F67CFA50053EA0C /* UITextField+RACCommandSupport.m */; }; + 5C6AE5C91F67CFA50053EA0C /* UITextField+RACCommandSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5C51F67CFA50053EA0C /* UITextField+RACCommandSupport.m */; }; + 5C6AE5D01F67E2E90053EA0C /* RACTestUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5CB1F67E2BF0053EA0C /* RACTestUITextField.m */; }; + 5C6AE5D11F67E2EA0053EA0C /* RACTestUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5CB1F67E2BF0053EA0C /* RACTestUITextField.m */; }; + 5C6AE5D31F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5D21F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m */; }; + 5C6AE5D41F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AE5D21F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m */; }; 7A7065811A3F88B8001E8354 /* RACKVOProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A70657E1A3F88B8001E8354 /* RACKVOProxy.m */; }; 7A7065821A3F88B8001E8354 /* RACKVOProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A70657E1A3F88B8001E8354 /* RACKVOProxy.m */; }; 7A7065841A3F8967001E8354 /* RACKVOProxySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A7065831A3F8967001E8354 /* RACKVOProxySpec.m */; }; @@ -770,6 +778,11 @@ 57A4D2451BA13F9700F7D4B1 /* tvOS-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Base.xcconfig"; sourceTree = ""; }; 57A4D2461BA13F9700F7D4B1 /* tvOS-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Framework.xcconfig"; sourceTree = ""; }; 57A4D2471BA13F9700F7D4B1 /* tvOS-StaticLibrary.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-StaticLibrary.xcconfig"; sourceTree = ""; }; + 5C6AE5C41F67CFA50053EA0C /* UITextField+RACCommandSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextField+RACCommandSupport.h"; sourceTree = ""; }; + 5C6AE5C51F67CFA50053EA0C /* UITextField+RACCommandSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITextField+RACCommandSupport.m"; sourceTree = ""; }; + 5C6AE5CA1F67E2BF0053EA0C /* RACTestUITextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RACTestUITextField.h; sourceTree = ""; }; + 5C6AE5CB1F67E2BF0053EA0C /* RACTestUITextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RACTestUITextField.m; sourceTree = ""; }; + 5C6AE5D21F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UITextFieldRACSupportSpec.m; sourceTree = ""; }; 7A70657D1A3F88B8001E8354 /* RACKVOProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RACKVOProxy.h; sourceTree = ""; }; 7A70657E1A3F88B8001E8354 /* RACKVOProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RACKVOProxy.m; sourceTree = ""; }; 7A7065831A3F8967001E8354 /* RACKVOProxySpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RACKVOProxySpec.m; sourceTree = ""; }; @@ -1375,6 +1388,8 @@ D03764E319EDA41200A782A9 /* UITableViewHeaderFooterView+RACSignalSupport.m */, D03764E419EDA41200A782A9 /* UITextField+RACSignalSupport.h */, D03764E519EDA41200A782A9 /* UITextField+RACSignalSupport.m */, + 5C6AE5C41F67CFA50053EA0C /* UITextField+RACCommandSupport.h */, + 5C6AE5C51F67CFA50053EA0C /* UITextField+RACCommandSupport.m */, D03764E619EDA41200A782A9 /* UITextView+RACSignalSupport.h */, D03764E719EDA41200A782A9 /* UITextView+RACSignalSupport.m */, ); @@ -1452,6 +1467,9 @@ D0C3131B19EF2D9700984962 /* RACTestSchedulerSpec.m */, D0C3131C19EF2D9700984962 /* RACTestUIButton.h */, D0C3131D19EF2D9700984962 /* RACTestUIButton.m */, + 5C6AE5CA1F67E2BF0053EA0C /* RACTestUITextField.h */, + 5C6AE5CB1F67E2BF0053EA0C /* RACTestUITextField.m */, + 5C6AE5D21F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m */, D04725FA19E49ED7006002AA /* Supporting Files */, ); path = ReactiveObjCTests; @@ -1579,6 +1597,7 @@ 57A4D2281BA13D7A00F7D4B1 /* RACQueueScheduler.h in Headers */, 57DC89A51C50675700E367B7 /* UITextField+RACSignalSupport.h in Headers */, 57A4D2291BA13D7A00F7D4B1 /* RACQueueScheduler+Subclass.h in Headers */, + 5C6AE5C71F67CFA50053EA0C /* UITextField+RACCommandSupport.h in Headers */, 57A4D22A1BA13D7A00F7D4B1 /* RACReplaySubject.h in Headers */, 57DC89A21C50673C00E367B7 /* UISegmentedControl+RACSignalSupport.h in Headers */, 57A4D22B1BA13D7A00F7D4B1 /* RACScheduler.h in Headers */, @@ -1733,6 +1752,7 @@ D037666419EDA43C00A782A9 /* ReactiveObjC.h in Headers */, D037652519EDA41200A782A9 /* NSObject+RACPropertySubscribing.h in Headers */, D03765B119EDA41200A782A9 /* RACQueueScheduler.h in Headers */, + 5C6AE5C61F67CFA50053EA0C /* UITextField+RACCommandSupport.h in Headers */, D037662519EDA41200A782A9 /* UIButton+RACCommandSupport.h in Headers */, D037672819EDA63500A782A9 /* RACBehaviorSubject.h in Headers */, D037660119EDA41200A782A9 /* RACTestScheduler.h in Headers */, @@ -2133,6 +2153,7 @@ 57A4D2011BA13D7A00F7D4B1 /* RACTuple.m in Sources */, 57A4D2021BA13D7A00F7D4B1 /* RACTupleSequence.m in Sources */, 57A4D2031BA13D7A00F7D4B1 /* RACUnarySequence.m in Sources */, + 5C6AE5C91F67CFA50053EA0C /* UITextField+RACCommandSupport.m in Sources */, 57A4D2041BA13D7A00F7D4B1 /* RACUnit.m in Sources */, 57A4D2051BA13D7A00F7D4B1 /* RACValueTransformer.m in Sources */, ); @@ -2154,6 +2175,7 @@ 7DFBED3E1CDB8DE300EE435B /* RACBlockTrampolineSpec.m in Sources */, 7DFBED401CDB8DE300EE435B /* RACChannelExamples.m in Sources */, 7DFBED411CDB8DE300EE435B /* RACChannelSpec.m in Sources */, + 5C6AE5D11F67E2EA0053EA0C /* RACTestUITextField.m in Sources */, 7DFBED421CDB8DE300EE435B /* RACCommandSpec.m in Sources */, 7DFBED431CDB8DE300EE435B /* RACCompoundDisposableSpec.m in Sources */, 7DFBED451CDB8DE300EE435B /* RACControlCommandExamples.m in Sources */, @@ -2176,6 +2198,7 @@ 7DFBED5A1CDB8DE300EE435B /* RACSubjectSpec.m in Sources */, 7DFBED5C1CDB8DE300EE435B /* RACSubscriberExamples.m in Sources */, 7DFBED5D1CDB8DE300EE435B /* RACSubscriberSpec.m in Sources */, + 5C6AE5D41F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m in Sources */, 7DFBED5E1CDB8DE300EE435B /* RACSubscriptingAssignmentTrampolineSpec.m in Sources */, 7DFBED5F1CDB8DE300EE435B /* RACTargetQueueSchedulerSpec.m in Sources */, 7DFBED601CDB8DE300EE435B /* RACTupleSpec.m in Sources */, @@ -2469,6 +2492,7 @@ D037658919EDA41200A782A9 /* RACErrorSignal.m in Sources */, D03765F719EDA41200A782A9 /* RACSubscriptingAssignmentTrampoline.m in Sources */, D037661319EDA41200A782A9 /* RACUnit.m in Sources */, + 5C6AE5C81F67CFA50053EA0C /* UITextField+RACCommandSupport.m in Sources */, D037662319EDA41200A782A9 /* UIBarButtonItem+RACCommandSupport.m in Sources */, D03765A119EDA41200A782A9 /* RACKVOTrampoline.m in Sources */, D037665B19EDA41200A782A9 /* UITableViewHeaderFooterView+RACSignalSupport.m in Sources */, @@ -2531,6 +2555,7 @@ D037670619EDA60000A782A9 /* RACSubscriberExamples.m in Sources */, D03766D819EDA60000A782A9 /* RACBlockTrampolineSpec.m in Sources */, D037670019EDA60000A782A9 /* RACStreamExamples.m in Sources */, + 5C6AE5D31F67E5DD0053EA0C /* UITextFieldRACSupportSpec.m in Sources */, D03766CC19EDA60000A782A9 /* NSObjectRACSelectorSignalSpec.m in Sources */, D03766E219EDA60000A782A9 /* RACControlCommandExamples.m in Sources */, D03766C019EDA60000A782A9 /* NSNotificationCenterRACSupportSpec.m in Sources */, @@ -2540,6 +2565,7 @@ D03766E619EDA60000A782A9 /* RACDisposableSpec.m in Sources */, D03766D419EDA60000A782A9 /* NSUserDefaultsRACSupportSpec.m in Sources */, D03766DC19EDA60000A782A9 /* RACChannelSpec.m in Sources */, + 5C6AE5D01F67E2E90053EA0C /* RACTestUITextField.m in Sources */, D037671A19EDA60000A782A9 /* UIActionSheetRACSupportSpec.m in Sources */, D03766DA19EDA60000A782A9 /* RACChannelExamples.m in Sources */, D03766F619EDA60000A782A9 /* RACSequenceExamples.m in Sources */, diff --git a/ReactiveObjC/ReactiveObjC.h b/ReactiveObjC/ReactiveObjC.h index 6defe8aa8..85324f603 100644 --- a/ReactiveObjC/ReactiveObjC.h +++ b/ReactiveObjC/ReactiveObjC.h @@ -72,6 +72,7 @@ FOUNDATION_EXPORT const unsigned char ReactiveObjCVersionString[]; #import #import #import + #import #import #if TARGET_OS_IOS diff --git a/ReactiveObjC/UITextField+RACCommandSupport.h b/ReactiveObjC/UITextField+RACCommandSupport.h new file mode 100644 index 000000000..df8cde36b --- /dev/null +++ b/ReactiveObjC/UITextField+RACCommandSupport.h @@ -0,0 +1,24 @@ +// +// UITextField+RACCommandSupport.h +// ReactiveObjC +// +// Created by Andrew Urban on 9/12/17. +// Copyright © 2017 GitHub. All rights reserved. +// + +#import + +@class RACCommand<__contravariant InputType, __covariant ValueType>; + +NS_ASSUME_NONNULL_BEGIN + +@interface UITextField (RACCommandSupport) + +/// Sets the command for the text field's return key. When the return key is clicked, the command is +/// executed with the sender of the event. The text field's enabledness is bound +/// to the command's `canExecute`. +@property (nonatomic, strong, nullable) RACCommand<__kindof UITextField *, id> *rac_returnCommand; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ReactiveObjC/UITextField+RACCommandSupport.m b/ReactiveObjC/UITextField+RACCommandSupport.m new file mode 100644 index 000000000..c54858df6 --- /dev/null +++ b/ReactiveObjC/UITextField+RACCommandSupport.m @@ -0,0 +1,56 @@ +// +// UITextField+RACCommandSupport.m +// ReactiveObjC +// +// Created by Andrew Urban on 9/12/17. +// Copyright © 2017 GitHub. All rights reserved. +// + +#import "UITextField+RACCommandSupport.h" +#import +#import "RACCommand.h" +#import "RACDisposable.h" +#import "RACSignal+Operations.h" +#import + +static void *UITextFieldRACReturnCommandKey = &UITextFieldRACReturnCommandKey; +static void *UITextFieldEnabledDisposableKey = &UITextFieldEnabledDisposableKey; + +@implementation UITextField (RACCommandSupport) + +- (RACCommand *)rac_returnCommand { + return objc_getAssociatedObject(self, UITextFieldRACReturnCommandKey); +} + +- (void)setRac_returnCommand:(RACCommand *)command { + objc_setAssociatedObject(self, UITextFieldRACReturnCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Check for stored signal in order to remove it and add a new one + RACDisposable *disposable = objc_getAssociatedObject(self, UITextFieldEnabledDisposableKey); + [disposable dispose]; + + if (command == nil) return; + + disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]; + objc_setAssociatedObject(self, UITextFieldEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [self rac_hijackActionAndTargetIfNeeded]; +} + +- (void)rac_hijackActionAndTargetIfNeeded { + SEL hijackSelector = @selector(rac_commandPerformAction:); + + for (NSString *selector in [self actionsForTarget:self forControlEvent:UIControlEventEditingDidEndOnExit]) { + if (hijackSelector == NSSelectorFromString(selector)) { + return; + } + } + + [self addTarget:self action:hijackSelector forControlEvents:UIControlEventEditingDidEndOnExit]; +} + +- (void)rac_commandPerformAction:(id)sender { + [self.rac_returnCommand execute:sender]; +} + +@end diff --git a/ReactiveObjCTests/RACTestUITextField.h b/ReactiveObjCTests/RACTestUITextField.h new file mode 100644 index 000000000..d6bcc96c7 --- /dev/null +++ b/ReactiveObjCTests/RACTestUITextField.h @@ -0,0 +1,16 @@ +// +// RACTestUITextField.h +// ReactiveObjC +// +// Created by Andrew Urban on 9/12/17. +// Copyright © 2017 GitHub. All rights reserved. +// + +#import + +// Enables use of -sendActionsForControlEvents: in unit tests. +@interface RACTestUITextField : UITextField + ++ (instancetype)textField; + +@end diff --git a/ReactiveObjCTests/RACTestUITextField.m b/ReactiveObjCTests/RACTestUITextField.m new file mode 100644 index 000000000..8ac938fe7 --- /dev/null +++ b/ReactiveObjCTests/RACTestUITextField.m @@ -0,0 +1,27 @@ +// +// RACTestUITextField.m +// ReactiveObjC +// +// Created by Andrew Urban on 9/12/17. +// Copyright © 2017 GitHub. All rights reserved. +// + +#import "RACTestUITextField.h" + +@implementation RACTestUITextField + ++ (instancetype)textField { + RACTestUITextField *textField = [self new]; + return textField; +} + +// Required for unit testing – controls don't work normally +// outside of normal apps. +-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:action withObject:self]; +#pragma clang diagnostic pop +} + +@end diff --git a/ReactiveObjCTests/UITextFieldRACSupportSpec.m b/ReactiveObjCTests/UITextFieldRACSupportSpec.m new file mode 100644 index 000000000..d7255197d --- /dev/null +++ b/ReactiveObjCTests/UITextFieldRACSupportSpec.m @@ -0,0 +1,42 @@ +// +// UITextFieldRACSupportSpec.m +// ReactiveObjC +// +// Created by Andrew Urban on 9/12/17. +// Copyright © 2017 GitHub. All rights reserved. +// + +@import Quick; +@import Nimble; + +#import "RACControlCommandExamples.h" +#import "RACTestUITextField.h" + +#import "UITextField+RACCommandSupport.h" +#import "RACCommand.h" +#import "RACDisposable.h" + +QuickSpecBegin(UITextFieldRACSupportSpec) + +qck_describe(@"UITextField", ^{ + __block UITextField *textField; + + qck_beforeEach(^{ + textField = [RACTestUITextField textField]; + expect(textField).notTo(beNil()); + }); + + qck_itBehavesLike(RACControlCommandExamples, ^{ + return @{ + RACControlCommandExampleControl: textField, + RACControlCommandExampleActivateBlock: ^(UITextField *textField) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [textField sendActionsForControlEvents:UIControlEventEditingDidEndOnExit]; +#pragma clang diagnostic pop + } + }; + }); +}); + +QuickSpecEnd