How to exclude scroll indicators while enumerating subviews of a UIScrollView? - iphone

I'm trying to adjust my subviews in a UIScrollView subclass, but I don't want to disturb the scroll indicators. There doesn't seem to be any public interface to access these and I want to check if a view is one of the scroll indicators or not (so that I can ignore it).
UIScrollView.h declares these two iVars:
UIImageView* _verticalScrollIndicator;
UIImageView* _horizontalScrollIndicator;
...but I tried the following and got a linker error:
for(UIView* v in self.subviews)
{
// Ignore the scroll-indicator views
if( (v == _horizontalScrollIndicator) ||
(v == _verticalScrollIndicator))
{
continue;
}
// View is one of mine - do stuff to it...
}
Apple obviously don't want you messing with these, in which case they should do something clever so that the array returned from subviews doesn't include them (come on Apple, it's not that hard!), but until then how can I ignore them?

This ended up being my work-around
BOOL showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
BOOL showsHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
// loop through self.subviews here
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;

Presumably you're in control of the views that you put in the UIScrollView. Why not maintain your own array of just those views? That way, you are safe against any future changes in the implementation of UIScrollView.

i'm up against the same issue and i'm starting to think about looking at the values of those two UIImageViews to figure out if its them or not. they do have a pretty unique signature:
<UIImageView: 0x8ab3e00; frame = (313 812.5; 7 3.5); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x8ab9740>>
<UIImageView: 0x8ab3e90; frame = (314.5 522; 3.5 341); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x8ab3f20>>
amongst other hooks that you could listen for. not ideal but not much in the way of options at this point...
heres an example. in this case, i was looping through the scrollview's subviews to figure out the optimal contentSize height for the scrollview based on the subviews when it started coming out with some funky values which lead me to think that it was the scrollbars that were screwing with me...
CGRect contentRect = CGRectZero;
for (UIView *view in self.myScrollView.subviews) {
//exclude scroll bars, the width/height is 3.5 for non-retina or 7
//for retina. you can put some logic in here if you like to check
//the appropriate 3.5/7.0 values for the respective screen densities
if(view.frame.size.width > 7.0f && view.frame.size.height > 7.0f) {
contentRect = CGRectUnion(contentRect, view.frame);
}
}
container.contentSize = CGSizeMake(container.contentSize.width, contentRect.size.height);
if checking the frame of each UIView isn't specific enough for you, you could also ensure that it is a UIImageView.
i'm thinking this is the only "true" solution at this point. i'd personally rather not have to maintain another array of views.
EDIT:
here's my consolidated method that i use:
-(bool)isProbablyAScrollBar:(UIView*)view {
CGFloat screenScale = [[UIScreen mainScreen] scale];
return (([view isKindOfClass:[UIImageView class]]) && (view.frame.size.width <= (screenScale*3.5f) || view.frame.size.height <= (screenScale*3.5f)));
}

Are the scroll indicator views added and removed throughout the lifetime of the scroll view, or are they just added once and hidden and shown as necessary? If it's the latter, you could store your own references to them in init... instead of using the private ivars and proceed as you have tried to already.

My solution was:
UIView* MYContentView=[[UIView alloc] initWithRect: CGrectMake:(0,0,Scrollview.frame.size.width, scrollview.frame.size.height)];
[ScrollView addsubview: MYContentView];
Then i added all the images to the MYContentView and later Rescaled the MYContentView and
[ScrollView setContentSize:Cgsize];
Now you can Enumerate all Images in the MYContentView without worrying about the scrollbars.
Doing:
while ([[MYContentView subviews] count]>0) {
[[[MYContentView subviews]lastObject] removeFromSuperview];
}
Removed only the images. Hope this helps

Based on MrTristan's method. I think he mistakenly multiplied with screen scale.
+ (NSArray <UIView *> *)reservedSubviewsInUIScrollView:(UIScrollView *)view
{
BOOL (^isProbablyAScrollBar)(UIView *s) = ^BOOL(UIView *s) {
return [s isKindOfClass:[UIImageView class]] && (s.frame.size.width <= 3.5 || s.frame.size.height <= 3.5);
};
NSMutableArray <UIView *> *reserved = [NSMutableArray new];
for(UIView *s in view.subviews)
{
if(isProbablyAScrollBar(s))
{
[reserved addObject:s];
}
}
return reserved;
}

Related

Slideshow using UIScrollView and AutoLayout

I wanted to create a simple slideshow of images using an UIScrollView and I noticed that, using the new AutoLayout mode, the operation became way more complicated compared to iOS5. I wasn't able to find any SIMPLE and SHORT example/tutorial for accomplishing it. There was a lot of material about "pure" and "hybrid" approaches,but honestly nothing really worked for me. Maybe the material is not clear enough, maybe I'm not good enough..who knows. Anyway, I thought it could be useful sharing my finding and the consequent snippet of code which is currently doing the job for me. Here we go:
- (void)setupSlideshow {
NSInteger nPhotos = [self.profilePhotos count];
UIScrollView *scrollView;
UIImageView *imageView;
NSDictionary *viewsDictionary;
// Create the scroll view and the image view.
scrollView = self.slideShow;
CGFloat sWidth = scrollView.frame.size.width;
CGFloat sHeight = scrollView.frame.size.height;
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, nPhotos * sWidth, sHeight)];
scrollView.pagingEnabled = YES;
CGFloat cx = 0;
for (int i = 0; i < nPhotos; i++) {
imageView = [[UIImageView alloc] init];
// Add an image to the image view.
[imageView setImage:[UIImage imageNamed:self.profilePhotos[i]]];
CGRect imFrame = imageView.frame;
imFrame.origin.x = cx;
imFrame.origin.y = 0;
imFrame.size.width = sWidth;
imFrame.size.height = sHeight;
imageView.frame = imFrame;
[container addSubview:imageView];
cx += sWidth;
}
container.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:container];
// Set the constraints for the scroll view and the image view.
viewsDictionary = NSDictionaryOfVariableBindings(scrollView, container);
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"H:|-0-[container(%f)]-0-|",cx]
options:0
metrics: 0
views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[container]-0-|" options:0 metrics: 0 views:viewsDictionary]];
}
The scrollView has been instantiated in the StoryBoard.
As you can notice, I adopted an approach based on a UIView container for the pictures I want to use which has been added to a scrollview. I have then added some constraints using the Visual Constraints Format notation. I tried to match the width of the container to the scroller's one using this:
[NSString stringWithFormat:#"H:|-0-[container(==scrollView)]-0-|"]
but it doesn't work (to be precise it doesn't scroll but sticks to the first image), so I opted for a dynamic approach using a simple string format.
As I said, it does the job, but I'm sure I can make it way more elegant. Besides that, is it possible to fix the problem relative the dynamic width?
I hope to receive some useful feedback, in order to refine this example. I will then setup a GitHub for making it publicly available for all of those who spent 3 days banging their head against a wall...like I did.

iOS : How to do proper paging in UIScrollView?

I've a UIScrollView of size (320,160). I'm adding some UIImageView into it, which are of size (213,160). The first UImageView starting from 54 (x) and so on, I've added a space of 5.0 in between each UIImageView. I've also enabled pagingEnable in IB & in coding. What my problem is its not properly working as per its property! When I scroll it should show me UIImageViews in each single page instead it showing me something like see screenshot I want output something like this see output screenshot
Where I'm doing wrong? I also having function of< (previous) & > (next) there to show images. I've asked one question yesterday which was I accepted however my requirement is little change and it won't become my solution. see my question.
Is there any special property that I've to set, or some logic I should implement? All examples I've checked and tried but I find that my requirement is some special. Point me! Thanks.
EDITED:
- (void) setImages
{
CGFloat contentOffset = 0.0f;
for (int i=0; i<[arrImgUrls count]; i++)
{
CGRect imageViewFrame = CGRectMake(contentOffset, 0.0f, 213, scrollImages.frame.size.height);
AsyncImageView *asyncImageView = [[AsyncImageView alloc] initWithFrame:imageViewFrame];
[asyncImageView.layer setMasksToBounds:YES];
NSString *urlImage = [arrImgUrls objectAtIndex:i];
NSURL *url = [NSURL URLWithString:urlImage];
[asyncImageView loadImageFromURL:url];
[scrollImages addSubview:asyncImageView];
contentOffset += asyncImageView.frame.size.width+increment;
[asyncImageView release];
}
scrollImages.contentSize = CGSizeMake([arrImgUrls count] * scrollImages.frame.size.width, scrollImages.frame.size.height);
scrollImages.pagingEnabled = YES;
scrollImages.clipsToBounds = NO;
}
-(IBAction)prevImage:(id)sender
{
_currentImage--;
[btnNext setEnabled:YES];
if (_currentImage==0)
{
[btnPrev setEnabled:NO];
[scrollImages setContentOffset:CGPointMake((_currentImage*imageWidth), 0) animated:YES];
return;
}
NSLog(#"previous:mult %d inc %d current %d",_currentImage*imageWidth,increment*_currentImage,_currentImage);
int nextImage=_currentImage+2;
[scrollImages setContentOffset:CGPointMake((((_currentImage*imageWidth)-(increment*_currentImage)))+(nextImage*increment), 0) animated:YES];
}
-(IBAction)nextImage:(id)sender
{
_currentImage++;
NSLog(#"next:mult %d inc %d current %d",_currentImage*imageWidth,increment*_currentImage,_currentImage);
[scrollImages setContentOffset:CGPointMake((_currentImage*imageWidth)+(increment*_currentImage), 0) animated:YES];
[btnPrev setEnabled:YES];
if (_imageCount-1 == _currentImage)
{
[btnNext setEnabled:NO];
}
}
Paging scroll views alway page multiples of their frame size. So in your example paging is always +320.
This behavior is good if you have content portions matching the frame of the scroll view.
What you have to do, is giving your scroll view a width of 213 and set its clipsToBounds property to NO.
After that your scroll view pages exactly how you want and you see what's left and right outside the frame.
Additionally you have to do a trick to make this left and right area delegate touches to the scroll view.
It's greatly explained in this answer.
You are forgetting to set scrollview content size.
make sure that you have set the content size to fit N number of images.
if you want to scrollview to scroll for 10 images with an image on each page
set scrollView contentSize as
[scrollView setContentSize:CGSizeMake(320 * 10,200)];

iOS: How To swipe through an image with pagination on

Basically, I want to have an app with only one view that has an image on it. I want to be able to swipe left or right and have the first image go out of the view and the second image to come in. The images are the same and I want it to look like they are connected (like scrolling down a rope where the pattern just repeats, but it looks like a constant scroll). I need it to be able to change the image or restart after a series of swipes. I know that I need to turn pagination ON in the UIScrollView, but I am new to iOS and am having trouble.
Ultimately, I want to have the iPhone vibrate every so-and-so swipes (and restart the pattern).
I'm sure that there are a lot of ways to do this (i.e. a TableView) so feel free to just point me in the direction of some references if the answer is tedious to explain.
Thanks!
FOLLOW UP:
I found an Apple example that did very nearly what I wanted to do. I made a lot of adjustments to it, but I'm banging my head against a wall trying to get the images to cycle. Here is what I think is the offending code, but I'm not sure what the solution is, as the ScrollView is functional, it just doesn't reset the center to the current view. Any ideas?
- (void)layoutScrollImages
{
UIImageView *view = nil;
NSArray *subviews = [scrollView1 subviews];
// reposition all image subviews in a horizontal serial fashion
CGFloat curXLoc = 0;
for (view in subviews)
{
if ([view isKindOfClass:[UIImageView class]] && view.tag > 0)
{
CGRect frame = view.frame;
frame.origin = CGPointMake(curXLoc, 0);
view.frame = frame;
curXLoc += (kScrollObjWidth);
}
}
// set the content size so it can be scrollable
[scrollView1 setContentSize:CGSizeMake((kNumImages * kScrollObjWidth), [scrollView1 bounds].size.height)];
}
I'd just use a UIScrollView. Set the contentWidth to be 3 times the width/height of the view (for 3 pages) and set the contentOffset to be the center 'page' (view.bounds.size.width or view.bounds.size.height depending on whether you're scrolling horizontally/vertically respectively) . You'll need to setup a delegate for the UIScrollView (probably the view controller) and implement - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView. This will be called when the scroll view has finished decelerating. Once it has finished decelerating, reset the contentOffset back to the center view. This should give the impression of an infinite scroll. You can also set a counter to increment in the scrollViewDidEndDecelerating method to increment the counter or initiate the vibration.
You shouldn't need to keep repositioning the images. Just set the images once in the scrollView:
//Horizontal arrangement
UIImage *image = [UIImage imageNamed:#"nameOfImage.png"];
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:image];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:image];
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:image];
NSArray *imageViews = [NSArray arrayWithObjects:imageView1, imageView2, imageView3];
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview: scrollView]; //This code assumes it's in a UIViewController
CGRect cRect = scrollView.bounds;
UIImageView *cView;
for (int i = 0; i < imageViews.count; i++){
cView = [imageViews objectAtIndex:i];
cView.frame = cRect;
[scrollView addSubview:cView];
cRect.origin.x += cRect.size.width;
}
scrollView.contentSize = CGSizeMake(cRect.origin.x, scrollView.bounds.size.height);
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0); //should be the center page in a 3 page setup
So the images are setup, you don't need to mess with them anymore. Just reset the contentOffset when the scroll views stops (note: you need to make sure you're the delegate of the scroll view or you'll not receive the message when the scroll view stops):
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
}
Please forgive any typos. I wrote it out by hand.
Look on cocoacontrols.com for a custom photo album view. As for the vibration, this code snippet vibrates the phone (make sure you link to and #import <AudioToolbox/AudioToolbox.h>):
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);

UITableView headers on the left side of sections (like Spotlight)

Been searching for this for quite a while, and I still haven't found a way to do it.
I'd like to reproduce the section headers of the iPhone's Spotlight into a UITableView.
"Regular" table view headers stay visible at the top of a section when you scroll, as we all know. But there is one kind of header that I've never seen elsewhere than in the Spotlight page of Springboard: there, the headers span the whole height of the section and are not stuck on the top of the section, but on the left side.
How the heck is that achieved?
Good question. I made a little experiment. It almost looks like the view from spotlight. But it's lacking one import feature. The lower "image" doesn't push the upper image to the top if they collide.
I doubt that there is a built in solution without the use of private frameworks.
I achieved this:
As you can see the two header images at the top overlap. They don't push each other like normal headers to. And I had to deactivate the cell separators. If I would use them they would appear at the whole cell. So you have to draw them yourself. Not a big deal.
But I have no idea how I could fix the overlapping.
Here is the code that made this happen:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 1;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 44)];
contentView.backgroundColor = [UIColor lightGrayColor];
UIImageView *imageView = [[[UIImageView alloc] initWithFrame:CGRectMake(5, 5, 34, 34)] autorelease];
imageView.image = [UIImage imageNamed:[NSString stringWithFormat:#"%d", section]];;
[contentView addSubview:imageView];
return [contentView autorelease];
}
Okay I have done something similar to what the music app and spotlight search does.
I have not subclassed UITableView, I have just tracked the sections through it's scrollViewDidScroll method, and added the header views to the left of the tableView (so you will have to put the tableview to the right in your viewController's view, which means you can't use UITableViewController).
this method should be called in the scrollViewDidScroll, ViewDidLoad, and in didRotateFromInterfaceOrientation: (if you support rotation)
keep in mind that you will have to make space in the left equal to the size of the headers in any interface orientation that you support.
-(void)updateHeadersLocation
{
for (int sectionNumber = 0; sectionNumber != [self numberOfSectionsInTableView:self.tableView]; sectionNumber++)
{
// get the rect of the section from the tableview, and convert it to it's superview's coordinates
CGRect rect = [self.tableView convertRect:[self.tableView rectForSection:sectionNumber] toView:[self.tableView superview]];
// get the intersection between the section's rect and the view's rect, this will help in knowing what portion of the section is showing
CGRect intersection = CGRectIntersection(rect, self.tableView.frame);
CGRect viewFrame = CGRectZero; // we will start off with zero
viewFrame.size = [self headerSize]; // let's set the size
viewFrame.origin.x = [self headerXOrigin];
/*
three cases:
1. the section's origin is still showing -> header view will follow the origin
2. the section's origin isn't showing but some part of the section still shows -> header view will stick to the top
3. the part of the section that's showing is not sufficient for the view's height -> will move the header view up
*/
if (rect.origin.y >= self.tableView.frame.origin.y)
{
// case 1
viewFrame.origin.y = rect.origin.y;
}
else
{
if (intersection.size.height >= viewFrame.size.height)
{
// case 2
viewFrame.origin.y = self.tableView.frame.origin.y;
}
else
{
// case 3
viewFrame.origin.y = self.tableView.frame.origin.y + intersection.size.height - viewFrame.size.height;
}
}
UIView* view = [self.headerViewsDictionary objectForKey:[NSString stringWithFormat:#"%i", sectionNumber]];
// check if the header view is needed
if (intersection.size.height == 0)
{
// not needed, remove it
if (view)
{
[view removeFromSuperview];
[self.headerViewsDictionary removeObjectForKey:[NSString stringWithFormat:#"%i", sectionNumber]];
view = nil;
}
}
else if(!view)
{
// needed, but not available, create it and add it as a subview
view = [self headerViewForSection:sectionNumber];
if (!self.headerViewsDictionary && view)
self.headerViewsDictionary = [NSMutableDictionary dictionary];
if (view)
{
[self.headerViewsDictionary setValue:view forKey:[NSString stringWithFormat:#"%i", sectionNumber]];
[self.view addSubview:view];
}
}
[view setFrame:viewFrame];
}
}
also we need to declare a property that would keep the views that are visible:
#property (nonatomic, strong) NSMutableDictionary* headerViewsDictionary;
these methods return the size and X axis offset of the header views:
-(CGSize)headerSize
{
return CGSizeMake(44.0f, 44.0f);
}
-(CGFloat)headerXOrigin
{
return 10.0f;
}
I have Built the code so that any header view that's not needed gets removed, so we need a method that would return the view whenever needed:
-(UIView*)headerViewForSection:(NSInteger)index
{
UIImageView* view = [[UIImageView alloc] init];
if (index % 2)
{
[view setImage:[UIImage imageNamed:#"call"]];
}
else
{
[view setImage:[UIImage imageNamed:#"mail"]];
}
return view;
}
here's how it will look :
How it will look in lanscape, I have used contraints to give 44px in the left of the tableView
hope this helps :).
Good news: See answer of How to achieve an advanced table view header
result http://i.minus.com/jyea3I5qbUdoQ.png

How would you implement an scrollable grid in your iPhone app?

I have about 50 images of 50 x 50 pixel size each. I want the user to pick one of them. So first I thought about an UITableView, but that's just not the right thing. It wastes a lot of screen space. Rather than putting all images one below the other, it would be better to show a grid of lets say 6 columns and n rows.
I would use an UIScrollView and fill it up with UIView objects which I automatically arrange so that they appear like a grid. Is that the way to go? Or any other suggestions?
I'm doing something very similar in my app- an NxN grid with an image underneath, and another subview on top to draw the "lines", all owned by a UIScrollView. I recommend having a separate view to draw the images, something like:
-(void) drawRect(CGRect rect) {
CGRect smallerRect = CGRectMake(x, y, width, height);
[yourImage drawRect: smallerRect];
// repeat as needed to draw the grid
}
Another poster mentioned that you won't be able to get touch events if your view is owned by a UIScrollView- this is simply not true. I have it working. You might need to set the following though:
[yourScrollView setUserInteractionEnabled: YES]
[yourGridView setUserInteractionEnabled: YES]
The three20 library has a class that does this.
A table view doesn't necessarily imply showing one image per row, as you suggest. Table cells can be customized, and a cell subclass with six 50x50 UIImageViews would be pretty simple. It's maybe less flexible than a scroll view but if six images per row is your goal then a table view is the quickest way to get it.
three20 is horrible,ive used it, i dont recommend it... displaying a grid is easy...
- (void) reloadGridView
{
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(5, 54, scrollViewWidth, scrollViewHeight8)];
scrollView.delegate = self;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.userInteractionEnabled = YES;
scrollView.scrollEnabled = YES;
[self.view addSubview:scrollView];
int x = 10;
int y = 10;
divisor = 1;
for (int i = 0; i < [self.photosArray count]; i++) {
int buttonTag = divisor;
UIImage *thumb = [UIImage imageWithContentsOfFile:[self thumbPathAtIndex:i]];
//********* CREATE A BUTTON HERE **********
//********* use the thumb as its backgroundImage *******
if(divisor%4==0){
y+=70;
x = 10;
}else{
x += 70;
}
divisor++;
}
[scrollView setContentSize:CGSizeMake(scrollViewWidth, ([self.photosArray count]%4 == 0) ? y : y+100)];
}
and if you want 6 images when in landscape - setup a BOOL for isLandscape
if (isLandscape) {
//change the divisor to change # 6 instead of 4
}
if you need a more advanced gridView check out AQGridView (Google it)