how to move one direction in UIScrollView when scrolling - ios

I'm new in objective-c. I create UIScrollView object and add in my view with this code:
height = self.view.frame.size.height;
width = self.view.frame.size.width;
scrollbar = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
scrollbar.delegate = self;
scrollbar.backgroundColor = [UIColor whiteColor];
scrollbar.maximumZoomScale = 1.0;
scrollbar.minimumZoomScale = 1.0;
scrollbar.clipsToBounds = YES;
scrollbar.showsHorizontalScrollIndicator = YES;
scrollbar.pagingEnabled = YES;
[scrollbar setContentSize:CGSizeMake(width*4, height*4)];
[self.view addSubview:scrollbar];
for (int i = 1; i <= 4; i++) {
first = [[FirstViewController alloc]initWithNibName:#"FirstViewController" bundle:nil];
first.view.frame = CGRectMake((i-1)*width, 0, width, height*4);
[scrollbar addSubview:first.view];
switch (i) {
ase 1:
first.view.backgroundColor = [UIColor blueColor];
break;
case 2:
first.view.backgroundColor = [UIColor redColor];
break;
case 3:
first.view.backgroundColor = [UIColor greenColor];
break;
case 4:
first.view.backgroundColor = [UIColor grayColor];
break;
default:
break;
}
}
in my code I add 4 view in my ScrollView with different color now I want when scrolling on my ScrollView detect dx & dy (dx: driving distance on Axis.x & dy:driving distance on Axis.y) and check these two variable and when :
Notic: I want when any one touch on ScrollView and moving touch on Axis (x or y) or touch on Both Axis (x and y) check this :
if (dx > dy) disable horizontal scroll and moving in vertical side!!!
else moving in horizontal side and disable vertical scroll!!!
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGRect visibleRect = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.contentOffset.x + scrollView.bounds.size.width, scrollView.contentOffset.y + scrollView.bounds.size.height);
NSLog(#"%f,%f",visibleRect.origin.x,visibleRect.origin.y);
/*NSLog(#"x : %f",scrollView.contentOffset.x);
NSLog(#"y : %f",scrollView.contentOffset.y);*/
if (fabsf(scrollView.contentOffset.x) > fabsf(scrollView.contentOffset.y)) {
NSLog(#"Vertical Side");
}
else {
NSLog(#"Horizontal Side");
}
}
please guide me guys. I can't disable one side and move another side!!! thanks

If i understood you right, you want to disable scrolling in the direction that has been dragged less by the user. If so, UIScrollView already offers an option to do so:
scrollView.directionalLockEnabled = YES;
When this option is set to true UIScrollView will handle the correct direction lock by itself.
For more information look at the documentation: link

You can achieve the result you want adding some code to your delegate. This is my ViewController.m file. -viewDidLoad, #import statements and the other methods are omitted.
#interface ViewController () {
CGPoint initialOffset;
NSInteger direction; // 0 undefined, 1 horizontal, 2 vertical
}
#end
#implementation ViewController
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// retrieve current offset
CGPoint currentOffset = scrollView.contentOffset;
// do we know which is the predominant direction?
if (direction == 0) {
// no.
CGFloat dx = currentOffset.x - initialOffset.x;
CGFloat dy = currentOffset.y - initialOffset.y;
// we need to decide in which direction we are moving
if (fabs(dx) >= fabs(dy)) {
direction = 1; // horizontal
} else if (fabs(dy) > fabs(dx)) {
direction = 2;
}
}
// ok now we have the direction. update the offset if necessary
if (direction == 1 && currentOffset.y != initialOffset.y) {
// remove y offset
currentOffset.y = initialOffset.y;
// update
[scrollView setContentOffset:currentOffset];
} else if (direction == 2 && currentOffset.x != initialOffset.x) {
currentOffset.x = initialOffset.x;
[scrollView setContentOffset:currentOffset];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
// store the current offset
initialOffset = scrollView.contentOffset;
// reset flag
direction = 0; // AKA undefined
}
#end
When you start dragging, the delegate will reset the flag direction to "unknown" state, and store the current content offset. After every dragging move, your -scrollViewDidScroll: will be called. There, you decide which is the predominant direction (if this hasn't been done yet) and correct the current scrolling offset accordingly, by removing the x (or y) offset.
I tested this with the same settings you provided, only I used a UIImageView inside UIScrollView and I set up everything via InterfaceBuilder, but it should work fine. Theoretically, with this method you could replace directionLock, but remember that -scrollViewDidScroll is called many times during an action, and every time it rewrites the content offset (if the scrolling is happening in both directions). So if you leave directionLock enabled, you save many of the calls to setContentOffset: that the delegate performs.

Related

How can I keep the same collection view cells on the screen when zooming a UICollectionView?

I have a UICollectionView that uses a UICollectionViewLayout subclass for its layout. The layout is a simple grid.
When I zoom the collection view in or out, the positions of the cells on the screen change. In some cases, when zooming in, cells move off the screen entirely. I zoom the cells with a pinch gesture recognizer that sends x and y scale values to the layout class and then invalidates the layout.
As the cells get bigger, they move because their origins are calculated relative to the 0,0 position of the collection view.
I want to be able to zoom the collection view in, while having as many of the cells that were originally on the screen stay there. A good solution would be to have the cell in the center of the screen stay in the center as it becomes larger. Cells around the center cell would grow, and that might push them off the screen.
I've tried adjusting the collection view's content offset, but I haven't achieved what I want. I'm not quite sure how to calculate its new value, and I've learned that the changes caused by invalidateLayout do not happen immediately.
I tried a key value observer for the collection view's content size, but that caused stuttering because the changes in the KVO method happened well after the original zooming.
I've also worked a little bit with scrollToItemAtIndexPath, but the code in my full app is not guaranteed to have a cell at the exact center of the screen. That solution is less desirable for me.
Here is the code where the pinch recognizer sends changes to the layout class:
[self.gridLayout updateCellWidthScale:xScale];
[self.gridLayout updateCellHeightScale:yScale];
[self.gridLayout invalidateLayout];
Here is the code in the layout class
(numberOfRows and numberOfColumns are both set to 20):
-(id)initWithNumberOfRows:(NSUInteger)numberOfRows
andNumberOfColumns:(NSUInteger)numberOfColumns
{
self = [super init];
if (self)
{
_numberOfRows = numberOfRows;
_numberOfColumns = numberOfColumns;
_cellWidth = 80.0f;
_cellHeight = 80.0f;
_cellWidthScale = 1.0f;
_cellHeightScale = 1.0f;
}
return self;
}
-(void)updateCellWidthScale:(CGFloat)newWidthScale
{
self.cellWidthScale *= newWidthScale;
}
-(void)updateCellHeightScale:(CGFloat)newHeightScale
{
self.cellHeightScale *= newHeightScale;
}
-(CGSize)collectionViewContentSize
{
CGSize returnValue = CGSizeMake(self.numberOfColumns * self.cellWidth * self.cellWidthScale,
self.numberOfRows * self.cellHeight * self.cellHeightScale);
return returnValue;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
CGRect rect = [self frameForItemAtIndexPath:path];
attributes.size = CGSizeMake(rect.size.width, rect.size.height);
attributes.center = CGPointMake(rect.origin.x + (0.5f * rect.size.width),
rect.origin.y + (0.5f * rect.size.height));
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *returnValue = [[NSMutableArray alloc] init];
for (NSInteger i=0; i < self.numberOfRows; i++)
{
for (NSInteger j=0; j < self.numberOfColumns; j++)
{
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
CGRect frame = [self frameForItemAtIndexPath:indexPath];
if (CGRectIntersectsRect(frame, rect))
{
[returnValue addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
}
return returnValue;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGRect returnValue = CGRectMake(indexPath.section * self.cellWidth * self.cellWidthScale,
indexPath.row * self.cellHeight * self.cellHeightScale,
self.cellWidth * self.cellWidthScale,
self.cellHeight * self.cellHeightScale);
return returnValue;
}
You need to set your collectionView contentOffset to the value it was before starting to zoom multiplied the gestures scale.
Your pinch recognizer method should look like this (you need to add some more code to stop changing contentOffset when reaching the MAXIMUM_SCALE or MINIMUM_SCALE).
- (void)didReceivePinchGesture:(UIPinchGestureRecognizer*)gesture
{
static CGFloat scaleStart;
static CGPoint p;
if (gesture.state == UIGestureRecognizerStateBegan)
{
scaleStart = self.scale;
p = self.collectionView.contentOffset;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
CGFloat tempScale = scaleStart * gesture.scale;
if (tempScale < MINMUM_SCALE)
{
self.scale = MINMUM_SCALE;
}
else if (tempScale > MAXIMUM_SCALE)
{
self.scale = MAXIMUM_SCALE;
}
else
{
self.scale = tempScale ;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
self.collectionView.contentOffset = CGPointMake(0, p.y * gesture.scale);
});
}
}

UIScrollView Adjust Bounciness

Is it possible to adjust the amount of bounce in a UIScrollView?
For example, if dragging the scroll view 100pt past the end of the content moves the scroll view by 50pt, I'd like to be able to reduce the distance travelled by the same drag to 20pt.
I have experimented with a few things without success.
Adjusting the contentOffset in the scrollViewDidScroll: delegate method does not seem possible because you lose the original scrolling position when the value is set and the content offset approaches zero.
Adding a transform which translates the scroll view by some fraction of the content offset in reverse seems like a good idea. This breaks all the touch tracking and causes a mess of problems. Additionally, in many other situations, moving the scroll view would break the layout of a view.
One thing you can do is to make your own scroll view with UIView + UIPanGestureRecognizer.
(Kind of an over-kill, depending on how bad you want this custom effect.)
Here's a good explanation of Apple's rubber band algorithm.
The core of it is:
b = (1.0 – (1.0 / ((x * c / d) + 1.0))) * d
where:
x = distance from the edge
c = constant value, UIScrollView uses 0.55
d = dimension, either width or height
You can then play with the value of c and x to get the effect you desired.
Some code snippet to get you started:
- (IBAction)handlePan:(UIPanGestureRecognizer *)sender
{
static CGRect beginRect;
CGPoint translation = [sender translationInView:sender.view.superview];
CGPoint velocity = [sender velocityInView:sender.view.superview];
CGRect currentRect = CGRectOffset(beginRect, 0, translation.y);
if (sender.state == UIGestureRecognizerStateBegan) {
beginRect = sender.view.frame;
}
else if (sender.state == UIGestureRecognizerStateChanged) {
if (currentRect.origin.y > self.naturalHidePosition) {
CGFloat distanceFromEdge = currentRect.origin.y - self.naturalHidePosition;
CGFloat height = CGRectGetHeight(sender.view.frame);
CGFloat b = [self rubberBandDistance:distanceFromEdge dimension:height];
currentRect.origin.y = self.naturalHidePosition + b;
}
else if (currentRect.origin.y < self.naturalShowPosition) {
CGFloat distanceFromEdge = self.naturalShowPosition - currentRect.origin.y;
CGFloat height = CGRectGetHeight(sender.view.frame);
CGFloat b = [self rubberBandDistance:distanceFromEdge dimension:height];
currentRect.origin.y = self.naturalShowPosition - b;
}
sender.view.frame = currentRect;
}
else if (sender.state == UIGestureRecognizerStateEnded) {
if (velocity.y < 0) {
currentRect.origin.y = self.naturalShowPosition;
}
else if (velocity.y > 0) {
currentRect.origin.y = self.naturalHidePosition;
}
else if (currentRect.origin.y > (0.5 * (self.naturalShowPosition + self.naturalHidePosition))) {
currentRect.origin.y = self.naturalHidePosition;
}
else {
currentRect.origin.y = self.naturalShowPosition;
}
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
sender.view.frame = currentRect;
} completion:nil];
}
}
- (CGFloat)rubberBandDistance:(CGFloat)distanceFromEdge dimension:(CGFloat)d
{
CGFloat c = 0.55;
CGFloat b = (1.0 - (1.0 / ((distanceFromEdge * c / d) + 1.0))) * d;
return b;
}

UIKit Dynamics: Attachment inside UITableViewCell

My table view cells contain a circle in an UIView, indicating a value. I want to add the UIKit Dynamics attachment behaviour to that circle in order to for it to lag a bit when scrolling.
I don't want to attach the individual cells to each other but only the circle view to the UITableViewCell. The rest of the cell should scroll as usual.
Problem: The UITableViewCell has its origin always at (0, 0). How can I add the circle to a view that actually does move when scrolling?
I finally got it to work. The UITableView moves the coordinate system of every cell and of all views contained within that cell. Therefor I needed to manually move my view inside the UITableViewCell during scrolling while still referring to the initial anchor point.
The table view controller:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
BOOL scrollingUp = '\0';
if (self.lastContentOffset > scrollView.contentOffset.y) {
scrollingUp = YES;
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
scrollingUp = NO;
}
NSInteger offset = 64; // To compensate for the navigation bar.
if (scrollingUp) {
offset = offset - scrollView.contentOffset.y;
}
else {
offset = offset + scrollView.contentOffset.y;
}
// Limit the offset so the views will not disappear during fast scrolling.
if (offset > 10) {
offset = 10;
}
else if (offset < -10) {
offset = -10;
}
// lastContentOffset is an instance variable.
self.lastContentOffset = scrollView.contentOffset.y;
for (UITableViewCell *cell in self.tableView.visibleCells) {
// Use CoreAnimation to prohibit flicker.
[UIView beginAnimations:#"Display notification" context:nil];
[UIView setAnimationDuration:0.5f];
[UIView setAnimationBeginsFromCurrentState:YES];
cell.view.frame = CGRectMake(cell.view.frame.origin.x, offset, cell.view.frame.size.width, cell.view.frame.size.height);
[UIView commitAnimations];
[cell.dynamicAnimator updateItemUsingCurrentState:cell.view];
}
}
The table view cell:
-(void)layoutSubviews {
[super layoutSubviews];
// _view is the animated UIView.
UIDynamicItemBehavior *viewBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[_view]];
viewBehavior.elasticity = 0.9f;
UIAttachmentBehavior *attachmentBehaviorView = [[UIAttachmentBehavior alloc] initWithItem:_view attachedToAnchor:CGPointMake(_anchorView.frame.origin.x + _anchorView.frame.size.width / 2.0f, _anchorView.frame.origin.y + _anchorView.frame.size.height / 2.0f)];
attachmentBehaviorView.damping = 8.0f;
attachmentBehaviorView.frequency = 4.0f;
attachmentBehaviorView.length = 0.0f;
[_dynamicAnimator addBehavior:viewBehavior];
[_dynamicAnimator addBehavior:attachmentBehaviorView];
}
You can change the anchorPoint of UIAttachmentBehavior during -[scrollViewDidScroll:]. You may refer to the following code snippet:
- (void)viewDidLoad
{
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UIAttachmentBehavior *behavior1 = [[UIAttachmentBehavior alloc] initWithItem:self.circleView
attachedToAnchor:[self tableViewAnchor]];
behavior1.length = 10.0;
behavior1.damping = 0.3;
behavior1.frequency = 2.5;
[animator addBehavior:behavior1];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
behavior1.anchorPoint = [self.tableView convertPoint:[self tableViewAnchor] toView:self.view];
}
- (CGPoint)tableViewAnchor
{
return CGPointMake(160.0, 154.0); // return your target coordination w.r.t. the table view
}
Preview:

Use UIPinchGestureRecognizer to scale view in direction of pinch [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
I needed a Pinch Recognizer that would scale in x, or y, or both directions depending on the direction of the pinch. I looked through many of the of the other questions here and they only had parts of the answer. Here's my complete solution that uses a custom UIPinchGestureRecognizer.
I created a custom version of a UIPinchGestureRecognizer. It uses the slope of line between the two fingers to determine the direction of the scale. It does 3 types: Vertical; Horizontal; and Combined(diagonal). Please see my notes at the bottom.
-(void) scaleTheView:(UIPinchGestureRecognizer *)pinchRecognizer
{
if ([pinchRecognizer state] == UIGestureRecognizerStateBegan || [pinchRecognizer state] == UIGestureRecognizerStateChanged) {
if ([pinchRecognizer numberOfTouches] > 1) {
UIView *theView = [pinchRecognizer view];
CGPoint locationOne = [pinchRecognizer locationOfTouch:0 inView:theView];
CGPoint locationTwo = [pinchRecognizer locationOfTouch:1 inView:theView];
NSLog(#"touch ONE = %f, %f", locationOne.x, locationOne.y);
NSLog(#"touch TWO = %f, %f", locationTwo.x, locationTwo.y);
[scalableView setBackgroundColor:[UIColor redColor]];
if (locationOne.x == locationTwo.x) {
// perfect vertical line
// not likely, but to avoid dividing by 0 in the slope equation
theSlope = 1000.0;
}else if (locationOne.y == locationTwo.y) {
// perfect horz line
// not likely, but to avoid any problems in the slope equation
theSlope = 0.0;
}else {
theSlope = (locationTwo.y - locationOne.y)/(locationTwo.x - locationOne.x);
}
double abSlope = ABS(theSlope);
if (abSlope < 0.5) {
// Horizontal pinch - scale in the X
[arrows setImage:[UIImage imageNamed:#"HorzArrows.png"]];
arrows.hidden = FALSE;
// tranform.a = X-axis
NSLog(#"transform.A = %f", scalableView.transform.a);
// tranform.d = Y-axis
NSLog(#"transform.D = %f", scalableView.transform.d);
// if hit scale limit along X-axis then stop scale and show Blocked image
if (((pinchRecognizer.scale > 1.0) && (scalableView.transform.a >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.a <= 0.1))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale along X-axis
scalableView.transform = CGAffineTransformScale(scalableView.transform, pinchRecognizer.scale, 1.0);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
}else if (abSlope > 1.7) {
// Vertical pinch - scale in the Y
[arrows setImage:[UIImage imageNamed:#"VerticalArrows.png"]];
arrows.hidden = FALSE;
NSLog(#"transform.A = %f", scalableView.transform.a);
NSLog(#"transform.D = %f", scalableView.transform.d);
// if hit scale limit along Y-axis then don't scale and show Blocked image
if (((pinchRecognizer.scale > 1.0) && (scalableView.transform.d >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.d <= 0.1))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale along Y-axis
scalableView.transform = CGAffineTransformScale(scalableView.transform, 1.0, pinchRecognizer.scale);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
} else {
// Diagonal pinch - scale in both directions
[arrows setImage:[UIImage imageNamed:#"CrossArrows.png"]];
blocked.hidden = TRUE;
arrows.hidden = FALSE;
NSLog(#"transform.A = %f", scalableView.transform.a);
NSLog(#"transform.D = %f", scalableView.transform.d);
// if we have hit any limit don't allow scaling
if ((((pinchRecognizer.scale > 1.0) && (scalableView.transform.a >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.a <= 0.1))) || (((pinchRecognizer.scale > 1.0) && (scalableView.transform.d >= 2.0)) || ((pinchRecognizer.scale < 1.0) && (scalableView.transform.d <= 0.1)))) {
blocked.hidden = FALSE;
arrows.hidden = TRUE;
} else {
// scale in both directions
scalableView.transform = CGAffineTransformScale(scalableView.transform, pinchRecognizer.scale, pinchRecognizer.scale);
pinchRecognizer.scale = 1.0;
blocked.hidden = TRUE;
arrows.hidden = FALSE;
}
} // else for diagonal pinch
} // if numberOfTouches
} // StateBegan if
if ([pinchRecognizer state] == UIGestureRecognizerStateEnded || [pinchRecognizer state] == UIGestureRecognizerStateCancelled) {
NSLog(#"StateEnded StateCancelled");
[scalableView setBackgroundColor:[UIColor whiteColor]];
arrows.hidden = TRUE;
blocked.hidden = TRUE;
}
}
Remember to add the protocol to the view controller header file:
#interface WhiteViewController : UIViewController <UIGestureRecognizerDelegate>
{
IBOutlet UIView *scalableView;
IBOutlet UIView *mainView;
IBOutlet UIImageView *arrows;
IBOutlet UIImageView *blocked;
}
#property (strong, nonatomic) IBOutlet UIView *scalableView;
#property (strong, nonatomic) IBOutlet UIView *mainView;
#property (strong, nonatomic)IBOutlet UIImageView *arrows;
#property (strong, nonatomic)IBOutlet UIImageView *blocked;
-(void) scaleTheView:(UIPinchGestureRecognizer *)pinchRecognizer;
#end
And add the recognizer in the viewDidLoad:
- (void)viewDidLoad
{
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(scaleTheView:)];
[pinchGesture setDelegate:self];
[mainView addGestureRecognizer:pinchGesture];
arrows.hidden = TRUE;
blocked.hidden = TRUE;
[scalableView setBackgroundColor:[UIColor whiteColor]];
}
This is set up to use the main view to capture the pinch; and manipulate a second view. This way you can still scale it as the view gets small. You can change it to react directly to the scalable view.
LIMITS: I arbitrarily chose the starting size of my view so a scale limit of 2.0 would equal full screen. My lower scale is set at 0.1.
USER INTERACTION: I mess around with a lot of user interaction things like changing the view's background color and adding/changing arrows over the view to show direction. It's important to give them feedback during the scaling process, especially when changing directions like this codes allows.
BUG: There is a bug in Apple's UIPinchGestureRecognizer. It registers UIGestureRecognizerStateBegan with the touch of 2 fingers as you would expect. But once it is in StateBegan or StateChanged you can lift one finger and the state remains. It doesn't move to StateEnded or StateCancelled until BOTH fingers are lifted. This created a bug in my code and many headaches! The if numberOfTouches > 1 fixes it.
FUTURE: You can change the slope settings to scale in just one direction, or just 2. If you add the arrows images, you can see them change as you rotate your fingers.
Here's a solution in Swift:
extension UIPinchGestureRecognizer {
func scale(view: UIView) -> (x: CGFloat, y: CGFloat)? {
if numberOfTouches() > 1 {
let touch1 = self.locationOfTouch(0, inView: view)
let touch2 = self.locationOfTouch(1, inView: view)
let deltaX = abs(touch1.x - touch2.x)
let deltaY = abs(touch1.y - touch2.y)
let sum = deltaX + deltaY
if sum > 0 {
let scale = self.scale
return (1.0 + (scale - 1.0) * (deltaX / sum), 1.0 + (scale - 1.0) * (deltaY / sum))
}
}
return nil
}
}
This alternative solution determines the direction of scaling based on bearing angle rather than slope. I find it a bit easier to adjust the different zones using angle measurements.
#objc func viewPinched(sender: UIPinchGestureRecognizer) {
// Scale the view either vertically, horizontally, or diagonally based on the axis of the initial pinch
let locationOne = sender.location(ofTouch: 0, in: sender.view)
let locationTwo = sender.location(ofTouch: 1, in: sender.view)
let diffX = locationOne.x - locationTwo.x
let diffY = locationOne.y - locationTwo.y
// Break the plane into 3 equal segments
// Inverse tangent will return between π/2 and -π/2. Absolute value can be used to only consider 0 to π/2 - don't forget to handle divide by 0 case
// Breaking π/2 into three equal pieces, we get regions of 0 to π/6, π/6 to 2π/6, and 2π/6 to π/2 (note 2π/6 = π/3)
// Radian reminder - π/2 is 90 degreees :)
let bearingAngle = diffY == 0 ? CGFloat.pi / 2.0 : abs(atan(diffX/diffY))
if sender.state == .began {
// Determine type of pan based on bearing angle formed by the two touch points.
// Only do this when the pan begins - don't change type as the user rotates their fingers. Require a new gesture to change pan type
if bearingAngle < CGFloat.pi / 6.0 {
panType = .vertical
} else if bearingAngle < CGFloat.pi / 3.0 {
panType = .diagonal
} else if bearingAngle <= CGFloat.pi / 2.0 {
panType = .horizontal
}
}
// Scale the view based on the pan type
switch panType {
case .diagonal: transform = CGAffineTransform(scaleX: sender.scale, y: sender.scale)
case .horizontal: transform = CGAffineTransform(scaleX: sender.scale, y: 1.0)
case .vertical: transform = CGAffineTransform(scaleX: 1.0, y: sender.scale)
}
}

content jumps on zooming out with UIScrollView

I want help with my UIScrollView sample.
I created a simple program that scrolls and zooms the content (UIImageView). It works fine, except that the content frequently disappears to the right-bottom when I try zooming out. But since I set minimumZoomScale to 1.0f, it is actually not zooming out, only the content is jumping out of the view. And what is even more weird is that I cannot scroll up after this. Apparently content size is messed up as well.
The setup I have in my sample code is as in the figure below.
When I checked the status after (trying) zooming out, I found two wrong things.
_scrollView.contentSize is 480x360, which should not be smaller than 1000x1000
_scrollView.bounds jumped to the top somehow (i.e., _scrollView.bounds.origin.y is always 0)
To cope with the two items above, I added following code in my UIScrollViewDelegate and now it works fine.
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
if(scrollView == _scrollView && view == _contentView)
{
// Setting ivars for scrollViewDidZoom
_contentOffsetBeforeZoom = _scrollView.contentOffset;
_scrollViewBoundsBeforeZoom = _scrollView.bounds;
}
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if(scrollView == _scrollView)
{
// If you zoom out, there are cases where ScrollView content size becomes smaller than original,
// even though minimum zoom scale = 1. In that case, it will mess with the contentOffset as well.
if(_scrollView.contentSize.width < CONTENT_WIDTH || _scrollView.contentSize.height < CONTENT_HEIGHT)
{
_scrollView.contentSize = CGSizeMake(CONTENT_WIDTH, CONTENT_HEIGHT);
_scrollView.contentOffset = _contentOffsetBeforeZoom;
}
// If you zoom out, there are cases where ScrollView bounds goes outsize of contentSize rectangle.
if(_scrollView.bounds.origin.x + _scrollView.bounds.size.width > _scrollView.contentSize.width ||
_scrollView.bounds.origin.y + _scrollView.bounds.size.height > _scrollView.contentSize.height)
{
_scrollView.bounds = _scrollViewBoundsBeforeZoom;
}
}
}
However, does it need to come down to this? This is a very simple sequence, and it is hard to believe that Apple requires us to put this kind of effort. So, my bet is I am missing something here...
Following is my original code. Please help me find what I am doing wrong (or missing something)!
#define CONTENT_WIDTH 1000
#define CONTENT_HEIGHT 1000
>>>> Snip >>>>
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 460)];
_scrollView.contentSize = CGSizeMake(CONTENT_WIDTH, CONTENT_HEIGHT);
_scrollView.backgroundColor = [UIColor blueColor];
_scrollView.maximumZoomScale = 8.0f;
_scrollView.minimumZoomScale = 1.0f;
_scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
_scrollView.scrollEnabled = YES;
_scrollView.delegate = self;
[self.view addSubview:_scrollView];
_contentView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"sample.jpg"]]; // sample.jpg is 480x360
CGPoint center = (CGPoint){_scrollView.contentSize.width / 2, _scrollView.contentSize.height / 2};
_contentView.center = center;
[_scrollView addSubview:_contentView];
_scrollView.contentOffset = (CGPoint) {center.x - _scrollView.bounds.size.width / 2, center.y - _scrollView.bounds.size.height / 2};
}
- (UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if(scrollView == _scrollView)
{
return _contentView;
}
return nil;
}
I created a quick sample project and had the same issue you described using the code you pasted. I don't exactly know what the "proper" way to zoom is in iOS but I found this tutorial which says that you need to recenter your contentView after the scrollView has been zoomed. I would personally expect it to be automatically re-centered given that it is the view you're returning in the viewForZoomingInScrollView delegate method but apparently not.
- (void)centerScrollViewContents {
CGSize boundsSize = _scrollView.bounds.size;
CGRect contentsFrame = _contentView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
_contentView.frame = contentsFrame;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// The scroll view has zoomed, so we need to re-center the contents
[self centerScrollViewContents];
}
The code above is not written by me but is simply copied from the tutorial. I think its pretty straightforward. Also, centring the contentView seems to be a lot more elegant then constantly changing the bounds and content size of the scrollview so give it a try.
If anyone is having an issue of bouncing when you zooming out resulting background to show, try removing bounces (Bounces Zoom) in Interface Builder.
I was able to fix this problem using the delegate answer that adjusted the rates after zoom... but then I remembered I was using auto-layout, and just adding constraints for centering horizontally and vertically (in addition to the constraints tying the image to each edge of the scroll view) solved the issue for me without using the delegate methods.
Olshansk answer in swift 5
func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerScrollViewContents()
}
func centerScrollViewContents() {
let boundsSize = scrollView.bounds.size;
var contentsFrame = container.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0;
} else {
contentsFrame.origin.x = 0.0;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0;
} else {
contentsFrame.origin.y = 0.0;
}
container.frame = contentsFrame;
}

Resources