iOS: ScrollView infinite paging - Duplicate end caps - ios

I have a question about infinite paging in a ScrollView. In my app I have only 3 subviews in ScrollView. Each subview is loaded from xib file. Normally it looks like ABC in ScrollView. I wanted to make infinite paging so I added end caps and now it looks like CABCA. If the user is on the first C, it jumps to regular C and if user is on the last A, it jumps to regular A. Here is a code:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender {
if (scrollView.contentOffset.x == 0)
{
[scrollView scrollRectToVisible:CGRectMake
((scrollView.frame.size.width * 3), 0,
scrollView.frame.size.width,
scrollView.frame.size.height) animated:NO];
}
else if (scrollView.contentOffset.x == scrollView.frame.size.width * 4)
{
[scrollView scrollRectToVisible:CGRectMake
(scrollView.frame.size.width, 0,
scrollView.frame.size.width,
scrollView.frame.size.height) animated:NO];
}
}
It works perfectly now. But I have ViewController for each subview and this is how I am adding them to ScrollView:
subViewController1 = [[SubViewController1 alloc] initWithNibName:#"SubView" bundle:nil];
subViewController1.view.frame =
CGRectMake(0, 0, scrollView.frame.size.width, scrollView.frame.size.height);
[scrollView addSubview:subViewController1.view];
Problem is that there is one duplicate of A and C view so now I have 5 controllers instead of 3. And If I want to add something into a A view, I have to add it also into duplicate of A view.
Is there a way how to control view A and duplicate of A with one controller so I don't have to create two instances of one controller? Thank you.

Better still, you don't need to have duplicate view A's and duplicate view C's, you just move them around in - (void)scrollViewDidScroll:(UIScrollView *)scrollView while manipulating the contentOffset.
Setup: probably very similar to how you already do it.
Set your UIScrollView to have contentSize 3 times it's bounds width. Make sure paging is turned on and bouncing off.
Add your ABC subviews to the UIScrollView from left to right.
Also have an array in your ViewController called _contentViews
that contains your UIViews ABC.
Then implement this which will reset the content offset and move your subviews around at the same time when the scrollview reaches the edges:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
if(scrollView.contentOffset.x == 0) {
CGPoint newOffset = CGPointMake(scrollView.bounds.size.width+scrollView.contentOffset.x, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsRight];
}
else if(scrollView.contentOffset.x == scrollView.bounds.size.width*2) {
CGPoint newOffset = CGPointMake(scrollView.contentOffset.x-scrollView.bounds.size.width, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsLeft];
}
}
-(void)rotateViewsRight {
UIView *endView = [_contentViews lastObject];
[_contentViews removeLastObject];
[_contentViews insertObject:endView atIndex:0];
[self setContentViewFrames];
}
-(void)rotateViewsLeft {
UIView *endView = _contentViews[0];
[_contentViews removeObjectAtIndex:0];
[_contentViews addObject:endView];
[self setContentViewFrames];
}
-(void) setContentViewFrames {
for(int i = 0; i < 3; i++) {
UIView * view = _contentViews[i];
[view setFrame:CGRectMake(self.view.bounds.size.width*i, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
}
}

Related

Slide up UIView following finger similar to Control Center in iOS

I have a UITableView that is at the bottom of my main UIViewController, it currently only shows the top two rows. To show the rest I don't want it to scroll, I want the user to be able to "pull" the view up to reveal the additional 4 rows (can then swipe down on it to "push" it back into its original place), and I want the "pull" to be similar to how control center works in iOS. The spring that it has is really great.
Looks like I can add a UIPanGestureRecognizer to do the pull:
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;
[self addGestureRecognizer:pan];
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self];
[UIView animateWithDuration:0.01f
animations:^{
CGRect oldFrame = _viewToChange.frame;
_viewToChange.frame = CGRectMake(oldFrame.origin.x, currentPoint.y, oldFrame.size.width, ([UIScreen mainScreen].bounds.size.height - currentPoint.y));
}];
}
Ref this question.
Doing this does sort of work, but my UITableView flashes as you pull it up and it eventually disappears.
There is also no spring, and no way to set a "max" so that you can't pull up the view past a certain point.
Does anyone have ideas on how this can be achieved?
this is how i write the pan:. t is tableView(initially user interaction is disabled). 40 in code is for more realistic look in pan. 200 is max value i used and 60 is min value. i directly add t to tableview with height 60 and increased the height with animation.
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self.view];
CGRect fr = t.frame;
if ((currentPoint.y - fr.origin.y) < 40 && (fr.size.height <= 200) )
{
float nh = (currentPoint.y >= self.view.frame.size.height - 200) ? self.view.frame.size.height-currentPoint.y : 200;
if (nh < 60) {
nh = 60;
}
[UIView animateWithDuration:0.01f animations:^{
[t setFrame:CGRectMake(0, self.view.frame.size.height-nh, t.frame.size.width, nh)];
}];
if (nh == 200) {
[t setUserInteractionEnabled:YES];
}
else
{
[t setUserInteractionEnabled:NO];
}
}
}
or without pan; with using tableview's scrollview delegate methods, when tableview begin dragging in closed state, opening the large mode and when tableview scroll to top sliding down
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"scroll");
if (scrollView.contentOffset.y == 0) {
[UIView animateWithDuration:0.2f animations:^{
[scrollView setFrame:CGRectMake(0, self.view.frame.size.height-200, t.frame.size.width, 200)];
}];
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y == 0) {
[UIView animateWithDuration:0.2f animations:^{
[scrollView setFrame:CGRectMake(0, self.view.frame.size.height-60, t.frame.size.width, 60)];
}];
}
}

UIView that follows UICollectionView indicator

I want to create a custom UIView that contains a UIImageView and a UILabel, that points to the UICollectionView scroll indicator, and scrolls with it.
i am using this code :
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
// get the relative position of the indicator in the main window
CGPoint p = [verticalIndicator convertPoint:verticalIndicator.bounds.origin toView:[[UIApplication sharedApplication] keyWindow]];
// set the custom view new position
CGRect indicatorFrame = self.indicatorView.frame;
indicatorFrame.origin.y = p.y;
self.indicatorView.frame = indicatorFrame;
}
But the indicatorView does not follow the indicator exactly !!
This worked for me: Please look at the attached gif. I have added the custom uiview with blue color parallel to the scrool indicator.
I tried for the table view, but this also works for the collection view too.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
dispatch_async(dispatch_get_main_queue(), ^{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
// get the relative position of the indicator in the main window
// CGPoint p = [verticalIndicator convertPoint:verticalIndicator.bounds.origin toView:[[UIApplication sharedApplication] keyWindow]];
CGPoint p = CGPointMake(verticalIndicator.frame.origin.x-10,
verticalIndicator.frame.origin.y - scrollView.contentOffset.y);
// set the custom view new position
CGRect indicatorFrame = CGRectMake(verticalIndicator.frame.origin.x, verticalIndicator.frame.origin.y, 10, 10);
self.indicatorView.frame = indicatorFrame;
});
}
Also maske sure you added the uiview in you view did load method.
//in viewDidLoad:
Note that I have used the sample values for the indicatorView postion.You can replace these as per your need.
indicatorView=[[UIView alloc]initWithFrame:CGRectMake( p.x, p.y , 10, 10)];
indicatorView.backgroundColor=[UIColor blueColor];
[self.tableView addSubview:indicatorView];
For me this works:
....
scrollIndicatorView = [self scrollIndicator];
[self.collectionView addSubview:scrollIndicatorView];
....
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// percentage of the content reached * height
CGFloat yPos = (self.collectionView.contentOffset.y / (self.collectionView.contentSize.height - self.collectionView.bounds.size.height)) * self.collectionView.bounds.size.height;
yPos += self.collectionView.contentOffset.y; // add content offset becasue view is inside colelctionview which content moves
CGRect indicatorFrame = CGRectMake(self.collectionView.bounds.size.width - 10,
yPos,
10,
30);
scrollIndicatorView.frame = indicatorFrame;
}

how to get a subview of UIScrollView using a CGPoint

I have a UIScrollView with a number of rectangular subviews lined up, of equal sizes. Then I need to be able to pass a CGPoint to that UIScrollView and I want it to give me the rectangular subview that contains the CGPoint. That's basically hitTest:event, except hitTest:event: doesn't work with UIScrollView once CGPoint goes beyond the UIScrollView bounds and doesn't look into its actual content.
What's everyone been doing about this? How to "hit test" on a UIScrollView content view?
Here's some code to illustrate the problem:
NSArray *rectangles = [self getBeautifulRectangles];
CGFloat rectangleLength;
rectangleLength = 100;
// add some rectangle subviews
for (int i = 0; i < rectangles.count; i++) {
UIView *rectangle = [rectangles objectAtIndex:i];
[rectangle setFrame:CGRectMake(i * rectangleLength, 0, rectangleLength, rectangleLength)];
[_scrollView addSubview:rectangle];
}
[_scrollView setContentSize:CGSizeMake(rectangleLength * rectangles.count, rectangleLength)];
// add scroll view to parent view
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,320, rectangleLength)];
[containerView addSubview:_scrollView];
// compute CGPoint to center of first rectangle
CGPoint number1RectanglePoint = CGPointMake(0 * rectangleLength + 50, 50);
// compute CGPoint to center of fifth rectangle
CGPoint number5RectanglePoint = CGPointMake(4 * rectangleLength + 50, 50);
UIView *firstSubview = [containerView hitTest:number1RectanglePoint withEvent:nil];
UIView *fifthSubview = [containerView hitTest:number5RectanglePoint withEvent:nil];
if (firstSubview) NSLog(#"first rectangle OK");
if (fifthSubview) NSLog(#"fifth rectangle OK");
output: first rectangle OK
You should be able to loop through the scrollview subviews
+(UIView *)touchedViewIn:(UIScrollView *)scrollView atPoint:(CGPoint)touchPoint {
CGPoint actualPoint = CGPointMake(touchPoint.x + scrollView.contentOffset.x, scrollView.contentOffset.y + touchPoint.y);
for (UIView * subView in scrollView.subviews) {
if(CGRectContainsPoint(subView.frame, actualPoint)) {
NSLog(#"THIS IS THE ONE");
return subView;
}
}
//Nothing touched
return nil;
}
I guess you pass the wrong CGPoint coordinate to the hitTest:withEvent: method causing wrong behavior if the scroll view is scrolled.
The coordinate you pass to this method must be in the target views coordinate system. I guess your coordinate is in the UIScrollView's superview's coordinate system.
You can convert the coordinate prior to using it for the hit test using CGPoint hitPoint = [scrollView convertPoint:yourPoint fromView:scrollView.superview].
In your example you let the container view perform the hit testing, but the container can only see & hit the visible portion of the scroll view and thus your hit fails.
In order to hit subviews of the scroll view which are outside of the visible area you have to perform the hit test on the scroll view directly:
UIView *firstSubview = [_scrollView hitTest:number1RectanglePoint withEvent:nil];
UIView *fifthSubview = [_scrollView hitTest:number5RectanglePoint withEvent:nil];

in UIScrollView setContentSize working only one

In my application, I have some information in UIScrollView.
Content in UIScrollView can be changed by filter.
First time, when ViewController loads, I put all items in ScrollView, and everything working perfect.
If user wants to apply some filter to ScrollView, I remove all old items, make new, and set a new setContentSize:
- (void) displayCars:(NSMutableArray*)cars {
NSArray *viewsToRemove = [scrollView subviews];
for (UIView *v in viewsToRemove) [v removeFromSuperview];
int itemHeight = 100;
CGFloat scrollViewHeight = cars.count * itemHeight;
if (scrollViewHeight < scrollView.frame.size.height) scrollViewHeight = scrollView.frame.size.height;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, scrollViewHeight)];
for (int i=0; i < cars.count; i++) {
VitoCarsAdv * adv = [cars objectAtIndex:i];
VIOneCar *carView = [[VIOneCar alloc] init];
[carView setFrame:CGRectMake (0, (i * itemHeight), scrollView.frame.size.width, itemHeight)];
[carView setData:adv];
[scrollView addSubview:carView];
}
}
ScrollView is filling up by new items, but the contentSize is the same, like in previous situation, where was all items inside. Actually, it is not changed second time.
It is not comfortable behaviour for user, can somebody advice me, where I should look?
Please note, that Autolayout is swithed off for this View Controller.
Thanks.
You're doing it right I think you just mistyped some code. Here's what you wrote:
int itemHeight = 100;
CGFloat scrollViewHeight = cars.count * itemHeight;
if (scrollViewHeight < scrollView.frame.size.height) {
scrollViewHeight = scrollView.frame.size.height;
}
With that code, the scrollViewHeight will never shrink to a smaller size. You are setting a minimum size of whatever the scrollView.frame.size.height currently is. Is that your intended function? If not, it you should just remove the if statement leaving you with:
CGFloat scrollViewHeight = cars.count * itemHeight;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, scrollViewHeight)];

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