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
Related
Summary:
I subclassed
GMSSyncTileLayer
and overwrote
tileForX:y:zoom:
but its only called once no matter how much I pan.
why?
DETAIL
We have implemented our own TileServer which is behind a secure webservice so we need to pass a login token to get the tiles.
The call is asynchronous POST which is quiet common for a webservice call.
Because I had to pass login token in the NSURLSession header I couldnt just pass GET urls to
GMSTileURLConstructor urls = http://<tileserver>/gettile?x=3&y=4&zoom=5
So I subclassed GMSSyncTileLayer
#interface SNSyncTileLayer : GMSSyncTileLayer
overwrote
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
When tileForX:y:Zoom: is called the first time I call a webservice to get the tile UIImage.
The UIImage returns on a delegate and is stored in a NSDictionary with key in format TILE_x_y_Zoom.
The call to the WS is asynch so - (UIImage *)tileForX:y:Zoom: always returns nil for that tile the first time its called.
What i've noticed is that tileForX:y:Zoom: is never called again no matter how much I pan back and forth.
For instance at the current zoom I pan across europe.
I see tileForX:y:Zoom: being called once and ws calls being mad and images being stored in my dictionary.
But if i keep panning at the same zoom I come back to Europe and tileForX:y:Zoom: is not called again.
Fix one - clear cache when new tile downloaded
I tried creating a delegate on SNSyncTileLayer and everytime a new tile downloaded it called:
[self.snSyncTileLayer clearTileCache];
But this wipes out ALL tiles and reloads them so as you pan you get a terrible flashing.
My only idea next is to measure how much the map has panned and if its more than half a width or height then to call clearTileCache.
SO the big question why isnt tileForX:y:Zoom: called everytime?
My overridden class
//
// SNSyncTileLayer.m
//
#import "SNSyncTileLayer.h"
#import "SNAppDelegate.h"
#import "GoogleTileRequest.h"
#import "SNGoogleTileRequest.h"
#import "GoogleTileImageResult.h"
#interface SNSyncTileLayer()<SNSeaNetWebServicesManagerDelegate>{
BOOL _debugOn;
}
#property (nonatomic, retain) NSMutableDictionary * tileImageCacheDict;
#end
#implementation SNSyncTileLayer
- (instancetype)init
{
self = [super init];
if (self) {
_tileImageCacheDict = [NSMutableDictionary dictionary];
_debugOn = TRUE;
}
return self;
}
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
if(_debugOn)ImportantLog(#"tileForX:(%lu) y:(%lu) zoom:(%lu)", (unsigned long)x,(unsigned long)y,(unsigned long)zoom);
UIImage *tileImage_ = nil;
//tileImage_ = [UIImage imageNamed:#"EmptyTile1.png"];
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%lu_%lu_%lu", (unsigned long)x,(unsigned long)y,(unsigned long)zoom];
id dictObj_ = [self.tileImageCacheDict objectForKey:keyForTile_];
if (dictObj_) {
if([dictObj_ isMemberOfClass:[NSNull class]])
{
if(_debugOn)DebugLog(#"tile has been called before but image not downloaded yet:[%#]",keyForTile_);
}
else if([dictObj_ isMemberOfClass:[UIImage class]])
{
if(_debugOn)DebugLog(#"cached image found in dict_ return it:[%#]",keyForTile_);
tileImage_ = (UIImage *)dictObj_;
}
else{
ErrorLog(#"ITEM IN self.tileImageCacheDict not NSNull or UIImage:[%#]", dictObj_);
}
}else{
if(_debugOn)ImportantLog(#"tileForX: CACHED IMAGE NOT FOUND: DOWNLOAD IT[%#]",keyForTile_);
//-----------------------------------------------------------------------------------
//add in temp object - tyring to check if tileForX:Y:Zoom is called more than once
[self.tileImageCacheDict setObject:[NSNull null] forKey:keyForTile_];
//-----------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------
SNAppDelegate *appDelegate = (SNAppDelegate *)[[UIApplication sharedApplication] delegate];
GoogleTileRequest * googleTileRequest_ = [[GoogleTileRequest alloc]init];
googleTileRequest_.X = [NSNumber numberWithInteger:x];
googleTileRequest_.Y = [NSNumber numberWithInteger:y];
googleTileRequest_.Zoom = [NSNumber numberWithInteger:zoom];
#pragma mark TODO - NOW - thur11dec - load from settings
googleTileRequest_.MapType = #"Dark";
//for general errors
appDelegate.snSeaNetWebServicesManager.delegate = self;
//Request should know what class to return too
googleTileRequest_.delegateForRequest = self;
[appDelegate.snSeaNetWebServicesManager ITileController_GoogleTile:googleTileRequest_];
//-----------------------------------------------------------------------------------
return kGMSTileLayerNoTile;
//-----------------------------------------------------------------------------------
}
return tileImage_;
}
#pragma mark -
#pragma mark SNSeaNetWebServicesManagerDelegate
#pragma mark -
-(void) snSeaNetWebServicesManager:(SNSeaNetWebServicesManager *)SNSeaNetWebServicesManager
wsReponseReceivedForRequest:(SNWebServiceRequest *)snWebServiceRequest_
error:(NSError *)error
{
#pragma mark TODO - NOW - thur11dec2014
if(error){
ErrorLog(#"error:%#",error);
}else{
if(snWebServiceRequest_){
if([snWebServiceRequest_ isMemberOfClass:[SNGoogleTileRequest class]])
{
//Result is JSONModel ivar in Request
if(snWebServiceRequest_.resultObject){
GoogleTileImageResult * googleTileImageResult_= (GoogleTileImageResult *)snWebServiceRequest_.resultObject;
UIImage * responseImage_ = googleTileImageResult_.responseImage;
if(responseImage_){
//-----------------------------------------------------------------------------------
//build the key from the parameters
if(snWebServiceRequest_.bodyJsonModel){
NSDictionary *paramsDict = [snWebServiceRequest_.bodyJsonModel toDictionary];
if(paramsDict){
NSString *keyX_ = [paramsDict objectForKey:#"X"];
NSString *keyY_ = [paramsDict objectForKey:#"Y"];
NSString *keyZoom_ = [paramsDict objectForKey:#"Zoom"];
if(keyX_){
if(keyY_){
if(keyZoom_){
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%#_%#_%#", keyX_,keyY_,keyZoom_];
if(_debugOn)ImportantLog(#"TILE DOWNLOADED ADD TO CACHE[%#]",keyForTile_);
[self.tileImageCacheDict setObject:responseImage_ forKey:keyForTile_];
//if(_debugOn)DebugLog(#"[[self.tileImageCacheDict allKeys]count]:%lu", (unsigned long)[[self.tileImageCacheDict allKeys]count]);
//-----------------------------------------------------------------------------------
//I ADDED THIS SO delegate could clearTileCache but causes flashing as ALL tiles get reloaded visible ones and ones downloaded but not on map
if(self.delegate){
if([self.delegate respondsToSelector:#selector(snSyncTileLayer:tileDownloadedForX:Y:Zoom:)]){
[self.delegate snSyncTileLayer:self
tileDownloadedForX:keyX_
Y:keyY_
Zoom:keyZoom_
];
}else {
ErrorLog(#"<%# %#:(%d)> %s delegate[%#] doesnt implement snSyncTileLayer:tileDownloadedForX:Y:Zoom:", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__ ,self.delegate);
}
}else{
ErrorLog(#"<%# %#:(%d)> %s self.delegate is nil", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__);
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"keyZoom_ is nil");
}
}else{
ErrorLog(#"keyY_ is nil");
}
}else{
ErrorLog(#"keyX_ is nil");
}
}else{
ErrorLog(#"paramsDict is nil");
}
}else{
ErrorLog(#"self.downloadingTasksDictionary is nil");
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"responseImage_ is nil");
}
}else{
ErrorLog(#"snWebServiceRequest_.resultJsonModel is nil");
}
}
else {
ErrorLog(#"UNHANDLED snWebServiceRequest_:%#", snWebServiceRequest_.class);
}
}else{
ErrorLog(#"snWebServiceRequest_ is nil");
}
}
}
#end
I haven't done this so I'm not sure, but my guess from reading the documentation is that you should be subclassing GMSTileLayer instead of GMSSyncTileLayer.
GMSSyncTileLayer is designed for cases where you are able to synchronously (ie immediately) return the tile for that location. By returning kGMSTileLayerNoTile, you are specifically indicating that 'there is no tile here', and so it never calls your class again for that location, as you've already responded that there is no tile there. (BTW, your description says you are returning nil, which indicates a transient error, but your code is actually returning kGMSTileLayerNoTile).
The GMSTileLayer class is designed for the asynchronous approach that you're using. If you subclass GMSTileLayer, your requestTileForX:y:zoom:receiver: method should start the background process to fetch the tile. When the tile request succeeds, then it is passed off to the GMSTileReceiver that was provided in that method (you should keep a copy of that receiver along with your request).
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!
It's been asked a dozen times but this error is a result of several different causes and I have no idea if my problem is relevant to any other causes.
I get these errors in console:
2014-03-17 16:15:41.190 [31659:70b] setting to solo ambient
2014-03-17 16:15:42.606 [31659:70b] backing dimensions: (640,960)
2014-03-17 16:15:42.835 [31659:70b] no other audio playing
2014-03-17 16:15:42.836 [31659:70b] no audio player..
2014-03-17 16:15:42.854 [31659:70b] Application windows are expected to have a root view controller at the end of application launch
(lldb)
I get Thread 1: breakpoint 1.1 message in this code:
#import <SemiSecret/SemiSecret.h>
#implementation SemiSecretFont
- (NSString *) description
{
return [NSString stringWithFormat:#"<SemiSecretFont: name:%#, size:%.1f>", NSStringFromClass([self class]), size];
}
+ (SemiSecretFont *)fontWithName:(NSString *)name
size:(CGFloat) size;
{
//dynamically search for a class with this name
**Class klass = NSClassFromString([NSString stringWithFormat:#"%#Font", name]); //error is on this line**
//NSLog(#"looking for font: %#", name);
// NSLog(#"klass: %#", klass);
SemiSecretFont * font = nil;
if (klass)
// font = [[[klass alloc] initWithSize:size] autorelease];
font = [[(SemiSecretFont *)[klass alloc] initWithSize:size] autorelease];
return font;
}
- (id) fontWithSize:(CGFloat)s
{
Class klass = [self class];
SemiSecretFont * f = nil;
//f = [[[klass alloc] initWithSize:s] autorelease];
f = [[(SemiSecretFont *)[klass alloc] initWithSize:size] autorelease];
return f;
}
//this is not meant to be instantiated directly!
- (id) initWithSize:(CGFloat)fontsize
{
if ((self = [super init])) {
size = fontsize;
font = nil;
}
return self;
}
In AppDelegate, I have:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//in canabalt, we never want linear filtering (not even on ipad)
[SemiSecretTexture setTextureFilteringMode:SSTextureFilteringNearest];
[application setStatusBarOrientation:UIInterfaceOrientationLandscapeRight
animated:NO];
game = [[Canabalt alloc] init];
//preload textures here, now that opengl stuff should be created
preloadTextureAtlases();
return YES;
}
- (void) applicationDidEnterBackground:(UIApplication *)application
{
[FlxG didEnterBackground];
}
- (void) applicationWillEnterForeground:(UIApplication *)application
{
[FlxG willEnterForeground];
}
- (void) applicationWillResignActive:(UIApplication *)application
{
[FlxG willResignActive];
}
- (void) applicationDidBecomeActive:(UIApplication *)application
{
[FlxG didBecomeActive];
}
- (void) applicationWillTerminate:(UIApplication *)application
{
}
- (void) dealloc
{
[game release];
[super dealloc];
}
#end
I am also trying to figure out how to make it so that audio player will be ignored. I want the game to be played normally.
In your case it is fairly simple.
In the application: didFinishLaunchingWithOptions:, the rootViewController property of the window is to be set; which does not seem to be happening in your code.
If you are doing it in storyboard / xib then you are not doing it right, as stated by the error Application windows are expected to have a root view controller at the end of application launch
You can refer the Apple Docs for more info on window and its rootViewController. To be precise, scroll down to the end of that page.
I am using Google AdMobs DFP to serve up mediation banners from other networks. I have added Millennial and InMobi fine but now I need to add a network (YOC group) that does not have an adapter for DFP so I need to implement 'custom event banners'. I have read the guides and technical documents on implementing unsupported mediator networks with custom events but still cannot understand how it hooks up the two SDKs (Google AdMobs SDK and the mediator network's SDK).
The network that doesn't have an adapter (YOC) works if I hard-code the yoc ad id (of the format '9A9A9AA99999999A999AA9A99AA99AAAA999A9AA') to a sample ad and send off the request. The banner comes back fine and uses the YOC SDK to show an interactive/rich media advert.
However in my app I only have a Google DFP account id (of the format '/9999/company.app/channel') which I send off a request for using Google AdMobs SDK to DFP. The request then returns a response with the specific mediator ad network to try and request a banner advert from. My current setup is that YOC serves 100% of ads in DFP.
Problem: I get a banner advert returned from the YOC ad network and it displays on screen. It registers page impressions (with DFP) but there is no response to a touch/press event the way it works as if I hard-code the initialisation parameters of the yoc ad view. I can't however hard-code the yoc ad id (when initialising) because it would only work for one banner advert and I need different banners for each specific advert in each channel.
Below is the sample code I am trying to implement using just NSLogs in the methods to log to the console and show that the methods are being called. It is a very basic app and puts all the code in one place for ease of reading.
AppDelegate.h
#import < UIKit/UIKit.h>
#import "GADBannerView.h"
#import "GADBannerViewDelegate.h"
#import "GADCustomEventBanner.h"
#import "GADCustomEventBannerDelegate.h"
#import < YOCAdSDK/YOCAdView.h>
#interface AppDelegate : UIResponder <UIApplicationDelegate, GADBannerViewDelegate, GADCustomEventBanner, GADCustomEventBannerDelegate, YOCAdViewDelegate>
#property (strong, nonatomic) UIWindow *window;
#property (strong, nonatomic) UIViewController *root;
#end
AppDelegate.m
#import "AppDelegate.h"
#import "GADBannerView.h"
#import <YOCAdSDK/YOCAdSize.h>
#implementation AppDelegate
#synthesize root;
#synthesize delegate; // GADCustomEventBannerDelegate set on GADCustomEventBanner
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CGRect bounds = [[UIScreen mainScreen] bounds];
self.window = [[UIWindow alloc] initWithFrame:bounds];
self.window.backgroundColor = [UIColor greenColor];
GADBannerView *banner = [[GADBannerView alloc] initWithAdSize:kGADAdSizeLeaderboard origin:CGPointMake(0, 0)];
self.root = [[UIViewController alloc] initWithNibName:nil bundle:nil];
UIView *base = [[UIView alloc] initWithFrame:bounds];
base.backgroundColor = [UIColor greenColor];
self.root.view = base;
// the adUnitID is always of our DFP account number of the format '/9999/company.app/aa_aa<channelName>_<channelName>app'
banner.adUnitID = #"/9999/company.app/channel_specific_id";
banner.delegate = self;
banner.rootViewController = self.root;
self.delegate = self;
[base addSubview:banner];
[base bringSubviewToFront:banner];
[banner loadRequest:[GADRequest request]];
[self.window setRootViewController:self.root];
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
[self.window makeKeyAndVisible];
return YES;
}
#pragma mark GADBannerViewDelegate
- (void)adViewDidReceiveAd:(GADBannerView *)view {
NSLog(#" adViewDidReceiveAd ");
NSLog(#" view: %# ", [view description]);
// for other ad networks here we get view.mediatedAdView = IMAdView (InMobi) or view.mediatedAdView = MMAdView (Millennial) but with YOC view.mediatedAdView = nil;
[self.delegate customEventBanner:self didReceiveAd:view];
}
- (void)adView:(GADBannerView *)view didFailToReceiveAdWithError:(GADRequestError *)error {
NSLog(#" didFailToReceiveAdWithError ");
[self.delegate customEventBanner:self didFailAd:error];
}
- (void)adViewWillPresentScreen:(GADBannerView *)adView {
NSLog(#" adViewWillPresentScreen ");
[self.delegate customEventBanner:self clickDidOccurInAd:adView];
[self.delegate customEventBannerWillPresentModal:self];
}
- (void)adViewWillDismissScreen:(GADBannerView *)adView {
NSLog(#" adViewWillDismissScreen ");
[self.delegate customEventBannerWillDismissModal:self];
}
- (void)adViewDidDismissScreen:(GADBannerView *)adView {
NSLog(#" adViewDidDismissScreen ");
[self.delegate customEventBannerDidDismissModal:self];
}
- (void)adViewWillLeaveApplication:(GADBannerView *)adView {
NSLog(#" adViewWillLeaveApplication ");
[self.delegate customEventBannerWillLeaveApplication:self];
}
#pragma mark GADCustomEventBanner
- (void)requestBannerAd:(GADAdSize)adSize
parameter:(NSString *)serverParameter
label:(NSString *)serverLabel
request:(GADCustomEventRequest *)request {
NSLog(#" requestBannerAd ");
// not sure if we initialiase the YOC tag here or how we would do this if can't hard code the yocTag to the format '9A9A9AA99999999A999AA9A99AA99AAAA999A9AA'
// and we only have the banner view returned from DFP with the id '/9999/company.app/channel_specific_id)'
YOCAdView *yocAdView = [[YOCAdView alloc] initWithYOCTag:serverParameter delegate:self size:kLeaderboard728x90 position:CGPointMake(0, 0)];
yocAdView.delegate = self;
[yocAdView load];
[self.root.view addSubview:yocAdView];
}
#pragma mark GADCustomEventBannerDelegate
- (void)customEventBanner:(id<GADCustomEventBanner>)customEvent didReceiveAd:(UIView *)view {
NSLog(#" [(GADBannerView *)view adUnitID]: %# ", [(GADBannerView *)view adUnitID]);
NSLog(#" [(GADBannerView *)view delegate]: %# ", [(GADBannerView *)view delegate]);
NSLog(#" [(id<YOCAdViewDelegate>)[customEvent delegate] viewControllerForPresentingYOCAdView]: %# ", [(id<YOCAdViewDelegate>)[customEvent delegate] viewControllerForPresentingYOCAdView]);
// not sure if we initialiase the YOC tag here or how we would do this if can't hard code the yocTag to '9A9A9AA99999999A999AA9A99AA99AAAA999A9AA"
// and we only have the banner view returned from DFP with the id '/9999/company.app/channel_specific_id'
[customEvent requestBannerAd:kGADAdSizeLeaderboard parameter:#"???" label:nil request:nil];
// the key might be that the [customEvent delegate] is of type YOCAdViewDelegate and we can do code specific to YOC here...
// but again not sure how to initialize the YOCAdView because we already have the banner view returned from DFP (with a different format account id)
// [(id<YOCAdViewDelegate>)[customEvent delegate] yocAdViewDidInitialize:yocAdView];
}
- (void)customEventBanner:(id<GADCustomEventBanner>)customEvent didFailAd:(NSError *)error {
NSLog(#" customEventBanner:didFailAd ");
}
- (void)customEventBanner:(id<GADCustomEventBanner>)customEvent clickDidOccurInAd:(UIView *)view {
NSLog(#" customEventBanner:clickDidOccurInAd ");
}
- (void)customEventBannerWillPresentModal:(id<GADCustomEventBanner>)customEvent {
NSLog(#" customEventBannerWillPresentModal ");
}
- (void)customEventBannerWillDismissModal:(id<GADCustomEventBanner>)customEvent {
NSLog(#" customEventBannerWillDismissModal ");
}
- (void)customEventBannerDidDismissModal:(id<GADCustomEventBanner>)customEvent {
NSLog(#" customEventBannerDidDismissModal ");
}
- (void)customEventBannerWillLeaveApplication:(id<GADCustomEventBanner>)customEvent {
NSLog(#" customEventBannerWillLeaveApplication ");
}
#pragma mark YOCAdViewDelegate
- (UIViewController *)viewControllerForPresentingYOCAdView {
NSLog(#" viewControllerForPresentingYOCAdView ");
return self.root;
}
- (void)yocAdViewDidInitialize:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidInitialize ");
}
- (void)yocAdView:(YOCAdView *)yocAdView didFailWithError:(NSError *)error {
NSLog(#" didFailWithError: %# ", error);
}
- (void)yocAdViewDidHide:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidHide ");
}
- (void)yocAdViewDidReload:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidReload ");
}
- (void)yocAdViewWillPresentModalViewController:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewWillPresentModalViewController ");
}
- (void)yocAdViewWillDismissModalViewController:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewWillDismissModalViewController ");
}
#end
Please can someone help give me things to try out and find out how to get the advert banner view returned from Google DFP responding to click events!?
There is a guide here on how to develop custom events.
If you're using AdMob SDK Mediation with ad unit /9999/company.app/channel, then this ad unit should be an SDK Mediation creative within DFP. One of your networks should be a Custom Event with the following settings:
Parameter: 9A9A9AA99999999A999AA9A99AA99AAAA999A9AA (or whatever your YOC tag is)
Label: YOC Group (This is just a label so you remember what this custom event is for)
Class Name: AppDelegate (You should really implement the custom event in it's own
class, and replace the class name with that class)
Then when implementing the custom event, you can reference serverParameter as the YOCTag like you already have.
You don't want to implement GADCustomEventBannerDelegate yourself. By your class implementing GADCustomEventBanner, and adding:
#synthesize delegate
at the top of your implementation, you have access to an instance of this object via:
self.delegate
You'll want to use that delegate to tell the custom event (and thereby AdMob Mediation) that YOC returned or failed to return an ad. Based on the YOC delegate, your mapping might look something like this:
- (UIViewController *)viewControllerForPresentingYOCAdView {
NSLog(#" viewControllerForPresentingYOCAdView ");
return self.root;
}
- (void)yocAdViewDidInitialize:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidInitialize ");
// Assuming this means the yocAdView was received.
[self.delegate customEventBanner:self didReceiveAd:yocAdView];
}
- (void)yocAdView:(YOCAdView *)yocAdView didFailWithError:(NSError *)error {
NSLog(#" didFailWithError: %# ", error);
[self.delegate customEventBanner:self didFailAd:error];
}
- (void)yocAdViewDidHide:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidHide ");
}
- (void)yocAdViewDidReload:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewDidReload ");
[self.delegate customEventBanner:self didReceiveAd:yocAdView];
}
- (void)yocAdViewWillPresentModalViewController:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewWillPresentModalViewController ");
[self.delegate customEventBanner:self clickDidOccurInAd:yocAdView];
[self.delegate customEventBannerWillPresentModal:self];
[self.delegate customEventBannerWillLeaveApplication:self];
}
- (void)yocAdViewWillDismissModalViewController:(YOCAdView *)yocAdView {
NSLog(#" yocAdViewWillDismissModalViewController ");
[self.delegate customEventBannerWillDismissModal:self];
}
Finally, you don't want to invoke GADCustomEventBannerDelegate methods in your GADBannerViewDelegate callback methods. These are invoked by AdMob telling you that mediation came back with an ad. The GADBannerViewDelegate implementation is part of your main app, and should stay out of your custom event class's implementation.
I know the guide invokes the custom event delegate methods in its GADBannerViewDelegate implementation. The difference is the guide is writing a custom event to implement AdMob, so in the context of the guide, the GADBannerViewDelegate is behaving like the YOCAdViewDelegate in your example.
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.