I have an idea for an ios5 navigation I'm doing on an app and I thought it wise to get some constructive criticism from SOF about my idea.
Idea:
UIView containing 6 or so buttons stacked vertically
UIButtons have a selected state.
Buttons static/global keeps track of last touched button and always resets the last touched button when a new UIButton is touched.
Question:
Can you read and access the children of the UIView?
eg. (pseudocode)
for (i in [myView children]) {
[[myView getChildAt:i] doSomethingToThisButton];
}
Thanks all!
Yes. Here's the non-pseudocode (well, mostly):
for (UIView *subview in [myView subviews]) {
[subview doSomethingToThisButton];
}
Or, if you prefer
for (int i = 0; i < [myView.subviews count]; i++) {
[[myView.subviews objectAtIndex:i] doSomethingToThisButton];
}
Don't make your last touched button a static variable because then you can only have one such control in your whole app. Make a UIView subclass to act as the container for your buttons, and have the last selected view be a property of that class.
You may also want to make your containing view a subclass of UIControl instead of UIView, then you can make it send events and bind to it using drag and drop in interface builder, just like a regular control (e.g. a button).
Related
I have a NSMutableArray, called self.tappableButtons, of UIButtons which all share the same parent view. At times they overlap, and I would love to sort my array based on their order in relation to each other (starting with the topmost button to the bottommost one in the hierarchy). What's the easiest way to achieve this?
I'm basically using fast enumeration to check if the current touch in my custom pan gesture recognizer falls within a button's bounds. The problem is that if two buttons overlap it returns the one that came first in my array when enumerating, rather than the topmost button within an overlap.
//Search through all our possible buttons
for (UIButton *button in self.tappableButtons) {
//Check if our tap falls within a button's view
if (CGRectContainsPoint(button.bounds, tapLocation)) {
return button;
}
}
The easiest way (without knowing more of your code) is probably to use the subviews order to determine the topmost button on your touch.
UIView* buttonParentView = ....
NSEnumerator* topToBottom = [buttonParentView.subviews reverseObjectEnumerator];
for (id theView in topToBottom) {
if (![self.tappableButtons containsObject: theView]) {
continue;
}
UIButton* button = (UIButton*)theView;
//Check if our tap falls within a button's view
if (CGRectContainsPoint(button.bounds, tapLocation)) {
return button;
}
}
If this function is executed a lot and your self.tappableButtons remains relatively stable, or your parentview has a -lot- of subviews that are not in self.tappableButtons it's probably better to simply use your function but sort the tappableButtons first based on where they appear in their parents subviews.
I have some UIButtons inside a UIScrollView. When the view first loads there are about 8 buttons showing in the UIScrollView, all these buttons are visible and clickable.
However, once I scroll, any button that wasn't there when the view initially loaded isn't clickable.
Given I have a function that creates every button programatically on view load, is it possible that the addTarget function isn't working? I create about 280 buttons at the start and add the UITouchUpInside event programatically.
--Edit--
This is more or less the code, called inside viewDidLoad function
for (int i = 0; i < numberOfButtons; i++){
//Display stuff here
MyButton *aButton = [[MyButton alloc] initWithFrame:CGRectMake(x,y,w,h)];
[aButton.titleLabel setFont:[UIFont fontWithName:#"QuicksandBook-Regular" size: 17.0]];
[aButton setTitle:[currentDisplayArray objectAtIndex:i] forState:UIControlStateNormal];
[aButton addTarget:self
action:#selector(didPressButton:)
forControlEvents:UIControlEventTouchUpInside];
[aButton setUserInteractionEnabled:TRUE];
NSLog(#"width of button = %f height = %f", [aButton frame].size.height, [aButton frame].size.width);
//I printed this to check the height and width were generated correctly.
[btnContainerView addSubview:aButton]; //UIView hooked up to storyboard
[buttonArray addObject:aButton]; //Array to maintain reference to all buttons
}
Remember, they all display correctly, its just the ones that aren't rendered in the initial frame don't trigger the "didPressButton" selector.
-- Edit --
After playing around, I think its some kind of issue with the UIScrollView it is in. Is this some kind of apple bug? I even tried adding the gesture recognizers in the scrollViewDidScroll function.
-- Edit --
Another interesting hint, If I make the UIScrollView bigger, I can click more of the buttons, if I make it smaller I can click less. It definitely has something to do with the first rendered buttons.
Maybe iOS says its initialising the buttons, but doesn't keep the selectors of all 200+ buttons in memory. Or has an inbuilt number of possible selectors/gesture recognisers per class.
Yay, I worked it out.
There was a UIView sitting in front of the UIScrollView that the buttons were being added to.
The views were the same size, but only the buttons that were inside the initial bounds (the UIView size) were clickable.
There was a UIView that I think was called btnContainerView or something. What was happening was the buttons were being added to this UIView instead of the UIScrollView.
I think the UIView was set as the delegate for the buttons because thats were they were rendered. So when the UIScrollView received an input the buck stopped there. Calling addTarget:self on the buttons would mean their target was added to the scrollView, but they were rendered onto the UIView. A confusing bug because you would expect the UIScrollView not to scroll and render properly but it did, its just the delegates for the selectors were getting confused.
Thanks everyone for the help, this was quite tricky because the buttons scrolled correctly (UIScrollView got touch input) but the UIButtons didn't get the touch input.
TL;DR; The solution was removing the intermeditary UIView as it was interfering
I hope that you are using gestures int the view controller, if so you can use the below code
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (([touch.view isKindOfClass:[UIButton class]] && touch.view.tag==<Btn_Tag>)) {
return NO;
}
return YES;
}
When using the gestures, gestures will respond first rather than the button action.So, this code will help to move the responder the button's action.
Pressing the button quickly and not holding for a short time, will not highlight the button.
Different from a UIButton on a common UIView.
Like the head photo in official Twitter client got same issue.
Instagram client seems solved this, all buttons works fine.
Find same question here:
Why doesn't UIButton showsTouchWhenHighlighted work when the button is on a UITableViewCell?
But I still don't know how to fix it.
Well... a UITableView is a subclass of UIScrollView and the UIScrollView class is known to eat touches for it's own purpose.
When it realizes the touch was not meant for it, it passes it to it's immediate subview.
This feature is the delaysContentTouches property (which by default is YES).
Which is why, the UIButton shows it's highlighted state only after a extended touch because the touch event was with the UITableView for a short while until it determined whether the touch was meant for scrolling or swiping the cell and on realizing the touch was for neither, it immediately passes the touch event to the subView directly below it.
In case of a quick-tap, the button's highlighted state is bypassed due to this delay and the target selector method is called directly.
To show the highlighted state of the button in a UITableView (just as it would on a UIView) do:
For iOS7+:
In -viewDidLoad or anywhere appropriate do:
[yourTableViewObject setDelaysContentTouches:NO];
Also... The cell.subviews has a class UITableViewCellScrollView which apparently is another scrollView and we need to disable the delaysContentTouches property of this class as well.
So... in the -cellForRowAtIndexPath: method (just before return cell;) do:
NSArray *test = cell.subviews;
for (UIView *currentView in cell.subviews) {
if ([NSStringFromClass([currentView class]) isEqualToString:#"UITableViewCellScrollView"]) {
UIScrollView *svTemp = (UIScrollView *) currentView;
[svTemp setDelaysContentTouches:NO];
break;
}
}
For iOS 6-:
In iOS6, the cell.subviews has a UITableViewCellContentView class which is not a scrollView subclass and so all it takes is setting one parameter for the tableView alone.
So, in -viewDidLoad or anywhere appropriate, this is all that you need:
[yourTableViewObject setDelaysContentTouches:NO];
PS: By doing this, it will mess up with the scrolling of the tableView so use your better judgement.
I need to add a title over the top toolbar in MPMoviePlayerViewController, and if am playing a video, a user tap should hide the title just like it hide any other controls.
Currently I am adding a UILabel as a subview of moviePlayer view. Though this approach adds a title over the topbar (I am setting the frame accordingly), it does not hide the title when user taps over the screen.
Is there any direct api/hack through which I can get access to the top toolbar of MPMoviePlayerViewController? I am thinking like, if I can add the title as a subView of top toolbar, hiding process will be handled by MPMoviePlayerViewController. Any thoughts?
Thanks in advance!
Note that my answer is referring to MPMoviePlayerController, not MPMoviePlayerViewController. I would advise you to use it either directly or via its reference on MPMoviePlayerViewController.
First of all, never ever add anything directly onto MPMoviePlayerController's view. That is prone to have all kinds of weird side-effects and is clearly advised against in the documentation. Instead use its backgroundView for hosting your custom stuff or the parent of MPMoviePlayerController's view (making it a sibling).
But then again, that won't solve the issue you describe with having that view/label/whatever dis/reappear together with the controls if the user taps or simply waits.
There are ways to do that, but I am afraid those are in the grey-zone - or in other words, have a chance of getting rejected by Apple. In fact its not just a grey-zone but a clear violation of Apple's development guidelines.
Just, I have used this trick in the past on major apps and never got detected/rejected.
First, you need to locate the interface-view of your MPMoviePlayerController instance.
/**
* This quirky hack tries to locate the interface view within the supposingly opaque MPMoviePlayerController
* view hierachy.
* #note This has a fat chance of breaking and/or getting rejected by Apple
*
* #return interface view reference or nil if none was found
*/
- (UIView *)interfaceView
{
for (UIView *views in [self.player.view subviews])
{
for (UIView *subViews in [views subviews])
{
for (UIView *controlView in [subViews subviews])
{
if ([controlView isKindOfClass:NSClassFromString(#"MPInlineVideoOverlay")])
{
return controlView;
}
}
}
}
return nil;
}
UIView *interfaceView = [self interfaceView];
Now that you got the instance, simply add your view/label/whatever onto that view.
[interfaceView addSubview:myAwesomeCustomView];
Our designers have come up with a nice design that involves the index on the right side of the UITableView to be taller than the UITableView itself. It also has some custom images for search and better font/color than the default implementation. However, since UITableView does not support customizing its index (that I know of) I've been trying to get this going manually. The UI looks like this:
This should be a simple task, but I'm having a hell of a time making it work. I have set up a UIView to the right of the table with UIButtons lined up from top to bottom to represent my index. The goal being as the user drags in/out of the UIButtons, I would jump the UITableView to the right section.
I have searched around and it seems that the thing to do this would be to listen to the UIControlEventTouchDragOutside and UIControlEventTouchDragEnter events to know when I'm in/out of one of the UIButtons.
To this end, I have set up the entire list of buttons as an IBOutletCollection which I init in ViewDidLoad like so:
#property (retain, nonatomic) IBOutletCollection(UIButton) NSArray* indexButtons;
#implementation ViewBeerListViewController
...
#synthesize indexButtons = _indexButtons;
- (void)viewDidLoad
{
[super viewDidLoad];
...
// Go through the index buttons and set the change function
for (int i = 0; i < [self.indexButtons count]; ++i)
{
UIButton* button = [self.indexButtons objectAtIndex:i];
[button addTarget:self action:#selector(touchDragOutside:) forControlEvents:UIControlEventTouchDragOutside];
[button addTarget:self action:#selector(touchDragEnter:) forControlEvents:UIControlEventTouchDragEnter];
}
}
The functions touchDragOutside and touchDragEnter look like this:
- (IBAction)touchDragOutside:(UIButton*)sender
{
NSLog(#"Button %# touch dragged outside", [sender titleLabel].text);
}
- (IBAction)touchDragEnter:(UIButton*)sender
{
NSLog(#"Button %# touch dragged enter", [sender titleLabel].text);
}
This all builds and runs. However, it appears that I only ever get events for the button on which I initiated a touch. E.G. if I touch down on the letter "G", and start dragging up and down, I will only see logs for touch dragging out of "G" and into "G". I get no events for any of the other UIButtons as I go over them.
Any help with resolving this issue will be immensely appreciated. I've been stuck on what seems to be a very trivial problem for a very long time.
Thanks!
Instead of making them with UIButtons, try creating a custom UIView that contains UILabels with each letter. Then in the custom UIView class, override the following:
– touchesBegan:withEvent:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
and use those to determine which labels are being touched. Within touchesMoved, for example, you could have:
UITouch * touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
for(UIView * indexView in self.indexViews) {
if(CGRectContainsPoint(indexView.frame,point)) {
//indexView was touched. do something here
break;
}
}
Note that you can also use a UIImageView for the top one instead of UILabels and this will still work.