I'm using google maps SDK 1.7.2 on a projectd using iOS 7 (I just upgraded it from iOS6). For some reason all the GMSMapViewDelegate callbacks work but this one
- (BOOL) didTapMyLocationButtonForMapView: (GMSMapView *)mapView
I'm assuming this should be called when the arrow button is tapped right? Any idea why it isn't?
This is how I instantiate the mapsview:
mapView_ = [GMSMapView mapWithFrame:[[self mainView] bounds]
camera:[self currentCameraUseStandartZoom:YES]];
[[mapView_ settings] setMyLocationButton:YES];
[mapView_ setDelegate:self];
[[self mainView] addSubview:mapView_];
I use a pretty reliable method to find the my location button.
Initially when a map view is created the button is hidden. And I go through map view hierarchy to find all hidden buttons. Then I set self.mapView.settings.myLocationButton = YES and check if any of the buttons I found is not hidden anymore.
Here is the code I use:
- (UIButton*)findAndShowMyLocationButton
{
NSMutableArray* hiddenButtons = [NSMutableArray array];
[self findHiddenButtonsInView:self.mapView hiddenButtons:hiddenButtons];
self.mapView.settings.myLocationButton = YES;
for (UIButton* button in hiddenButtons) {
if (!button.hidden) return button;
}
return nil;
}
- (void)findHiddenButtonsInView:(UIView*)view hiddenButtons:(NSMutableArray*)hiddenButtons
{
for (UIView* subview in view.subviews) {
if (subview.hidden && [subview isKindOfClass:[UIButton class]]) {
[hiddenButtons addObject:subview];
} else {
[self findHiddenButtonsInView:subview hiddenButtons:hiddenButtons];
}
}
}
And finally
- (void)viewDidLoad
{
...
UIButton* myLocationButton = [self findAndShowMyLocationButton];
[myLocationButton addTarget:self action:#selector(myLocationClick) forControlEvents:UIControlEventTouchUpInside];
}
just in case someone is having the same problem.. i pretty much resolved this by using a hack.. here it is:
in my main.m file I customized the UIResponder class:
int main(int argc, char *argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([TPMUApplication class]), NSStringFromClass([TPMUAppDelegate class]));
}
}
TPMUApplication.h
#import <UIKit/UIKit.h>
#class TPMUTaxiRequestVC;
#interface TPMUApplication : UIApplication
// basically the view controller that will be informed that
// the user has tapped the my location button, normally it would subscribe to
// the GMSMapViewDelegate protocol, and it should have a GMSMapView property
#property (nonatomic, strong) TPMUTaxiRequestVC *taxiRequestVC;
#end
TPMUApplication.m
#import "TPMUApplication.h"
#import "TPMUTaxiRequestVC.h"
#implementation TPMUApplication
- (void)sendEvent:(UIEvent *)event
{
[super sendEvent:event];
UIView *touchReceipientView =((UITouch *)[event.allTouches anyObject]).view;
NSLog(#"");
CGRect myLocationButtonFourchIncFrame = CGRectMake(256, 525, 64, 54);
CGRect myLocationButtonThreeHalfIncFrame = CGRectMake(256, 336, 64, 54);
if (CGRectEqualToRect(touchReceipientView.frame, myLocationButtonFourchIncFrame) ||
CGRectEqualToRect(touchReceipientView.frame, myLocationButtonThreeHalfIncFrame)) {
if (self.taxiRequestVC.mapState != TPMUMapStateInMotionAsResultOfMyLocationButtonTap) {
self.taxiRequestVC.mapState = TPMUMapStateInMotionAsResultOfMyLocationButtonTap;
// notice that didTapMyLocationButtonForMapView is actually
// a method in the GMSMapViewDelegate protocol.. and since
// taxiRequestVC subscribes to that protocol.. we simply call it here
// as if it was natively called
[self.taxiRequestVC didTapMyLocationButtonForMapView:self.taxiRequestVC.mapView];
}
}
}
#end
and just in case you were wondering, TPMUMapStateInMotionAsResultOfMyLocationButtonTap is a state of a state machine variable with the following states:
typedef enum
{
TPMUMapStateIdle = 0,
TPMUMapStateInMotionAsResultOfUserGesture,
TPMUMapStateInMotionAsResultOfMyLocationButtonTap
} TPMUMapState;
since I wanted to track motion in the map as a result of location button tap vs user gesture.
hope this helps!
Related
There's a behavior in the Line messenger app (the de facto messenger app in Japan) that I'm trying to emulate.
Basically, they have a modal view controller with a scroll view inside. When the scroll action reaches the top of its content, the view controller seamlessly switches to an interactive dismissal animation. Also, when the gesture returns the view to the top of the screen, control is returned to the scroll view.
Here's a gif of how it looks.
For the life of me, I can't figure out how they did it. I've tried a few different methods, but they've all failed, and I'm out of ideas. Can anyone point me in the right direction?
EDIT2
To clarify, the behavior that I want to emulate isn't just simply dragging the window down. I can do that, no problem.
I want to know how the same scroll gesture (without lifting the finger) triggers the dismissal transition and then transfers control back to the scroll view after the view has been dragged back to the original position.
This is the part that I can't figure out.
End EDIT2
EDIT1
Here's what I have so far. I was able to use the scroll view delegate methods to add a target-selector that handles the regular dismissal animation, but it still doesn't work as expected.
I create a UIViewController with a UIWebView as a property. Then I put it in a UINavigationController, which is presented modally.
The navigation controller uses animation/transition controllers for the regular interactive dismissal (which can be done by gesturing over the navigation bar).
From here, everything works fine, but the dismissal can't be triggered from the scroll view.
NavigationController.h
#interface NavigationController : UINavigationController <UIViewControllerTransitioningDelegate>
#property (nonatomic, strong) UIPanGestureRecognizer *gestureRecog;
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer;
#end
NavigationController.m
#import "NavigationController.h"
#import "AnimationController.h"
#import "TransitionController.h"
#implementation NavigationController {
AnimationController *_animator;
TransitionController *_interactor;
}
- (instancetype)init {
self = [super init];
self.transitioningDelegate = self;
_animator = [[AnimationController alloc] init];
_interactor = [[TransitionController alloc] init];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set the gesture recognizer
self.gestureRecog = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:_gestureRecog];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
if (animator == _animator && _interactor.hasStarted) {
return _interactor;
}
return nil;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
if (dismissed == self || [self.viewControllers indexOfObject:dismissed] != NSNotFound) {
return _animator;
}
return nil;
}
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecog {
CGFloat threshold = 0.3f;
CGPoint translation = [gestureRecog translationInView:self.view];
CGFloat verticalMovement = translation.y / self.view.bounds.size.height;
CGFloat downwardMovement = fmaxf(verticalMovement, 0.0f);
CGFloat downwardMovementPercent = fminf(downwardMovement, 1.0f);
switch (gestureRecog.state) {
case UIGestureRecognizerStateBegan: {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged: {
if (!_interactor.hasStarted) {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
_interactor.shouldFinish = downwardMovementPercent > threshold;
[_interactor updateInteractiveTransition:downwardMovementPercent];
break;
}
case UIGestureRecognizerStateCancelled: {
_interactor.hasStarted = NO;
[_interactor cancelInteractiveTransition];
break;
}
case UIGestureRecognizerStateEnded: {
_interactor.hasStarted = NO;
if (_interactor.shouldFinish) {
[_interactor finishInteractiveTransition];
} else {
[_interactor cancelInteractiveTransition];
}
break;
}
default: {
break;
}
}
}
#end
Now, I have to get that gesture handling to trigger when the scroll view has reached the top. So, here's what I did in the view controller.
WebViewController.m
#import "WebViewController.h"
#import "NavigationController.h"
#interface WebViewController ()
#property (weak, nonatomic) IBOutlet UIWebView *webView;
#end
#implementation WebViewController {
BOOL _isHandlingPan;
CGPoint _topContentOffset;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.webView.scrollView setDelegate:self];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ((scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan ||
scrollView.panGestureRecognizer.state == UIGestureRecognizerStateChanged) &&
! _isHandlingPan &&
scrollView.contentOffset.y < self.navigationController.navigationBar.translucent ? -64.0f : 0) {
NSLog(#"Adding scroll target");
_topContentOffset = CGPointMake(scrollView.contentOffset.x, self.navigationController.navigationBar.translucent ? -64.0f : 0);
_isHandlingPan = YES;
[scrollView.panGestureRecognizer addTarget:self action:#selector(handleGesture:)];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"Did End Dragging");
if (_isHandlingPan) {
NSLog(#"Removing action");
_isHandlingPan = NO;
[scrollView.panGestureRecognizer removeTarget:self action:#selector(handleGesture:)];
}
}
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
[(NavigationController*)self.navigationController handleGesture:gestureRecognizer];
}
This still doesn't work quite right. Even during the dismissal animation, the scroll view is still scrolling with the gesture.
End EDIT1
That is a custom interactive transition.
First, you need set transitioningDelegate of UIViewController
id<UIViewControllerTransitioningDelegate> transitioningDelegate;
Then implment these two method to
//Asks your delegate for the transition animator object to use when dismissing a view controller.
- animationControllerForDismissedController:
//Asks your delegate for the interactive animator object to use when dismissing a view controller.
- interactionControllerForDismissal:
When drag to top, you start the transition, you may use UIPercentDrivenInteractiveTransition to control the progress during scrolling.
You can also refer to the source code of ZFDragableModalTransition
Image of ZFDragableModalTransition
As explained here the solution is quite complex. The person who answered, #trungduc, programmed a little demo published on github doing the sought behaviour. You can find it here.
The easiest way of making this work is to copy the 4 files found in /TestPanel/Presentation/ in the attached github repository, to your project. Then add the PanelAnimationControllerDelegate to your View Controller containing the scroll view (i.e. using the protocol).
Add the following to your View Controller, to satisfy the protocol:
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
Add this to deactivate the bouncing effect at the top:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Set scrollView.delegate = self
Before presenting your View Controller containing the scroll view set the following propreties to your View Controller:
ScrollViewController.transitioningDelegate = self.panelTransitioningDelegate
ScrollViewController.modalPresentationStyle = .custom
If you want to change the size of your ScrollViewController, you will need to comment out the override of the frameOfPresentedViewInContainerView in the PanelPresentationController file (one of the 4). Then in the presentationTransitionWillBegin method, you will need to set let frameOfPresentedViewInContainerView = self.frameOfPresentedViewInContainerView.insetBy(dx: 0, dy: 20) with the wanted inset of dx and dy.
Thank you to trungduc for this amazing solution!!
I need to add Speech Recognition to an App for a Personal Project.
I need the iOS built-in speech recognition framework because it is fast, accurate and it can also recognise your contact names and other information about yourself.
So far, I think I have found the framework which contains the headers for the speech recognition on iOS 8: the SAObjects.framework
I got the headers of Github and added them successfully in my Xcode Project.
The headers I have tried so far are these:
<SAObjects/SASRecognition.h>
<SAObjects/SASStartSpeechDictation.h>
<SAObjects/SASSpeechRecognized.h>
However, I am not sure how to work with them. For instance, these are two possible methods that can fire a Speech Recognition:
SASStartSpeechDictation *object1 = [SASStartSpeechDictation startSpeechDictation];
SASSpeechRecognized *object2 = [SASSpeechRecognized speechRecognized];
When I debug it though, I cannot find any string in any of these objects. So, obviously something is wrong. Maybe I need to set a notification observer?
Another Solution could be to start a Dictation (through the Keyboard)
to a hidden text field (without the keyboard showing).
Like the Activator action for Jailbroken devices, if you are familiar with it.
But I haven't found any methods that can start the Keyboard dictation, or the Activator action source code to find it out.
Maybe someone has experimented with these things and can give me some help?
Please tell me if you need more information about this question :)
Thanks a lot!
So, I managed to find an answer myself. I luckily found a Github repo with some helpful code: https://github.com/erica/useful-things
The code I found is under the appstore unsafe pack/DictationHelper directory. This code helps to use the UIDictationController and start and stop the Dictation, and get the text value. Of course, without any Text Fields...
Important: In order for this to work, you need to have the headers of the UIKit framework, link the framework to the Target and import them in the Project!
However, I modified the code a bit, because the sample code is only available to speak for a specific duration. I needed to stop speaking by pressing a button.
This is the modified code, for anyone who might be interested in the future:
DicationHelper.h:
/*
Erica Sadun, http://ericasadun.com
NOT APP STORE SAFE BUT HANDY
Siri-ready devices only. Will not work in simulator.
Example:
[SpeechHelper speakModalString:#"Please say something"];
[[DictationHelper sharedInstance] dictateWithDuration:5.0f completion:^(NSString *dictationString) {
if (dictationString)
NSLog(#"You said:'%#'", dictationString);
else
NSLog(#"No response");}];
//-> OR: (My modification)
[SpeechHelper speakModalString:#"Please say something"];
[[DictationHelper sharedInstance] startDictation:0 completion:^(NSString *dictationString) {
if (dictationString)
NSLog(#"You said:'%#'", dictationString);
else
NSLog(#"No response");}];
// Then you need to call this to stop the Dictation: [[DictationHelper sharedInstance] stopDictation]
*/
#import <UIKit/UIKit.h>
//#import <Foundation/Foundation.h>
extern NSString *const DictationStringResults;
typedef void (^DictationBlock)(NSString *dictationString);
#interface DictationHelper : NSObject
+ (instancetype) sharedInstance;
- (void) dictateWithDuration: (CGFloat) duration;
- (void) dictateWithDuration: (CGFloat) duration completion:(DictationBlock) completionBlock;
-(void) startDictation:(CGFloat) whatever completion:(DictationBlock) completionBlock;
-(void) stopDictationWithFallback;
#property (nonatomic, readonly) BOOL inUse;
#end
DictationHelper.m
/*
Erica Sadun, http://ericasadun.com
NOT APP STORE SAFE BUT HANDY
Siri-ready devices only. Will not work in simulator.
*/
#import "DictationHelper.h"
#define MAKELIVE(_CLASSNAME_) Class _CLASSNAME_ = NSClassFromString((NSString *)CFSTR(#_CLASSNAME_));
NSString *const DictationStringResults = #"Dictation String Results";
static DictationHelper *sharedInstance = nil;
#class UIDictationController;
#interface UIDictationController
+ (UIDictationController *) sharedInstance;
- (void) startDictation;
- (void) stopDictation;
- (void) preheatIfNecessary;
#end;
#interface DictationHelper () <UITextFieldDelegate>
#end
#implementation DictationHelper
{
UITextField *secretTextField;
id dictationController;
DictationBlock completion;
BOOL handled;
}
- (void) preheat
{
if (!secretTextField)
{
secretTextField = [[UITextField alloc] initWithFrame:CGRectZero];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[window addSubview:secretTextField];
secretTextField.inputView = [[UIView alloc] init];
secretTextField.delegate = self;
}
if (!dictationController)
{
MAKELIVE(UIDictationController);
dictationController = [UIDictationController sharedInstance];
[dictationController preheatIfNecessary];
}
}
+ (instancetype) sharedInstance
{
if (!sharedInstance)
{
sharedInstance = [[self alloc] init];
[sharedInstance preheat];
}
return sharedInstance;
}
- (BOOL) textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *tftext = textField.text;
tftext = [tftext stringByReplacingCharactersInRange:range withString:string];
[[NSNotificationCenter defaultCenter] postNotificationName:DictationStringResults object:tftext];
if (completion) completion(tftext);
// Treat this dictation as handled
handled = YES;
_inUse = NO;
completion = nil;
// Resign first responder
[textField resignFirstResponder];
return YES;
}
- (void) fallback
{
// 1. Test completion
if (!completion) return;
// 2. Check for handled
if (handled)
{
_inUse = NO;
handled = NO;
return;
}
// 3. Assume the dictation didn't work
completion(nil);
// 4. Reset everything
handled = NO;
_inUse = NO;
completion = nil;
// 5. Resign first responder
[secretTextField resignFirstResponder];
}
-(void) startDictation:(CGFloat) whatever completion:(DictationBlock) completionBlock{
if (completionBlock) completion = completionBlock;
if (_inUse)
{
NSLog(#"Error: Dictation Helper already in use");
return;
}
_inUse = YES;
handled = NO;
secretTextField.text = #"";
[secretTextField becomeFirstResponder];
[[UIDevice currentDevice] playInputClick];
[dictationController startDictation];
}
- (void) dictateWithDuration: (CGFloat) numberOfSeconds
{
if (_inUse)
{
NSLog(#"Error: Dictation Helper already in use");
return;
}
_inUse = YES;
handled = NO;
secretTextField.text = #"";
[secretTextField becomeFirstResponder];
[[UIDevice currentDevice] playInputClick];
[dictationController startDictation];
[self performSelector:#selector(stopDictation) withObject:nil afterDelay:numberOfSeconds];
[self performSelector:#selector(fallback) withObject:nil afterDelay:numberOfSeconds + 1.0f];
}
- (void) dictateWithDuration: (CGFloat) duration completion:(DictationBlock) completionBlock
{
if (completionBlock) completion = completionBlock;
[self dictateWithDuration:duration];
}
- (void) stopDictation
{
[dictationController stopDictation];
}
- (void) stopDictationWithFallback
{
[self performSelector:#selector(stopDictation) withObject:nil afterDelay:0.0];
[self performSelector:#selector(fallback) withObject:nil afterDelay:1.0f];
}
#end
#undef MAKELIVE
My task is to create custom UIAlertView with multiline text input field and everything works pretty much well up to the point where I need to do some actions if dismiss button was tapped. As example I'm using: http://iphonedevelopment.blogspot.co.uk/2010/05/custom-alert-views.html
I have created very simple popup for testing purpose. Basically it's UIViewController xib with single button in it. The main class is demonstrated below:
#import "testViewController.h"
#interface testViewController ()
#end
#implementation testViewController
- (id)init
{
self = [super initWithNibName:#"testViewController" bundle:nil];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)showInView:(UIView *)view
{
[view addSubview:[self view]];
[[self view] setFrame: [view bounds]];
[[self view] setCenter:[view center]];
}
- (IBAction)onTap:(id)sender {
NSLog(#"All OK");
}
#end
then in root UIViewController I'm calling my custom alert:
- (IBAction)showAlert:(id)sender
{
dispatch_async(dispatch_get_main_queue(), ^{
testViewController *alert = [[testViewController alloc] init];
[alert showInView:[self view]];
});
}
So far I have tried Main thread or global queue or even synchronous dispatch, but everything ends up with break on:
int main(int argc, char * argv[]) {
#autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); <-- Thread 1:EXC_BAD_ACCESS (code=1, address=0xa000000c)
}
}
tried to add observer for the button but still no go..
Any help is much appreciated
Here what happing is "if you want to add one viewcontroller view to other viewcontroller's view then after adding of views you should convey to the compiler that second view controller is going to be child to the first"
i.e pre intimation of parent to child relationship has to be done.
now just modify your showAlert method with the following . it is working 100%.
- (IBAction)showAlert:(id)sender {
dispatch_async(dispatch_get_main_queue(), ^{
testViewController *alert = [[testViewController alloc] init];
[alert showInView:[self view]];
[self addChildViewController:alert];
[alert didMoveToParentViewController:self];
});
}
Check if your UIButton is assigned multiple IBoutlets or multiple IBActions. This a common issue which happens without our notice sometimes.
It turns out that my testViewController was released by ARC before I tap the button
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.
I have an app that uses a GoogleMap view.
Into that app, I want to display some custom anotations, and a custom view for the userLocation. When the location manager gives a heading, I want to display that custom view for userLocation, if it fails to deliver one, I want the default blue animated dot to come back, etc...
Project and Source code available HERE
Well, there are 2 problems here :
1st problem, THE REALLY BAD ONE. You can play for hours with that tiny app. But... Launch it, wait it loads, send it to the background, put your iPhone in sleep mode, wait a minute, wake up your iPhone, launch the app (that wakes it up) : crash, EXC_BAD_ACCESS. Do the same thing not putting the iPhone in sleep mode : no crash. Not waiting for a minute but a few seconds, no crash. 10 seconds, no crash, 20 seconds, no crash, nears 30 seconds, perhaps a crash, near a minute, crash !
2nd problem (bonus :-) ), that can be linked with the first one, but that is not really the heart of this question : the solution I use to achieve the userPinLocation to update from/to the blue pin seems to have a really bad side effect : the userLocation does not move on the map as you move in the street. For any answer for that 2nd problem, if it's not directly linked with the crash, please use comments not answers. This is not the heart of the question... I think...
To find the faulty code, I've reduced my app at its minimum. That gives the following code (some tips in the source code as comments) :
EXC_BAD_ACCESS_TestViewController.h
#import <MapKit/MapKit.h>
#interface EXC_BAD_ACCESS_TestViewController : UIViewController<CLLocationManagerDelegate> {
CLLocationManager* locationMgr;
NSMutableArray* customAnnotations;
CLLocationDirection userHeading;
MKMapView* myMapView;
}
#property(nonatomic, retain) CLLocationManager* locationMgr;
#property(nonatomic, retain) NSMutableArray* customAnnotations;
#property(nonatomic, assign) CLLocationDirection userHeading;
#property(nonatomic, retain) IBOutlet MKMapView* myMapView;
- (void) loadAnnotations;
#end
EXC_BAD_ACCESS_TestViewController.m
// On first launch, due to code simplification, the app may crash when asked authorization to use geo hardware. Just kill/relaunch after accepting.
// Breakpoints on each method entry : EXC_BAD_ACCESS crash without any break-stop
#import "EXC_BAD_ACCESS_TestViewController.h"
#import "CustomAnnotation.h"
#implementation EXC_BAD_ACCESS_TestViewController
#synthesize locationMgr, customAnnotations, myMapView, userHeading;
// ===========================================================================================================
-(id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self) return nil;
self.userHeading = -1;
return self;
}
// ===========================================================================================================
- (void) viewDidLoad
{
[self loadAnnotations];
self.myMapView.mapType = MKMapTypeStandard;
self.myMapView.showsUserLocation = YES;
// -----------------------------------------------
// Just comment this sole line : no more crash
// -----------------------------------------------
for (CustomAnnotation* pin in self.customAnnotations) [self.myMapView addAnnotation:pin];
// locationManager
self.locationMgr = [[CLLocationManager alloc] init];
self.locationMgr.desiredAccuracy = kCLLocationAccuracyBest;
self.locationMgr.distanceFilter = 1.0;
self.locationMgr.headingFilter = 1.0;
self.locationMgr.purpose = #"Some purpose.";
self.locationMgr.delegate = self;
[self.locationMgr startUpdatingHeading];
[super viewDidLoad];
}
// ===========================================================================================================
- (void) loadAnnotations
{
// Code for testing, real code gets the datas using another way of doing, as simple as this one
self.customAnnotations = [NSMutableArray array];
double latitude = 45.0;
double longitude = -45.0;
for (int i=1; i<=400; i++) {
CLLocationCoordinate2D pinLocation = CLLocationCoordinate2DMake(latitude, longitude);
CustomAnnotation* pin = [[[CustomAnnotation alloc] initWithCoordinate:pinLocation] autorelease];
[self.customAnnotations addObject:pin];
latitude += 0.01;
longitude += 0.01;
}
}
// ===========================================================================================================
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>) annotation
{
MKAnnotationView* pinView = nil;
NSString* annotationIdentifier;
UIImage* pinImage;
BOOL canShowCallout;
// User location
if ([annotation isKindOfClass:[MKUserLocation class]] && self.userHeading >= 0) {
annotationIdentifier = #"UserLocationAnnotation";
pinImage = [UIImage imageNamed:#"custom_userlocation.png"];
canShowCallout = NO;
}
// Custom Pin
else if ([annotation isKindOfClass:[CustomAnnotation class]]) {
annotationIdentifier = #"CustomAnnotation";
pinImage = [UIImage imageNamed:#"custom_pin.png"];
canShowCallout = YES;
}
// Others
else {
return nil;
}
pinView = (MKAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (pinView) {
pinView.annotation = annotation;
}
else {
pinView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier] autorelease];
if (pinView) {
pinView.image = pinImage;
pinView.canShowCallout = canShowCallout;
if (canShowCallout) {
pinView.calloutOffset = CGPointMake(-5, 5);
pinView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
}
}
return pinView;
}
// -----------------------------------------------
// Just comment this method : no more crash
// -----------------------------------------------
// ===========================================================================================================
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
if (!self.myMapView) return;
if (!self.myMapView.userLocation) return;
CLLocationDirection compassHeading_True = newHeading.trueHeading;
CLLocationDirection compassHeading_Magnetic = newHeading.magneticHeading;
CLLocationDirection heading;
if (compassHeading_True == -1) heading = compassHeading_Magnetic;
else if (newHeading.headingAccuracy >= 0) heading = compassHeading_True;
else heading = -1;
// If we get/loose the heading, update pin image
// I didn't found any other solution to force the user pin to update its display.
if ((self.userHeading == -1 || heading == -1) && self.userHeading != heading) {
[self.myMapView removeAnnotation:self.myMapView.userLocation];
self.userHeading = heading;
[self.myMapView addAnnotation:self.myMapView.userLocation];
}
self.userHeading = heading;
// ------------------------------------
// Some non bugued code was there
// ------------------------------------
}
// ===========================================================================================================
- (void) dealloc
{
self.myMapView = nil;
self.customAnnotations = nil;
self.locationMgr = nil;
[super dealloc];
}
#end
CustomAnnotation.h
#import <MapKit/MapKit.h>
#interface CustomAnnotation : NSObject<MKAnnotation> {
CLLocationCoordinate2D coordinate;
}
#property(nonatomic, assign) CLLocationCoordinate2D coordinate;
- (id)initWithCoordinate:(CLLocationCoordinate2D) c;
#end
CustomAnnotation.m
#import "CustomAnnotation.h"
#implementation CustomAnnotation
#synthesize coordinate;
// ===========================================================================================================
- (id)initWithCoordinate:(CLLocationCoordinate2D) c
{
self = [super init];
if (!self) return nil;
self.coordinate = c;
return self;
}
// ===========================================================================================================
- (NSString*)subtitle
{
return #"";
}
// ===========================================================================================================
- (NSString*)title
{
return #"";
}
#end
I tried breakpoints in each methods, trying to enable Zombies with environment variables (but as this needs to be ran on the device, I'm not sure they've been really activated), without any start of solution... I still don't have any idea of what's going wrong.
Do you see how to solve that EXC_BAD_ACCESS problem ?
For any answer for the 2nd problem, please use comments and not answers if it don't solve the crash. This problem is not the heart of the question as far as I've seen.
Tip : I've put 2 comments in the code above where I found some start of solution. There are 2 places, that does not seems connected, that can be put out of the code one OR the other to solve the problem. What makes me crazy is the OR.
Runned on an iPhone 4 ios 4.2.1
[self.myMapView removeAnnotation:self.myMapView.userLocation];
This line is causing the crash from stacktrace this is what I've seen.
When I saw exc_bad_access I did a bt in gdb. I have added Guard Malloc breakpoint. Looking at the stack it said about removing annotation.
You can try overriding a method called didReceiveMemoryWarning in your view controller .m file.
What happens sometimes is that memory on the device gets low due to running apps and IOS sends a message to the app. Now the trouble is when you don't override memory warning method, its default action is to remove any subviews from memory which are not visible. This can lead to zombie variables when your application runs.
Check out the apple doc