UIScrollView - Custom Map - Prevent marker subview on map from scaling with map - objective-c

I have a custom map of a limited area, and have it set up to correctly show the users' location. The map is a 1600px square image within a UIScrollView.
I have a crosshair image to show the current location of the user, which at zoomScale 1.0 is the desired size. When I pinch and zoom the scrollView, the crosshair scales with it. I would like to have the subview remain the same size on screen.
I haven't been able to find any information on this, what would be the best way to go about this?
If there is anything I can provide you with to help the answer, please let me know.
Many thanks!
EDIT -
Having looked in to this further, there is a UIScrollViewDelegate method - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale which I tried using to take the marker's current center and size, then adjust, but this only scales at the end of the zoom. I would prefer to have the marker remain the same size while the user is zooming.
EDIT 2-
Cake has provided a great answer below, but I haven't been able to implement this in the way I imagined it would be.
I have the UIImageView as a placeholder, with alpha set to 0. This placeholder moves around relative to the map to show the user location. This operates as I expect it to. Unfortunately, this resizes with the map, as it is a subview of the map (so it stays in place).
Taking Cake's below answer, I have created the non-scaling crosshair image, and added it as a sibling subview to the scrollview. The maths, once Cake had pointed them out, were quite simple to get the new frame for the crosshair:
CGPoint ULPC = userLocationPlaceholder.center;
float zs = scrollView.zoomScale;
CGRect newFrame = CGRectMake(((ULPC.x * zs) - scrollView.contentOffset.x) - 20, ((ULPC.y * zs) - scrollView.contentOffset.y) - 20, 40, 40);
Where the image is 40points wide. This matches the centers perfectly.
The problem I now have is that I cannot get the crosshair image to stay locked to the placeholder.
I have tried using a self calling animation as such:
-(void)animeUserLocationAttachment
{
[UIView animateWithDuration:0.05
delay:0
options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear )
animations:^{
userLocationDotContainer.frame = newFrame;
} completion:^(BOOL finished){
// Call self
[self animateUserLocationAttachment];
}];
}
As soon as I start scrolling/zooming, this locks the animation so that the crosshair just sits in place until I release the scrolling/zooming, then it correctly updates it's location.
Is there any way I can get around this, or an alternative method I can apply?
Many thanks
EDIT 3 -
I've re-accepted Cake's answer as it covers 90% of the issue. Further to his answer I have implemented the ScrollViewDelegate methods scrollViewWillBeginDragging: andscrollViewWillBeginDecelerating: to scale the placeholder to match the current size of the crosshair relative to the map, show the placeholder (that is a subview of the map image) and hide the crosshair image. The delegate method scrollviewWillBeginZooming:withView: does not show the placeholder because it scales with the map. As Cake recommends, I'll make a new question for this issue.
The counterpart methods (scrollViewDidEndZooming:withView:atScale:, scrollViewDidEndDragging:willDecelerate: and -scrollViewDidEndDecelerating:`) all hide the placeholder, and re-show the crosshair.

The question is old but for the future similar questions I've recently resolved a similar problem applying the hint of Andrew Madsen of another post.
I'had a UIScrollView, with an UIImageView in it. Attached to the UIImageView I had many MKAnnotationView (those are my subviews that I didn't want scaling with the superview).
I did subclass UIImageView and implement setTransform: method like here:
#import "SLImageView.h"
#implementation SLImageView
- (void)setTransform:(CGAffineTransform)transform
{
[super setTransform:transform];
CGAffineTransform invertedTransform = CGAffineTransformInvert(transform);
for (id obj in self.subviews)
{
if ([obj isKindOfClass:[MKAnnotationView class]])
{
[((UIView *)obj) setTransform:invertedTransform];
}
}
}
#end
This works perfectly!
Mick.

Create another crosshair image that's associated with the view or view controller that contains the scrollview. Then have this one always snap to the center of the crosshair image you already have. Then, hide your original crosshair image. Then you can avoid having the scrollview scale the disassociated crosshair, and it should stay the same size.
Relative coordinate systems
Each view in cocoa touch has a frame property that has an origin. In order to position an object owned by one view properly relative to another view, all you have to do is figure out the differences in their origins. If one view is a subview of another, then this isn't too difficult.
Get the origin of the container view
Get the location of the subview inside of the container view
Get the origin of the subview
Calculate the difference in the positions of the origins
Get the location of the object you want to overlap (relative to the subview)
Calculate the location of the object you want to overlap relative to the container view
Move your crosshair to this position

Swift equivalent for Mick's answer:
class MapContainerView:UIView {
#IBOutlet var nonScalingViews: [UIView]!
override var transform: CGAffineTransform {
didSet {
guard let nonScalingViews = nonScalingViews else {
return
}
let invertedTransform = CGAffineTransformInvert(transform)
for view in nonScalingViews {
view.transform = invertedTransform
}
}
}
}

Related

iOS 8 custom view controller presentation: changing size of presented VC during animation

I'm writing an app with a series of cards in a table view, similar to the layout of the Google app for iOS when Google Now cards are enabled. When the user taps on a card, there should be a custom transition to a new view controller which is basically the card looking bigger, almost filling the screen, and it has more details on it. The custom transition itself should look like the card is animated upward and growing in size until it reaches its final size and position, which is now the new view controller holding the card.
I have been trying to approach this using a custom view controller transition. When the card is tapped, I initiate a custom view controller transition with UIModalPresentationCustom, and I set a transition delegate, which itself vends a custom animator and a custom UIPresentationController. In animateTransition:, I add the new view controller's view into the container view, setting the frame to the card's frame initially (so it looks like the card is still there and unchanged). Then I attempt to perform an animation where the presented view's frame grows in size and changes in position so that it moves into its final position.
Here's some of my code which does what I've described above - I'm trying to keep it short and sweet, but I can provide more info if needed:
Transition Delegate
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// NOWAnimationDelegate is my own custom protocol which defines the method for asking the presenting VC for the tapped card's frame.
UIViewController<NOWAnimationDelegate> *fromVC = (UIViewController<NOWAnimationDelegate> *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *finalVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
// Ask the presenting view controller for the frame of the tapped card - this method works.
toView.frame = [fromVC rectForSelectedCard];
[transitionContext.containerView addSubview:toView];
CGRect finalRect = [transitionContext finalFrameForViewController:finalVC];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toView.frame = finalRect;
}completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
Custom UIPresentationController
-(CGSize)sizeForChildContentContainer:(id<UIContentContainer>)container withParentContainerSize:(CGSize)parentSize {
return CGSizeMake(0.875*parentSize.width, 0.875*parentSize.height);
}
-(CGRect)frameOfPresentedViewInContainerView {
CGRect presentedViewFrame = CGRectZero;
CGRect containerBounds = self.containerView.bounds;
presentedViewFrame.size = [self sizeForChildContentContainer:(UIView<UIContentContainer> *)self.presentedView withParentContainerSize:containerBounds.size];
presentedViewFrame.origin.x = (containerBounds.size.width - presentedViewFrame.size.width)/2;
presentedViewFrame.origin.y = (containerBounds.size.height - presentedViewFrame.size.height)/2 + 10;
return presentedViewFrame;
}
What I'm finding is happening is that the new view is being automatically set to its final size immediately at the start of the animation, and then the animation is just the new view animating upwards. Using breakpoints, I'm noticing that frameOfPresentedViewInContainerView is called during the [transitionContext.containerView addSubview:toView] call, which would probably explain why this is happening - frameOfPresentedViewInContainerView returns "The frame rectangle to assign to the presented view at the end of the animations" according to the UIPresentationController documentation.
However, I'm not sure how to proceed, or if it's even really possible. All the examples of custom view controller transitions I've seen have all had the final size of the presented view controller static and unchanging during the animation. Is there any way to perform a custom view controller transition with a changing size of the presented view during the animation, or do I have to approach this in a different way?
Basically what you need to do is animating your toView.transform using CGAffineTransform in your Transition Delegate. Steps to do:
Before animating set your toView.frame = your frame when it's not showing
Create, let's say, CGAffineTransformMakeScale to scale from hidden frame to your desired final presented frame
On animation block set toView.transform to the transform that you create on step 2
As Aditya mentions CGAffineTransform is the way to go here.
Ive got this working now with it maintaining the width:
CGFloat targetscale=initialHeight/finalHeight;
CGFloat targetyoffset=(finalHeight-(finalHeight*targetscale))/2;
int targety=roundf(initialPosition-targetyoffset);
CGAffineTransform move=CGAffineTransformMakeTranslation(0, targety);
CGAffineTransform scale=CGAffineTransformMakeScale(1,targetscale);
toView.transform=CGAffineTransformConcat(scale,move);
This positions the incoming view taking into account the determined scale and then performs both transforms concurrently so the view scales & moves to the final position and size.
You then just set
toView.transform=CGAffineTransformIdentity;
in the animation block and it'll scale to the final location and size.
Note, this only moves and scales in the vertical dimension but can be adapted to scale in all directions like so:
+(CGAffineTransform)transformView:(UIView*)view toTargetRect:(CGRect)rect{
CGFloat targetscale=rect.size.height/view.frame.size.height;
CGFloat targetxoffset=(view.frame.size.width-(view.frame.size.width*targetscale))/2;
int targetx=roundf(rect.origin.x-view.frame.origin.x-targetxoffset);
CGFloat targetyoffset=(view.frame.size.height-(view.frame.size.height*targetscale))/2;
int targety=roundf(rect.origin.y-view.frame.origin.y-targetyoffset);
CGAffineTransform move=CGAffineTransformMakeTranslation(targetx, targety);
CGAffineTransform scale=CGAffineTransformMakeScale(targetscale,targetscale);
return CGAffineTransformConcat(scale, move);
}
And don't forget to transform the cell's rect the global coordinate space so the start frame is correct for the whole window not just the cells position in the table.
CGRect cellRect=[_tableView rectForRowAtIndexPath:[_tableView indexPathForSelectedRow]];
cellRect=[self.tableView convertRect:cellRect toView:presenting.view];
hope this helps!
Apple provides a wwdc sample app 'LookInside', accompanying talk 228: 'A look inside presentation controllers'. It features 2 custom presentations, of which one animates the size of the presented view controller. A look inside that code should help you out ;)

Zoom with a drawing view

I'm looking for a solution in order to have a beautiful zoom on a drawing view.
In my app, I have a view with an other UIView (which is used like a drawing view) and when I draw a stroke on it, the stroke is perfect. But when I zoom the view, I have this really ugly effect (a pixelised stroke) :
(source: imagup.com)
url image
Is there a solution in order to have a proper stroke ?
My UIViewController has a hierarchy like that :
UIViewController
ScrollView
View zoomable (defined with the viewForZoomingInScrollView method)
Image view
Drawing view
Thanks a lot !
Regards,
Sébastien ;)
I'm in the process of making a vector drawing application and let me tell you, this is NOT a trivial task to do correctly and requires quite a bit of work.
Some issues to keep in mind:
If you are not using vector graphics (CGPaths, for example, are
vectors) you will NOT be able to remove the pixelation. A UIImage,
for example, only has so much resolution.
In order to get your drawing to not look pixelated, you are going to
have to redraw everything. If you have a lot of drawing, this can be
an expensive task to perform.
Having good resolution WHILE zooming is nearly impossible because it would require an excessively large context and your drawing would likely exceed the capabilities of the device
I use core graphics to do my drawing, so the way I solved this issue was by allocating and managing multiple CGContexts and using them as buffers. I have one context that is ALWAYS kept at my least zoomed level (scale factor of 1). That context is drawn into at all times and makes it so that when unzooming completely, no time is spent redrawing since it is already done. Another context is used soley for drawing when zoomed. When not zoomed, that context is ignored (since it will have to be redrawn based on the new zoom level anyway). A high level algorithm for how I perform my zooming is as follows:
- (IBAction)handlePinchGesture:(UIGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateBegan)
{
//draw an image from the unzoomedContext into my current view
//set the scale transformation of my current view to be equal to "currentZoom", a property of the view that keeps track of the actual zoom level
}
else if(sender.state == UIGestureRecognizerStateChanged)
{
//determine the new zoom level and transform the current view, keeping track in the currentZoom property
//zooming will be pixelated.
}
else if(sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled)
{
if(currentZoom == 1.0)
{
//you are done because the unzoomedContext image is already drawn into the view!
}
else
{
//you are zoomed in and will have to do special drawing
//perform drawing into your zoomedContext
//scale the zoomedContext
//set the scale of your current view to be equal to 1.0
//draw the zoomedContext into the current view. It will not be pixelated!
//any drawing done while zoomed needs to be "scaled" based on your current zoom and translation amounts and drawn into both contexts
}
}
}
This gets even more complicated for me because I have additional buffers for the buffers because drawing images of my paths is much faster than drawing paths when there is lots of drawing.
Between managing multiple contexts, tweaking your code to draw efficiently into multiple contexts, following proper OOD, scaling new drawing based on your current zoom and translation, etc, this is a mountain of a task. Hopefully this either motivates you and puts you on the right track, or you decide that getting rid of that pixelation isn't worth the effort :)
I had the same problem and found a solution: tell the view to use a CATiledLayer as backing layer, then tell the view how many levels of zoom it supports. This worked for me, my drawing methods get automatically called when the (parent) view is zoomed.
A short explanation of levelsOfDetail and levelsOfDetailBias:
levelsOfDetail determine how many zooming levels there are in total
levelsOfDetailBias determine how many of those are zooming in.
So in my example I have 4 zooming levels, 3 are zoomed in and 1 is the non-zoomed level, meaning my view only redraws when zooming in.
#imprementation MyZoomableView
+ (Class)layerClass
{
return [CATiledLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
((CATiledLayer *)self.layer).levelsOfDetail = 4;
((CATiledLayer *)self.layer).levelsOfDetailBias = 3;
}
return self;
}
#end
Use [self setContentScaleFactor:scale]; in your scrollViewDidEndZooming: delegate method.

Infinite horizontal scrolling UIScrollView

I have a UIScrollView whose content size is 1200x480. I have some image views on it, whose width adds up to 600. When scrolling towards the right, I simply increase the content size and set the offset so as to make everything smooth (I then want to add other images, but that's not important right now). So basically, the images currently in the viewport remain somewhere on the left, eventually to be removed from the superview.
Now, the problem that I have happens when scrolling towards the left. What I do is I move the images to the end of the content size (so add 600 to each image view's origin.x), and then set the content offset accordingly. It works when the finger is on the screen and the user drags (scrollView.isTracking = YES). When the user scrolls towards the left and lets go (scrollView.isTracking = NO), the image views end up moving too fast towards the right and disappear almost instantly. Does anyone know how I could have the images move nicely and not disappear even when the user's not manually dragging the view and has already let go?
Here's my code for dragging horizontally:
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = self.scrollView.contentOffset;
CGSize size = self.scrollView.contentSize;
CGPoint newXY = CGPointMake(size.width-600, size.height-480);
// this bit here allows scrolling towards the right
if (offset.x > size.width - 320) {
[self.scrollView setContentSize:CGSizeMake(size.width+320, size.height)];
[self.scrollView setContentOffset: offset];
}
// and this is where my problem is:
if (offset.x < 0) {
for (UIImageView *imageView in self.scrollView.subviews) {
CGRect frame = imageView.frame;
[imageView setFrame:CGRectMake
(frame.origin.x+newXY.x, frame.origin.y, 200, frame.size.height)];
}
[self.scrollView setContentOffset:CGPointMake(newXY.x+offset.x, offset.y)];
}
}
EDIT: This is now working - I had a look at StreetScroller and it's all good now.
However, I now want to zoom in on the scrollview, but viewForZoomingInScrollView is never called. Is it not possible to zoom in on a scrollview with a large content size?
There are some approaches floating around here. Just use the site search …
If you want an more "official" example created by Apple take a look at the StreetScroller Demo. For some more information about this example take a look at last years WWDC session no. 104 Advanced Scroll View Techniques.
There is also an UIScrollView subclass on Github called BAGPagingScrollView, which is paging & infinite, but it has a few bugs you have to fix on your own, because it's not under active development (especially the goToPage: method leads to problems).

Why aren't my views being positioned correctly on interfaceOrientation changes?

I have a UIViewController one UIWebView in it. I'd like the UIWebView to be positioned in the centre of the iPad screen in landscape and portrait modes. So, I've implemented it like this
// UIViewController
// InfoGraphicView is the UIWebView
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
return YES;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration {
if (toInterfaceOrientation == UIInterfaceOrientationPortrait ||
toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
[self layoutPortrait];
} else {
[self layoutLandscape];
}
}
- (void)layoutLandscape {
NSLog(#"Layout Landscape");
infoGraphicView.frame = CGRectMake(100, 100, 936, 700);
}
- (void)layoutPortrait {
NSLog(#"Layout Portrait");
infoGraphicView.frame = CGRectMake(100, 100, 700, 936);
}
However, it's not behaving as I expected. In the above code, I would expectt he UIWebView to be 100px (or points or whatever the unit is) away from the top and the left. But it's not. In Portrait mode it appears flush with the top left of the screen, and in Landscape mode it seems to be partially offscreen in the top left.
If I set the frame as CGRectMake(-100, 100, 700, 936) then I get it positioned in the center of the screen as I'd like it to be, but I've no idea why.
As usual, there's most likely something simple I'm overlooking but I can't figure it out. Any help greatly appreciated as always.
The coordinates you set on infoGraphicView are relative to its superview, not to the screen generally. And views don't necessarily clip their subviews. Furthermore, the shape set automatically to self.view will depend on the scaling flags set in Interface Builder. However, I think that by default it is set to fill the whole screen.
That said, I think the mistake is in your use of willRotateToInterfaceOrientation:duration:. That is called before the rotation begins, so self.view has the old size (ie, it'll still be portrait sized if rotating from portrait to landscape and vice versa). Probably better to hook willAnimateRotationToInterfaceOrientation:duration: — then the correct size has been set and you'll be within the CoreAnimation block so your view will grow/shrink as part of the rotation animation.
It's also worth checking which resizing flags you have set on infoGraphicView. They'll take effect automatically, in addition to any changes you make. So you probably want to disable them all.
This probably is an issue with the view that the web view is in. The coordinate system used is that of the view’s superview. If that view isn’t being resized on rotation, then you’ll see unexpected layout like this. You can access the superview of a view through the superview property; one way to see its frame would be to use its description. Put this line in one of your layout methods:
NSLog(#"Superview: %#", [infoGraphicView superview]);
That should print out a description of the view.
Once you get that figured out, if you want the web view to have the same layout, you can use its autoresizingMask property. If you set it like this:
infoGraphicView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Then the view will automatically change its width and height to keep the top, left, right, and bottom margins the same.

Trying to detect collision between animating UIImageViews

I am very new to cocoa . Well I have multiple UIImageView (animationView)flying around the screen. I am using UIAnimation class to animate them. While I have one more UIImageView (myObject).I m trying to collide these while i move "myObject" around the screen using touchesMoved method. The problem is I am not able to detect the collision. I m using the following method :
if (CGRectIntersectsRect(animationView.frame, myObject.frame)) {
NSLog(#"Collision occurred");
}
I assume you are talking about CAAnimation, not UIAnimation.
In Core Animation the current values of animated properties are not reflected in the original objects (views, layers) on which they have been applied. Instead, you have to look at a layer's presentationLayer to get the currently effective value:
CGRect viewFrame = ((CALayer*)[animationView.layer presentationLayer]).frame;
CGRect objectFrame = ((CALayer*)[myObject.layer presentationLayer]).frame;
if (CGRectIntersectsRect(viewFrame, objectFrame)) {
NSLog(#"Collision occurred");
}