I'm using a IBAction button to turn a map layer on. This code turns it on when the button is tapped.
- (IBAction)lightingLayer:(id)sender {
[_mapView addTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
}
Now I'd like to adjust it so when the user taps it once, the layers turns on. And when it's tapped again, it turns on and so forth. I took a stab at it by borrowing code from a similar example but it doesn't work.
- (IBAction)lightingLayer:(id)sender {
_Bool *isON = NULL;
isON = !isON;
if(isON) {
[_mapView addTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
} else {
[_mapView removeTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
}
This flags, incompatible integer to pointer conversion assigning bool from int. Can someone provide some simple code to help me achieve my goal. Thanks in advance for your time.
This error it's because you are assigning a bool value to a pointer. A pointer is nothing but a integer value, which holds a memory position as a hexadecimal number.
But actually, to accomplish what you want, you don't need a pointer, just use a property to store this bool and create a toggle funcionality.
declare this private property:
#property (strong, assign) BOOL isChecked;
And in your action:
- (IBAction)lightingLayer:(id)sender {
self.isChecked = !self.isChecked;
if(self.isChecked) {
[_mapView addTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
} else {
[_mapView removeTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
}
}
Ps: I only focused here in explain the error you are getting now. This add/remove tile logic is probably wrong too. I think you still would have to save the same reference to be added and later removed.
Do this way
BOOL isON;
- (IBAction)lightingLayer:(id)sender {
if(isON) {
[_mapView addTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
isON=NO;
} else {
[_mapView removeTileSource:[[RMMapBoxSource alloc] initWithMapID:#"MapID"]];
isON=YES;
}
This is what I went with. It's a slight adjustment from Lucas' answer. This will alternate turning the map on an off. Thanks for the responses.
//.h
#property BOOL *isChecked;
//.m
self.isChecked = !self.isChecked;
if((self.isChecked)) {
[_mapView addTileSource:onlineSource atIndex:1];
} else {
[_mapView setHidden:YES forTileSourceAtIndex:1 ];
Related
When I show an alert with UIAlertController, the alert itself presented in a new window. (for now at least) And when the alert window dismisses, system seems to set a random window key-window.
I am presenting a new "banner" window to render some banners over status-bar (AppStore compatibility is out of topic here), and usually, this "banner" window becomes next key window, and causes many problems on user input and first responder management.
So, I want to prevent this "banner" window to become a key window, but I cannot figure out how. For now, as a workaround, I am just re-setting my main window to be a key window again as soon as that "banner" window becomes key window. But it doesn't feel really good.
How can I prevent a window to become a key window?
As a workaround, we can set main window key again as soon as the "banner" window becomes a key like this.
class BannerWindow: UIWindow {
weak var mainWindow: UIWindow?
override func becomeKeyWindow() {
super.becomeKeyWindow()
mainWindow?.makeKeyWindow()
}
}
Faced with this too. It seems that it's enough to just make:
class BannerWindow: UIWindow {
override func makeKey() {
// Do nothing
}
}
This way you don't need to keep a reference to a previous keyWindow, which is especially cool if it might get changed.
For Objective-C it's:
#implementation BannerWindow
- (void)makeKeyWindow {
// Do nothing
}
#end
I've been trying to solve this problem for years. I finally reported a Radar for it: http://www.openradar.me/30064691
My workaround looks something like this:
// a UIWindow subclass that I use for my overlay windows
#implementation GFStatusLevelWindow
...
#pragma mark - Never become key
// http://www.openradar.me/30064691
// these don't actually help
- (BOOL)canBecomeFirstResponder
{
return NO;
}
- (BOOL)becomeFirstResponder
{
return NO;
}
- (void)becomeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"become key window");
[[self class] findAndSetSuitableKeyWindow];
}
- (void)makeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"make key window");
}
- (void)makeKeyAndVisible
{
LookbackStatusWindowBecameKey(self, #"make key and visible window");
}
#pragma mark - Private API overrides for status bar appearance
// http://www.openradar.me/15573442
- (BOOL)_canAffectStatusBarAppearance
{
return NO;
}
#pragma mark - Finding better key windows
static BOOL IsAllowedKeyWindow(UIWindow *window)
{
NSString *className = [[window class] description];
if([className isEqual:#"_GFRecordingIndicatorWindow"])
return NO;
if([className isEqual:#"UIRemoteKeyboardWindow"])
return NO;
if([window isKindOfClass:[GFStatusLevelWindow class]])
return NO;
return YES;
}
void LookbackStatusWindowBecameKey(GFStatusLevelWindow *self, NSString *where)
{
GFLog(GFError, #"This window should never be key window!! %# when in %#", self, where);
GFLog(GFError, #"To developer of %#: This is likely a bug in UIKit. If you can get a stack trace at this point (by setting a breakpoint at LookbackStatusWindowBecameKey) and sending that stack trace to nevyn#lookback.io or support#lookback.io, I will report it to Apple, and there will be rainbows, unicorns and a happier world for all :) thanks!", [[NSBundle mainBundle] gf_displayName]);
}
+ (UIWindow*)suitableWindowToMakeKeyExcluding:(UIWindow*)notThis
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSInteger index = windows.count-1;
UIWindow *nextWindow = [windows objectAtIndex:index];
while((!IsAllowedKeyWindow(nextWindow) || nextWindow == notThis) && index >= 0) {
nextWindow = windows[--index];
}
return nextWindow;
}
+ (UIWindow*)findAndSetSuitableKeyWindow
{
UIWindow *nextWindow = [[self class] suitableWindowToMakeKeyExcluding:nil];
GFLog(GFError, #"Choosing this as key window instead: %#", nextWindow);
[nextWindow makeKeyWindow];
return nextWindow;
}
My code looks like proper but isEqual used to compare two object is not working. i'm new to iOS. there is no much resource to check. [box2 isEqual:box1] is always gives me No. but it supposed to give Yes. anything wrong in my code, please suggest me the correct thing. thanks in advance. expecting best suggestion .
#import <Foundation/Foundation.h>
#interface Box:NSObject
{
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
}
#property(nonatomic, readwrite) double height; // Property
-(double) volume;
//-(id)initWithLength:(double)l andBreadth:(double)b;
#end
#implementation Box
#synthesize height;
-(id)init
{
self = [super init];
if(self)
{
length = 2.0;
breadth = 3.0;
}
return self;
}
-(double) volume
{
return length*breadth*height;
}
#end
#class Box;
int main (int argc, const char * argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Box *box1 = [[Box alloc]init]; // Create box1 object of type Box
Box *box2 = [[Box alloc]init];
NSLog (#"hello world");
box1.height = 5.0;
NSLog (#"%ld",[box1 volume]);
if([box2 isEqual:box1])
{
NSLog(#"%b",[box2 isEqual:box1]);
}
[pool drain];
return 0;
}
You have to override isEqual method in your object.
e.g. in your "#implementation Box"
- (BOOL)isEqual:(id)object
{
if ([object isKindOfClass:[Box class]]) {
Box*box = (Box*)object;
return self.height == box.height; // compare heights values
}
return [super isEqual:object];
}
isEqual: works just fine and as documented, but not as you expected.
isEqual: is a property of NSObject. It returns YES if two object pointers are the same, and NO if they are not. If you want a different result, you need to override isEqual for your Box class. Note that the argument to isEqual: could be anything, not just a Box, so you need to be careful.
Obviously you shouldn't declare instance variables put properties, so the code below assumes that. The way your code is written, you will have some confusion with an instance variable height, a property height, and an instance variable _height, which will give you headaches. It also assumes that you are not subclassing Box (for example you could have a coloured box, which should not be equal to a Box with same width, height and breadth).
- (BOOL)isEqual:(id)other
{
if (! [other isKindOfClass:[Box class]]) return NO;
Box* otherBox = other;
return _length == other.length && _breadth == other.breadth
&& _height == other.height;
}
I'm expecting to get a little help with a UIButtons staying .hidden. I'm new to this site so please give me a min to best describe this problem I face.
Below is a picture of 2 UIButtons, in the middle of these UIButtons there is another one called OnRoute. Once the Acknowledged button is pressed it is hidden to which sends a status and reveals the OnRoute UIButton. Now the Acknowledged button is hidden you will only see on screen under the Runsheet button the OnRoute button to which you also press that sends a status and then hides it self.
Once these buttons are pressed you are sent to a UITableView and at this point all is well, but when you go back to the menu screen the buttons are reappear as if the buttons have not been pressed. And you can repeat over and over sending status.
The idea of this is to send a job status once the buttons are pressed which in turn shows on software on a server. Once these have been sent and the UIButtons hide for that job number, I would like to keep them hides until job has gone from hand set.
This is complex problem but if anyone has any ideas of this, I would really be thankful.
//This is in ViewDidLoad
self.onroute.hidden = YES;
NSNumber *num = [NSNumber numberWithInt:10.00];
self.acknow.hidden = YES;
if((self.consignment.cur_status_no < num) || [self.consignment.newjob isEqual:#(YES)]){
self.acknow.hidden = NO;
//This is in IBAction
- (IBAction)acknowledgebtn:(id)sender {
if (self.onroute.hidden == YES){
self.acknow.hidden = NO;
self.onroute.hidden = NO;
self.acknow.hidden = YES;
//and this is for the other IBAction
if (self.acknow.hidden == YES){
self.onroute.hidden = YES;
As I'm new to the site it will not let me post picture of UIButton sorry for this.
My suggestion would be to use some booleans instead of relying on the buttons hidden property. Then save the booleans when transferring to a new view. Then when you return to the main menu check the booleans and see what should be hidden and what should not be.
Also when I name variables I like to pretend that someone else will be looking at my code. So instead of just onroute as the button name, I would make it onrouteBut. This makes it a lot easier when I go back through my code as well so I know exactly what each variable is just by looking at the name.
As for the code I don't know how you are presenting views, so I can't really give a full answer. But I think this will help.
in your .h
#property (nonatomic) BOOL onrouteBool;
#property (nonatomic) BOOL acknowBool;
//whatever other bools you need instead of using button.hidden == YES/NO
in your .m
#synthesize onrouteBool, acknowBool;
-(void)viewDidLoad {
onrouteBut.hidden = YES;
onrouteBool = YES;
NSNumber *num = [NSNumber numberWithInt:10.00];
acknowBut.hidden = YES;
acknowBool = YES;
if((self.consignment.cur_status_no < num) || [self.consignment.newjob isEqual:#(YES)]) {
acknowBut.hidden = NO;
acknowBool = NO;
}
}
-(IBAction)acknowledgeBtn:(id)sender {
if (onrouteBool == YES) {
acknowBut.hidden = NO;
onrouteBut.hidden = NO;
acknowBool = NO;
onrouteBool = NO;
//this part doesn't make sense you set the button to visible and then hidden right after
acknowBut.hidden = YES;
acknowBool = YES;
}
}
-(IBAction)onrouteBtn:(id)sender {
if (acknowBool == YES) {
onrouteBut.hidden = YES;
onrouteBool = YES;
}
}
So now before you transition to your next view call this method to save the bools
-(void)saveTheBools {
//save the bools however you want before you transition the view
//one way is nsuserdefaults
[[NSUserDefaults standardUserDefaults]setBool:onrouteBool forKey:#"onrouteBool"];
[[NSUserDefaults standardUserDefaults]setBool:acknowBool forKey:#"acknowBool"];
[[NSUserDefaults standardUserDefaults]synchronize];
//how you save them
}
then when you transition back to the main menu check the bools to see if the buttons should be hidden
-(void)checkTheBools {
onrouteBool = [[NSUserDefaults standardUserDefaults] boolForKey:#"onrouteBool"];
acknowBool = [[NSUserDefaults standardUserDefaults] boolForKey:#"acknowBool"];
if (onrouteBool == YES) {
onrouteBut.hidden = YES;
}
else {
onrouteBut.hidden = NO;
}
if (acknowBool == YES) {
acknowBut.hidden = YES;
}
else {
acknowBut.hidden = NO;
}
//whatever else you need to hidden or make visible
}
This is all just to give you some ideas of what to do. Use what you need to make it work. This is how I would do it, I don't know if this is best way to do it but it's a starting point. I can't really give a specific answer without seeing all of your code, since I don't know how you're transitioning views, what you are initializing, retaining, etc.
Hope this helps you out, if not my bad. Just keep working at it and you'll find something that works for you eventually.
edit:
As for the status problem you are having I can't really help because I don't have the code to look at. I think it probably has to do with saving your variables so you can access them across classes. So like I showed you how to save the booleans and use them you probably will have to do something similar to check if the status has sent or not.
I suggested using nsuserdefaults because that is the easiest thing to do, however it is not the best to rely on that for saving all of your variables. You can also look into singletons, core data, or anything that will allow you to save the variables that you need across classes. You just have to find the way that works best for what you are trying to do.
The only way you are going to learn is to struggle at times, do some research, and try different things until you find a solution. Also take advantage of the resources apple provides you with as a developer. I think you will be able to figure this one out. Good luck
Just wanted to update anyone having this problem, I managed to fix this using doubleValue.
onroute.hidden = YES;
onrouteBool = YES;
NSNumber *num1 = [NSNumber numberWithDouble:10.00];
if(([self.consignment.cur_status_no doubleValue] < [num1 doubleValue] ) ) {
if([self.consignment.newjob isEqual:#(NO)]) {
onroute.hidden = NO;
onrouteBool = NO;
}
}
acknow.hidden = YES;
acknowBool = YES;
if([self.consignment.newjob isEqual:#(YES)]) {
acknow.hidden = NO;
acknowBool = NO;
}
Thanks again for all your help.
Firstly here is my .h files code where i declared everything...
#interface NextlLevel : UIViewController{
IBOutlet UIImageView *Coin1;
int score;
IBOutlet UILabel *scorelable;
}
#property (nonatomic, assign) int score;
#property (nonatomic, retain) IBOutlet UILabel *scorelable;
#end
I have also synthesised my implementations in my .m file here is the code...
#synthesize score;
#synthesize scorelable;
Secondly I have some code which just checks if my drag-able image has collided with a still image. Here is the code...
-(void) ifCollided {
if(CGRectIntersectsRect(Coin1.frame,MoneyBag.frame)){
Coin1.hidden = YES;
}
}
This code is just a basic way of checking for a collision but once the image has collided I want to add to a int which is displayed in a label.
The only way i thought i could do this was by ...
firstly adding this line of code to 'ifCollided'
[self scorecheck];
So now 'ifCollided' looks something like this:
-(void) ifCollided {
if(CGRectIntersectsRect(Coin1.frame,MoneyBag.frame)){
Coin1.hidden = YES;
[self scorecheck];
}
}
Thirdly I had to make 'scorecheck' I did this by...
-(void)scorecheck{
score++;
scorelable.text = [NSString stringWithFormat:#"%i", score];
}
}
Here is a image to show that the code I'm currently using is not working correctly. And therefore is not just adding just one to the int that i set to 0. I'm displaying the int in the score label (at the top of my screen). Due to the code not correctly working as long as you touch, drag and hold whilst colliding in the boundaries of the image with the collission rule (in this case the moneybag)
Here is to the image i was talking about:
Any help would be very appreciated
Edit:Here is what i was meant to do in the void ifCollided
-(void) ifCollided {
if (!Coin1.hidden)
{
Coin1.hidden = YES;
[self scorecheck];
}
}
Thanks #Elliott Perry
So, to clarify, you would like a mechanism that guarantees that the score will only increment once per each coin colliding with the moneybag?
There are any number of ways to do this but they all require you to keep track of which coins have already been processed in some way. You could give each of your coins a unique reference and store a collection of ids that represent coins that have already been processed, avoiding calling scorecheck if the collection contains the id of the coin in question.
Alternatively, and the method I'd go with, you can mark each coin to let yourself know not to process that coin a second time. Since your hiding the coins on collision, why not use the hidden property to determine whether to increment the score or not:
-(void)ifCollided
{
if(CGRectIntersectsRect(Coin1.frame,MoneyBag.frame))
{
if (!Coin1.hidden)
{
Coin1.hidden = YES;
[self scorecheck];
}
}
}
If for whatever reason that isn't feasible here is an example marking the coins using the tag property:
-(void)ifCollided
{
if(CGRectIntersectsRect(Coin1.frame,MoneyBag.frame))
{
if (Coin1.tag != 1)
{
Coin1.hidden = YES;
[self scorecheck];
Coin1.tag = 1;
}
}
}
Edit: Updated to make question more obvious
Edit 2: Made question more accurate to my real-world problem. I'm actually looking to take action if they tap anywhere EXCEPT in an on-screen text-field. Thus, I can't simply listen for events within the textfield, I need to know if they tapped anywhere in the View.
I'm writing unit tests to assert that a certain action is taken when a gesture recognizer recognizes a tap within certain coordinates of my view. I want to know if I can programmatically create a touch (at specific coordinates) that will be handled by the UITapGestureRecognizer. I'm attempting to simulate the user interaction during a unit test.
The UITapGestureRecognizer is configured in Interface Builder
//MYUIViewControllerSubclass.m
-(IBAction)viewTapped:(UITapGestureRecognizer*)gesture {
CGPoint tapPoint = [gesture locationInView:self.view];
if (!CGRectContainsPoint(self.textField, tapPoint)) {
// Do stuff if they tapped anywhere outside the text field
}
}
//MYUIViewControllerSubclassTests.m
//What I'm trying to accomplish in my unit test:
-(void)testThatTappingInNoteworthyAreaTriggersStuff {
// Create fake gesture recognizer and ViewController
MYUIViewControllerSubclass *vc = [[MYUIViewControllersSubclass alloc] init];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer initWithView: vc.view];
// What I want to do:
[[ Simulate A Tap anywhere outside vc.textField ]]
[[ Assert that "Stuff" occured ]]
}
There is a much simpler way to trigger a touch for a UITapGestureRecognizer in a unit test using a single line. Assuming you have a var that holds a reference to the tap gesture recognizer all you need is the following:
singleTapGestureRecognizer?.state = .ended
I think you have multiple options here:
May be the simplest would be to send a push event action to your view but i don't think that what you really want since you want to be able to choose where the tap action occurs.
[yourView sendActionsForControlEvents: UIControlEventTouchUpInside];
You could use UI automation tool that is provided with XCode instruments. This blog explains well how to automate your UI tests with script then.
There is this solution too that explain how to synthesize touch events on the iPhone but make sure you only use those for unit tests. This sounds more like a hack to me and I will consider this solution as the last resort if the two previous points doesn't fulfill your need.
What you attempt to do is very hard (but not entirely impossible) while staying on the (iTunes-)legal path.
Let me first draft the right way;
The proper way out for doing this is using UIAutomation. UIAutomation does exactly what you ask for, it simulates user behaviour for all kinds of tests.
Now that hard way;
The issue that your problems boils down to is to instantiate a new UIEvent. (Un)fortunately UIKit does not offer any constructors for such events due to obvious security reasons. There are however workarounds that did work in the past, not sure if they still do.
Have a look at Matt Galagher's awesome blog drafting a solution on how to synthesise touch events.
If used in tests you can use either a test library called SpecTools which helps with all this and more or use it's code directly:
// Return type alias
public typealias TargetActionInfo = [(target: AnyObject, action: Selector)]
// UIGestureRecognizer extension
extension UIGestureRecognizer {
// MARK: Retrieving targets from gesture recognizers
/// Returns all actions and selectors for a gesture recognizer
/// This method uses private API's and will most likely cause your app to be rejected if used outside of your test target
/// - Returns: [(target: AnyObject, action: Selector)] Array of action/selector tuples
public func getTargetInfo() -> TargetActionInfo {
var targetsInfo: TargetActionInfo = []
if let targets = self.value(forKeyPath: "_targets") as? [NSObject] {
for target in targets {
// Getting selector by parsing the description string of a UIGestureRecognizerTarget
let selectorString = String.init(describing: target).components(separatedBy: ", ").first!.replacingOccurrences(of: "(action=", with: "")
let selector = NSSelectorFromString(selectorString)
// Getting target from iVars
let targetActionPairClass: AnyClass = NSClassFromString("UIGestureRecognizerTarget")!
let targetIvar: Ivar = class_getInstanceVariable(targetActionPairClass, "_target")
let targetObject: AnyObject = object_getIvar(target, targetIvar) as! AnyObject
targetsInfo.append((target: targetObject, action: selector))
}
}
return targetsInfo
}
/// Executes all targets on a gesture recognizer
public func execute() {
let targetsInfo = self.getTargetInfo()
for info in targetsInfo {
info.target.performSelector(onMainThread: info.action, with: nil, waitUntilDone: true)
}
}
}
Both, library as well as the snippet use private API's and will probably cause a rejection if used outside of your test suite ...
Answer by #Ondrej updated to Swift 4:
// Return type alias
typealias TargetActionInfo = [(target: AnyObject, action: Selector)]
// UIGestureRecognizer extension
extension UIGestureRecognizer {
// MARK: Retrieving targets from gesture recognizers
/// Returns all actions and selectors for a gesture recognizer
/// This method uses private API's and will most likely cause your app to be rejected if used outside of your test target
/// - Returns: [(target: AnyObject, action: Selector)] Array of action/selector tuples
func getTargetInfo() -> TargetActionInfo {
guard let targets = value(forKeyPath: "_targets") as? [NSObject] else {
return []
}
var targetsInfo: TargetActionInfo = []
for target in targets {
// Getting selector by parsing the description string of a UIGestureRecognizerTarget
let description = String(describing: target).trimmingCharacters(in: CharacterSet(charactersIn: "()"))
var selectorString = description.components(separatedBy: ", ").first ?? ""
selectorString = selectorString.components(separatedBy: "=").last ?? ""
let selector = NSSelectorFromString(selectorString)
// Getting target from iVars
if let targetActionPairClass = NSClassFromString("UIGestureRecognizerTarget"),
let targetIvar = class_getInstanceVariable(targetActionPairClass, "_target"),
let targetObject = object_getIvar(target, targetIvar) {
targetsInfo.append((target: targetObject as AnyObject, action: selector))
}
}
return targetsInfo
}
/// Executes all targets on a gesture recognizer
func sendActions() {
let targetsInfo = getTargetInfo()
for info in targetsInfo {
info.target.performSelector(onMainThread: info.action, with: self, waitUntilDone: true)
}
}
}
Usage:
struct Automator {
static func tap(view: UIView) {
let grs = view.gestureRecognizers?.compactMap { $0 as? UITapGestureRecognizer } ?? []
grs.forEach { $0.sendActions() }
}
}
let myView = ... // View under UI Logic Test
Automator.tap(view: myView)
I was facing the same issue, trying to simulate a tap on a table cell to automate a test for a view controller which handles tapping on a table.
The controller has a private UITapGestureRecognizer created as below:
gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(didRecognizeTapOnTableView)];
The unit test should simulate a touch so that the gestureRecognizer would trigger the action as it was originated from the user interaction.
None of the proposed solutions worked in this scenario, so I solved it decorating UITapGestureRecognizer, faking the exact methods called by the controller. So I added a "performTap" method that call the action in a way the controller itself is unaware of where the action is originated from. This way, I could make a test unit for the controller independent of the gesture recognizer, just of the action triggered.
This is my category, hope it helps someone.
CGPoint mockTappedPoint;
UIView *mockTappedView = nil;
id mockTarget = nil;
SEL mockAction;
#implementation UITapGestureRecognizer (MockedGesture)
-(id)initWithTarget:(id)target action:(SEL)action {
mockTarget = target;
mockAction = action;
return [super initWithTarget:target action:action];
// code above calls UIGestureRecognizer init..., but it doesn't matters
}
-(UIView *)view {
return mockTappedView;
}
-(CGPoint)locationInView:(UIView *)view {
return [view convertPoint:mockTappedPoint fromView:mockTappedView];
}
-(UIGestureRecognizerState)state {
return UIGestureRecognizerStateEnded;
}
-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
mockTappedView = view;
mockTappedPoint = point;
[mockTarget performSelector:mockAction];
}
#end
Okay, I've turned the above into a category that works.
Interesting bits:
Categories can't add member variables. Anything you add becomes static to the class and thus is clobbered by Apple's many UITapGestureRecognizers.
So, use associated_object to make the magic happen.
NSValue for storing non-objects
Apple's init method contains important configuration logic; we could guess at what is set (number of taps, number of touches, what else?
But this is doomed. So, we swizzle in our init method that preserves the mocks.
The header file is trivial; here's the implementation.
#import "UITapGestureRecognizer+Spec.h"
#import "objc/runtime.h"
/*
* With great contributions from Matt Gallagher (http://www.cocoawithlove.com/2008/10/synthesizing-touch-event-on-iphone.html)
* And Glauco Aquino (http://stackoverflow.com/users/2276639/glauco-aquino)
* And Codeshaker (http://codeshaker.blogspot.com/2012/01/calling-original-overridden-method-from.html)
*/
#interface UITapGestureRecognizer (SpecPrivate)
#property (strong, nonatomic, readwrite) UIView *mockTappedView_;
#property (assign, nonatomic, readwrite) CGPoint mockTappedPoint_;
#property (strong, nonatomic, readwrite) id mockTarget_;
#property (assign, nonatomic, readwrite) SEL mockAction_;
#end
NSString const *MockTappedViewKey = #"MockTappedViewKey";
NSString const *MockTappedPointKey = #"MockTappedPointKey";
NSString const *MockTargetKey = #"MockTargetKey";
NSString const *MockActionKey = #"MockActionKey";
#implementation UITapGestureRecognizer (Spec)
// It is necessary to call the original init method; super does not set appropriate variables.
// (eg, number of taps, number of touches, gods know what else)
// Swizzle our own method into its place. Note that Apple misspells 'swizzle' as 'exchangeImplementation'.
+(void)load {
method_exchangeImplementations(class_getInstanceMethod(self, #selector(initWithTarget:action:)),
class_getInstanceMethod(self, #selector(initWithMockTarget:mockAction:)));
}
-(id)initWithMockTarget:(id)target mockAction:(SEL)action {
self = [self initWithMockTarget:target mockAction:action];
self.mockTarget_ = target;
self.mockAction_ = action;
self.mockTappedView_ = nil;
return self;
}
-(UIView *)view {
return self.mockTappedView_;
}
-(CGPoint)locationInView:(UIView *)view {
return [view convertPoint:self.mockTappedPoint_ fromView:self.mockTappedView_];
}
//-(UIGestureRecognizerState)state {
// return UIGestureRecognizerStateEnded;
//}
-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
self.mockTappedView_ = view;
self.mockTappedPoint_ = point;
// warning because a leak is possible because the compiler can't tell whether this method
// adheres to standard naming conventions and make the right behavioral decision. Suppress it.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.mockTarget_ performSelector:self.mockAction_];
#pragma clang diagnostic pop
}
# pragma mark - Who says we can't add members in a category?
- (void)setMockTappedView_:(UIView *)mockTappedView {
objc_setAssociatedObject(self, &MockTappedViewKey, mockTappedView, OBJC_ASSOCIATION_ASSIGN);
}
-(UIView *)mockTappedView_ {
return objc_getAssociatedObject(self, &MockTappedViewKey);
}
- (void)setMockTappedPoint_:(CGPoint)mockTappedPoint {
objc_setAssociatedObject(self, &MockTappedPointKey, [NSValue value:&mockTappedPoint withObjCType:#encode(CGPoint)], OBJC_ASSOCIATION_COPY);
}
- (CGPoint)mockTappedPoint_ {
NSValue *value = objc_getAssociatedObject(self, &MockTappedPointKey);
CGPoint aPoint;
[value getValue:&aPoint];
return aPoint;
}
- (void)setMockTarget_:(id)mockTarget {
objc_setAssociatedObject(self, &MockTargetKey, mockTarget, OBJC_ASSOCIATION_ASSIGN);
}
- (id)mockTarget_ {
return objc_getAssociatedObject(self, &MockTargetKey);
}
- (void)setMockAction_:(SEL)mockAction {
objc_setAssociatedObject(self, &MockActionKey, NSStringFromSelector(mockAction), OBJC_ASSOCIATION_COPY);
}
- (SEL)mockAction_ {
NSString *selectorString = objc_getAssociatedObject(self, &MockActionKey);
return NSSelectorFromString(selectorString);
}
#end
CGPoint tapPoint = [gesture locationInView:self.view];
should be
CGPoint tapPoint = [gesture locationInView:gesture.view];
because the cgpoint should be retrieved from exactly where the gesture target is rather than trying to guess where in the view it's in