I drag 2 IBActions from a UIButton, one with touchDown event and second with drag Inside.
- (IBAction)clickButton:(UIButton *)sender {
NSLog(#"Click Button");
}
- (IBAction)dragInsideButton:(UIButton *)sender {
NSLog(#"Drag Button");
}
But when I drag inside, the touchDown action also gets fired.
How to disable touchDown event when dragInside.
Thanks!
i have solved a problem like this with using drag Events
add events to your button in .xib file or programatically.
programmatically is:
[mybut addTarget:self action:#selector(dragBegan:withEvent: )
forControlEvents: UIControlEventTouchDown];
[mybut addTarget:self action:#selector(dragMoving:withEvent: )
forControlEvents: UIControlEventTouchDragInside];
[mybut addTarget:self action:#selector(dragEnded:withEvent: )
forControlEvents: UIControlEventTouchUpInside |
UIControlEventTouchUpOutside];
then defininitons of events are:
- (void) dragBegan: (UIButton *) c withEvent:ev
{
NSLog(#"dragBegan......");
count=NO;//bool Value to decide the Down Event
c.tag=0;
[self performSelector:#selector(DownSelected:) withObject:mybut afterDelay:0.1];
//user must begin dragging in 0.1 second else touchDownEvent happens
}
- (void) dragMoving: (UIButton *) c withEvent:ev
{
NSLog(#"dragMoving..............");
c.tag++;
}
- (void) dragEnded: (UIButton *) c withEvent:ev
{
NSLog(#"dragEnded..............");
if (c.tag>0 && !count)
{
NSLog(#"make drag events");
}
}
-(void)DownSelected:(UIButton *)c
{
if (c.tag==0) {
NSLog(#"DownEvent");
count=YES;//count made Yes To interrupt drag event
}
}
This method is tested, and should do what I think you're trying to do. You can change the delay in the timer to get the effect you want. I had to connect 3 actions to the button to make this work -- the third action is a touchUp that resets the system to the starting condition.
#interface LastViewController ()
#property (nonatomic) BOOL touchedDown;
#property (strong,nonatomic) NSTimer *downTimer;
#end
#implementation LastViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.touchedDown = NO;
}
-(IBAction)clickDown:(id)sender {
self.downTimer = [NSTimer scheduledTimerWithTimeInterval:.3 target:self selector:#selector(buttonAction:) userInfo:nil repeats:NO];
}
-(IBAction)dragInside:(id)sender {
[self.downTimer invalidate];
[self buttonAction:self];
}
-(void) buttonAction:(id) sender {
if ([sender isKindOfClass:[NSTimer class]]) {
self.touchedDown = YES;
NSLog(#"click down");
}else{
if (! self.touchedDown) {
NSLog(#"Drag");
}
}
}
-(IBAction)touchUpAction:(id)sender {
self.touchedDown = NO;
}
Try this way:
isClicked is property of type BOOL. Set to YES.
- (IBAction)clickButton:(UIButton *)sender { //touch down
if(isClick==YES){
NSLog(#"Click Button");
//do all your stuffs here
}
isClick=YES;
}
- (IBAction)dragInsideButton:(UIButton *)sender {
NSLog(#"Drag Button");
isClick=NO;
}
On top of this you can also implement removeTarget:Action:
Which ever method gets called first set isClick=NO, I expect clickButton is called first in button action.
I got from Two action methods for an UIButton; next track and seek forward:
Change it As per your requirement.
Personally I'd just track the button's state with an integer on your view controller or within a button subclass. If you track what the button is doing you can control what each of the actions do. In your .h file put in some stuff like this:
enum {
MyButtonScanning,
MyButtonStalling,
MyButtonIdle
};
#interface YourClass : UIViewController {
NSInteger buttonModeAt;
}
#property (nonatomic) NSInteger buttonModeAt;
-(IBAction)buttonPushedDown:(id)sender;
-(void)tryScanForward:(id)sender;
-(IBAction)buttonReleasedOutside:(id)sender;
-(IBAction)buttonReleasedInside:(id)sender;
#end
And then in your .m file throw in some of this stuff:
#implementation YourClass
///in your .m file
#synthesize buttonModeAt;
///link this to your button's touch down
-(IBAction)buttonPushedDown:(id)sender {
buttonModeAt = MyButtonStalling;
[self performSelector:#selector(tryScanForward:) withObject:nil afterDelay:1.0];
}
-(void)tryScanForward:(id)sender {
if (buttonModeAt == MyButtonStalling) {
///the button was not released so let's start scanning
buttonModeAt = MyButtonScanning;
////your actual scanning code or a call to it can go here
[self startScanForward];
}
}
////you will link this to the button's touch up outside
-(IBAction)buttonReleasedOutside:(id)sender {
if (buttonModeAt == MyButtonScanning) {
///they released the button and stopped scanning forward
[self stopScanForward];
} else if (buttonModeAt == MyButtonStalling) {
///they released the button before the delay period finished
///but it was outside, so we do nothing
}
self.buttonModeAt = MyButtonIdle;
}
////you will link this to the button's touch up inside
-(IBAction)buttonReleasedInside:(id)sender {
if (buttonModeAt == MyButtonScanning) {
///they released the button and stopped scanning forward
[self stopScanForward];
} else if (buttonModeAt == MyButtonStalling) {
///they released the button before the delay period finished so we skip forward
[self skipForward];
}
self.buttonModeAt = MyButtonIdle;
}
After that just link the button's actions to what I've noted in the comments before the IBactions. I haven't tested this but it should work.
i'm not sure it will work but you can try
- (IBAction)dragInsideButton:(UIButton *)sender {
[sender removeTarget:self forSelector:#selector(clickButton:) forControlEvent:UIControlEventTouchDown];
}
Related
I'm very new to objective-c. This is what I want to achieve:
I have two buttons named buttonOne and buttonTwo. The default title of buttonOne is "One". Now, when I touch buttonTwo, I want it to stay highlighted (background changes) like being pressed and at the same time I want the title of buttonOne to change to "Two". When I touch buttonTwo again, I want it to change back to its normal state and also the title of buttonOne will change back to "One". I've done something like this:
[_buttonTwo addTarget:self action:#selector(buttonTwoPressed:) forControlEvents:UIControlEventTouchDown];
[_buttonTwo addTarget:self action:#selector(buttonTwoReleased:) forControlEvents:UIControlEventTouchUpInside];
- (IBAction)buttonTwoPressed:(id)sender {
[_buttonOne setTitle:#"Two" forState:UIControlStateNormal];
[sender setBackgroundColor:[UIColor grayColor]];
}
- (IBAction)buttonTwoReleased:(id)sender {
[_buttonOne setTitle:#"One" forState:UIControlStateNormal];
}
This doesn't work properly. When I touch buttonTwo, the background the buttonTwo changes but the title of buttonOne doesn't change.I have to constantly hold down buttonTwo to see the title of buttonOne change to "Two". Also, when I touch buttonTwo again, nothing happens.
Thanks in advance!
In your current code, you are resetting the title immediately when user takes the finger off from the button.
You can do it using a single method:
[_buttonTwo addTarget:self action:#selector(buttonTwoPressed:) forControlEvents:UIControlEventTouchUpInside];
Implement the method like:
- (IBAction)buttonTwoPressed:(UIButton *)sender
{
sender.selected = !sender.selected;
if (sender.selected)
{
[_buttonOne setTitle:#"Two" forState:UIControlStateNormal];
[sender setBackgroundColor:[UIColor grayColor]];
}
else
{
[_buttonOne setTitle:#"One" forState:UIControlStateNormal];
}
}
There are circumstances that you may have overlooked in this implementation. For example, because you have no action on control event UIControlEventTouchUpOutside, you can catch the change of state on touch down but miss the change back because the touch area moves before the touch event ends (drag off the button before releasing/touchUp..)
I'd be looking at catching the change of control state rather than touchEvents. Unfortunately -(UIControlState )state is read only. so there is no setter to override, but we can catch the events which immediately precede/cause the change of controlState. This is UIControl stuff, let's call this strategyA
Another approach might be to override the touch events from UIView, lets call this strategyB.
Both of these strategies leverage the default implementation, so it's important to pass the message on to super(superclass, i.e. what would happen if we didn't implement the method at all) before we inform our delegate
Subclass UIButton and do something like this. Don't forget you'll need to go change out the class of your buttons in your nib or storyboard or whatever you've done to set them up..
interface:
#import <UIKit/UIKit.h>
#protocol StateButtonDelegate;
#interface StateButton : UIButton
#property (nonatomic, weak) id<StateButtonDelegate> delegate;
#end
#protocol StateButtonDelegate <NSObject>
-(void)button:(StateButton *)button didChangeState:(UIControlState )newState;
#end
implementation:
// StateButton.m
#import "StateButton.h"
#implementation StateButton
//strategyA
-(void)setSelected:(BOOL)selected{
[super setSelected:selected];
[self.delegate button:self didChangeState:self.state];
}
-(void)setHighlighted:(BOOL)highlighted{
[super setHighlighted:highlighted];
[self.delegate button:self didChangeState:self.state];
}
-(void)setEnabled:(BOOL)enabled{
[super setEnabled:enabled];
[self.delegate button:self didChangeState:self.state];
}
//or
//strategyB
-(void )touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
[self.delegate button:self didChangeState:self.state];
}
-(void )touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
[self.delegate button:self didChangeState:self.state];
}
-(void )touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
[self.delegate button:self didChangeState:self.state];
}
#end
then add the protocol to your controller, set it delegate to both buttons and you might have some control
-(void)button:(StateButton *)button didChangeState:(UIControlState )newState{
if (button == _buttonOne){
if (newState & UIControlStateNormal){
//doStuff
}else if (newState & UIControlStateHighlighted){
//doOtherStuff
}else if (newState & UIControlStateSelected){
//doDifferentStuffAgain
}
}else if (button == _buttonTwo){
if (newState & UIControlStateNormal){
//doStuff
}else if (newState & UIControlStateHighlighted){
//doOtherStuff
}else if (newState & UIControlStateSelected){
//doDifferentStuffAgain
}
}
}
I am trying to build an app where two buttons need to be pressed simultaneously.
if (self->button1.touchInside && self->button2.touchInside) {
NSLog(#"Two buttons pressed");
}
else if (!self->button1.touchInside | !self->button2.touchInside){
NSLog(#"One button pressed");
}
Both buttons are attached to the View Controller using the 'Touch Down' gesture option. When I press both buttons at the same time (with one press) the console window prints:
One button pressed
Two buttons pressed
This interferes with how my application works. I only want the console to print
Two buttons pressed
Thanks
What i understand is you need to take some action when both the buttons are pressed. Watever you try, there is going to be a lag between the touches of these buttons. A better approach would be to check if press on both buttons is finished. Hope following works for you -
#property(nonatomic, assign) BOOL firstButtonPressed;
#property(nonatomic, assign) BOOL secondButtonPressed;
//in init or viewDidLoad or any view delegates
_firstButtonPressed = NO;
_secondButtonPressed = NO;
//Connect following IBActions to Touch Down events of both buttons
- (IBAction)firstButtonPressed:(UIButton *)sender {
_firstButtonPressed = YES;
[self checkForButtonPress];
}
- (IBAction)secondButtonPressed:(UIButton *)sender {
_ secondButtonPressed = YES;
[self checkForButtonPress];
}
- (void)checkForButtonPress {
if (_firstButtonPressed && _secondButtonPressed) {
NSlog(#"Two buttons pressed");
}
}
You can do it using two boolean flags like this:
#property(nonatomic, assign) BOOL firstButtonPressed;
#property(nonatomic, assign) BOOL secondButtonPressed;
- (void)firstButtonTouchDown:(id)sender
{
_firstButtonPressed = YES;
if (_secondButtonPressed)
{
// Both buttons pressed.
}
}
- (void)secondButtonTouchDown:(id)sender
{
_secondButtonPressed = YES;
if (_firstButtonPressed)
{
// Both buttons pressed.
}
}
- (void)firstButtonTouchCancelled:(id)sender
{
_firstButtonPressed = NO;
}
- (void)secondButtonTouchCancelled:(id)sender
{
_secondButtonPressed = NO;
}
Alternatively, you can also start a timer when a touch is down and check if the second touch happens during time interval you specify.
I need to build a custom keyboard for my iPhone app. Previous questions and answers on the topic have focused on the visual elements of a custom keyboard, but I'm trying to understand how to retrieve the keystrokes from this keyboard.
Apple provides the inputView mechanism which makes it easy to associate a custom keyboard with an UITextField or UITextView, but they do not provide the functions to send generated keystrokes back to the associated object. Based on the typical delegation for these objects, we'd expect three functions : one of normal characters, one for backspace and one for enter. Yet, no one seems to clearly define these functions or how to use them.
How do I build a custom keyboard for my iOS app and retrieve keystrokes from it?
Greg's approach should work but I have an approach that doesn't require the keyboard to be told about the text field or text view. In fact, you can create a single instance of the keyboard and assign it to multiple text fields and/or text views. The keyboard handles knowing which one is the first responder.
Here is my approach. I'm not going to show any code for creating the keyboard layout. That's the easy part. This code shows all of the plumbing.
Edit: This has been updated to properly handle UITextFieldDelegate textField:shouldChangeCharactersInRange:replacementString: and UITextViewDelegate textView:shouldChangeTextInRange:replacementText:.
The header file:
#interface SomeKeyboard : UIView <UIInputViewAudioFeedback>
#end
The implementation file:
#implmentation SomeKeyboard {
id<UITextInput> _input;
BOOL _tfShouldChange;
BOOL _tvShouldChange;
}
- (id)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(checkInput:) name:UITextFieldTextDidBeginEditingNotification object:nil];
}
return self;
}
// This is used to obtain the current text field/view that is now the first responder
- (void)checkInput:(NSNotification *)notification {
UITextField *field = notification.object;
if (field.inputView && self == field.inputView) {
_input = field;
_tvShouldChange = NO;
_tfShouldChange = NO;
if ([_input isKindOfClass:[UITextField class]]) {
id<UITextFieldDelegate> del = [(UITextField *)_input delegate];
if ([del respondsToSelector:#selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
_tfShouldChange = YES;
}
} else if ([_input isKindOfClass:[UITextView class]]) {
id<UITextViewDelegate> del = [(UITextView *)_input delegate];
if ([del respondsToSelector:#selector(textView:shouldChangeTextInRange:replacementText:)]) {
_tvShouldChange = YES;
}
}
}
}
// Call this for each button press
- (void)click {
[[UIDevice currentDevice] playInputClick];
}
// Call this when a button on the keyboard is tapped (other than return or backspace)
- (void)keyTapped:(UIButton *)button {
NSString *text = ???; // determine text for the button that was tapped
if ([_input respondsToSelector:#selector(shouldChangeTextInRange:replacementText:)]) {
if ([_input shouldChangeTextInRange:[_input selectedTextRange] replacementText:text]) {
[_input insertText:text];
}
} else if (_tfShouldChange) {
NSRange range = [(UITextField *)_input selectedRange];
if ([[(UITextField *)_input delegate] textField:(UITextField *)_input shouldChangeCharactersInRange:range replacementString:text]) {
[_input insertText:text];
}
} else if (_tvShouldChange) {
NSRange range = [(UITextView *)_input selectedRange];
if ([[(UITextView *)_input delegate] textView:(UITextView *)_input shouldChangeTextInRange:range replacementText:text]) {
[_input insertText:text];
}
} else {
[_input insertText:text];
}
}
// Used for a UITextField to handle the return key button
- (void)returnTapped:(UIButton *)button {
if ([_input isKindOfClass:[UITextField class]]) {
id<UITextFieldDelegate> del = [(UITextField *)_input delegate];
if ([del respondsToSelector:#selector(textFieldShouldReturn:)]) {
[del textFieldShouldReturn:(UITextField *)_input];
}
} else if ([_input isKindOfClass:[UITextView class]]) {
[_input insertText:#"\n"];
}
}
// Call this to dismiss the keyboard
- (void)dismissTapped:(UIButton *)button {
[(UIResponder *)_input resignFirstResponder];
}
// Call this for a delete/backspace key
- (void)backspaceTapped:(UIButton *)button {
if ([_input respondsToSelector:#selector(shouldChangeTextInRange:replacementText:)]) {
UITextRange *range = [_input selectedTextRange];
if ([range.start isEqual:range.end]) {
UITextPosition *newStart = [_input positionFromPosition:range.start inDirection:UITextLayoutDirectionLeft offset:1];
range = [_input textRangeFromPosition:newStart toPosition:range.end];
}
if ([_input shouldChangeTextInRange:range replacementText:#""]) {
[_input deleteBackward];
}
} else if (_tfShouldChange) {
NSRange range = [(UITextField *)_input selectedRange];
if (range.length == 0) {
if (range.location > 0) {
range.location--;
range.length = 1;
}
}
if ([[(UITextField *)_input delegate] textField:(UITextField *)_input shouldChangeCharactersInRange:range replacementString:#""]) {
[_input deleteBackward];
}
} else if (_tvShouldChange) {
NSRange range = [(UITextView *)_input selectedRange];
if (range.length == 0) {
if (range.location > 0) {
range.location--;
range.length = 1;
}
}
if ([[(UITextView *)_input delegate] textView:(UITextView *)_input shouldChangeTextInRange:range replacementText:#""]) {
[_input deleteBackward];
}
} else {
[_input deleteBackward];
}
[self updateShift];
}
#end
This class requires a category method for UITextField:
#interface UITextField (CustomKeyboard)
- (NSRange)selectedRange;
#end
#implementation UITextField (CustomKeyboard)
- (NSRange)selectedRange {
UITextRange *tr = [self selectedTextRange];
NSInteger spos = [self offsetFromPosition:self.beginningOfDocument toPosition:tr.start];
NSInteger epos = [self offsetFromPosition:self.beginningOfDocument toPosition:tr.end];
return NSMakeRange(spos, epos - spos);
}
#end
I have created a full working example of a keyboard for the iPad, available on Github here:
https://github.com/lnafziger/Numberpad
Numberpad is a custom numeric keyboard for the iPad which works with
both UITextField's and UITextView's requiring no changes other than
adding an instance of the Numberpad class as the inputView of the text
field/view.
Features:
It is covered under the MIT licence, so may be freely copied and used per its' terms.
It works with UITextFields and UITextViews
It does not require a delegate to be set.
It automatically keeps track of which view is the first responder (so you don't have to)
You do not have to set the size of the keyboard, or keep track of it.
There is a shared instance that you can use for as many input views as you like, without using extra memory for each one.
Usage is as simple as including Numberpad.h and then:
theTextField.inputView = [Numberpad defaultNumberpad];
Everything else is taken care of automatically!
Either grab the two class files and the xib from Github (link above), or create the buttons (in code or in a storyboard/xib) with their actions set to the appropriate methods in the class (numberpadNumberPressed, numberpadDeletePressed, numberpadClearPressed, or numberpadDonePressed).
The following code is out of date. See the Github project for the latest code.
Numberpad.h:
#import <UIKit/UIKit.h>
#interface Numberpad : UIViewController
// The one and only Numberpad instance you should ever need:
+ (Numberpad *)defaultNumberpad;
#end
Numberpad.m:
#import "Numberpad.h"
#pragma mark - Private methods
#interface Numberpad ()
#property (nonatomic, weak) id<UITextInput> targetTextInput;
#end
#pragma mark - Numberpad Implementation
#implementation Numberpad
#synthesize targetTextInput;
#pragma mark - Shared Numberpad method
+ (Numberpad *)defaultNumberpad {
static Numberpad *defaultNumberpad = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultNumberpad = [[Numberpad alloc] init];
});
return defaultNumberpad;
}
#pragma mark - view lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
// Keep track of the textView/Field that we are editing
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(editingDidBegin:)
name:UITextFieldTextDidBeginEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(editingDidBegin:)
name:UITextViewTextDidBeginEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(editingDidEnd:)
name:UITextFieldTextDidEndEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(editingDidEnd:)
name:UITextViewTextDidEndEditingNotification
object:nil];
}
- (void)viewDidUnload {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UITextFieldTextDidBeginEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UITextViewTextDidBeginEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UITextFieldTextDidEndEditingNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UITextViewTextDidEndEditingNotification
object:nil];
self.targetTextInput = nil;
[super viewDidUnload];
}
#pragma mark - editingDidBegin/End
// Editing just began, store a reference to the object that just became the firstResponder
- (void)editingDidBegin:(NSNotification *)notification {
if (![notification.object conformsToProtocol:#protocol(UITextInput)]) {
self.targetTextInput = nil;
return;
}
self.targetTextInput = notification.object;
}
// Editing just ended.
- (void)editingDidEnd:(NSNotification *)notification {
self.targetTextInput = nil;
}
#pragma mark - Keypad IBActions
// A number (0-9) was just pressed on the number pad
// Note that this would work just as well with letters or any other character and is not limited to numbers.
- (IBAction)numberpadNumberPressed:(UIButton *)sender {
if (!self.targetTextInput) {
return;
}
NSString *numberPressed = sender.titleLabel.text;
if ([numberPressed length] == 0) {
return;
}
UITextRange *selectedTextRange = self.targetTextInput.selectedTextRange;
if (!selectedTextRange) {
return;
}
[self textInput:self.targetTextInput replaceTextAtTextRange:selectedTextRange withString:numberPressed];
}
// The delete button was just pressed on the number pad
- (IBAction)numberpadDeletePressed:(UIButton *)sender {
if (!self.targetTextInput) {
return;
}
UITextRange *selectedTextRange = self.targetTextInput.selectedTextRange;
if (!selectedTextRange) {
return;
}
// Calculate the selected text to delete
UITextPosition *startPosition = [self.targetTextInput positionFromPosition:selectedTextRange.start offset:-1];
if (!startPosition) {
return;
}
UITextPosition *endPosition = selectedTextRange.end;
if (!endPosition) {
return;
}
UITextRange *rangeToDelete = [self.targetTextInput textRangeFromPosition:startPosition
toPosition:endPosition];
[self textInput:self.targetTextInput replaceTextAtTextRange:rangeToDelete withString:#""];
}
// The clear button was just pressed on the number pad
- (IBAction)numberpadClearPressed:(UIButton *)sender {
if (!self.targetTextInput) {
return;
}
UITextRange *allTextRange = [self.targetTextInput textRangeFromPosition:self.targetTextInput.beginningOfDocument
toPosition:self.targetTextInput.endOfDocument];
[self textInput:self.targetTextInput replaceTextAtTextRange:allTextRange withString:#""];
}
// The done button was just pressed on the number pad
- (IBAction)numberpadDonePressed:(UIButton *)sender {
if (!self.targetTextInput) {
return;
}
// Call the delegate methods and resign the first responder if appropriate
if ([self.targetTextInput isKindOfClass:[UITextView class]]) {
UITextView *textView = (UITextView *)self.targetTextInput;
if ([textView.delegate respondsToSelector:#selector(textViewShouldEndEditing:)]) {
if ([textView.delegate textViewShouldEndEditing:textView]) {
[textView resignFirstResponder];
}
}
} else if ([self.targetTextInput isKindOfClass:[UITextField class]]) {
UITextField *textField = (UITextField *)self.targetTextInput;
if ([textField.delegate respondsToSelector:#selector(textFieldShouldEndEditing:)]) {
if ([textField.delegate textFieldShouldEndEditing:textField]) {
[textField resignFirstResponder];
}
}
}
}
#pragma mark - text replacement routines
// Check delegate methods to see if we should change the characters in range
- (BOOL)textInput:(id <UITextInput>)textInput shouldChangeCharactersInRange:(NSRange)range withString:(NSString *)string
{
if (!textInput) {
return NO;
}
if ([textInput isKindOfClass:[UITextField class]]) {
UITextField *textField = (UITextField *)textInput;
if ([textField.delegate respondsToSelector:#selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
if (![textField.delegate textField:textField
shouldChangeCharactersInRange:range
replacementString:string]) {
return NO;
}
}
} else if ([textInput isKindOfClass:[UITextView class]]) {
UITextView *textView = (UITextView *)textInput;
if ([textView.delegate respondsToSelector:#selector(textView:shouldChangeTextInRange:replacementText:)]) {
if (![textView.delegate textView:textView
shouldChangeTextInRange:range
replacementText:string]) {
return NO;
}
}
}
return YES;
}
// Replace the text of the textInput in textRange with string if the delegate approves
- (void)textInput:(id <UITextInput>)textInput replaceTextAtTextRange:(UITextRange *)textRange withString:(NSString *)string {
if (!textInput) {
return;
}
if (!textRange) {
return;
}
// Calculate the NSRange for the textInput text in the UITextRange textRange:
int startPos = [textInput offsetFromPosition:textInput.beginningOfDocument
toPosition:textRange.start];
int length = [textInput offsetFromPosition:textRange.start
toPosition:textRange.end];
NSRange selectedRange = NSMakeRange(startPos, length);
if ([self textInput:textInput shouldChangeCharactersInRange:selectedRange withString:string]) {
// Make the replacement:
[textInput replaceRange:textRange withText:string];
}
}
#end
Here's my custom keyboard which I believe addresses these as completely as Apple will allow:
// PVKeyboard.h
#import <UIKit/UIKit.h>
#interface PVKeyboard : UIView
#property (nonatomic,assign) UITextField *textField;
#end
// PVKeyboard.m
#import "PVKeyboard.h"
#interface PVKeyboard () {
UITextField *_textField;
}
#property (nonatomic,assign) id<UITextInput> delegate;
#end
#implementation PVKeyboard
- (id<UITextInput>) delegate {
return _textField;
}
- (UITextField *)textField {
return _textField;
}
- (void)setTextField:(UITextField *)tf {
_textField = tf;
_textField.inputView = self;
}
- (IBAction)dataPress:(UIButton *)btn {
[self.delegate insertText:btn.titleLabel.text];
}
- (IBAction)backPress {
if ([self.delegate conformsToProtocol:#protocol(UITextInput)]) {
[self.delegate deleteBackward];
} else {
int nLen = [_textField.text length];
if (nLen)
_textField.text = [_textField.text substringToIndex:nLen-1];
}
}
- (IBAction)enterPress {
[_textField.delegate textFieldShouldReturn:_textField];
}
- (UIView *)loadWithNIB {
NSArray *aNib = [[NSBundle mainBundle]loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
UIView *view = [aNib objectAtIndex:0];
[self addSubview:view];
return view;
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
[self loadWithNIB];
return self;
}
#end
In XCode 4.3 and later, you need to create an objective-Class (for the .h & .m files) based on UIView and a User Interface View file (for the .xib file). Make sure all three files have the same name. Using the Identity Inspector, make sure to set the XIB's File's Owner Custom Class to match the new object's name. Using the Attributes Inspector, set the form's size to Freeform and set the Status Bar to none. Using the Size Inspector, set the form's size, which should match the width of the standard keyboard (320 for iPhone portrait and 480 for iPhone landscape), but you can choose any height you like.
The form is ready to be used. Add buttons and connect them to the dataPress, backPress and enterPress as appropriate. The initWithFrame: and loadWithNIB functions will do all the magic to allow you to use a keyboard designed in Interface Builder.
To use this keyboard with a UITextField myTextField, just add the following code to your viewDidLoad:
self.keyboard = [[PVKeyboard alloc]initWithFrame:CGRectMake(0,488,320,60)];
self.keyboard.textField = self.myTextField;
Because of some limitations, this keyboard isn't reusable, so you'll need one per field. I can almost make it reusable, but I'm just not feeling that clever. The keyboard is also limited to UITextFields, but that's mainly because of limitations in implementing the enter key functionality, which I'll explain below.
Here's the magic that should allow you to design a better keyboard than this starter framework...
I've implemented the only property of this keyboard, textField, using a discreet a discrete setter (setTextField) because:
we need the UITextField object to handle the enter problem
we need UITextField because it conforms to the UITextInput protocol which conforms to UIKeyInput, which does much of our heavy lifting
it was a convenient place to set the UITextInput's inputView field to use this keyboard.
You'll notice a second private property named delegate, which essentially typecasts the UITextField pointer to a UITextInput pointer. I probably could have done this cast inline, but I sensed this might be useful as a function for future expansion, perhaps to include support for UITextView.
The function dataPress is what inserts text input the edited field using the insertText method of UIKeyInput. This seems to work in all versions back to iOS 4. For my keyboard, I'm simply using the label of each button, which is pretty normal. Use whatever NSStrings strike your fancy.
The function dataBack does the backspace and is a little more complicated. When the UIKeyInput deleteBackward works, it works wonderfully. And while the documentation says it works back to iOS 3.2, it seems to only work back to iOS 5.0, which is when UITextField (and UITextView) conformed to the UITextInput protocol. So prior to that, you're on your own. Since iOS 4 support is a concern to many, I've implemented a lame backspace which works on the UITextField directly. If not for this requirement, I could have made this keyboard work with UITextView. And this backspace isn't as general, only deleting the last character, while deleteBackward will work properly even if the user moves the cursor.
The function enterPress implements the enter key, but is a complete kludge because Apple doesn't seem to give a method for invoking the enter key. So enterPress simply calls the UITextField's delegate function textFieldShouldReturn:, which most programmers implement. Please note that the delegate here is the UITextFieldDelegate for the UITextField and NOT the delegate property for the keyboard itself.
This solution goes around the normal keyboard processing, which hardly matters in the case of UITextField, but makes this technique unusable with UITextView since there is now way to insert line breaks in the text being edited.
That's pretty much it. It took 24 hours of reading and cobbling to make this work. I hope it helps somebody.
(This is mostly taken from http://blog.carbonfive.com/2012/03/12/customizing-the-ios-keyboard/)
In iOS, the keyboard for a view is managed by the UIResponder part of the view inheritance chain. When any UIResponder that needs a keyboard becomes the first responder (is taped or otherwise activated), the UIResponder looks in its inputView property for the view to display as the keyboard. So, to make a custom keyboard and respond to event on it, you have to create a view with letter buttons, associate a view controller with that view, and with the buttons to handle the presses, and you have to set that view as the inputView of some textbox.
Take a look at the link for more information.
In Keynote (and other apps), I've noticed the "standard" interface of doing Undo/Redo is by providing an Undo button on the tool bar.
Clicking the button (that is always enabled) Undos the recent operation.
(If there is not recent operation to undo, it will show the Undo/Redo menu).
Long-clicking the Undo button opens an Undo/Redo menu.
I searched for methods of implementing this, and the best answer I found so far is at the following link.
I wonder if anyone knows of a simpler way?
Thanks!
After reviewing all methods and discussing with friends, below is the solution I used, for a UIBarButtonItem the responds to both taps and long-press (TapOrLongPressBarButtonItem).
It is based on the following principals:
Subclass UIBarButtonItem
Use a custom view (so it's really trivial to handle the long-press - since our custom view has no problem responding to a long-press gesture handler...)
... So far - this approach was in the other SO thread - and I didn't like this approach since I couldn't find and easy enough way of making the custom view appear like an iPad navigation bar button... Soooo...
Use UIGlossyButton by Water Lou (thanks water!). This use is encapsulated within the subclass...
The resulting code is as follows:
#protocol TapOrPressButtonDelegate;
#interface TapOrPressBarButtonItem : UIBarButtonItem {
UIGlossyButton* _tapOrPressButton;
__weak id<TapOrPressButtonDelegate> _delegate;
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate;
#end
#protocol TapOrPressButtonDelegate<NSObject>
- (void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
- (void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
#end
#implementation TapOrPressBarButtonItem
- (void)buttonLongPressed:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if([_delegate respondsToSelector:#selector(buttonLongPressed:withBarButtonItem:)]) {
[_delegate buttonLongPressed:_tapOrPressButton withBarButtonItem:self];
}
}
- (void)buttonTapped:(id)sender {
if (sender != _tapOrPressButton) {
return;
}
if([_delegate respondsToSelector:#selector(buttonTapped:withBarButtonItem:)]) {
[_delegate buttonTapped:_tapOrPressButton withBarButtonItem:self];
}
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate {
if (self = [super init]) {
// Store delegate reference
_delegate = delegate;
// Create the customm button that will have the iPad-nav-bar-default appearance
_tapOrPressButton = [UIGlossyButton buttonWithType:UIButtonTypeCustom];
[_tapOrPressButton setTitle:title forState:UIControlStateNormal];
[_tapOrPressButton setNavigationButtonWithColor:[UIColor colorWithRed:123.0/255 green:130.0/255 blue:139.0/255 alpha:1.0]];
// Calculate width...
CGSize labelSize = CGSizeMake(1000, 30);
labelSize = [title sizeWithFont:_tapOrPressButton.titleLabel.font constrainedToSize:labelSize lineBreakMode:UILineBreakModeMiddleTruncation];
_tapOrPressButton.frame = CGRectMake(0, 0, labelSize.width+20, 30);
// Add a handler for a tap
[_tapOrPressButton addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// Add a handler for a long-press
UILongPressGestureRecognizer* buttonLongPress_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(buttonLongPressed:)];
[_tapOrPressButton addGestureRecognizer:buttonLongPress_];
// Set this button as the custom view of the bar item...
self.customView = _tapOrPressButton;
}
return self;
}
// Safe guards...
- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithCustomView:(UIView *)customView {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
#end
And all you need to do is:
1. Instantiate is as follows:
TapOrPressBarButtonItem* undoMenuButton = [[TapOrPressBarButtonItem alloc] initWithTitle:NSLocalizedString(#"Undo", #"Undo Menu Title") andDelegate:self];
2. Connect the button to the navigation bar:
[self.navigationItem setLeftBarButtonItem:undoMenuButton animated:NO];
3. Implement the TapOrPressButtonDelegate protocol, and you're done...
-(void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self menuItemUndo:barButtonItem];
}
-(void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self undoMenuClicked:barButtonItem];
}
Hope this helps anyone else...
If you are using IB (or in Xcode4 the designer...i guess it is called) then you can select "Undo" from the First responder and drag that action to a button. I can give you more specific instructions if that doesn't cover it.
Here's what it looks like
It's on the left underneath the column "Received actions" at the bottom
I believe the key is actually in the UINavigationBar itself. Unlike UIButtons or other normal touch tracking objects, I suspect UIBarItems don't handle their own touches. They don't inherit UIResponder or UIControl methods. However UINavigationBar of course does. And I've personally added gestures straight to a UINavigationBar many times.
I suggest you override touch handling in a UINavigationBar subclass and check the touches against its children. If the child is your special Undo button you can handle it accordingly.
UIButton* undoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[undoButton addTarget:self action:#selector(undoPressStart:) forControlEvents:UIControlEventTouchDown];
[undoButton addTarget:self action:#selector(undoPressFinish:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* navButton = [[[UIBarButtonItem alloc] initWithCustomView:undoButton] autorelease];
self.navigationItem.rightBarButtonItem = navButton;
You don't necessarily have to add the UIBarButtonItem as the rightBarButtonItem, this is just and easy way to show you how to create your UIBarButtonItem with a custom view that is the UIButton you want to handle events.
You'll need to implement the undoPressStart: and undoPressFinish: by maintaining state. I'd say on start, store the current NSDate or some granular representation of the time. On finish, if check the time elapsed and if it is beyond a certain threshold, show the menu - otherwise (as well as if the start date was never captured) perform the undo.
As an improvement, you'll likely want to observe the UIControlEventTouchDragExit event as well to cancel the long press.
I have a view with several UIButtons. I have successfully implemented using UILongPressGestureRecognizer with the following as the selector;
- (void)longPress:(UILongPressGestureRecognizer*)gesture {
if ( gesture.state == UIGestureRecognizerStateEnded ) {
NSLog(#"Long Press");
}
}
What I need to know within this method is which UIButton received the longpress since I need to do something different, depending on which button received the longpress.
Hopefully the answer is not some issue of mapping the coordinates of where the longpress occured to the bounds of the buttons - would rather not go there.
Any suggestions?
Thanks!
This is available in gesture.view.
Are you adding the long tap gesture controller to the UIView that has the UIButtons as subviews? If so, something along the lines of #Magic Bullet Dave's approach is probably the way to go.
An alternative is to subclass UIButton and add to each UIButton a longTapGestureRecogniser. You can then get your button to do what you like. For example, it could send a message identifying itself to a view controller. The following snippet illustrates methods for the subclass.
- (void) setupLongPressForTarget: (id) target;
{
[self setTarget: target]; // property used to hold target (add #property and #synthesise as appropriate)
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:button action:#selector(longPress:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
- (void) longPress: (UIGestureRecognizer*) recogniser;
{
if (![recogniser isEnabled]) return; // code to prevent multiple long press messages
[recogniser setEnabled:NO];
[recogniser performSelector:#selector(setEnabled:) withObject: [NSNumber numberWithBool:YES] afterDelay:0.2];
NSLog(#"long press detected on button");
if ([[self target] respondsToSelector:#selector(longPressOnButton:)])
{
[[self target] longPressOnButton: self];
}
}
In your view controller you might have code something like this:
- (void) viewDidLoad;
{
// set up buttons (if not already done in Interface Builder)
[buttonA setupLongPressForTarget: self];
[buttonB setupLongPressForTarget: self];
// finish any other set up
}
- (void) longPressOnButton: (id) sender;
{
if (sender = [self buttonA])
{
// handle button A long press
}
if (sender = [self buttonB])
{
// handle button B long press
}
// etc.
}
If your view contains multiple subViews (like lots of buttons) you can determine what was tapped:
// Get the position of the point tapped in the window co-ordinate system
CGPoint tapPoint = [gesture locationInView:nil];
UIView *viewAtBottomOfHeirachy = [self.window hitTest:tapPoint withEvent:nil];
if ([viewAtBottomOfHeirachy isKindOfClass:[UIButton class]])