I'm a beginner for IOS development. So wish someone could explain it with some details. The method is for viewforannotation.
If I keep
[mv setRegion:region animated:yes]
at the end of the function then the code will be in a infinite loop somehow, when I zoom in the map.
If I remove it, the mapview works perfectly fine.
Since I did not write the code, I do not see the purpose of using the line. Could someone tell me is that line necessary?
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation {
UIButton *abutton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[annotationView setRightCalloutAccessoryView:abutton];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(annotation.coordinate, 8000, 8000);
[mapview setRegion:region animated:YES];
return annotationView;
}
You should not edit the map's location within viewforannotation. viewForAnnotation is called when the map needs to draw the annotations on the map, if while doing that you change the part of the map that the MKMapView is moved there will be different annotations to be drawn, so from within viewForAnnotation you're making it call viewForAnnotation again ==> infinite loop.
So if like you say the code works fine without it, then remove it and be happy. That's my advice for any code. If the application works without it, remove it. No point in bulking up your application with unnecessary code.
Related
We've seen an unexpected behaviour in one of our apps - one one screen we're showing annotations on a map view and the user can change the annotations on display by clicking a button.
When we rebuild the app with iOS7 the screen would freeze regularly, i.e. no more user input was possible on the MKMapView once the code below had been called several times (with different sets of annotations) - the view is embedded in both a tab bar and a nav controller and all of their UI elements still worked, but the mapview itself would not accept any user input (pinching/zooming).
The code that displays the annotations is here:
[self.mapView removeAnnotations:self.mapView.annotations];
for (MyObject *my in self.mydata)
{
MyAnnotation *annotation = [MyAnnotationFactory createAnnotationFor:my];
[self.mapView addAnnotation:annotation];
}
CLLocationCoordinate2D mycenter;
mycenter.latitude = -38.967659;
mycenter.longitude = 172.873534;
[self.mapView setRegion:MKCoordinateRegionMake(mycenter, MKCoordinateSpanMake(15, 18))
animated:YES];
[self.mapView setCenterCoordinate:mycenter];
What I found is that by setting the region without animating it, i.e. by changing the above code to
[self.mapView setRegion:MKCoordinateRegionMake(mycenter, MKCoordinateSpanMake(15, 18))
animated:NO];
the problem goes away and the MKMapView behaves nicely on iOS7 as well.
If you have an idea as to why this is happening, and why it is happening only in iOS7 and not for earlier versions, I'd appreciate the clarification.
Also, review your mapView:regionDidChangeAnimated: and mapView:regionWillChangeAnimated: methods. Implementing only one might work for you; one of those might not be needed for you to implement.
Try running the setRegion in a function from the main thread:
[self performSelectorOnMainThread:#selector(animateMapRegion) withObject:nil waitUntilDone:NO];
-(void)animateMapRegion
{
CLLocationCoordinate2D mycenter;
mycenter.latitude = -38.967659;
mycenter.longitude = 172.873534;
[self.mapView setRegion:MKCoordinateRegionMake(mycenter, MKCoordinateSpanMake(15, 18)) animated:animated];
}
I'm not exactly sure what's going on and hope that I can provide enough relevant code to find an answer. I've set up my gesture recognizer in my appDelegate.m:
CCScene *scene = [HomeLayer scene];
HomeLayer *layer = (HomeLayer *) [scene.children objectAtIndex:0];
UIPanGestureRecognizer *gestureRecognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:layer action:#selector(handlePanFrom:)] autorelease];
[director_.view addGestureRecognizer:gestureRecognizer];
m._gestureRecognizer = gestureRecognizer;
I've inserted some debugging messages to try to pinpoint at what point the app crashes:
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {
NSLog(#"Handle Pan From");
as well as some printouts for ccTouchBegan/Moved/Ended.
Every time the app crashes, it's while things are "moving", (ended never gets called), and handlePanFrom never gets called either.
Background info: My app has buttons that I use to switch between scenes, for example:
- (void) doSomethingThree: (CCMenuItem *) menuItem
{
NSLog(#"The third menu was called");
[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:[HomeLayer scene] ]];
}
If I start up my app and go directly to the HomeLayer scene, and try to drag, the app crashes instantly 100% of the time (ccMoved gets called 1-2 times before crash). Clicking does not cause the app to crash, only anything that would invoke handlePanFrom.
The strange thing is that if I drag around on any other scene, the app does not crash, and handlePanFrom successfully gets called. Then, when I go back to the HomeLayer scene and drag around, it won't crash for some time, and it seems directly related to how long I spend dragging around on a different scene.
Has anyone seen these symptoms before? I'm not sure if the information I provided is relevant or correct, I'm still trying to learn my way around iphone dev. I'll also be happy for any debugging tips (those assembly looking hex lines aren't particularly enlightening to me...)
I figured out the problem with the help of NSZombies, finding out that the program was crashing while trying to reference the deallocated method handlePanFrom.
The ultimate root of the problem was that HomeLayer was being instantiated twice, the first time in appDelegate.m, and the 2nd time when i was doing the replaceScene.
This resulted in the first layer eventually losing all of its references and being deallocated while the gestureRecognizer was still trying to reference [layer handlePanFrom], causing the crash.
The problem was fixed by moving the gestureRecognizer from the appDelegate.m to HomeLayer.m, and for anyone who needs gestures across multiple layers, here's a piece of code that will remove any existing references of the gestureRecognizer to the view, and then add a new one that targets a method in the layer:
+(CCScene *) scene
{
HomeLayer *layer = [HomeLayer node];
[scene addChild: layer];
for (UIGestureRecognizer *gr in [[CCDirector sharedDirector].view gestureRecognizers]) {
// note that sharedDirector is a singleton and therefore is the same CCDirector
// as the one used in appDelegate.m
[[CCDirector sharedDirector].view removeGestureRecognizer:gr];
}
UIPanGestureRecognizer *gestureRecognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:layer action:#selector(handlePanFrom:)] autorelease];
[[CCDirector sharedDirector].view addGestureRecognizer:gestureRecognizer];
return scene;
}
Hopefully that may help someone in the future who is trying to work with multiple scenes/layers in a view =)
It's been a long day at the keyboard so I'm reaching out :-)
I have a UIPageViewController in a typical implementation that basically follows Apple's standard template. I am trying to add an overlay that will allow the user to do things like touch a button to jump to certain pages or dismiss the view controller to go to another part of the app.
My problem is that the UIPageViewController is trapping all events from my overlay subview and I am struggling to find a workable solution.
Here's some code to help the example...
In viewDidLoad
// Page creation, pageViewController creation etc....
self.pageViewController.delegate = self;
[self.pageViewController setViewControllers:pagesArray
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
self.pageViewController.dataSource = self;
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
// self.overlay being the overlay view
if (!self.overlay)
{
self.overlay = [[MyOverlayClass alloc] init]; // Gets frame etc from class init
[self.view addSubview:self.overlay];
}
This all works great. The overlay gets created, it gets show over the top of the pages of the UIPageViewController as you would expect. When pages flip, they flip underneath the overlay - again just as you would expect.
However, the UIButtons within the self.overlay view never get the tap events. The UIPageViewController responds to all events.
I have tried overriding -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch per the suggestions here without success.
UIPageViewController Gesture recognizers
I have tried manually trapping all events and handling them myself - doesn't work (and to be honest even if it did it would seem like a bit of a hack).
Does anyone have a suggestion on how to trap the events or maybe a better approach to using an overlay over the top of the UIPageViewController.
Any and all help very much appreciated!!
Try to iterate through UIPageViewController.GestureRecognizers and assign self as a delegate for those gesture and implement
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
Your code may be like this:
In viewDidLoad
for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
gesRecog.delegate = self;
}
And add the following method:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (touch.view != self.pageViewController.view]
{
return NO;
}
return YES;
}
The documented way to prevent the UIPageViewController from scrolling is to not assign the dataSource property. If you assign the data source it will move into 'gesture-based' navigation mode which is what you're trying to prevent.
Without a data source you manually provide view controllers when you want to with setViewControllers:direction:animated:completion method and it will move between view controllers on demand.
The above can be deduced from Apple's documentation of UIPageViewController (Overview, second paragraph):
To support gesture-based navigation, you must provide your view controllers using a data source object.
first of all: my question is very theoretical. Even if I post an example, I just want to know which implementation is the best one to solve this kind of problem. Maybe you will laugh when you read this question because it is very fundamentally - but I want to understand how to deal with such a situation.
Imagine the following: You have got an application which communicates with an extern API via XML. As a fact of this the view cannot appear immediately, because the API needs time to react. My idea was to implement a subview which contains a loading animation. When the API sends a response this subview is removed and the main view appears. Here is my example:
- (void)viewWillAppear:(BOOL)animated {
UIView *myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
[myView setBackgroundColor:[UIColor whiteColor]];
myView.tag = 1;
UIActivityIndicatorView *loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
loadingView.frame = CGRectMake(145, 160, 25, 25);
[myView addSubview:loadingView];
[self.view addSubview:myView];
[loadingView startAnimating];
}
- (void)viewDidAppear:(BOOL)animated
{
[self loadXMLData];
[self.tableView reloadData];
[[self.view viewWithTag:1] removeFromSuperview];
}
Everthing works fine. My problem is the following: it's not only this view where I have to do that. My applications consists of many views, so what is the best way to avoid repeating this code? I thought about following: I modify the (UIKit UIViewController) viewDidAppear selector and put the code in it. I don't know if Apple allows to change their frameworks. Furthermore it looks very dirty to me ;o) Is someone able to tell me how this is usually done? Thank you!
( hope you understand me, my first language is not English :-( )
Why not just make a subclass of UIViewController which has this code implemented in viewWillAppear and viewDidAppear, and then inherit every other view controller in the application from this "base" subclass rather than UIViewController? That will be much easier than trying to do anything to UIViewController directly.
I'm using a UIPanGestureRecognizer to recognize horizontal sliding in a UITableView (on a cell to be precise, though it is added to the table itself). However, this gesture recognizer obviously steals the touches from the table. I already got the pangesturerecognizer to recognize horizontal sliding and then snap to that; but if the user starts by sliding vertical, it should pass all events from that touch to the tableview.
One thing i have tried was disabling the recognizer, but then it wouldn't scroll untill the next touch event. So i'd need it to pass the event right away then.
Another thing i tried was making it scroll myself, but then you will miss the persistent speed after stopping the touch.
Heres some code:
//In the viewdidload method
UIPanGestureRecognizer *slideRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(sliding:)];
[myTable addGestureRecognizer:slideRecognizer];
-(void)sliding:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint translation = [recognizer translationInView:favoritesTable];
if (sqrt(translation.x*translation.x)/sqrt(translation.y*translation.y)>1) {
horizontalScrolling = YES; //BOOL declared in the header file
NSLog(#"horizontal");
//And some code to determine what cell is being scrolled:
CGPoint slideLocation = [recognizer locationInView:myTable];
slidingCell = [myTable indexPathForRowAtPoint:slideLocation];
if (slidingCell.row == 0) {
slidingCell = nil;
}
}
else
{
NSLog(#"cancel");
}
if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled)
{
horizontalScrolling = NO;
}
if (horizontalScrolling)
{
//Perform some code
}
else
{
//Maybe pass the touch from here; It's panning vertically
}
}
So, any advice on how to pass the touches?
Addition: I also thought to maybe subclass the tableview's gesture recognizer method, to first check if it's horizontal; However, then i would need the original code, i suppose... No idea if Apple will have problems with it.
Also: I didn't subclass the UITableView(controller), just the cells. This code is in the viewcontroller which holds the table ;)
I had the same issue and came up with a solution that works with the UIPanGestureRecognizer.
In contrast to Erik I've added the UIPanGestureRecognizer to the cell directly, as I need just one particular cell at once to support the pan. But I guess this should work for Erik's case as well.
Here's the code.
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
UIView *cell = [gestureRecognizer view];
CGPoint translation = [gestureRecognizer translationInView:[cell superview]];
// Check for horizontal gesture
if (fabsf(translation.x) > fabsf(translation.y))
{
return YES;
}
return NO;
}
The calculation for the horizontal gesture is copied form Erik's code – I've tested this with iOS 4.3.
Edit:
I've found out that this implementation prevents the "swipe-to-delete" gesture. To regain that behavior I've added check for the velocity of the gesture to the if-statement above.
if ([gestureRecognizer velocityInView:cell].x < 600 && sqrt(translate...
After playing a bit on my device I came up with a velocity of 500 to 600 which offers in my opinion the best user experience for the transition between the pan and the swipe-to-delete gesture.
My answer is the same as Florian Mielke's, but I've simplified and corrected it some.
How to use:
Simply give your UIPanGestureRecognizer a delegate (UIGestureRecognizerDelegate). For example:
UIPanGestureRecognizer *panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panDetected:)];
panner.delegate = self;
[self addGestureRecognizer:panner];
Then have that delegate implement the following method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint translation = [(UIPanGestureRecognizer *)gestureRecognizer translationInView:gestureRecognizer.view.superview];
return fabsf(translation.x) > fabsf(translation.y);
}
Maybe you can use the UISwipeGestureRecognizer instead? You can tell it to ignore up/down swipes via the direction property.
You may try using the touch events manually instead of the gesture recognizers. Always passing the event back to the tableview except when you finally recognize the swipe gesture.
Every class that inherits from UIResponder will have the four touch functions (began, ended, canceled, and moved). So the simplest way to "forward" a call is to handle it in your class and then call it explicitly on the next object that you would want to handle it (but you should make sure to check if the object responds to the message first with respondsToSelector: since it is an optional function ). This way, you can detect whatever events you want and also allow the normal touch interaction with whatever other elements need it.
Thanks for the tips! I eventually went for a UITableView subclass, where i check if the movement is horizontal (in which case i use my custom behaviour), and else call [super touchesMoved: withEvent:];.
However, i still don't really get why this works. I checked, and super is a UITableView. It appears i still don't fully understand how this hierarchy works. Can someone try and explain?