diff --git a/Form Input Accessory View Demo.xcodeproj/project.pbxproj b/Form Input Accessory View Demo.xcodeproj/project.pbxproj index aa5bda4..8e25b14 100644 --- a/Form Input Accessory View Demo.xcodeproj/project.pbxproj +++ b/Form Input Accessory View Demo.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8532576D17E4937A00D8E13D /* XCDResponderChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 8532576C17E4937A00D8E13D /* XCDResponderChain.m */; }; C215427C164FD1C800262EB1 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C2154275164FD1C800262EB1 /* Default-568h@2x.png */; }; C215427D164FD1C800262EB1 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = C2154276164FD1C800262EB1 /* Default.png */; }; C215427E164FD1C800262EB1 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C2154277164FD1C800262EB1 /* Default@2x.png */; }; @@ -21,6 +22,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 8532576B17E4937A00D8E13D /* XCDResponderChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCDResponderChain.h; sourceTree = ""; }; + 8532576C17E4937A00D8E13D /* XCDResponderChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCDResponderChain.m; sourceTree = ""; }; C2154275164FD1C800262EB1 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; C2154276164FD1C800262EB1 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; C2154277164FD1C800262EB1 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; @@ -58,7 +61,7 @@ isa = PBXGroup; children = ( C2F07169164F0E1E008F4BA2 /* XCDFormInputAccessoryView */, - C2F07148164DCA75008F4BA2 /* Form Accessory View Demo */, + C2F07148164DCA75008F4BA2 /* Form Input Accessory View Demo */, C2F07141164DCA75008F4BA2 /* Frameworks */, C2F0713F164DCA75008F4BA2 /* Products */, ); @@ -82,7 +85,7 @@ name = Frameworks; sourceTree = ""; }; - C2F07148164DCA75008F4BA2 /* Form Accessory View Demo */ = { + C2F07148164DCA75008F4BA2 /* Form Input Accessory View Demo */ = { isa = PBXGroup; children = ( C2F07151164DCA75008F4BA2 /* AppDelegate.h */, @@ -97,7 +100,7 @@ C215427A164FD1C800262EB1 /* main.m */, C215427B164FD1C800262EB1 /* MainStoryboard.storyboard */, ); - path = "Form Accessory View Demo"; + path = "Form Input Accessory View Demo"; sourceTree = ""; }; C2F07169164F0E1E008F4BA2 /* XCDFormInputAccessoryView */ = { @@ -105,6 +108,8 @@ children = ( C2F0716A164F0E32008F4BA2 /* XCDFormInputAccessoryView.h */, C2F0716B164F0E32008F4BA2 /* XCDFormInputAccessoryView.m */, + 8532576B17E4937A00D8E13D /* XCDResponderChain.h */, + 8532576C17E4937A00D8E13D /* XCDResponderChain.m */, ); path = XCDFormInputAccessoryView; sourceTree = ""; @@ -125,7 +130,7 @@ dependencies = ( ); name = "Form Input Accessory View Demo"; - productName = "Form Accessory View Demo"; + productName = "Form Input Accessory View Demo"; productReference = C2F0713E164DCA75008F4BA2 /* Form Input Accessory View Demo.app */; productType = "com.apple.product-type.application"; }; @@ -178,6 +183,7 @@ C2F07153164DCA75008F4BA2 /* AppDelegate.m in Sources */, C2F0715F164DCA75008F4BA2 /* DemoViewController.m in Sources */, C2F0716C164F0E32008F4BA2 /* XCDFormInputAccessoryView.m in Sources */, + 8532576D17E4937A00D8E13D /* XCDResponderChain.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -254,8 +260,8 @@ "\"$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks\"", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "Form Accessory View Demo/Form Input Accessory View Demo-Prefix.pch"; - INFOPLIST_FILE = "Form Accessory View Demo/Form Input Accessory View Demo-Info.plist"; + GCC_PREFIX_HEADER = "Form Input Accessory View Demo/Form Input Accessory View Demo-Prefix.pch"; + INFOPLIST_FILE = "Form Input Accessory View Demo/Form Input Accessory View Demo-Info.plist"; PRODUCT_NAME = "Form Input Accessory View Demo"; WRAPPER_EXTENSION = app; }; @@ -269,8 +275,8 @@ "\"$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks\"", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "Form Accessory View Demo/Form Input Accessory View Demo-Prefix.pch"; - INFOPLIST_FILE = "Form Accessory View Demo/Form Input Accessory View Demo-Info.plist"; + GCC_PREFIX_HEADER = "Form Input Accessory View Demo/Form Input Accessory View Demo-Prefix.pch"; + INFOPLIST_FILE = "Form Input Accessory View Demo/Form Input Accessory View Demo-Info.plist"; PRODUCT_NAME = "Form Input Accessory View Demo"; WRAPPER_EXTENSION = app; }; diff --git a/Form Accessory View Demo/AppDelegate.h b/Form Input Accessory View Demo/AppDelegate.h similarity index 100% rename from Form Accessory View Demo/AppDelegate.h rename to Form Input Accessory View Demo/AppDelegate.h diff --git a/Form Accessory View Demo/AppDelegate.m b/Form Input Accessory View Demo/AppDelegate.m similarity index 100% rename from Form Accessory View Demo/AppDelegate.m rename to Form Input Accessory View Demo/AppDelegate.m diff --git a/Form Accessory View Demo/Default-568h@2x.png b/Form Input Accessory View Demo/Default-568h@2x.png similarity index 100% rename from Form Accessory View Demo/Default-568h@2x.png rename to Form Input Accessory View Demo/Default-568h@2x.png diff --git a/Form Accessory View Demo/Default.png b/Form Input Accessory View Demo/Default.png similarity index 100% rename from Form Accessory View Demo/Default.png rename to Form Input Accessory View Demo/Default.png diff --git a/Form Accessory View Demo/Default@2x.png b/Form Input Accessory View Demo/Default@2x.png similarity index 100% rename from Form Accessory View Demo/Default@2x.png rename to Form Input Accessory View Demo/Default@2x.png diff --git a/Form Accessory View Demo/DemoViewController.h b/Form Input Accessory View Demo/DemoViewController.h similarity index 87% rename from Form Accessory View Demo/DemoViewController.h rename to Form Input Accessory View Demo/DemoViewController.h index ea6fd80..33e177a 100644 --- a/Form Accessory View Demo/DemoViewController.h +++ b/Form Input Accessory View Demo/DemoViewController.h @@ -10,4 +10,6 @@ @property (nonatomic, strong) IBOutletCollection(UIView) NSArray *textInputs; +- (IBAction)selectNextResponder:(id)sender; + @end diff --git a/Form Accessory View Demo/DemoViewController.m b/Form Input Accessory View Demo/DemoViewController.m similarity index 96% rename from Form Accessory View Demo/DemoViewController.m rename to Form Input Accessory View Demo/DemoViewController.m index 2ec0b18..e14aec9 100644 --- a/Form Accessory View Demo/DemoViewController.m +++ b/Form Input Accessory View Demo/DemoViewController.m @@ -76,4 +76,8 @@ - (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexP return indexPath.row == 2 ? 200 : tableView.rowHeight; } +- (IBAction)selectNextResponder:(id)sender { + [_inputAccessoryView.responderChain selectNextResponder]; +} + @end diff --git a/Form Accessory View Demo/Form Input Accessory View Demo-Info.plist b/Form Input Accessory View Demo/Form Input Accessory View Demo-Info.plist similarity index 100% rename from Form Accessory View Demo/Form Input Accessory View Demo-Info.plist rename to Form Input Accessory View Demo/Form Input Accessory View Demo-Info.plist diff --git a/Form Accessory View Demo/Form Input Accessory View Demo-Prefix.pch b/Form Input Accessory View Demo/Form Input Accessory View Demo-Prefix.pch similarity index 100% rename from Form Accessory View Demo/Form Input Accessory View Demo-Prefix.pch rename to Form Input Accessory View Demo/Form Input Accessory View Demo-Prefix.pch diff --git a/Form Accessory View Demo/MainStoryboard.storyboard b/Form Input Accessory View Demo/MainStoryboard.storyboard similarity index 92% rename from Form Accessory View Demo/MainStoryboard.storyboard rename to Form Input Accessory View Demo/MainStoryboard.storyboard index f40cfed..9070dfe 100644 --- a/Form Accessory View Demo/MainStoryboard.storyboard +++ b/Form Input Accessory View Demo/MainStoryboard.storyboard @@ -1,8 +1,8 @@ - + - + @@ -38,7 +38,10 @@ - + + + + @@ -69,7 +72,10 @@ - + + + + @@ -126,6 +132,7 @@ + diff --git a/Form Accessory View Demo/main.m b/Form Input Accessory View Demo/main.m similarity index 100% rename from Form Accessory View Demo/main.m rename to Form Input Accessory View Demo/main.m diff --git a/XCDFormInputAccessoryView/XCDFormInputAccessoryView.h b/XCDFormInputAccessoryView/XCDFormInputAccessoryView.h index 0ba942e..4168390 100644 --- a/XCDFormInputAccessoryView/XCDFormInputAccessoryView.h +++ b/XCDFormInputAccessoryView/XCDFormInputAccessoryView.h @@ -6,13 +6,17 @@ // #import +#import "XCDResponderChain.h" + @interface XCDFormInputAccessoryView : UIView - (id) initWithResponders:(NSArray *)responders; // Objects must be UIResponder instances - @property (nonatomic, strong) NSArray *responders; +- (id) initWithResponderChain:(XCDResponderChain *)responderChain; +@property (nonatomic, strong) XCDResponderChain *responderChain; + @property (nonatomic, assign) BOOL hasDoneButton; // Defaults to YES on iPhone, NO on iPad - (void) setHasDoneButton:(BOOL)hasDoneButton animated:(BOOL)animated; diff --git a/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m b/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m index 06a1fc2..c3e32e8 100644 --- a/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m +++ b/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m @@ -4,7 +4,7 @@ // Created by Cédric Luthi on 2012-11-10 // Copyright (c) 2012 Cédric Luthi. All rights reserved. // - +#import "XCDResponderChain.h" #import "XCDFormInputAccessoryView.h" static NSString * UIKitLocalizedString(NSString *string) @@ -13,21 +13,6 @@ return UIKitBundle ? [UIKitBundle localizedStringForKey:string value:string table:nil] : string; } -static NSArray * EditableTextInputsInView(UIView *view) -{ - NSMutableArray *textInputs = [NSMutableArray new]; - for (UIView *subview in view.subviews) - { - BOOL isTextField = [subview isKindOfClass:[UITextField class]]; - BOOL isEditableTextView = [subview isKindOfClass:[UITextView class]] && [(UITextView *)subview isEditable]; - if (isTextField || isEditableTextView) - [textInputs addObject:subview]; - else - [textInputs addObjectsFromArray:EditableTextInputsInView(subview)]; - } - return textInputs; -} - @implementation XCDFormInputAccessoryView { UIToolbar *_toolbar; @@ -39,11 +24,16 @@ - (id) initWithFrame:(CGRect)frame } - (id) initWithResponders:(NSArray *)responders +{ + return [self initWithResponderChain:responders ? [[XCDResponderChain alloc] initWithResponders:responders] : nil]; +} + +- (id) initWithResponderChain:(XCDResponderChain *)responderChain { if (!(self = [super initWithFrame:CGRectZero])) return nil; - _responders = responders; + _responderChain = responderChain; _toolbar = [[UIToolbar alloc] init]; _toolbar.tintColor = nil; @@ -102,19 +92,23 @@ - (void) textInputDidBeginEditing:(NSNotification *)notification - (NSArray *) responders { - if (_responders) - return _responders; - - NSArray *textInputs = EditableTextInputsInView([[UIApplication sharedApplication] keyWindow]); - return [textInputs sortedArrayUsingComparator:^NSComparisonResult(UIView *textInput1, UIView *textInput2) { - UIView *commonAncestorView = textInput1.superview; - while (commonAncestorView && ![textInput2 isDescendantOfView:commonAncestorView]) - commonAncestorView = commonAncestorView.superview; - - CGRect frame1 = [textInput1 convertRect:textInput1.bounds toView:commonAncestorView]; - CGRect frame2 = [textInput2 convertRect:textInput2.bounds toView:commonAncestorView]; - return [@(CGRectGetMinY(frame1)) compare:@(CGRectGetMinY(frame2))]; - }]; + return self.responderChain.responders; +} + +- (void) setResponders:(NSArray *)responders +{ + self.responderChain.responders = responders; +} + +- (XCDResponderChain*) responderChain +{ + if (_responderChain == nil) + { + _responderChain = [XCDResponderChain + responderChainWithTextFieldsInView:[[UIApplication sharedApplication] keyWindow]]; + [_responderChain sortRespondersByPosition]; + } + return _responderChain; } - (void) setHasDoneButton:(BOOL)hasDoneButton @@ -144,18 +138,14 @@ - (void) setHasDoneButton:(BOOL)hasDoneButton animated:(BOOL)animated - (void) selectAdjacentResponder:(UISegmentedControl *)sender { - NSArray *firstResponders = [self.responders filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIResponder *responder, NSDictionary *bindings) { - return [responder isFirstResponder]; - }]]; - UIResponder *firstResponder = [firstResponders lastObject]; - NSInteger offset = sender.selectedSegmentIndex == 0 ? -1 : +1; - NSInteger firstResponderIndex = [self.responders indexOfObject:firstResponder]; - NSInteger adjacentResponderIndex = firstResponderIndex != NSNotFound ? firstResponderIndex + offset : NSNotFound; - UIResponder *adjacentResponder = nil; - if (adjacentResponderIndex >= 0 && adjacentResponderIndex < (NSInteger)[self.responders count]) - adjacentResponder = [self.responders objectAtIndex:adjacentResponderIndex]; - - [adjacentResponder becomeFirstResponder]; + if (sender.selectedSegmentIndex == 0) + { + [self.responderChain selectPreviousResponder]; + } + else + { + [self.responderChain selectNextResponder]; + } } - (void) done diff --git a/XCDFormInputAccessoryView/XCDResponderChain.h b/XCDFormInputAccessoryView/XCDResponderChain.h new file mode 100644 index 0000000..207494f --- /dev/null +++ b/XCDFormInputAccessoryView/XCDResponderChain.h @@ -0,0 +1,31 @@ +// +// XCDResponderChain.h +// +// Created by Jude Venn on 14/09/2013. +// Copyright (c) 2013 Cuttlefish Multimedia Ltd. All rights reserved. +// + +#import + +typedef BOOL (^XCDResponderTestCallback)(id obj, BOOL *stop); + +@interface XCDResponderChain : NSObject + ++ (XCDResponderChain*)responderChainWithTextFieldsInView:(UIView *)view; ++ (XCDResponderChain*)responderChainInView:(UIView*)view passingTest:(XCDResponderTestCallback)predicate; + +// Responders must be an array of UIResponders +- (id) initWithResponders:(NSArray *)responders; + +@property (nonatomic, strong) NSArray *responders; + +// Move the input focus to the adjacent responder +- (void)selectNextResponder; +- (void)selectPreviousResponder; + +// By default +responderChainInView will return views order by z-order, which allows +// the developer to order views in the xib. Sometimes it's preferable to have the views +// sorted by vertical position like XCDFormInputAccessoryView, so this method does that. +- (void)sortRespondersByPosition; + +@end diff --git a/XCDFormInputAccessoryView/XCDResponderChain.m b/XCDFormInputAccessoryView/XCDResponderChain.m new file mode 100644 index 0000000..6ac7b00 --- /dev/null +++ b/XCDFormInputAccessoryView/XCDResponderChain.m @@ -0,0 +1,102 @@ +// +// XCDResponderChain.m +// +// Created by Jude Venn on 14/09/2013. +// Copyright (c) 2013 Cuttlefish Multimedia Ltd. All rights reserved. +// + +#import "XCDResponderChain.h" + +@interface XCDResponderChain () + ++ (NSArray*)descendantsOfView:(UIView*)view passingTest:(XCDResponderTestCallback)predicate; + +- (void)selectAdjacentResponder:(NSInteger)direction; + +@end + + +@implementation XCDResponderChain + +- (id)initWithResponders:(NSArray *)responders { + self = [super init]; + if (self) { + self.responders = responders; + } + return self; +} + +- (void)sortRespondersByPosition { + self.responders = [self.responders sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) { + if (![view1 isKindOfClass:[UIView class]] || ![view2 isKindOfClass:[UIView class]]) { + return NSOrderedSame; + } + + UIView *commonAncestorView = view1.superview; + while (commonAncestorView && ![view2 isDescendantOfView:commonAncestorView]) { + commonAncestorView = commonAncestorView.superview; + } + + CGRect frame1 = [view1 convertRect:view1.bounds toView:commonAncestorView]; + CGRect frame2 = [view2 convertRect:view2.bounds toView:commonAncestorView]; + return [@(CGRectGetMinY(frame1)) compare:@(CGRectGetMinY(frame2))]; + }]; +} + ++ (NSArray*)descendantsOfView:(UIView*)view passingTest:(XCDResponderTestCallback)predicate { + NSMutableArray *descendants = [NSMutableArray array]; + BOOL stop = NO; + for (UIView *subview in view.subviews) { + if (predicate(subview, &stop)) { + [descendants addObject:subview]; + if (stop) break; + } + if ([subview.subviews count]) { + [descendants addObjectsFromArray:[self descendantsOfView:subview + passingTest:predicate]]; + } + } + return descendants; +} + ++ (XCDResponderChain*)responderChainInView:(UIView*)view passingTest:(XCDResponderTestCallback)predicate { + return [[self alloc] initWithResponders:[self descendantsOfView:view passingTest:predicate]]; +} + ++ (XCDResponderChain*)responderChainWithTextFieldsInView:(UIView *)view { + XCDResponderTestCallback isEditableTextField = ^BOOL(id obj, BOOL *stop) { + BOOL isTextField = [obj isKindOfClass:[UITextField class]]; + BOOL isEditableTextView = [obj isKindOfClass:[UITextView class]] && [(UITextView *)obj isEditable]; + return (isTextField || isEditableTextView); + }; + return [XCDResponderChain + responderChainInView:view + passingTest:isEditableTextField]; +} + +- (void)selectNextResponder { + [self selectAdjacentResponder:1]; +} + +- (void)selectPreviousResponder { + [self selectAdjacentResponder:-1]; +} + +- (void)selectAdjacentResponder:(NSInteger)direction { + if (direction == 0) return; + NSInteger offset = direction < 0 ? -1 : +1; + + NSArray *firstResponders = [self.responders filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIResponder *responder, NSDictionary *bindings) { + return [responder isFirstResponder]; + }]]; + UIResponder *firstResponder = [firstResponders lastObject]; + NSInteger firstResponderIndex = [self.responders indexOfObject:firstResponder]; + NSInteger adjacentResponderIndex = firstResponderIndex != NSNotFound ? firstResponderIndex + offset : NSNotFound; + UIResponder *adjacentResponder = nil; + if (adjacentResponderIndex >= 0 && adjacentResponderIndex < (NSInteger)[self.responders count]) + adjacentResponder = [self.responders objectAtIndex:adjacentResponderIndex]; + + [adjacentResponder becomeFirstResponder]; +} + +@end