I'm using storyboard (iOS 6.0) to create a photo gallery viewer for my app. This is how my imageViewController is set up in storyboard:
I've made sure to enable userInteraction and multiple touches on both the imageView and scrollView. What I want to do is, on pinch I want to zoom into the imageView (maximum scale 3) and be able to pan around. This is what I currently have, however, even though the pinch gesture is detected the scale does not change.
- (IBAction)imagePinched:(id)sender {
if (pinchRecognizer.state == UIGestureRecognizerStateEnded || pinchRecognizer.state == UIGestureRecognizerStateChanged) {
NSLog(#"gesture.scale = %f", pinchRecognizer.scale);
CGFloat currentScale = self.fullScreenView.frame.size.width / self.fullScreenView.bounds.size.width;
CGFloat newScale = currentScale * pinchRecognizer.scale;
if (newScale < 1) {
newScale = 1;
}
if (newScale > 3) {
newScale = 3;
}
CGAffineTransform transform = CGAffineTransformMakeScale(newScale, newScale);
self.fullScreenView.transform = transform;
pinchRecognizer.scale = 1;
}
}
Most questions and tutorials online deal with programmatically creating the views and doing this, but the less code the better (in my eyes). What's the best way to get this to work with storyboard? Thank you in advance!!!
UPDATED:
Here is my full .m file code:
- (void)viewDidLoad
{
[super viewDidLoad];
//Assign an image to this controller's imageView
fullScreenView.image = [UIImage imageNamed:imageString];
//Allows single and double tap to work
[singleTapRecognizer requireGestureRecognizerToFail: doubleTapRecognizer];
}
- (IBAction)imageTapped:(id)sender {
NSLog(#"Image Tapped.");
//On tap, fade out viewController like the twitter.app
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)imageDoubleTapped:(id)sender {
NSLog(#"Image Double Tapped.");
//On double tap zoom into imageView to fill in the screen.
[fullScreenView setContentMode:UIViewContentModeScaleAspectFill];
}
- (IBAction)imagePinched:(id)sender {
if (pinchRecognizer.state == UIGestureRecognizerStateEnded || pinchRecognizer.state == UIGestureRecognizerStateChanged) {
NSLog(#"gesture.scale = %f", pinchRecognizer.scale);
CGFloat currentScale = self.fullScreenView.frame.size.width / self.fullScreenView.bounds.size.width;
CGFloat newScale = currentScale * pinchRecognizer.scale;
if (newScale < 1) {
newScale = 1;
}
if (newScale > 3) {
newScale = 3;
}
CGAffineTransform transform = CGAffineTransformMakeScale(newScale, newScale);
self.fullScreenView.transform = transform;
pinchRecognizer.scale = 1;
}
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.fullScreenView;
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
I think better solution in Apple Documentation
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.scrollView.minimumZoomScale=0.5;
self.scrollView.maximumZoomScale=6.0;
self.scrollView.contentSize=CGSizeMake(1280, 960);
self.scrollView.delegate=self;
}
Check Apple Documentation
The first step is to make sure your views have the correct delegates implemented. For example in the .m file
#interface myRootViewController () <.., UIGestureRecognizerDelegate, UIScrollViewDelegate, ...>
From the documentation, make sure you have this implemented:
The UIScrollView class can have a delegate that must adopt the UIScrollViewDelegate protocol. For zooming and panning to work, the delegate must implement both viewForZoomingInScrollView: and scrollViewDidEndZooming:withView:atScale:; in addition, the maximum (maximumZoomScale) and minimum ( minimumZoomScale) zoom scale must be different.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.fullScreenView;
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{}
The scrollViewDidEndZooming method can stay empty for now. If you already did that or it still doesnt work, please post more of your code and then it is easier to help more specifically.
You need to do what Wadi suggested above but also set the setMinimumZoomScale to be different from setMaximumZoomScale. They are both 1.0f by default.
After that, the UIImageView should be pinchable
I have created a fully working demo application (similar in nature to Facebook's default photo gallery) which demonstrates pinch to zoom of Image View nested inside a scroll view with AutoLayout and storyboards. View my project here: http://rexstjohn.com/facebook-like-ios-photo-modal-gallery-swipe-gestures/.
It also includes async photo loading via MKNetworkKit and gesture based swiping through a photo gallery. Enjoy and I hope it saves everyone time trying to figure this out because it is somewhat annoying.
Here is how Apple recomends to do what you want to do. I used the code provided by Apple and it worked like a charm! If you want to optimize memory management, you can use iCarousel (made by nicklockwood), which have a cache management for dealloc unused views!
I've created a class to handle zooming of UIImageViews similar to Instagram. Might be what you're looking for, otherwise you can look at how I achieved it. https://github.com/twomedia/TMImageZoom
Related
I'm not talking about how pinch to zooming works here.
Any one noticed Instagram's zooming? It works as a default pinch to work but when you start zooming in it imageView expands to the whole screen of device, how do they it so smoothly? Anyone know can point out that?
I created a simple Github project that does exactly what you're after. Check it out here:
https://github.com/twomedia/TMImageZoom
I achieved this by creating a class that handled the gesture state changes. I then create a UIImageView on the window and position/size it to the original image view creating the effect of the user zooming the imageView. Here's a small snippet from the above link:
-(void) gestureStateChanged:(id)gesture withZoomImageView:(UIImageView*)imageView {
// Insure user is passing correct UIPinchGestureRecognizer class.
if (![gesture isKindOfClass:[UIPinchGestureRecognizer class]]) {
NSLog(#"(TMImageZoom): Must be using a UIPinchGestureRecognizer, currently you're using a: %#",[gesture class]);
return;
}
UIPinchGestureRecognizer *theGesture = gesture;
// Prevent animation issues if currently animating reset.
if (isAnimatingReset) {
return;
}
// Reset zoom if state = UIGestureRecognizerStateEnded
if (theGesture.state == UIGestureRecognizerStateEnded || theGesture.state == UIGestureRecognizerStateCancelled || theGesture.state == UIGestureRecognizerStateFailed) {
[self resetImageZoom];
}
// Ignore other views trying to start zoom if already zooming with another view
if (isHandlingGesture && hostImageView != imageView) {
NSLog(#"(TMImageZoom): 'gestureStateChanged:' ignored since this imageView isnt being tracked");
return;
}
// Start handling gestures if state = UIGestureRecognizerStateBegan and not already handling gestures.
if (!isHandlingGesture && theGesture.state == UIGestureRecognizerStateBegan) {
isHandlingGesture = YES;
// Set Host ImageView
hostImageView = imageView;
imageView.hidden = YES;
// Convert local point to window coordinates
CGPoint point = [imageView convertPoint:imageView.frame.origin toView:nil];
startingRect = CGRectMake(point.x, point.y, imageView.frame.size.width, imageView.frame.size.height);
// Post Notification
[[NSNotificationCenter defaultCenter] postNotificationName:TMImageZoom_Started_Zoom_Notification object:nil];
// Get current window and set starting vars
UIWindow *currentWindow = [UIApplication sharedApplication].keyWindow;
firstCenterPoint = [theGesture locationInView:currentWindow];
// Init zoom ImageView
currentImageView = [[UIImageView alloc] initWithImage:imageView.image];
currentImageView.contentMode = UIViewContentModeScaleAspectFill;
[currentImageView setFrame:startingRect];
[currentWindow addSubview:currentImageView];
}
// Reset if user removes a finger (Since center calculation would cause image to jump to finger as center. Maybe this could be improved later)
if (theGesture.numberOfTouches < 2) {
[self resetImageZoom];
return;
}
// Update scale & center
if (theGesture.state == UIGestureRecognizerStateChanged) {
NSLog(#"gesture.scale = %f", theGesture.scale);
// Calculate new image scale.
CGFloat currentScale = currentImageView.frame.size.width / startingRect.size.width;
CGFloat newScale = currentScale * theGesture.scale;
[currentImageView setFrame:CGRectMake(currentImageView.frame.origin.x, currentImageView.frame.origin.y, startingRect.size.width*newScale, startingRect.size.height*newScale)];
// Calculate new center
UIWindow *currentWindow = [UIApplication sharedApplication].keyWindow;
int centerXDif = firstCenterPoint.x-[theGesture locationInView:currentWindow].x;
int centerYDif = firstCenterPoint.y-[theGesture locationInView:currentWindow].y;
currentImageView.center = CGPointMake((startingRect.origin.x+(startingRect.size.width/2))-centerXDif, (startingRect.origin.y+(startingRect.size.height/2))-centerYDif);
// Reset gesture scale
theGesture.scale = 1;
}
}
I would recommend to use third-party library for this. They are always easy to integrate & use.
Try to check on these:
https://github.com/NYTimes/NYTPhotoViewer
This one is not inspiring confidence https://github.com/StuartMorris0/SPMZoomableUIImageView
Official apple Guide
Rey Wenderlish guides are pretty nice too
In any case, just Google it! and good luck!
One way to replicate this behavior might be to add a UIPinchGestureRecognizer to your UIImageView. Then use the scale property of the UIPinchGestureRecognizer and scale the frame of the UIImageView accordingly. example
I have two UIScrollViews. Both the scroll views contain an image. Now, I want to zoom second scroll view programmatically if I zoom in the first scroll view manually. I am using -(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView method to manually zoom the first scroll view. The second scroll view should be zoomed to the same point and scale. How can I achieve this?
By using the tag or object of those two scrollviews you can identify the which scroll view you need zoom in viewForZoomingInScrollView method.
e.g
//need to set the tag for scrollviews
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if(scrollView.tag == 1)
{
return firstImageView;
}
return secondImageView;
}
OR
//need to create the object of used scrollviews
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if(scrollView == objScrollViewFirst)
{
return firstImageView;
}
return secondImageView;
}
How about something like this:
CGRect zoomRect;
zoomRect.size.height = manualScrollView.frame.size.height / manualScrollView.zoomScale;
zoomRect.size.width = manualScrollView.frame.size.width / manualScrollView.zoomScale;
zoomRect.origin.x = manualScrollView.center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = manualScrollView.center.y - (zoomRect.size.height / 2.0);
[autoScrollView zoomToRect:zoomRect animated:YES];
[autoScrollView setZoomScale:manualScrollView.zoomScale animated:YES];
Try something like this in the UIScrollViewDelegate method:
override func scrollViewDidScroll(scrollView: UIScrollView){
otherScrollView.contentOffset = scrollView.contentOffset
otherScrollView.zoomScale = scrollView.zoomScale
}
I'm trying to move a view while the scrollview is being scrolled with the same intensity.
I'm trying:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// here I detect when user scroll
}
How could I get the intensity that the scrollview is being scrolled?
Try to use these methods:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
//scrolling on
NSLog(#"scrollViewWillBeginDragging");
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if(!decelerate) {
//not scrolling
}
NSLog(#"scrollViewDidEndDragging");
}
You can do that by reading the contentOffset property on the scrollView
// You can save this as a property or an iVar
CGPoint lastContentOffset;
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
lastContentOffset = scrollView.contentOffset;
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint difference = CGPointSubstract(scrollView.contentOffset, lastContentOffset);
lastContentOffset = scrollView.contentOffset;
// Use difference to know by how much the scroll view scrolled
// IMPORTANT: CGPointSubstract is not a standard function, you can implement it yourself.
}
I don't want to give you the full code but i'll help with how to achieve it.
1) You need to use the time.
2) When the time difference is greater than an amount of time passed to measure (0.05s?)
3) Check what the old content offset is compared to the new one
4) The difference is your velocity
5) reset your current timer
6) set current offsets to last offset
Hope this helps
Ben
distance calcs
CGFloat distanceY = currentOffset.y - lastOffset.y;
CGFloat distanceX = currentOffset.X - lastOffset.X;
I want to achieve something very close to what Google Maps (iOS) does and I have some doubts.
But first, a better explanation and things to take into account:
-------------- ANSWER --------------
Thanks to Jugale's contribution, here's a repository so everybody can download and test everything out.
https://github.com/vCrespoP/VCSlidingView
-------------- ORIGINAL QUESTION -----------
You tap in a point inside the map, a view comes in from the bottom but then when you interact with that summary view:
Notice when pulling just a bit, the navigation bar already has set.
When you have scrolled it to the top, you can continue scrolling and the inner scrollview will continue scrolling.
When you 'reverse' the action to dismiss the view, the PanGesture doesn't mess up with the inner scrollView (same for the other way, scrollView VS dismiss)
Here it is in action:
Doubts:
I've tried to do it as an interactive transition (UIPercentDrivenInteractiveTransition) and separating Map from Details in 2 controllers but I'm having troubles with the UIPanGesture interfering with the scrollView.
Maybe it's better to do it as a subview and handle everything there? More or less like MBPullDownController (Although it has some issues with iOS8) -> https://github.com/matej/MBPullDownController
So, anybody knows any framework, has done it, or knows how to do this in a good way?
Thank you for your time :D
Looking through my implementation it seems the following are true:
I have a subclass of UIViewController that is the view controller
I have a subclass of UIView that is the overlay (and henceforth with the known as "the overlay") (actually for me this is a UIScrollView because it needs to go sideways too, but I'll try and filter out the unnecessary code)
I have another subclass of UIView that loads the overlay's content ("the content wrapper")
The content wrapper has a UIScrollView property, in which all other views are loaded ("the content view")
The view controller is responsible for initializing the overlay, setting it's initial frame (where the height is the height of the screen) and passing content to it, nothing more.
From it's -initWithFrame method, the overlay sets itself up with a UIDynamicItemBehavior. It also creates some UICollisionBehavior objects: one at the top of the screen and one below the bottom of the screen at just the right y position for the top of the overlay to be partially visible (as seen in the first frame of your GIF). A UIGravityBehavior is also set up to keep the overlay sitting on the lower collision boundary. Of course, _animator = [[UIDynamicAnimator alloc... is set up too.
Finally:
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan)];
_pan.delegate = self;
_pan.cancelsTouchesInView = FALSE;
The overlay class also has some other helpful methods such as changing the gravity's direction so that the overlay can appear to snap to the top or bottom of the screen.
The _pan handler uses a UISnapBehavior to keep the overlay moving dynamically up and down the screen underneath the user's finger:
- (void)handlePan
{
[self handlePanFromPanGestureRecogniser:_pan];
}
- (void)handlePanFromPanGestureRecogniser:(UIPanGestureRecognizer *)pan
{
CGFloat d = [pan velocityInView:self.superview.superview].y;
CGRect r = self.frame;
r.origin.y = r.origin.y + (d*0.057);
if (r.origin.y < 20)
{
r.origin.y = 20;
}
else if (r.origin.y > [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT)
{
r.origin.y = [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT;
}
if (pan.state == UIGestureRecognizerStateEnded)
{
[self panGestureEnded];
}
else if (pan.state == UIGestureRecognizerStateBegan)
{
[self snapToBottom];
[self removeGestureRecognizer:_tap];
}
else
{
[_animator removeBehavior:_findersnap];
_findersnap = [[UISnapBehavior alloc] initWithItem:self snapToPoint:CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r))];
[_animator addBehavior:_findersnap];
}
}
- (void)panGestureEnded
{
[_animator removeBehavior:_findersnap];
CGPoint vel = [_dynamicSelf linearVelocityForItem:self];
if (fabsf(vel.y) > 250.0)
{
if (vel.y < 0)
{
[self snapToTop];
}
else
{
[self snapToBottom];
}
}
else
{
if (self.frame.origin.y > (self.superview.bounds.size.height/2))
{
[self snapToBottom];
}
else
{
[self snapToTop];
}
}
}
The content wrapper listens for scroll events generated by the content view:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//this is our fancy way of getting the pan to work when the scrollview is in the way
if (scrollView.contentOffset.y <= 0 && _dragging)
{
_shouldForwardScrollEvents = TRUE;
}
if (_shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapper:isForwardingGestureRecogniserTouches:)])
{
[_delegate theContentWrapper:self isForwardingGestureRecogniserTouches:scrollView.panGestureRecognizer];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_dragging = FALSE;//scrollviewdidscroll must not be called after this
if (scrollView.contentOffset.y <= 0 || _shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapperStoppedBeingDragged:)])
{
[_delegate theContentWrapperStoppedBeingDragged:self];
}
}
_shouldForwardScrollEvents = FALSE;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_dragging = TRUE;
}
As you can see, when the bool shouldForwardScrollEvents is TRUE then we send scrollView.panGestureRecognizer to the content wrapper's delegate (the overlay). The overlay implements the delegate methods like so:
- (void)theContentWrapper:(TheContentWrapper *)contentWrapper isForwardingGestureRecogniserTouches:(UIPanGestureRecognizer *)contentViewPan
{
[self handlePanFromPanGestureRecogniser:contentViewPan];
}
- (void)theContentWrapperStoppedBeingDragged:(TheContentWrapper *)contentWrapper
{
//because the scrollview internal pan doesn't tell use when it's state == ENDED
[self panGestureEnded];
}
Hopefully at least some of this is useful to someone!
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.