Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

AxoNeuron

macrumors 65816
Original poster
Apr 22, 2012
1,251
855
The Left Coast
Hi Everyone!

I am making an app that uses a custom map of an area and uses GPS coordinates to describe your position. I could have used the Google Maps API but it doesn't provide the level of detail I need for this specific project.

Everything is working fantastically. The only problem is the GPS location indicator I made. I put the entire map (and the GPS location indicator) inside of a UIScrollView so that the user can scroll in and around the map:

FullSizeRender.jpg


The problem is that the GPS indicator gets bigger or smaller when the user zooms in or out, and I want it to stay the same size. I still want it in the scroll view so that it's (x, y) position is effected when the user scrolls around the map, but I don't want the actual zooming in or out to change its size, just position on the screen.

Is there ANY way to make it so that the GPS indicator's position is effected by zooming in and out, but its size stays the same (ie. doesn't get bigger when the user zooms in, but it does move around when they scroll up or down)?

Thanks!
 
I haven't done this but my guess is that you need two separate views. Either a simple overlay view that shows the GPS indicator or possibly two scrollviews; one with the map and the other with the GPS indicator. The view with the GPS indicator won't respond to touch; only the Map scrollview will respond to touch. If the user scrolls the map then you need to also scroll the view with the GPS indicator. I'm not sure what should happen when the user pinches/zooms the Map view. The GPS view won't also pinch/zoom but it might move.
 
I haven't done this either, but I like PhoneyDeveloper's idea of a seperate overlay view.

UIView has this (and other similar methods)
Code:
- (CGPoint)convertPoint:(CGPoint)point
                 toView:(UIView *)view

Maybe you could set the center of the GPS indicator to the point returned by that method? Whenever the scrollView is zoomed or scrolled of course.
 
I can't say that I have done this, but it might be worth looking into...

What if every time there is a zoom (in or out) you reset the size of the GPS object to a constant.

I assume you have control to set the size of each object, and you know when a zoom even takes place, so if you set a constant, then every time after it changes (zooms) you reset the size of the object.

Another idea would be a clear overlay that only has the GPS object and track it as needed and make it not zoomable. If this sits on top of the other view, you'd pass the zoom message to the lower view.
 
Couldn't you pass the zoomScale of the UIScrollView to the gps indicator view and then ask the indicator view to redraw, using the zoomScale to calculate the necessary size of the indicator?
 
Thank you guys so much for the suggestions! I am investigating them as we speak.

I do have another problem though.

For some reason, when I create a UIImageView and a UILabel and I add them both as subviews of a single UIView and then return them out of this method, it never shows the actual label itself (just the UIImageView). Does anyone know why this is?

Code:
+(UIView *)getDetailViewForCar:(Car *)car withLocation:(CGPoint)location withView:(UIView *)view
{
    //creates a detail marker icon
    UIImageView *detailView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"marker_icon_left.png"]];
    detailView.frame = CGRectMake(location.y-20, location.x-20, 200, 200);
    
    //creates a label on top of the detail view icon
    UILabel *carNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(location.y + 20, location.x + 20, 180, 30)];
    carNameLabel.textColor = [UIColor whiteColor];
    carNameLabel.text = car.model;
    carNameLabel.font = [UIFont fontWithName:@"Helvetica" size:30];
    
    UIView *viewOfDetailLabel = [[UIView alloc] initWithFrame:view.frame];
    [viewOfDetailLabel addSubview:detailView];
    [viewOfDetailLabel addSubview:carNameLabel];
    return viewOfDetailLabel;
}
 
Last edited:
Try setting the backgroundColor of the label to something like yellowColor so you can see it if it's there. Maybe there's a problem with using whiteColor as the textColor. Maybe the frame is outside the containerView's frame.
 
That worked BRILLIANTLY! Thank you PhoneyDeveloper. Took an embarrassing amount of time but I was able to find out that I was using an incorrect frame x and y location.

I have one last problem I was hoping you could help me with though.

Lets say you have two UIViews. One of the UIViews is big and takes up the entire screen. The other UIView is smaller and is positioned on top of the UIView, as so:

viewQuestion.png


You have a point inside of UIView #1. Say that a user zooms in to the screen, and only UIView #1 is hooked up to the scroll view, so that when the user zooms in, that point changes its position in the screen even though it's position in UIView #1 remains the same. How would you find that same points new location in UIView #2's frame, since it isn't hooked up to the scroll view?
 
Last edited:
You should be able to use something like

Code:
[view2 convertPoint:pointZ fromView:view1];

Check the documentation for the convert functions in UIView documentation which can be found in the "Converting between view coordinate systems" section
 
That's a very good suggestion. I've been playing around with it, but it just doesn't seem to be working when I try to use it with a UIScrollView. So in my example, say that View #1 is a UIScrollView. Would this method you're talking about, convertPoint:fromView: work for finding the point of a UIScrollView in a UIView?

Basically I am trying to find out how to convert the coordinate of an object in a UIScrollView in to it's coordinate in self.view.

This means that when the user zooms in or out, this coordinate will change appropriately (because it has moved in relation to the display) even though it's actual position inside of the UIScrollView remains the same.
 
waterskier2007, I am having luck with those methods but they only work properly when the user is zoomed in all the way in to the UIScrollView, they work flawlessly then. But when you start zooming out they cease to function properly. Any ideas why this could be? The convertPoint:toView: methods throw out some weird numbers when zoomed out.
 
waterskier2007, I am having luck with those methods but they only work properly when the user is zoomed in all the way in to the UIScrollView, they work flawlessly then. But when you start zooming out they cease to function properly. Any ideas why this could be? The convertPoint:toView: methods throw out some weird numbers when zoomed out.

Sorry, no I haven't used it in a UIScrollView but in theory it *should* work :(

How come you don't have all of the views encapsulated in the UIScrollView?
 
Sorry, no I haven't used it in a UIScrollView but in theory it *should* work :(

How come you don't have all of the views encapsulated in the UIScrollView?
I cannot even begin to tell you how helpful you've been. I really, really appreciate the info you've given me!!!

As far as encapsulating the views in the scrollview, it's complicated. There are elements of the map (pins, labels) that I don't want to get bigger or smaller when the user zooms in/out in the scroll view, but I still want them to be tied to their respective locations on the scroll view map.

I finally got it working. I needed to use that convertPoint:toView: method twice to convert from the map view to the scroll view, then from the scroll view to self.view itself. That got it working perfectly.

Now I am using the scrollview delegate methods so that whenever the user zooms in or out, or scrolls around the scrollview, a displaylink timer gets set off that manually changes the positions of the non-resizable map elements to match the scrollview map, using core animation.

Thanks a ton!!! You've been extremely helpful.
 
Last edited:
I cannot even begin to tell you how helpful you've been. I really, really appreciate the info you've given me!!!

As far as encapsulating the views in the scrollview, it's complicated. There are elements of the map (pins, labels) that I don't want to get bigger or smaller when the user zooms in/out in the scroll view, but I still want them to be tied to their respective locations on the scroll view map.

I finally got it working. I needed to use that convertPoint:toView: method twice to convert from the map view to the scroll view, then from the scroll view to self.view itself. That got it working perfectly.

Now I am using the scrollview delegate methods so that whenever the user zooms in or out, or scrolls around the scrollview, a displaylink timer gets set off that manually changes the positions of the non-resizable map elements to match the scrollview map, using core animation.

Thanks a ton!!! You've been extremely helpful.

Might be a good idea to post the working code for those people that stumble over this thread looking for a solution.
 
Might be a good idea to post the working code for those people that stumble over this thread looking for a solution.

I second the motion. All in favor, say aye.

While it doesn't sound like something I need right now, I certainly would love to play with it. :D
 
Might be a good idea to post the working code for those people that stumble over this thread looking for a solution.

Great idea. Here's what I came up with.

Since my custom map is embedded in a scrollview, my first instinct was to embed the map icons (pins, etc) in the scrollview as well and just change their size whenever the user zoomed in or out. That actually did work just fine, but there were some issues with it's position on the map whenever this occurred, its position would warp slightly.

So instead, I created a second UIView (not embedded in the scrollview) and put all the non-zoomable map elements on that. Whenever the user zooms now, I have a custom method that triggers a displayLink timer. The displayLink timer is similar to NSTimer, except it fires about 60 times per second (coordinated with the refresh rate of the display). Whenever this timer fires, it calls a custom method that uses Core Animation to move the custom map elements to wherever their correct position is on the map.

I find this correct position by using the UIView/UIScrollView method convertPoint:toView: method. Since my application's display coordinate x,y system judges everything based on where it is in the actual map image, I first convert all points from that to the scrollview, then I convert it from the scrollview to self.view so that I can get their actual x,y position on the display. All of my non-zoomable map elements reside on a UIView with the same frame as self.view so it works.

Since some of the UIScrollView delegate methods will get called 10-20x when a user zooms in or zooms out, and I only want my displayLink to get created one time when they zoom in, I have to create a BOOL property called scrollIsActive to check and see if the UIScrollView methods have already triggered the displayLink timer.

I came to programming back in May and most of what I know is self-taught so if anyone can see any improvements that can be made I would be very grateful.

Here is the code:

ScrollView Delegate Methods that Trigger the Display Link Timer:

Code:
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    if (self.scrollIsActive == NO) {
        NSLog(@"DID BEGIN DRAGGING");
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(zoomDragDisplayLinkTriggeredForDetails)];
        [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        self.scrollIsActive = YES;
    }

}

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    NSLog(@"DID END DRAGGING");
    [self.displayLink invalidate];
    self.scrollIsActive = NO;
}

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (self.scrollIsActive == NO) {
        NSLog(@"DID BEGIN SCROLLING");
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(zoomDragDisplayLinkTriggeredForDetails)];
        [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        self.scrollIsActive = YES;
    }
}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    self.scrollIsActive = NO;
    [self.displayLink invalidate];
    NSLog(@"DID END SCROLLING");
}


Custom method that keeps non-zoomable map elements in the correct x,y position on the display:
Code:
-(void)zoomDragDisplayLinkTriggeredForDetails
{
    
    if (self.viewOfDetailExists) {
        
        CALayer *layerForDetail = self.viewOfDetailShown.layer;
        CGPoint currentPoint = layerForDetail.position;
        
        CGPoint convertedPoint = [self.mapView convertPoint:CGPointMake(self.currentPositionOfDetailInScrollView.y, self.currentPositionOfDetailInScrollView.x) toView:self.mapScrollView];
        CGPoint secondPoint = [self.mapScrollView convertPoint:convertedPoint toView:self.view];
        CGPoint thirdPoint = CGPointMake(secondPoint.x - 35, secondPoint.y - 40);
        self.locationDetail = thirdPoint;

        CABasicAnimation *basicPositionAnimationX = [CABasicAnimation animationWithKeyPath:@"position.x"];
        basicPositionAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        basicPositionAnimationX.duration = 1/60.0f;
        basicPositionAnimationX.repeatCount = 0;
        basicPositionAnimationX.autoreverses = NO;
        basicPositionAnimationX.removedOnCompletion = NO;
        basicPositionAnimationX.fillMode = kCAFillModeForwards;
        basicPositionAnimationX.fromValue = [NSNumber numberWithFloat:currentPoint.x];
        basicPositionAnimationX.toValue = [NSNumber numberWithFloat:thirdPoint.x];
        
        CABasicAnimation *basicPositionAnimationY = [CABasicAnimation animationWithKeyPath:@"position.y"];
        basicPositionAnimationY.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        basicPositionAnimationY.duration = 1/60.0f;
        basicPositionAnimationY.repeatCount = 0;
        basicPositionAnimationY.autoreverses = NO;
        basicPositionAnimationY.removedOnCompletion = NO;
        basicPositionAnimationY.fillMode = kCAFillModeForwards;
        basicPositionAnimationY.fromValue = [NSNumber numberWithFloat:currentPoint.y];
        basicPositionAnimationY.toValue = [NSNumber numberWithFloat:thirdPoint.y];
        
        [layerForDetail addAnimation:basicPositionAnimationX forKey:@"animatorX"];
        [layerForDetail addAnimation:basicPositionAnimationY forKey:@"animatorY"];
    }
}

Keep in mind, right now, I only have one actual element on the map that this applies to (custom labels I created) that I call details. Sorry if this method is a bit verbose, I didn't actually intend for anyone else to ever see any of this :D
 
Last edited:
My use of CADisplayLink is... nothing, but why do you use

Code:
if (self.viewOfDetailExists) {
    ...
}

Why not just say

Code:
if (_details) {
    ...
}

Where _details is your detail view

Other than that, looks good, and if it works it works
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.