Pinch to zoom respecting pinch location (Like in Safari) - ios

So I effectively have a image I'd like to zoom horizontally but also be able to respect the location of the pinch. So if you pinched on the left, It'd zoom into the left. Ideally, the points where you pinch would stay with your fingers.
My case is a little more specific, I'm plotting data on a graph, so I instead will be manipulating an array of data and taking a subset. However, I'm sure the math is similar. (BUT, I can't just use an Affinetransform as most examples I've found does)
Any ideas?

The default pinch gesture handler scales the graph around the point of the gesture. Use a plot space delegate to limit the scaling to only one axis.

Do you use a UIScrollView? As far as I know, you'll get this behaviour for free.

I built the below solution for standard UIViews where UIScrollView and CGAffineTransform zooms were not appropriate as I did not want the view's subviews to be skewed. I’ve also used it with a CorePlot line graph.
The zooming is centred on the point where the user starts the pinch.
Here’s a simple implementation:
#interface BMViewController ()
#property (nonatomic, assign) CGFloat pinchXCoord; // The point at which the pinch occurs
#property (nonatomic, assign) CGFloat minZoomProportion; // The minimum zoom proportion allowed. Used to control minimum view width
#property (nonatomic, assign) CGFloat viewCurrentXPosition; // The view-to-zoom’s frame’s current origin.x value.
#property (nonatomic, assign) CGFloat originalViewWidth, viewCurrentWidth; // original and current frame width of the view.
#end
#implementation BMViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Setup the pinch gesture recognizer
UIPinchGestureRecognizer *pinchZoomRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchZoomGestureDetected:)];
[self.view addGestureRecognizer:pinchZoomRecognizer];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.originalViewWidth = self.view.frame.size.width;
self.viewCurrentWidth = self.originalViewWidth;
}
- (void)pinchZoomGestureDetected:(UIPinchGestureRecognizer *)recognizer
{
CGPoint pinchLocation = [recognizer locationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.viewCurrentWidth = self.view.frame.size.width;
self.viewCurrentXPosition = self.view.frame.origin.x;
// Set the pinch X coordinate to a point relative to the bounds of the view
self.pinchXCoord = pinchLocation.x - self.viewCurrentXPosition;
self.minZoomProportion = self.originalViewWidth / self.viewCurrentWidth;
}
CGFloat proportion = recognizer.scale;
CGFloat width = self.viewCurrentWidth * MAX(proportion, self.minZoomProportion); // Set a minimum zoom width (the original size)
width = MIN(self.originalViewWidth * 4, width); // Set a maximum zoom width (original * 4)
CGFloat rawX = self.viewCurrentXPosition + ((self.viewCurrentWidth - width) * (self.pinchXCoord / self.viewCurrentWidth)); // Calculate the new X value
CGRect frame = self.view.frame;
CGFloat newXValue = MIN(rawX, 0); // Don't let the view move too far right
newXValue = MAX(newXValue, self.originalViewWidth - width); // Don't let the view move too far left
self.view.frame = CGRectMake(newXValue, frame.origin.y, width, frame.size.height);
NSLog(#"newXValue: %f, width: %f", newXValue, width);
}
#end
This is all that needs to be done to resize the horizontal axis of a UIView.
For Core-Plot, it is assumed the CPTGraphHostingView is a subview of the UIViewController’s view being resized. The CPTGraphHostingView is redrawn when its frame/bounds are changed so, in the layoutSubviews method of the containing view change the CPTGraphHostingView and plot area’s frame’s relative to the bounds of its parent (which is the view you should be resizing). Something like this:
self.graphHostingView.frame = self.bounds;
self.graphHostingView.hostedGraph.plotAreaFrame.frame = self.bounds;
I haven’t attempted to change data on the graph as it zooms, but I can’t imagine it would be too difficult. In layoutSubviews on your containing view, call reloadData on your CPTGraph.

Related

Horizontally center multiple UIViews

I want to horizontally center a number of UIViews (they happen to be circles) in the master UIView. It will end up basically looking like the dots on the standard Page Control.
I have all the code written to create the circle UIViews I just have no idea how to arrange them horizontally and dynamically at run time.
Essentially I need some kind of horizontal container where I can do this
-(void)addCircle{
[self addSubView:[CircleView init]];
}
And it will auto arrange however many children it has in the center.
I get confused with auto-layout as well from time to time but here is a way how you can do it programmatically: (I assume that you add your circle views to a containerView property of your view controller and you do not add any other views to it.)
Add these two properties to your view controller:
#property (nonatomic) CGRect circleViewFrame;
#property (nonatomic) CGFloat delta;
Initiate those properties with the desired values in your view controller's viewDidLoad method:
// the size (frame) of your circle views
self.circleViewFrame = CGRectMake(0, 0, 10, 10);
// the horizontal distance between your circle views
self.delta = 10.0;
Now we add your "automatic addCircle method":
- (void)addCircleView {
UIView *newCircleView = [self createCircleView];
[self.containerView addSubview:newCircleView];
[self alignCircleViews];
}
Of course we need to implement the createCircleView method...
- (UIView*)createCircleView {
// Create your circle view here - I use a simple square view as an example
UIView *circleView = [[UIView alloc] initWithFrame:self.circleViewFrame];
// Set the backgroundColor to some solid color so you can see the view :)
circleView.backgroundColor = [UIColor redColor];
return circleView;
}
... and the alignCircleViews method:
- (void)alignCircleViews {
int numberOfSubviews = [self.containerView.subviews count];
CGFloat totalWidth = (numberOfSubviews * self.circleViewFrame.size.width) + (numberOfSubviews - 1) * self.delta;
CGFloat x = (self.containerView.frame.size.width / 2) - (totalWidth / 2);
for (int i = 0; i < numberOfSubviews; i++) {
UIView *circleView = self.containerView.subviews[i];
circleView.frame = CGRectMake(x,
self.circleViewFrame.origin.y,
self.circleViewFrame.size.width,
self.circleViewFrame.size.height);
x += self.circleViewFrame.size.width + self.delta;
}
}
This is the most important method which will automatically realign all your subviews each time a new circleView is added. The result will look like this:
Simple steps: append circle to container view, resize container view, center align container view
-(void)addToContanerView:(CircleView*)circle{
circle.rect.frame = CGrectMake(containers_end,container_y,no_change,no_change);
[containerView addSubview:circle];
[containerView sizeToFit];
containerView.center = self.view.center;
}
Assumptions:
containers_end & containers_y you can get from CGRectMax function,
for UIView SizeToFit method check here
To take care of rotation use make sure your Autoresizing subviews are set for left, right bottom and top margin.
You can try using this library. I have used it on several of my projects and so far, it worked really well.
https://github.com/davamale/DMHorizontalView

Vertical Parallax Scrolling Of UIView in iOS

I'm trying to add a vertical parallax scrolling effect between several views that are all the size of the screen.
This wasn't too complicated when done through buttons, however I want it to be controlled by a UIPanGesture, similar to the Year Walk Companion App for iOS. The current equation shown here doesn't work correctly.
Currently this version only moves the first two views, because once I get those working the rest should be easy. I've also not included all the checks that would stop certain views moving in certain directions because that is also not relevant to the issue.
Here is a web version of it working with buttons: http://flycarpodacus.zxq.net/iPhone%20Parallax/event.html
Hope someone can help.
ViewController.h
#interface ViewController : UIViewController {
UIPanGestureRecognizer *panGesture;
int activeView;
int nextView;
float offset;
float speed;
}
#property (weak, nonatomic) IBOutlet UIView *view1;
#property (weak, nonatomic) IBOutlet UIView *view2;
#property (weak, nonatomic) IBOutlet UIView *view3;
#property (weak, nonatomic) IBOutlet UIView *view4;
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize view1, view2, view3, view4;
- (void)viewDidLoad
{
[super viewDidLoad];
activeView = 1;
offset = 220;
speed = 1;
panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panGesture];
if (self.view.frame.size.height != 568) {
CGRect frame = self.view.frame;
frame.size.height = self.view.frame.size.height;
view1.frame = frame;
view2.frame = frame;
view3.frame = frame;
view4.frame = frame;
}
view4.center = CGPointMake(view4.center.x, self.view.frame.size.height/2 + offset);
view3.center = CGPointMake(view3.center.x, self.view.frame.size.height/2 + offset);
view2.center = CGPointMake(view2.center.x, self.view.frame.size.height/2 + offset);
view1.center = CGPointMake(view1.center.x, self.view.frame.size.height/2);
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint panGestureTranslation = [panGestureRecognizer translationInView:self.view];
switch (activeView) {
case 1:
view1.center = CGPointMake(view1.center.x, panGestureTranslation.y + self.view.frame.size.height/2);
view2.center = CGPointMake(view2.center.x, view2.center.y - 2.5f);
break;
default:
break;
}
}
Ok so, in my point of view you could try to do something like that :
first use only two view, in your example only two view is visible at the same time and so you could re-use the view for
the next slide you want to display.
So at the start you have 2 views (lets say they have the same size than the screen 300x500, yes I know they are not the real size,
but it will be more easy for the math :p).
In you init function place you view one in position 150x250 (the center of the screen), and the screen 2 at 150x500 (so you see only
the upper half of your view).
lets call panGestureTranslation.y : DeltaY (it will be more quick). so in the function panGestureRecognizer you want to move the two view
with the parralex effect, one simple solution in this exact case will be to move view1 of 2*DeltaY and view2 of DeltaY, like that, when the view1
will have quit the screen completly, the view two will be right in the center of it.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
you can know if is finish by test the position of view one (if the position.y is <= than 0, so it's finish.
when is finish, you have two choice, first you put the view1 in the bottom of view2 (at position 150x500 and display behind view2 ) and you feel it with your 3rd content, then switch a booleen
to do the excact opposite the next frame (you will move view2 of 2*DeltaY and view1 of DeltaY).
other solution is to move the two view at the original position (view1 at 150x250 and view two at 150x500), and feel view1 with the content of view2 (you have to do it without animation
so the user doesn't see that the view have switch), and feel view2 with you 3rd content, and so next frame, the exact same code will work again. you can have an infinity of content with this method.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
let me know if I wasn't clear on some point ^^.
I would suggest checking our article on parallax effect with detailed explanation and open source implementation => http://blog.denivip.ru/index.php/2013/08/parallax-in-ios-applications/?lang=en

UIButton position while UIImage zoom on UIScrollView

I have a scenario where I need to implement an Offline Map concept for which I am using the image of map on a UIScrollView that zooms on PinchGesture, which works fine.
Problem
I have a UIButton on map. While zooming, the button does not track its position with respect to UIImageView which is being scaled.I am able to reframe the button without affecting its size. But the position is wrong.
TLDR,
I need to reproduce the mapView with annotation kinda concept on UIScrollView with UIImage on it. Can any one help?
Thanks in advance :)
I have found the answer for this. I initially stored the button value in a CGRect initialButtonFrame. Then I updated the button frame (only origins, not the size of the button size as I wanted the button not to zoom like the image ie; I button should not zoom) using the scrollview delegate
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
[self manageImageOnScrollView];//here i managed the image's coordinates and zoom
[self manageButtonCoordinatesWithRespectToImageWithScale:scale];
}
-(void)manageButtonCoordinatesWithRespectToImageWithScale:(float)scaleFactor
{
//initialButtonFrame is frame of button
self.button.frame = CGRectMake((initialButtonFrame.origin.x * scaleFactor),
(initialButtonFrame.origin.y * scaleFactor),
initialButtonFrame.size.width,
initialButtonFrame.size.height);
[self.scrollView addSubview:self.button];// I removed the button from superview while zooming and later added with updated button coordinates which I got here
}
If you know your current offset and zoom of your map, you should be able to compute the position of your button:
//Assuming your map image has its origin at 0, 0
CGPoint mapOffsetX, mapOffsetY; // these would come from your map as you calculated it.
CGPoint mapZoomFactor; // 1.0 means not zoomed, 3.0 means zooming in 3x, etc
CGPoint buttonAnchorPosition; //the position of your button on your map at 1.0 zoom
CGFloat buttonX = buttonAnchorPosition.x * mapZoomFactor + mapOffsetX;
CGFloat buttonY = buttonAnchorPosition.y * mapZoomFactor + mapOffsetY;
CGPoint buttonPosition = CGPointMake(buttonX, buttonY);
button.position = buttonPosition;
Try that, good luck

UIView in UIScrollview. frame.origin not changing on scrolling, can't detect collision

I have a screen containing:
UIView A
UIScrollView, which contains UIView B
The UIScrollView overlaps with UIView A (up until about halfway through it), allowing me to "drag" UIView B and make it overlap it with UIView A. This is so that when I let go of it, UIView B bounces nicely back into its original position (I don't want to be able to drag it all around the screen, just from its starting point onto UIView A)
Now, I'm trying to detect a collision when the UIViews overlap. However, none is ever detected. I am using convertRect:toView: to get coordinates on the same system, but from tracing UIView B's coordinates in its touchesMoved I see that "dragging" (scrolling the UIScrollView, really) doesn't affect its frame's origin coordinates. They are the same in touchedBegan as they are every time touchedMoved fires.
I'm assuming this is because UIView B isn't really moving anywhere, UIScrollView is, meaning that the UIView's origin point is always the same, even if its absolute coordinates aren't. Any suggestion on how to handle this?
There will be overlap when scrollView.frame.origin.x + viewB.frame.size.width + scrollView.contentOffset.x >= viewA.frame.origin.x
After Edit:
I couldn't get the scroll view to work so I tried it another way that worked pretty well. Instead of a scroll view, I just used a rectangular view with a background color, and added the blue square to it. I gave the blue square a set width and height in IB, centered it in the long skinny view (in the y direction), and had one other constraint to the left side of that long view. The IBOutlet leftCon is connected to that constraint. I then used this code to drag it, and have it go back when I let go:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (strong,nonatomic) UIPanGestureRecognizer *panner;
#property (strong,nonatomic) IBOutlet NSLayoutConstraint *leftCon;
#property (strong,nonatomic) CADisplayLink *displayLink;
#end
#implementation ViewController {
IBOutlet UIView *blueSquare;
}
-(void)viewDidLoad {
self.panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[blueSquare addGestureRecognizer:self.panner];
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender {
CGPoint translate = [sender translationInView:self.view];
if (self.leftCon.constant <160)
if (self.leftCon.constant > 100) NSLog(#"Bang! We have a collision");
self.leftCon.constant = translate.x;
if (sender.state == UIGestureRecognizerStateEnded){
[self startDisplayLink];
}
}
-(void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(bounceBack:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
-(void)bounceBack:(CADisplayLink *) link {
self.leftCon.constant -= 12;
if (self.leftCon.constant < 2) {
[self stopDisplayLink];
self.leftCon.constant = 2;
}
}
The numbers in the translate code (160 and 100) were determined empirically by logging. The 160 number keeps it from going off the right edge of the long view, and the 100 number is where it starts to overlap the black box.

Overriding "bitmap scaling" effect of UIScrollView zooming

In my iPad app, Universal Combat Log (new-layout branch), I have a UIView subclass (UCLLineChartView) which contains a UIScrollView and the scrollview in turn contains another UIView subclass (ChartView). ChartView has multiple sub-layers, one for each line of data that has been added to the chart. UCLLineChartView draws the axes and markers. The contents of these views/layers are entirely custom drawn, no stock views are used (e.g. UIImageView).
I'm having a problem with zooming -- it's scaling the ChartView like an image, which makes the drawn line all blurred and stretched. I want the line to stay sharp, preferably even while the user is in the act of zooming, but after 3 days of hacking at this, I cannot get it to work.
If I override setTransform on the ChartView to grab the scale factor from the transform but don't call [super setTransform], then the scrollview's zoomScale stays at 1. I tried keeping the given transform and overriding the transform method to return it. I tried replicating the effects of setTransform by changing the ChartView's center and bounds but I wasn't able to get the behaviour quite right and it still didn't seem to affect the scrollview's zoomScale. It seems that the scrollview's zoomScale depends on the effects of setTransform, but I cannot determine how.
Any assistance would be greatly appreciated.
What you will need to do is update the contentScaleFactor of the chartView. You can do that by adding the following code in either scrollViewDidEndZooming:withView:atScale: or scrollViewDidZoom:.
CGFloat newScale = scrollView.zoomScale * [[UIScreen mainScreen] scale];
[self.chartView setContentScaleFactor:newScale];
I have figured out a solution to my problem that is not too gross a hack. In your UIScrollViewDelegate:
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
[_contentView beginZoom];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
CGSize size = scrollView.bounds.size;
CGPoint contentOffset = _scrollView.contentOffset;
CGFloat newScale = _contentView.scale;
newScale = MAX(newScale, kMinZoomScale);
newScale = MIN(newScale, kMaxZoomScale);
[_scrollView setZoomScale:1.0 animated:NO];
_scrollView.minimumZoomScale = kMinZoomScale / newScale;
_scrollView.maximumZoomScale = kMaxZoomScale / newScale;
_contentView.scale = newScale;
CGSize newContentSize = CGSizeMake(size.width * newScale, size.height);
_contentView.frame = CGRectMake(0, 0, newContentSize.width, newContentSize.height);
_scrollView.contentSize = newContentSize;
[_scrollView setContentOffset:contentOffset animated:NO];
[_contentView updateForNewSize];
[_contentView setNeedsDisplay];
}
In your content view, declare a scale property and the following methods:
- (void)beginZoom
{
_sizeAtZoomStart = CGSizeApplyAffineTransform(self.frame.size, CGAffineTransformMakeScale(1/self.scale, 1));
_scaleAtZoomStart = self.scale;
}
- (void)setTransform:(CGAffineTransform)transform
{
self.scale = _scaleAtZoomStart * transform.a;
self.frame = CGRectMake(0, 0, _sizeAtZoomStart.width * self.scale, _sizeAtZoomStart.height);
[self updateForNewSize];
[self setNeedsDisplay];
}
And if your content view uses sub-layers, you'll need to disable their implicit animations by adding the following to the sub-layers' delegate(s):
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// prevent animation of the individual layers so that zooming doesn't cause weird jitter
return (id<CAAction>)[NSNull null];
}
The basic idea here is that the overridden setTransform uses the scale factor from the tranform matrix to calculate the new scale factor for the content view and then resizes the content view accordingly. The scrollview automatically adjusts the content offset to keep the content view centered.
The scrollViewDidEndZooming code keeps the zooming bounded.
There are further complexities for dealing with resizing the scrollview when rotating device for example.

Resources