I'm trying to build a simple UIScrollView with paging to horizontally scroll between 3 images.
The tricky part is that I would like that each image would be clickable and catch the click event.
My technique is to create 3 UIButtons that each consists UIImage. give each button a tag and set an action.
Problem: I can catch the click event - BUT it's not scrollable!
Here is my code:
- (void) viewDidAppear:(BOOL)animated {
_imageArray = [[NSArray alloc] initWithObjects:#"content_01.png", #"content_02.png", #"content_03.png", nil];
for (int i = 0; i < [_imageArray count]; i++) {
//We'll create an imageView object in every 'page' of our scrollView.
CGRect frame;
frame.origin.x = _contentScrollView.frame.size.width * i;
frame.origin.y = 0;
frame.size = _contentScrollView.frame.size;
//
//get the image to use, however you want
UIImage* image = [UIImage imageNamed:[_imageArray objectAtIndex:i]];
UIButton* button = [[UIButton alloc] initWithFrame:frame];
//set the button states you want the image to show up for
[button setImage:image forState:UIControlStateNormal];
[button setImage:image forState:UIControlStateHighlighted];
//create the touch event target, i am calling the 'productImagePressed' method
[button addTarget:self action:#selector(imagePressed:)
forControlEvents:UIControlEventTouchUpInside];
//i set the tag to the image #, i was looking though an array of them
button.tag = i;
[_contentScrollView addSubview:button];
}
//Set the content size of our scrollview according to the total width of our imageView objects.
_contentScrollView.contentSize = CGSizeMake(_contentScrollView.frame.size.width * [_imageArray count], _contentScrollView.frame.size.height);
_contentScrollView.backgroundColor = [ENGAppDelegate backgroundColor];
_contentScrollView.delegate = self;
}
Well, since UIButton is an UIControl subclass, it "eats up" the touches of your scroll view:
[UIScrollView touchesShouldCancelInContentView:] The default returned value is YES if view is not a UIControl object; otherwise, it returns NO.
(from https://developer.apple.com/library/ios/documentation/uikit/reference/UIScrollView_Class/Reference/UIScrollView.html#//apple_ref/occ/instm/UIScrollView/touchesShouldCancelInContentView:)
You could influence this by subclassing UIScrollView and overwriting touchesShouldCancelInContentView: (and/or touchesShouldBegin:withEvent:inContentView:). However, for your use case I'd not use buttons in the first place. Why not just add a tap gesture recognizer to the scrollview and use the touch point to determine which image has been tapped? That's much easier and should work without any issues.
This completely solved this issue for me:
scrollview.panGestureRecognizer.delaysTouchesBegan = YES;
Credit goes to https://stackoverflow.com/users/904365/kjuly for providing the right answer in this topic: ScrollView/TableView with UIControl/UIButton subviews not scrollable under iOS 8
Related
At the beggining, here is the screenshot of my ViewController in Action.
My ViewController is contained of Embedded YouTube video, below it is UIScrollView and under him is UITableView which is for now randomly populated. I want to get all the albums from the specific singer, and for each of them, a seperate UIButton as shown in screenshot ->
svi, lek za spavanje, balkan
The thing is, I will get all the albums from YouTube API, so I do not know how many albums singer has got. I have to add UIButtons programmatically in Swift.
On my language, "svi" means "all", so when I click "svi" I should get all the albums listed below it in UITableView. Similarly, all the albums and "svi"("all") should be in UIScrollView, so when the user clicks, for example, "Balkan", UITableView should be populated with all the tracks from that "Balkan" album.
Can anyone suggest me how to make and position all the UIButtons (as an Array of UIButtons) in UIScrollView according to the screenshot provided at the top? Any help appreciated.
Add Scrollview in storyboard / add programatically
Have a array with list of albums
Create UIButton programatically and add it to scroll view
Set size of the scrollview at the end.
Code snippet as follows,
UIScrollView * scView;
UIButton * btn;
NSMutableArray * array;
float btnWidth = 44;
for(int i=0;i<array.count;i++) {
btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(i*btnWidth, 0, btnWidth, 44);
[btn setTitle:[array objectAtIndex:i]forState:UIControlStateNormal];
[btn addTarget:self action:#selector(btnTapped) forControlEvents:UIControlEventTouchUpInside];
[scView addSubview:btn];
}
[scView setContentSize:CGSizeMake(btnWidth*array.count, 50)];
- (void)btnTapped {
// button click call back method
}
Check the size of the string with following method
- (CGSize)getHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height);
}
return size;
}
Pass your album name to this method and it returns CGSize, from that get width and set that width value to button width in above for loop.
I am creating a few buttons programmatically, similar to:
for( int i = 0; i < 5; i++ ) {
UIButton* aButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[aButton setTag:i];
[aButton addTarget:self action:#selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[aView addSubview:aButton];
}
And that I can implement the function of the button this way so that I know witch one was tapped:
- (void)buttonClicked:(UIButton*)button
{
NSLog(#"Button %ld clicked.", (long int)[button tag]);
}
But my question is a little bit trickier than that, I don't want just to access the button inside it's button method but also outside of it so that I can change the UIButton frame using an animation.
I was thinking that I should somehow be able to recreate the pointer to any of the buttons previously created by using the tags that I assign initially.
Can anyone point me in the right direction?
The correct way to do this is by using the next line of code:
UIButton *aButtonReconstruct = (UIButton *)[self.view viewWithTag:aTag];
where aTag is an int and is greater than 0, because all the views have the tag 0 by default, so in the for loop used in the first place, the count should start at minimum 1. In our case aTag having values from 1 to 6 after we change the for loop.
Also there shouldn't be more view with the same tag.
I created some UIImageViews inside a nested UIView by Storyboard and I created a different TAG for each UIImageView. This is how the tree of the ViewController looks according to Storyboard:
ViewController
View
ViewNested
UIImageView1
UIImageView1
UIImageView1
UIImageView1
I have to change programmatically these ImageViews so, to get the images I use the method viewWithTag but it doesn't work because it returns NIL.
This happens even if I add to my class the ViewNested IBOutlet and getting the views using the following code:
// View is the top View with Tag:40
UIView * view = [self.view viewWithTag:40]; //This works
// The nestedView with Tag:44
UIView * viewNested = [view viewWithTag:44]; //DOESN'T work it returns NIL even if the TAG is exact
Then if I try to access to the imageView using the same method of course, it returns NIL. I don't know why, I also tried to use this code to view all the recursive nested view but it seems that they don't exist even if they are present in the storyboard.
- (void)showAllSubView:(UIView *)view
{ int i = 0;
for (UIView * subView in [view subviews]) {
NSLog(#"%#, tag:%ld", subView, (long)subView.tag) ;
[self showAllSubView:subView] ;
i++;
}
NSLog(#"Numb of Views %d", i) ;
}
The TAGS are 40 for the root View, 44 for the nested and for the images are 1,2,3,4. So the TAGS are all different.
Any help will be appreciate :). Thanks in advance
UIImageView *imageView=(UIImageView *)[self.view viewWithTag:yourTag];
[imageView setImage:[UIImage imageNamed:#"yourImageName"]];
replace yourTag with UIImageView tag;
replace yourImageName with some Image name
and if you want change only images of ImageViews - you don't need
UIView * view = [self.view viewWithTag:40]; //This works
// The nestedView with Tag:44
UIView * viewNested = [view viewWithTag:44];
Also you can change all images:
for (int i=0; i<18; i++)
{
UIImageView *imageView=(UIImageView *)[self.view viewWithTag:i];
NSString *imageName = [NSString stringWithFormat:#"imageName_%d", i];
[imageView setImage:[UIImage imageNamed:imageName]];
}
I am creating a modal view in an App, which contains several subviews. Each one of those subviews has a button and an action related to that button. I use a loop to insert each subview in place. The problem is that the first 4 subviews are ok, from the 5th to the last there are no responses.
Here it is the simplified code related to the problem:
Simplified SubviewView.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// More info button
CGRect buttonFrame = CGRectMake(infoMarginLeft + infoWidthWithInfo, infoHeightControl, 25, 25);
UIButton * button = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[button setFrame:buttonFrame];
[button addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
}
return self;
}
-(void)buttonTapped:(id)sender
{
NSLog(#"Button tapped!";
}
Simplified view.m
- (void)drawRect:(CGRect)rect
{
for (int i = 0; i < 12; i++) {
// Color info container
SubviewView * miniView = [[SubviewView alloc] initWithFrame:CGRectMake(0, 20 * i, 15, 15)];
miniColorView.backgroundColor = [UIColor clearColor];
// Adding subview through array to keep track of objects
[self addSubview:miniColorView];
}
}
Thanks in advance, I have absolutely no ideia of what's going on :p
--
EDIT
Just found out what it was!
The parent view had as height the screen height. The thing is, some of the subviews were beyond the screen height limit. All I had to do was increase the parent view height =)
I would check to make sure you are not adding your view miniView over top of your button.
Also this should be moved outside of drawRect, just put this in your init method.
Edit:
Set your miniView's background color to another besides clear, and see if you still see your button. If not, then you covered up your button, and it won't receive touch events.
I have a method makeButtons (posted here), which is removing all buttons in the screen and adding them again. This works fine in viewDidLoad and viewDidAppear. I am accessing information from a webservice which is telling me I need a new button. When I am calling [self makebuttons] from that method, nothing happends, until I move forth and back with my NavigationController forcing viewDidAppear to do the work again. My question is why? I am doing exactly the same, unless it's not called from viewDidAppear, but from doneGettingInformation.
- (void) viewDidAppear:(bool) animated {
[self makebuttons]; // Works great!
}
- (void) doneGettingInformation : (ASIFormDataRequest *) request {
NSString *response = [request responseString];
[[self.temp.userInfo objectForKey:#"spillider"] addObject:response];
[self makebuttons]; // This gets called, but nothing changes in the view itself.
}
- (void) makeButtons {
NSLog(#"kjort");
int newAntall = [[self.temp.userInfo objectForKey:#"spillider"] count];
for (UIButton * button in gameButtons) {
NSString *tag = [NSString stringWithFormat:#"%i",button.tag];
[button removeFromSuperview];
if ([webviews objectForKey:tag]) {
[[webviews objectForKey:tag] removeFromSuperview];
[webviews removeObjectForKey:tag];
}
}
[gameButtons removeAllObjects];
scroller.contentSize = CGSizeMake(320, 480);
if (newAntall > 3) {
CGSize scrollContent = self.scroller.contentSize;
scrollContent.height = scrollContent.height+((newAntall-3)*BUTTON_HEIGTH);
self.scroller.contentSize = scrollContent;
}
int y = 163;
self.nyttSpillKnapp.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, 65);
for (int i=0; i<newAntall; i++) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setFont:[UIFont systemFontOfSize:15]];
button.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGTH);
button.enabled = YES;
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:#selector(deleteButton:)];
swipe.direction = UISwipeGestureRecognizerDirectionRight;
[button addGestureRecognizer:swipe];
button.tag = [[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i] intValue];
NSString * tittel = [NSString stringWithFormat:#"spill %#",[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i]];
[button setTitle:tittel forState:UIControlStateNormal];
UIButton *subButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
subButton.transform = CGAffineTransformMakeRotation(M_PI_2);
subButton.tag = i;
CGRect subframe = CGRectMake(230, 5, subButton.frame.size.width, subButton.frame.size.height);
subButton.frame = subframe;
CGRect myframe = self.nyttSpillKnapp.frame;
myframe.origin.y = myframe.origin.y+BUTTON_HEIGTH;
self.nyttSpillKnapp.frame = myframe;
[subButton addTarget:self action:#selector(clickGameButton:) forControlEvents:UIControlEventTouchUpInside];
[button addSubview:subButton];
[gameButtons addObject:button];
[self.scroller addSubview:button];
y += BUTTON_HEIGTH;
}
}
To sum up, it only works if I am changing viewcontrollers back and forth causing viewWillAppear to get called. Why is that?
I am sorry for my messy methods.
Thanks
If you change the contents of the view outside of the initial view appearing process or layout changes, it's your responsibility to call setNeedsDisplay and inform the run loop that it needs to be redrawn.
The system will ask the view to draw it's contents initially or during layout changes which is why it works as part of the process to first show the view. During that initial process, the viewWill/DidAppear delegates will get called.
From the UIView class reference:
The View Drawing Cycle
View drawing occurs on an as-needed basis. When a view is first shown,
or when all or part of it becomes visible due to layout changes, the
system asks the view to draw its contents. For views that contain
custom content using UIKit or Core Graphics, the system calls the
view’s drawRect: method. Your implementation of this method is
responsible for drawing the view’s content into the current graphics
context, which is set up by the system automatically prior to calling
this method. This creates a static visual representation of your
view’s content that can then be displayed on the screen.
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view. These methods let the
system know that it should update the view during the next drawing
cycle. Because it waits until the next drawing cycle to update the
view, you can call these methods on multiple views to update them at
the same time.
EDIT:
Also, make sure done getting images is not called on a background thread. You can't edit views on a background thread. If it is you can prepare all the data on a bg thread but then call makeButtons on on the main thread (performSelectorOnMainThread or use blocks.
See GCD, Threads, Program Flow and UI Updating