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

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)

Related

Simplest way to evenly distribute UIButtons horizontally across width of view controller?

I've looked through many answers and they all seem very complex! Most recently I was looking at this answer although I'd prefer not to have to put my buttons inside views.
I have 6 UIButtons that are all the same dimensions. I want to space them evenly horizontally across the full width and at the bottom of my root view controller.
| |
| |
| [b1] [b2] [b3] [b4] [b5] [b6] |
________________________________________
What is the simplest way to achieve this programmatically?
I think this is simpler than the link on the accepted answer. Ok, firstly lets create some buttons:
UIButton *button1 = [UIButton buttonWithType:UIButtonTypeSystem];
button1.translatesAutoresizingMaskIntoConstraints = NO;
[button1 setTitle:#"Btn1" forState:UIControlStateNormal];
... do that 6 times for 6 buttons, however you like then add them to the view:
[self.view addSubview:button1];
[self.view addSubview:button2];
[self.view addSubview:button3];
[self.view addSubview:button4];
[self.view addSubview:button5];
[self.view addSubview:button6];
Fix one button to the bottom of your view:
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:button1 attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:0]];
Then tell all the buttons to be equal width and spread out equally across the width:
NSDictionary *views = NSDictionaryOfVariableBindings(button1, button2, button3, button4, button5, button6);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[button1][button2(==button1)][button3(==button1)][button4(==button1)][button5(==button1)][button6(==button1)]|" options:NSLayoutFormatAlignAllBottom metrics:nil views:views]];
Result:
UIStackView was introduced in iOS9.
https://developer.apple.com/documentation/uikit/uistackview
I know this question is a bit old, but I've been programming in iOS for a few years now and dislike using autolayout. So I wrote a helper method to evenly space UIButtons horizontally and center them vertically within a UIView. This works great for menu bars.
- (void) evenlySpaceTheseButtonsInThisView : (NSArray *) buttonArray : (UIView *) thisView {
int widthOfAllButtons = 0;
for (int i = 0; i < buttonArray.count; i++) {
UIButton *thisButton = [buttonArray objectAtIndex:i];
[thisButton setCenter:CGPointMake(0, thisView.frame.size.height / 2.0)];
widthOfAllButtons = widthOfAllButtons + thisButton.frame.size.width;
}
int spaceBetweenButtons = (thisView.frame.size.width - widthOfAllButtons) / (buttonArray.count + 1);
UIButton *lastButton = nil;
for (int i = 0; i < buttonArray.count; i++) {
UIButton *thisButton = [buttonArray objectAtIndex:i];
if (lastButton == nil) {
[thisButton setFrame:CGRectMake(spaceBetweenButtons, thisButton.frame.origin.y, thisButton.frame.size.width, thisButton.frame.size.height)];
} else {
[thisButton setFrame:CGRectMake(spaceBetweenButtons + lastButton.frame.origin.x + lastButton.frame.size.width, thisButton.frame.origin.y, thisButton.frame.size.width, thisButton.frame.size.height)];
}
lastButton = thisButton;
}
}
Just copy and paste this method into any view controller. Then to access it, I first created all the buttons I wanted, then called the method with all of the buttons in an array, along with the UIView I wanted it in.
[self evenlySpaceTheseButtonsInThisView:#[menuButton, hierarchyMenuButton, downButton, upButton] :menuView];
The advantage of this method is that you don't need autolayout and it's super easy to implement. The disadvantage is that if your app works in landscape and portrait, you will need to make sure to call this method again after the view has been rotated.
I usually do something like:
int numButtons = 6;
float gap = 10.0f;
float y = 50.0f;
float width = (self.view.frame.size.width - gap * (numButtons + 1)) / numButtons;
float height = 60.0f;
for (int n=0;n<numButtons;n++) {
float x = gap * (n+1) + width * n;
UIButton *button = [self.buttons objectAtIndex:n]; //Or get button some other way/make button.
[button setFrame:CGRectMake(x,y,width,height)];
}
You can set numButtons to however many buttons you want in the row, and if you have an array of buttons, you can set it to the length of that array.
The y is just whatever y coordinate you want and the same goes for the height and gap, which is the space between buttons. The width is just a calculation of how wide each button will be based on the screen width and the gap space you want between each button.
You can send the Label/button/view's as array to this methods and arrange the button frames.
-(void)arrangeViewsXposition:(NSArray*)anyView y:(CGFloat)y width:(CGFloat)width height:(CGFloat)height mainViewWdith:(UIView*)mainview {
int count = (int)anyView.count;
CGFloat widthTemp = mainview.bounds.size.width, temp1 = widthTemp-(width*count),space = temp1/(count+1);
for (int i = 0; i<count; i++) {
UIView *btnTemp = (UIView*)[anyView objectAtIndex:i];
if (btnTemp) {
btnTemp.frame = CGRectMake(space+((space+width)*i), y, width, height);
}
}
}
Call this method like this:
[self arrangeViewsXposition:#[btnSave,btnCancel] y:5 width:80 height:30 mainViewWdith:footerView];
There are a number of decent auto-layout solutions discussed in this question:
Evenly space multiple views within a container view
Basically it can be a lot of manual constraint-definition code, which can be conveniently wrapped in a category extension for encapsulation/reuse.
I just cooked this up in Swift using Cartography.
func disributeEvenlyAcrossWithCartography(views: [UIView], enclosingBox: UIView, spacer: CGFloat = 10.0) {
var priorView = UIView() // never null
for (index, view) in views.enumerate() {
constrain(view, priorView, enclosingBox) { view, prior, enclosingBox in
view.height == enclosingBox.height
view.centerY == enclosingBox.centerY
if index == 0 {
view.width == enclosingBox.width / CGFloat(views.count) - spacer
view.left == enclosingBox.left
} else {
view.left == prior.right + (spacer + spacer / CGFloat(views.count - 1))
view.width == prior.width
}
}
priorView = view
}
}
Result with 5 views and a small spacer:
This is my first post on Stackoverflow. This site has been very helpful in getting me up to speed on Xcode and swift. Anyway, below is my quick-and-dirty fix to the above question.
Pin the left most button and the right most button, respectively.
Create "dummy" labels between each of the buttons. Set all of the "dummy" labels as equal widths. For each of the "dummy" label, add constraints (0 to the nearest neighbor on the left, and 0 to the nearest neighbor on the right). The buttons will then be equally spaced throughout, even when you change from portrait to landscape orientation.

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)];

How can I correctly position a UIImageView inside a UITableViewCell?

I have a custom UITableViewCell loaded from a nib. In it are three UIImageView views. In the -(UITableViewCell*) tableView:(UITableView*) cellForRowAtIndexPath:(NSIndexPath*)indexPath method I check a property for each row and determine whether each one is visible. If they are not visible, I shift the x position of the image so that there are no empty spaces. I am using:
cell.imageView1.hidden = YES;
cell.imageView2.hidden = YES;
cell.imageView3.hidden = YES;
int x = 0;
CGRect frame1 = cell.imageView1.frame;
if (property1)
{
cell.imageView1.hidden = NO;
frame1.origin.x = x;
x += SPACING;
}
CGRect frame2 = cell.imageView2.frame;
if (property2)
{
cell.imageView2.hidden = NO;
frame2.origin.x = x;
x += SPACING;
}
// etc...
For some reason, when the table is initially shown, the images are in the wrong location, but if I scroll up and down so that the cell is not shown then shown again, the image location goes to its correct place. What is causing this?
I'm not sure If I understood your code. But you could try calling setNeedsLayout after changing the frames.
[cell setNeedsLayout]
This method indicates that this views needs to layout on the next rendering.
Thanks to Felipe for pointing me to the right solution. In my UITableViewCell subclass I overrode the layoutSubviews method to properly layout the images there according to their state.

Scroll View ignoring contentInset for large content areas

I have an app which loads in a grid of images. I've used contentInset so that I have a nice 10pt margin at the top. When I load in an array of images that total less that the scrollView area, it works great. But when I load in an array of images that total more than the scrollView area, it completely ignores the contentInset value.
If I scroll down then back up it then acknowledges the fact there is a margin there. I'm going out of my mind trying to find out why. Anyone experienced this before?
float tempNumber = ((float)[faceArray count] / 3);
int numberOfRows = ceil(tempNumber);
// CGSizeMake (x, y, w, h)
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width,
(numberOfRows * (kThumbSpace + kThumbnailSize)));
scrollView.pagingEnabled = NO;
scrollView.showsVerticalScrollIndicator = YES;
scrollView.delegate = self;
scrollView.contentInset = UIEdgeInsetsMake(10.0, 0, 0, 0);
scrollView.alwaysBounceVertical = YES;
//.... code is then sent to a method which creates the grid of subviews in order
// to fill the scrollView
You could set the contentOffset property so that the scroll view always starts in the right position. It looks like it is automatically moving itself to show you as many subviews as possible, not sure if that is a setting somewhere you could change.

Calculate the contentsize of scrollview

I'm having a scrollview as the detailedview of tableview cell. There are multiple views on the detailedview like labels, buttons etc. which I'm creating through interface builder. What I'm creating through interface builder is static. I'm putting everything on a view of height 480.
A label on my detailedview is having dynamic text which can extend to any length. The problem is that I need to set the scrollview's content size for which I need its height.
How shall I set scrollview's height provided the content is dynamic?
You could try to use the scrollview'ers ContentSize. It worked for me and I had the same problem with the control using dynamic content.
// Calculate scroll view size
float sizeOfContent = 0;
int i;
for (i = 0; i < [myScrollView.subviews count]; i++) {
UIView *view =[myScrollView.subviews objectAtIndex:i];
sizeOfContent += view.frame.size.height;
}
// Set content size for scroll view
myScrollView.contentSize = CGSizeMake(myScrollView.frame.size.width, sizeOfContent);
I do this in the method called viewWillAppear in the controller for the view that holds the scrollview. It is the last thing i do before calling the viewDidLoad on the super.
Hope it will solve your problem.
//hannes
Correct shorter example:
float hgt=0; for (UIView *view in scrollView1.subviews) hgt+=view.frame.size.height;
[scrollView1 setContentSize:CGSizeMake(scrollView1.frame.size.width,hgt)];
Note that this only sums heights, e.g. if there are two subviews side by side their heights with both be added, making the sum greater than it should be. Also, if there are vertical gaps between the subviews, the sum will be less than it should be. Wrong height confuses scrollRectToVisible, giving random scroll positions :)
This loop is working and tested:
float thisy,maxy=0;for (UIView *view in scrollView1.subviews) {
thisy=view.frame.origin.y+view.frame.size.height; maxy=(thisy>maxy) ? thisy : maxy;
}
A somewhat easier way to do this is to nest your layout within a view then put that view within the scrollview. Assuming you use tags, this works as follows:
UIScrollView *scrollview = (UIScrollView *)[self.view viewWithTag:1];
UIView *longView = (UIView *)[self.view viewWithTag:2];
scrollview.contentSize = CGSizeMake(scrollView.frame.size.width, longView.frame.size.height);
That way the longView knows how tall it is, and the scrollview's content is just set to match.
This depends on the type of content you are going to add dynamically. So let's say you have a big text data to show, then use the UITextView and as it is a subclass of the UIScrollView, you can get the setContentSize of TextView when you assign the text content. Based on that you can set the total size of the UIScrollView.
float yPoint = 0.0f;
UIScrollView *myScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0f, yPoint, 320.0f, 400.0f)];
UITextView *calculatorTextView = [[UITextView alloc] init]; calculatorTextView.text = #"My looong content text ..... this has a dynamic content"; `
[calculatorTextView sizeToFit];
yPoint = yPoint + calculatorTextView.contentSize.height; // Bingo, we have the new yPoint now to start the next component.
// Now you know the height of your text and where it will end. So you can create a Label or another TextView and display your text there. You can add those components as subview to the scrollview.
UITextView *myDisplayContent = [[UITextView alloc] initWithFrame:CGRectMake(0.0f, yPoint, 300.f, calculatorTextView.contentSize.height)];
myDisplayContent.text = #"My lengthy text ....";
[myScrollView addSubview:myDisplayContent];
// At the end, set the content size of the 'myScrollView' to the total length of the display area.
[myScrollView setContentSize:yPoint + heightOfLastComponent];
This works for me.
I guess there's no auto in case of scrollview, and the contentsize should be calculated for static views on the screen at least and for dynamic once it should be calculated on the go.
scrollView.contentSize = [scrollView sizeThatFits:scrollView.frame.size]
I believe would also work
I had the same situation, but then I wrote a new version in Swift 4 mirroring the better answer in Objective-C by Hannes Larsson:
import UIKit
extension UIScrollView {
func fitSizeOfContent() {
let sumHeight = self.subviews.map({$0.frame.size.height}).reduce(0, {x, y in x + y})
self.contentSize = CGSize(width: self.frame.width, height: sumHeight)
}
}