Setting the accessibilityFrame of an element whose parent view will move - ios

I have a UITableView with a custom header (i.e. I create the UIView myself). I need to tweak the accessibilityFrame of one of the subviews of the view, but I can’t figure out how to set the coordinates of the frame appropriately—they need to be relative to the window, but I’m not sure how to accomplish that.
My code looks like
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger) section
{
CGRect bounds = CGRectMake(0, 0, [tableView frame].size.width, 48);
UIView *header = [[UIView alloc] initWithFrame:bounds];
UILabel *labelOne = [[UILabel alloc] initWithFrame:
CGRectMake(0, 0, bounds.size.width - 80, 18)];
UILabel *labelTwo = [[UILabel alloc] initWithFrame:
CGRectMake(0, 20, bounds.size.width - 80, 18)];
CGRect frameOne = [labelOne frame];
CGRect frameTwo = [labelTwo frame];
[labelTwo setIsAccessibilityElement:NO];
[labelOne setAccessibilityFrame:CGRectUnion(frameOne, frameTwo)];
// ...
return header;
}
I’ve got two UILabels, which I want to combine into one for the purposes of VoiceOver. I accomplish this by ignoring the second label and extending the frame of the first label to cover the area of the second label. (The second label is immediately below the first.) The problem is getting the frames. If I use the code as shown above, the accessibility frame is the correct size, but is positioned as if the UITableView’s header were in the top left corner of the screen. I tried to modify the code to say
CGRect frameOne = [header convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [header convertRect:[labelTwo frame] toView:nil];
but the same thing happened. Shouldn’t this latter piece of code convert the UILabels’ frames into window-relative coordinates?
I thought maybe the issue is that when the UIView is created, it doesn’t know where on screen it’s going to be positioned (and as part of a UITableView it may be scrolled all over the place). Is it necessary to implement accessibilityFrame as a message which checks the UIView’s position each time it is called?

There's a helper function that will assist you with doing exactly that: UIAccessibilityConvertFrameToScreenCoordinates. This function takes a CGRect and converts it from a view's coordinate system into screen coordinates.

I don't think it's the timing of when the UIView is created, as I believe the window should be not-nil by the time tableView:viewForHeaderInSection: is called. I think the problem is the receiver of the convertRect:toView: message. Rather than passing this message to header, you should be passing it to [self view].
You're converting from the receivers coordinate system to that of another view, in this case nil or the UIWindow in your app. When header receives this message, you're converting from header's coordinate system to window's coordinate system, but header itself is a subview of [self view]. Instead, you want to ask [self view] to do the conversion, which should take into account any UINavigationBar's, etc.
CGRect frameOne = [[self view] convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [[self view] convertRect:[labelTwo frame] toView:nil];

Related

Get a UIView’s bounds relative to the window?

I have a few UIButton instances inside a UIView, which in turn is inside the main UIView of the app. What I’m trying to get is a CGRect of one of the buttons’ frames, relative to the window.
So, for instance, if my inner view is at 50, 50 in relation to the main view, then the button is at 10, 10 inside that, I’d want to return 60, 60.
Is there an easy way to do that, without having to keep track of parent views and add them up, etc.?
You can convert a rect to a different view's coordinate system using -[UIView convertRect:toView] and -[UIView convertRect:fromView:].
So, if you have a reference to your outermost view (perhaps if you're using a navigation controller, that might be the outmost view):
UIView *outerView = self.navigationController.view;
UIView *innerView = self.myButton;
CGRect buttonRect = [self.view convertRect:self.innerView.frame toView:outerView];
There are also equivalent convertPoint: methods. It's then really just a case of working out which views you want to convert from / to.
UIView *parent = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 50, 50)];
UIView *child = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 10, 10)];
[parent addSubview:child];
[self.view addSubview:parent];
NSLog(NSStringFromCGRect([self.view convertRect:child.frame fromView:child.superview]));
Result in log:
{{60, 60}, {10, 10}}
Use below function.
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view;
CGPoint pt = [button convertPoint:CGPointMake(button.frame.origin.x, button.frame.origin.y) toView:yourWindow];

Creating an autolayout-based metrics view

I have a reusable view I will be using in UITableViewCell's and UICollectionViewCell's, and need to get its dimensions for tableView:heightForRowAtIndexPath:. Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:, instead my plan is to:
Instantiate the metrics view.
Set the size to include the desired width.
Populate it with data.
Update constraints / Layout subviews.
Grab the height of the view or an internal "sizing" view.
The problem I'm running into is that I cannot force the view to layout without inserting it into the view and waiting for the runloop.
I've distilled a rather boring example. Here's View.xib. The subview is misaligned to highlight that the view is never getting laid out even to the baseline position:
On the main thread I call:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
NSLog(#"Subviews: %#", view.subviews);
view.frame = CGRectMake(0, 0, 200, 200);
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
[self.view addSubview:view];
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Subviews: %#", view.subviews);
});
I get out the following view information:
1) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
2) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
3) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
4) "<UIView: 0x8bad9e0; frame = (0 100; 100 100); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
1 indicates that the fresh-out-of-the-NIB view hasn't been laid out. 2 indicates that updateConstraints/layoutSubviews did nothing. 3 indicates that adding it to the view hierarchy did nothing. 4 finally indicates that adding to the view hierarchy and one pass through the main-loop laid out the view.
I would like to get to the point where I can get the view's dimensions without having to let the application handle it or perform manual calculations (string height + constraint1 + constraint2) on my own.
Update
I've observed that if I place view inside a UIWindow I get a slight improvement:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
view.frame = CGRectMake(0, 0, 200, 200);
[window addSubview:view];
[view layoutSubviews];
If view.translatesAutoresizingMaskIntoConstraints == YES, the view's immediate subviews will be laid out, but none of their children.
The Autolayout Question
In the basic case you mentioned, you can get the correct size by calling setNeedsLayout and then layoutIfNeeded on the container view.
From the UIView class reference on layoutIfNeeded:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy. The UIView implementation of this calls the equivalent CALayer method and so has the same behavior as CALayer.
I don't think the "entire view hierarchy" applies to your use case since the metrics view presumably wouldn't have a superview.
Sample Code
In a sample empty project, with just this code, the correct frame is determined after layoutIfNeeded is called:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIView *redView;
#end
#implementation ViewController
#synthesize redView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 220, 468)];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view setNeedsLayout];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view layoutIfNeeded];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{0, 100}, {100, 100}}"
}
#end
Additional Considerations
Slightly outside the scope of your question, here are some other issues you may run into, since I've worked on this exact problem in a real app:
Calculating this in heightForRowAtIndexPath: might be expensive, so you may want to precalculate and cache the results
Precalculation should be done on a background thread, but UIView layout doesn't work well unless it's done on the main thread
You should definitely implement estimatedHeightForRowAtIndexPath: to reduce the impact of these performance issues
Using intrinsicContentSize
In response to:
Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:
You can use this method if you implement intrinsicContentSize, which lets a view suggest an optimal size for itself. One implementation for this might be:
- (CGSize) intrinsicContentSize {
[self layoutSubviews];
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
This simple approach will only work if your layoutSubviews method doesn't refer to an already-set size (like self.bounds or self.frame). If it does, you may need to do something like:
- (CGSize) intrinsicContentSize {
self.frame = CGRectMake(0, 0, 10000, 10000);
while ([self viewIsWayTooLarge] == YES) {
self.frame = CGRectInset(self.frame, 100, 100);
[self layoutSubviews];
}
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
Obviously, you'd need to adjust these values to match the particular layout of each view, and you may need to tune for performance.
Finally, I'll add that due in part to the exponentially increasing cost of using auto-layout, for all but the simplest table cells, I usually wind up using manual height calculation.
Presumably you're calling the demo code when the view controller first loads its view, like in viewDidLoad or another life cycle method. The nested subview's geometries won't reflect its constraints until viewDidLayoutSubviews is called. Nothing you do during the initial life cycle of a view controller will make that method arrive any faster.
Update 12/30/13: After testing Aaron Brager's sample code, I now realize that the previous paragraph is incorrect. Apparently, you can force layout in viewDidLoad by calling setNeedsLayout followed by layoutIfNeeded.
If you executed the demo code in response to a button click instead, I think you'll see the final geometries of your nested subview logged before the action method completes.
- (IBAction)buttonTapped:(id)sender
{
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
view.frame = CGRectMake(0, 0, 200, 200);
[self.view addSubview:view];
[self.view layoutIfNeeded];
NSLog(#"Subviews: %#", view.subviews);
}
In the latter case, you can request layout on-demand because the view controller has completed its initial setup.
But during a view controller's initial setup, how are you going to get the final geometries of your re-usable subview?
After you set the content for the re-usable subview, have your view controller ask the subview for its size. In other words, implement a method on your custom view that calculates the size based on the content.
For example, if the subview's content is an attributed string, you could use a method like boundingRectWithSize:options:context: to help determine the size of your subview.
CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsersLineFragmentOrigin context:nil];

Can't tap buttons on the left side of 4" iPhone

This is absolutely confusing. On a 3.5" iPhone simulator, all of the UIButtons on my app work just fine. However, when I launch on the 4" iPhone simulator, all of the UIButtons on the left side of the app do not receive any click events.
Below are screenshots of the 3.5" size and the 4" size. On the 4" size, I've added a line. Left of that line, none of the buttons receive click events. To the right of that line, all buttons behave normally. The left side of buttons 2, 5, and 8 do not respond to clicks, but the right sides of those buttons do respond.
UPDATE----
Thanks to #iccir, I've discovered more info. Apparently, my UIWindow is only 320x480 instead of 568x320 as it should be. I'm not touching the UIWindow in my code except to make it key and visible. In my MainWindow.xib I connect its IBOutlet to my rootViewController.
<UIWindow: 0xc097d50; frame = (0 0; 320 480); opaque = NO; autoresize = RM+BM; gestureRecognizers = <NSArray: 0xc098460>; layer = <UIWindowLayer: 0xc097e70>>
I'm flabberghasted. Any idea why the UIWindow is incorrectly sized?
This is a pretty common issue: Your UIButton is outside of the bounds of one of its superviews. If clipsToBounds/masksToBounds is set to NO (the default), your UIButton is still going to show up, but touch events aren't going to be sent to it.
Let's simplify this case. Suppose a view controller with the following code:
- (void) viewDidLoad
{
[super viewDidLoad];
UIColor *fadedRedColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:0.25];
UIColor *fadedBlueColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:0.25];
CGRect containerFrame = CGRectMake(25, 25, 100, 100);
CGRect buttonFrame = CGRectMake(100, 100, 64, 44);
UIView *container = [[UIView alloc] initWithFrame:containerFrame];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:#"Button" forState:UIControlStateNormal];
[button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[button setFrame:buttonFrame];
[button addTarget:self action:#selector(_handleButton:) forControlEvents:UIControlEventTouchUpInside];
[container setBackgroundColor:fadedRedColor];
[button setBackgroundColor:fadedBlueColor];
[container addSubview:button];
[[self view] addSubview:container];
}
- (void) _handleButton:(id)sender
{
NSLog(#"Moooooo!");
}
Which looks like this:
The button is contained in container, but it resides outside of the container's bounds (the container is 100 pixels wide and 100 pixels tall, the button's origin is at 100, 100).
When you touch the screen, UIKit is going to start at the top of the view hierarchy (UIWindow) and call -[UIView hitTest:withEvent:] recursively until it finds the view that should handle the touch. However, in this example, UIKit will never descend into the container (since you touched outside its boundary), and thus the button subview will not be hit.
If we instead change the buttonFrame to be 50, 50, it looks like this:
The part of the button that overlaps with the container will respond to touch event. The part that resides outside of the container will not:
To debug a view that isn't fully touchable, you can try a debugging function like the following:
static void sDebugViewThatIsntTouchable(UIView *view)
{
UIView *superview = [view superview];
while (superview) {
CGRect rectInSuperview = [view convertRect:[view bounds] toView:superview];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rectInSuperview), CGRectGetMinY(rectInSuperview));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rectInSuperview), CGRectGetMinY(rectInSuperview));
CGPoint bottomLeft = CGPointMake(CGRectGetMinX(rectInSuperview), CGRectGetMaxY(rectInSuperview));
CGPoint bottomRight = CGPointMake(CGRectGetMaxX(rectInSuperview), CGRectGetMaxY(rectInSuperview));
if (![superview pointInside:topLeft withEvent:nil]) {
NSLog(#"Top left point of view %# not inside superview %#", view, superview);
}
if (![superview pointInside:topRight withEvent:nil]) {
NSLog(#"Top right point of view %# not inside superview %#", view, superview);
}
if (![superview pointInside:bottomLeft withEvent:nil]) {
NSLog(#"Bottom left point of view %# not inside superview %#", view, superview);
}
if (![superview pointInside:bottomRight withEvent:nil]) {
NSLog(#"Bottom right point of view %# not inside superview %#", view, superview);
}
superview = [superview superview];
}
};
Edit:
As you mentioned in the comments, the culprit view was the main UIWindow, which was sized to 320x480 rather than 320x568. Turning on "Full Screen at Launch" in the xib fixed this.
Of course, the question is: "Why?" :)
If you pull up your xib file in a text editor, you will notice that a width of 320 and height of 480 are hardcoded to the window. When the xib is decoded at launch time, the window is initially constructed with this 320x480 frame.
UIKit then queries -[UIWindow resizesToFullScreen] (a private method). If this returns YES, the UIWindow does the equivalent of [self setFrame:[[self window] bounds]].
Toggling the "Full Screen at Launch" flag in Interface Builder directly toggles the private UIWindow.resizesToFullScreen flag.
Let me guess, this happens only in the landscape mode, right ?
I had the same issue in my app when I was developing specifically for the iPhone-4S. But when I began testing on the iPhone-5, touches on the bottom did not work. It were the frame. Make sure frames are set to bounds, both in the code, and the XIB file (if there is one). Different frames/bounds in the man and xib files, might also result in such behaviour.
I eventually removed the xib files, and did everything programmatically. One thing I learnt was, set your frames in viewWillAppear or viewDidAppear methods instead of viewDidLoad. Also check the frame/bounds in your RootViewController. One last thing, try not to use constant value for frames, use referential frames with respect to superview.
PS, one way to know if it really is the frames/bounds that are responsible for this behaviour is, setting the masksToBounds to YES, for the views. That way, your views will not be visible outside their rects.

How to get frame of subview after apply transform on mainView?

I have created mainView objcet of UIView and added one subview on it. I applied transform on mainView for reducing frame size. But frame of subview of mainView was not reduced. How to reduce the size of this subview.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat widthM=1200.0;
CGFloat heightM=1800.0;
UIView *mainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, widthM, heightM)];
mainView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"te.png"]];
[self.view addSubview:mainView];
CGFloat yourDesiredWidth = 250.0;
CGFloat yourDesiredHeight = yourDesiredWidth *heightM/widthM;
CGAffineTransform scalingTransform;
scalingTransform = CGAffineTransformMakeScale(yourDesiredWidth/mainView.frame.size.width, yourDesiredHeight/mainView.frame.size.height);
mainView.transform = scalingTransform;
mainView.center = self.view.center;
NSLog(#"mainView:%#",mainView);
UIView *subMainView= [[UIView alloc] initWithFrame:CGRectMake(100, 100, 1000, 1200)];
subMainView.backgroundColor = [UIColor redColor];
[mainView addSubview:subMainView];
NSLog(#"subMainView:%#",subMainView);
}
NSlog of these views:
mainView:<UIView: 0x8878490; frame = (35 62.5; 250 375); transform = [0.208333, 0, 0, 0.208333, 0, 0]; layer = <CALayer: 0x8879140>>
subMainView:<UIView: 0x887b8c0; frame = (100 100; 1000 1200); layer = <CALayer: 0x887c160>>
Here the width of mainView is 250, the width of subview is 1000. but when i get the output in simulator, subview is occupied correctly, but it's not cross the mainView. How it is possible? How to get frame of subview with respect mainView frame after transformation?
What you're seeing is expected behavior. The frame of an UIView is relative to its parent, so it doesn't change when you apply a transformation to its superview. While the view will appear 'distorted' too, the frame won't reflect the changes since it's still at exact the same position relative to its parent.
However, I assume you would like to get the frame of the view relative to the topmost UIView. In that case UIKit offers these functions:
– [UIView convertPoint:toView:]
– [UIView convertPoint:fromView:]
– [UIView convertRect:toView:]
– [UIView convertRect:fromView:]
I applied these to your example:
CGRect frame = [[self view] convertRect:[subMainView frame] fromView:mainView];
NSLog(#"subMainView:%#", NSStringFromCGRect(frame));
And this is the output:
subMainView:{{55.8333, 83.3333}, {208.333, 250}}
In addition to s1m0n answer, the beautiful thing about applying a transform matrix to your view, is that you can keep reasoning in terms of its original coordinate system (in your case, you can handle subMainView using the non-transformed coordinate system, which is why, even though subMainView's frame is bigger than mainView's transformed frame, it still doesn't cross the parent view, as it gets automatically transformed). This means that when you have a transformed parent view (for example rotated and scaled) and you want to add a subview in a particular point relative to this parent view, you don't have to first keep track of the previous transformations in order to do so.
If you really are interested in knowing the subview's frame in terms of the transformed coordinate system, it will be enough to apply the same transformation to the subview's rectangle with:
CGRect transformedFrame = CGRectApplyAffineTransform(subMainView.frame, mainView.transform);
If you then NSLog this CGRect, you will obtain:
Transformed frame: {{20.8333, 20.8333}, {208.333, 250}}
Which, I believe, are the values that you were looking for. I hope this answers your question!

How does UIActionSheet showInView method insert its view?

Im trying to implement my own CustomUIActionSheet.
I have it almost working, but I have no idea how does the showInView method works.
(void)showInView:(UIView *)view
giving a view, this method is capable of put its view in front of every single view (adding it to the windows maybe?) but its also capable of settings the rotation accordingly to the view in which is being added.
Ive tried adding it to the windows of the view that I recieve as a parameter.
CGFloat startPosition = view.window.bounds.origin.y + view.window.bounds.size.height;
self.frame = CGRectMake(0, startPosition, view.window.bounds.size.width, [self calculateSheetHeight]);
[view.window addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:view.window.bounds];
[view.window insertSubview:self.blackOutView belowSubview:self];
By doing this, all works, except that when I present the action sheet in landscape, the action sheet apears from the right (since the windows system reference its always the same)
I´ve also tried to add it to the rootViewController of the view windows like that:
UIView * view = view.window.rootViewController.view;
CGFloat startPosition = view.bounds.origin.y + view.bounds.size.height;
self.frame = CGRectMake(0, startPosition, view.bounds.size.width, [self calculateSheetHeight]);
[view addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:view.bounds];
[view insertSubview:self.blackOutView belowSubview:self];
but again, it fails in landscape, it doesnt add anything or at least, I can not see it
So, my question is, any clue of how can I add a view to the top of the hierarchy working in both orientations?
thanks a lot!
After reading some source code of custom alert views and other ui components that are dranw in the top of the hierarchy I found a working solution.
Adding the view in the first subview of the windows:
UIView * topView = [[view.window subviews] objectAtIndex:0];
So the final code is
UIView * topView = [[view.window subviews] objectAtIndex:0];
CGFloat startPosition = topView.bounds.origin.y + topView.bounds.size.height;
self.frame = CGRectMake(0, startPosition, topView.bounds.size.width, [self calculateSheetHeight]);
[topView addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:topView.bounds];
[topView insertSubview:self.blackOutView belowSubview:self];

Resources