Incoherent orientation ov UIViewController and StatusBar when using UIImagePicker - iphone

I've some UITabBarController. In landscape I hide tabBar and expand contents view. Code looks like this:
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
UIView *transView = [self.view.subviews objectAtIndex:0];
if(UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
transView.frame = self.view.bounds;
self.tabBar.hidden = YES;
} else {
self.tabBar.hidden = NO;
CGRect frame = self.view.bounds;
frame.size.height = self.tabBar.frame.origin.y;
transView.frame = frame;
}
}
This handles orientation changes without problem.
Problem appeared when I've added usage of UIImagePicker. UIImagePicker enforces portrait orientation when it is shown (Apple breaks own UI rules: do not enforce orientation).
When application is in landscape and UIImagePicker is shown viewWillLayoutSubviews is invoked twice and UIInterfaceOrientationIsLandscape(self.interfaceOrientation) returns YES.
So far so good.
Now when UIImagePicker is closed (by picking a photo or cancel), viewWillLayoutSubviews is invoked once and UIInterfaceOrientationIsLandscape(self.interfaceOrientation) returns YES, so it should be ok, but result looks like this:
As can be seen status bar is in landscape (ok), UI is in portrait (fail): ui is show as in portrait, also UIController left a space for status bar (black stripe on left), UI is also clipped at top by status bar.
Now this looks like a iOS bug, since UIInterfaceOrientationIsLandscape(self.interfaceOrientation) returns YES but UI remains in portrait.
Environment: iOS 6.1.3 iPod Touch 5g.
Is there a way to fix it: make status bar remain on left xor enforce UI in landscape?
Update:
after further investigation I've noticed that willRotateToInterfaceOrientation and didRotateFromInterfaceOrientation are never called during this scenario. I even added this log in various places and it always prints "3 3 3 3" (UIInterfaceOrientationLandscapeLeft):
NSLog(#"%d %d %d %d",
self.navigationController.interfaceOrientation,
self.interfaceOrientation,
self.selectedViewController.interfaceOrientation,
[UIDevice currentDevice].orientation);
So state information says UIInterfaceOrientationLandscapeLeft but I see portrait UI and landscape status bar.
I've added this kind of logs to see when rotation is performed:
UIView *view = self.view;
int i=0;
while(view) {
CGAffineTransform trans = view.transform;
NSLog(#"Transform: %d %#", i, NSStringFromCGAffineTransform(trans));
++i;
view = view.superview;
}
In case of normal rotation (from portrait to landscape) log looks like this:
Transform: 0 [1, 0, 0, 1, 0, 0]
Transform: 1 [1, 0, 0, 1, 0, 0]
Transform: 2 [1, 0, 0, 1, 0, 0]
Transform: 3 [0, 1, -1, 0, 0, 0]
Transform: 4 [1, 0, 0, 1, 0, 0]
In problematic case: device in landscape, closing UIImagePicker:
Transform: 0 [1, 0, 0, 1, 0, 0]
Transform: 1 [1, 0, 0, 1, 0, 0]
Transform: 2 [1, 0, 0, 1, 0, 0]
Transform: 3 [1, 0, -0, 1, 0, 0]
Transform: 4 [1, 0, 0, 1, 0, 0]
Transform: 5 [1, 0, 0, 1, 0, 0]
This is strange that view stack has changed (increased by 1). Also it can be seen that view with index 3 is problematic.

I found EVIL HACK on this mysterious issue:
- (void)doAWorkaroudnOnUnwantedRotation {
// check if is workaround nesesery:
if (UIInterfaceOrientationIsLandscape([UIDevice currentDevice].orientation)) {
double delayInSeconds = 0.5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
UIViewController *fake = [[[UIViewController alloc] init] autorelease];
UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
[rootController presentModalViewController: fake animated: NO];
[fake dismissModalViewControllerAnimated: NO];
});
}
}
I call this method when UIImagePicker is dismissed. Without dispatch_after nothing happens since picker is animated and until animation finishes this workaround will not work.

Related

IOS Hybrid App - Avoiding the safe area on iPhone 11

For my IOS hybrid app I create a WKWebView programatically as shown below
- (void)viewDidLoad
{
[super viewDidLoad];
_statBarH = [UIApplication sharedApplication].statusBarFrame.size.height ;
appH = self.view.frame.size.height ;
appW = self.view.frame.size.width ;
webView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:theConfiguration];
smallScreen = [deviceType hasPrefix:#"iPhone"];
if (smallScreen && appW > appH) {webView.frame = CGRectMake(0, 0 , appW, appH);}
else {webView.frame = CGRectMake(0, _statBarH, appW, appH - _statBarH);};
};
I also change the size of the web view when the device rotates as follows:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
appH = size.height ;
appW = size.width ;
if (smallScreen && appW > appH) {webView.frame = CGRectMake(0, 0 , appW, appH);}
else {webView.frame = CGRectMake(0, _statBarH, appW, appH - _statBarH);};
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
[webView evaluateJavaScript:#"IOSOrientationChanged()" completionHandler:nil];
}
];
}
The javascript function IOSOrientationChanged merely redraws my app content in either portrait or landscape format, detecting the new height and width from javascript window.innerWidth and window.innerHeight
This approach has worked fine for me, until Apple removed the home button on iPhone X, 11 ...
What happens on these newer phones (at least in the XCode 11 IOS 13 simulator as I cannot afford one for real) is that when the web view is first created, it respects the upper safe area, but does not respect the lower safe area.
However when I rotate the device in the simulator, the webview respects both the safe areas.
For iPhone 11, the variables appW and appH are 414 and 896 in portrait and 896 and 414 in landscape. The variable _statBarH is 44 in portrait and 0 in landscape.
However inside the web view, window.innerHeight in portrait is initially set to 852 (896-44), but after rotating it is set to 838.
Please can someone tell me what I am doing wrong? Are some internal view constraints not being enforced the first time, but are being enforced after rotation?
The solution to this, is two fold
Firstly create the WKWebView in viewDidAppear, rather than in viewDidLoad, as it is not until viewDidAppear that you can programatically access the safeAreaInserts values
Secondly, use the values in safeAreaInserts when creating the WKWebView frame, as below
smallScreen = [deviceType hasPrefix:#"iPhone"];
topMargin = [UIApplication sharedApplication].statusBarFrame.size.height ;
if (#available(iOS 11, *)) topMargin = self.view.safeAreaInsets.top ;
bottomMargin = 0 ;
if (#available(iOS 11, *)) bottomMargin = self.view.safeAreaInsets.bottom ;
int top = (smallScreen && appW > appH)? 0 : topMargin ;
int height = (smallScreen && appW > appH)? appH : appH - topMargin - bottomMargin ;
NSLog (#"Top %d, Height %d",top,height);
webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, top , appW, height) configuration:theConfiguration];

How to handle the change of orientation like the standard camera application on the iPhone

I use AVFoundation framework to get access to camera.
In the standard camera application, when the orientation changes only the rotation of the icons on the buttons.
But when I try to do something like, I have a problem related to the orientation changes animation. Preview view is rotated and the area behind it is black and has image jerks when turning.
Now I have this code:
-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration]
forKey:kCATransactionAnimationDuration];
if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft){
captureManager.previewLayer.transform=CATransform3DMakeRotation(M_PI/2, 0, 0, 1.0);
captureManager.previewLayer.frame = CGRectMake(0, 0, 480, 320);
} else if (toInterfaceOrientation==UIInterfaceOrientationPortrait){
captureManager.previewLayer.transform=CATransform3DMakeRotation(0, 0, 0, 1.0);
captureManager.previewLayer.frame = CGRectMake(0, 0, 320, 480);
} else if (toInterfaceOrientation==UIInterfaceOrientationLandscapeRight){
captureManager.previewLayer.transform=CATransform3DMakeRotation(M_PI+M_PI/2, 0, 0, 1.0);
captureManager.previewLayer.frame = CGRectMake(0, 0, 480, 320);
}
[CATransaction commit];
}
My idea it is use transform animation to deal with rotation animation but I get problems with glithes.
How can create camera preview view and solve problems with orientation change?
May be I must use other instrument to work with camera? Not AVFoundation framework?
I know that I can lock orientation and use accelerometer to deal with buttons icons. But the i get problems with some subviewcontrollers.
Sorry for terrible english.
You need to disable landscape orientation for your controller and update layout of icons etc by handling orientation change notifications. In this topic it is described right - How to handle autorotation in AVCaptureVideoPreviewLayer?
Nikita's answer works but requires extra code to handle the behavior of the UI elements when the device rotates. If that's too much trouble, another option (which I implemented and works) is to have 2 different UIWindow instances, one for the camera's preview layer and one for the UI elements to display on to. Notice that for this to work, the Window with the UI elements needs to have a transparent background.
I handle preview rotation change in the following way.
P.S all subviews have corresponding constraints set on storyboard so i only update the frame of videoPreviewLayer.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
DispatchQueue.main.async {
self.videoPreviewLayer.frame = self.previewView.bounds
self.setRotation()
}
}
func setRotation() {
let deviceOrientation = UIDevice.current.orientation
switch deviceOrientation {
case .landscapeLeft:
videoPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
break
case .landscapeRight:
videoPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.landscapeLeft
break
case .portrait:
videoPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
break
case .portraitUpsideDown:
videoPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portraitUpsideDown
break
default:
break
}
}

How to implement an non-rectangle scroll content on iPhone's scrollview?

Typically, the scrollView's content view is a rectangle. But I would like to implement that is not a rectangle.... For example....
The yellow, Grid 6 is the current position...Here is the example flow:
User swipe to left. (cannot scroll to left) Current: 6.
User swipe to right. (scroll to right) Current: 7.
User swipe to down. (scroll to down) Current: 8.
User swipe to down. (cannot scroll to down) Current: 8.
As you can see, the Content view of the scrollView is not rectangle. Any ideas on how to implement it? Thanks.
This is an interesting idea to implement. I can think of a few approaches that might work. I tried out one, and you can find my implementation in my github repository here. Download it and try it out for yourself.
My approach is to use a normal UIScrollView, and constrain its contentOffset in the delegate's scrollViewDidScroll: method (and a few other delegate methods).
Preliminaries
First, we're going to need a constant for the page size:
static const CGSize kPageSize = { 200, 300 };
And we're going to need a data structure to hold the current x/y position in the grid of pages:
typedef struct {
int x;
int y;
} MapPosition;
We need to declare that our view controller conforms to the UIScrollViewDelegate protocol:
#interface ViewController () <UIScrollViewDelegate>
#end
And we're going to need instance variables to hold the grid (map) of pages, the current position in that grid, and the scroll view:
#implementation ViewController {
NSArray *map_;
MapPosition mapPosition_;
UIScrollView *scrollView_;
}
Initializing the map
My map is just an array of arrays, with a string name for each accessible page and [NSNull null] at inaccessible grid positions. I'll initialize the map from my view controller's init method:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
[self initMap];
}
return self;
}
- (void)initMap {
NSNull *null = [NSNull null];
map_ = #[
#[ #"1", null, #"2"],
#[ #"3", #"4", #"5" ],
#[ null, #"6", #"7" ],
#[ null, null, #"8" ],
];
mapPosition_ = (MapPosition){ 0, 0 };
}
Setting up the view hierarchy
My view hierarchy will look like this:
top-level view (gray background)
scroll view (transparent background)
content view (tan background)
page 1 view (white with a shadow)
page 2 view (white with a shadow)
page 3 view (white with a shadow)
etc.
Normally I'd set up some of my views in a xib, but since it's hard to show xibs in a stackoverflow answer, I'll do it all in code. So in my loadView method, I first set up a “content view” that will live inside the scroll view. The content view will contain a subviews for each page:
- (void)loadView {
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [map_[0] count] * kPageSize.width, map_.count * kPageSize.height)];
contentView.backgroundColor = [UIColor colorWithHue:0.1 saturation:0.1 brightness:0.9 alpha:1];
[self addPageViewsToContentView:contentView];
Then I'll create my scroll view:
scrollView_ = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, kPageSize.width, kPageSize.height)];
scrollView_.delegate = self;
scrollView_.bounces = NO;
scrollView_.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin
| UIViewAutoresizingFlexibleRightMargin
| UIViewAutoresizingFlexibleTopMargin
| UIViewAutoresizingFlexibleBottomMargin);
I add the content view as a subview of the scroll view and set up the scroll view's content size and offset:
scrollView_.contentSize = contentView.frame.size;
[scrollView_ addSubview:contentView];
scrollView_.contentOffset = [self contentOffsetForCurrentMapPosition];
Finally, I create my top-level view and give it the scroll view as a subview:
UIView *myView = [[UIView alloc] initWithFrame:scrollView_.frame];
[myView addSubview:scrollView_];
myView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
self.view = myView;
}
Here's how I compute the scroll view's content offset for the current map position, and for any map position:
- (CGPoint)contentOffsetForCurrentMapPosition {
return [self contentOffsetForMapPosition:mapPosition_];
}
- (CGPoint)contentOffsetForMapPosition:(MapPosition)position {
return CGPointMake(position.x * kPageSize.width, position.y * kPageSize.height);
}
To create subviews of the content view for each accessible page, I loop over the map:
- (void)addPageViewsToContentView:(UIView *)contentView {
for (int y = 0, yMax = map_.count; y < yMax; ++y) {
NSArray *mapRow = map_[y];
for (int x = 0, xMax = mapRow.count; x < xMax; ++x) {
id page = mapRow[x];
if (![page isKindOfClass:[NSNull class]]) {
[self addPageViewForPage:page x:x y:y toContentView:contentView];
}
}
}
}
And here's how I create each page view:
- (void)addPageViewForPage:(NSString *)page x:(int)x y:(int)y toContentView:(UIView *)contentView {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(CGRectMake(x * kPageSize.width, y * kPageSize.height, kPageSize.width, kPageSize.height), 10, 10)];
label.text = page;
label.textAlignment = NSTextAlignmentCenter;
label.layer.shadowOffset = CGSizeMake(0, 2);
label.layer.shadowRadius = 2;
label.layer.shadowOpacity = 0.3;
label.layer.shadowPath = [UIBezierPath bezierPathWithRect:label.bounds].CGPath;
label.clipsToBounds = NO;
[contentView addSubview:label];
}
Constraining the scroll view's contentOffset
As the user moves his finger around, I want to prevent the scroll view from showing an area of its content that doesn't contain a page. Whenever the scroll view scrolls (by updating its contentOffset), it sends scrollViewDidScroll: to its delegate, so I can implement scrollViewDidScroll: to reset the contentOffset if it goes out of bounds:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint contentOffset = scrollView_.contentOffset;
First, I want to constrain contentOffset so the user can only scroll horizontally or vertically, not diagonally:
CGPoint constrainedContentOffset = [self contentOffsetByConstrainingMovementToOneDimension:contentOffset];
Next, I want to constrain contentOffset so that it only shows parts of the scroll view that contain pages:
constrainedContentOffset = [self contentOffsetByConstrainingToAccessiblePoint:constrainedContentOffset];
If my constraints modified contentOffset, I need to tell the scroll view about it:
if (!CGPointEqualToPoint(contentOffset, constrainedContentOffset)) {
scrollView_.contentOffset = constrainedContentOffset;
}
Finally, I update my idea of the current map position based on the (constrained) contentOffset:
mapPosition_ = [self mapPositionForContentOffset:constrainedContentOffset];
}
Here's how I compute the map position for a given contentOffset:
- (MapPosition)mapPositionForContentOffset:(CGPoint)contentOffset {
return (MapPosition){ roundf(contentOffset.x / kPageSize.width),
roundf(contentOffset.y / kPageSize.height) };
}
Here's how I constrain the movement to just horizontal or vertical and prevent diagonal movement:
- (CGPoint)contentOffsetByConstrainingMovementToOneDimension:(CGPoint)contentOffset {
CGPoint baseContentOffset = [self contentOffsetForCurrentMapPosition];
CGFloat dx = contentOffset.x - baseContentOffset.x;
CGFloat dy = contentOffset.y - baseContentOffset.y;
if (fabsf(dx) < fabsf(dy)) {
contentOffset.x = baseContentOffset.x;
} else {
contentOffset.y = baseContentOffset.y;
}
return contentOffset;
}
Here's how I constrain contentOffset to only go where there are pages:
- (CGPoint)contentOffsetByConstrainingToAccessiblePoint:(CGPoint)contentOffset {
return [self isAccessiblePoint:contentOffset]
? contentOffset
: [self contentOffsetForCurrentMapPosition];
}
Deciding whether a point is accessible turns out to be the tricky bit. It's not enough to just round the point's coordinates to the nearest potential page center and see if that rounded point represents an actual page. That would, for example, let the user drag left/scroll right from page 1, revealing the empty space between pages 1 and 2, until page 1 is half off the screen. We need to round the point down and up to potential page centers, and see if both rounded points represent valid pages. Here's how:
- (BOOL)isAccessiblePoint:(CGPoint)point {
CGFloat x = point.x / kPageSize.width;
CGFloat y = point.y / kPageSize.height;
return [self isAccessibleMapPosition:(MapPosition){ floorf(x), floorf(y) }]
&& [self isAccessibleMapPosition:(MapPosition){ ceilf(x), ceilf(y) }];
}
Checking whether a map position is accessible means checking that it's in the bounds of the grid and that there's actually a page at that position:
- (BOOL)isAccessibleMapPosition:(MapPosition)p {
if (p.y < 0 || p.y >= map_.count)
return NO;
NSArray *mapRow = map_[p.y];
if (p.x < 0 || p.x >= mapRow.count)
return NO;
return ![mapRow[p.x] isKindOfClass:[NSNull class]];
}
Forcing the scroll view to rest at page boundaries
If you don't need to force the scroll view to rest at page boundaries, you can skip the rest of this. Everything I described above will work without the rest of this.
I tried setting pagingEnabled on the scroll view to force it to come to rest at page boundaries, but it didn't work reliably, so I have to enforce it by implementing more delegate methods.
We'll need a couple of utility functions. The first function just takes a CGFloat and returns 1 if it's positive and -1 otherwise:
static int sign(CGFloat value) {
return value > 0 ? 1 : -1;
}
The second function takes a velocity. It returns 0 if the absolute value of the velocity is below a threshold. Otherwise, it returns the sign of the velocity:
static int directionForVelocity(CGFloat velocity) {
static const CGFloat kVelocityThreshold = 0.1;
return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}
Now I can implement one of the delegate methods that the scroll view calls when the user stops dragging. In this method, I set the targetContentOffset of the scroll view to the nearest page boundary in the direction that the user was scrolling:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if (fabsf(velocity.x) > fabsf(velocity.y)) {
*targetContentOffset = [self contentOffsetForPageInHorizontalDirection:directionForVelocity(velocity.x)];
} else {
*targetContentOffset = [self contentOffsetForPageInVerticalDirection:directionForVelocity(velocity.y)];
}
}
Here's how I find the nearest page boundary in a horizontal direction. It relies on the isAccessibleMapPosition: method, which I already defined earlier for use by scrollViewDidScroll::
- (CGPoint)contentOffsetForPageInHorizontalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x + direction, mapPosition_.y };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
And here's how I find the nearest page boundary in a vertical direction:
- (CGPoint)contentOffsetForPageInVerticalDirection:(int)direction {
MapPosition newPosition = (MapPosition){ mapPosition_.x, mapPosition_.y + direction };
return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}
I discovered in testing that setting targetContentOffset did not reliably force the scroll view to come to rest on a page boundary. For example, in the iOS 5 simulator, I could drag right/scroll left from page 5, stopping halfway to page 4, and even though I was setting targetContentOffset to page 4's boundary, the scroll view would just stop scrolling with the 4/5 boundary in the middle of the screen.
To work around this bug, we have to implement two more UIScrollViewDelegate methods. This one is called when the touch ends:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[scrollView_ setContentOffset:[self contentOffsetForCurrentMapPosition] animated:YES];
}
}
And this one is called when the scroll view stops decelerating:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGPoint goodContentOffset = [self contentOffsetForCurrentMapPosition];
if (!CGPointEqualToPoint(scrollView_.contentOffset, goodContentOffset)) {
[scrollView_ setContentOffset:goodContentOffset animated:YES];
}
}
The End
As I said at the beginning, you can download my test implementation from my github repository and try it out for yourself.
That's all, folks!
I'm assuming you're using the UIScrollView in paged mode (swipe to show an entire new screen).
With a bit jiggery-pokery you can achieve the effect you want.
The trick is to ensure that whatever square you're currently viewing, you have the UIScrollView configured so that only the visible central view, and the surrounding view that you could scroll too, are added to the scroll view (and at the correct offset). You also must ensure that the size of the scrollable content (and the current offset) is set correctly, to prevent scrolling in a direction that would take you to no content.
Example: suppose you're viewing square 6 currently. At that point, your scroll view would just have 4 views added to it: 4, 5, 6 and 7, in the correct relative offsets. And you set the content size of the scroll view to be equivelant to 2 x 2 squares size. This will prevent scrolling down or to the left (where there are no tiles) but will allow scrolling in the correct direction.
You'll need your delegate to detect scrollViewDidEndDecelerating:. In that instance, you then have to set up your views, content offset, and content size as described above, for the new location.

Having trouble getting UIView sizeToFit to do anything meaningful

When I add a subview to a UIView, or when I resize an existing subview, I would expect [view sizeToFit] and [view sizeThatFits] to reflect that change. However, my experience is that sizeToFit does nothing, and sizeThatFits returns the same value before and after the change.
My test project has a single view that contains a single button. Clicking the button adds another button to the view and then calls sizeToFit on the containing view. The bounds of the view are dumped to the console before and after adding the subview.
- (void) logSizes {
NSLog(#"theView.bounds: %#", NSStringFromCGRect(theView.bounds));
NSLog(#"theView.sizeThatFits: %#", NSStringFromCGSize([theView sizeThatFits:CGSizeZero]));
}
- (void) buttonTouched {
[self logSizes];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
btn.frame = CGRectMake(10.0f, 100.0f, 400.0f, 600.0f);
[theView addSubview:btn];
[theView sizeToFit];
[self performSelector:#selector(logSizes) withObject:nil afterDelay:1.0];
}
And the output is:
2010-10-15 15:40:42.359 SizeToFit[14953:207] theView.bounds: {{0, 0}, {322, 240}}
2010-10-15 15:40:42.387 SizeToFit[14953:207] theView.sizeThatFits: {322, 240}
2010-10-15 15:40:43.389 SizeToFit[14953:207] theView.bounds: {{0, 0}, {322, 240}}
2010-10-15 15:40:43.391 SizeToFit[14953:207] theView.sizeThatFits: {322, 240}
I must be missing something here.
Thanks.
The documentation is pretty clear on this. -sizeToFit pretty much calls -sizeThatFits: (probably with the view's current size as the argument), and the default implementation of -sizeThatFits: does almost nothing (it just returns its argument).
Some UIView subclasses override -sizeThatFits: to do something more useful (e.g. UILabel). If you want any other functionality (such as resizing a view to fit its subviews), you should subclass UIView and override -sizeThatFits:.
If you won't override UIView, u can just use extension.
Swift:
extension UIView {
func sizeToFitCustom () {
var size = CGSize(width: 0, height: 0)
for view in self.subviews {
let frame = view.frame
let newW = frame.origin.x + frame.width
let newH = frame.origin.y + frame.height
if newW > size.width {
size.width = newW
}
if newH > size.height {
size.height = newH
}
}
self.frame.size = size
}
}
The same code but 3 times faster:
extension UIView {
final func sizeToFitCustom() {
var w: CGFloat = 0,
h: CGFloat = 0
for view in subviews {
if view.frame.origin.x + view.frame.width > w { w = view.frame.origin.x + view.frame.width }
if view.frame.origin.y + view.frame.height > h { h = view.frame.origin.y + view.frame.height }
}
frame.size = CGSize(width: w, height: h)
}
}
You can do some like that using IB alone (xcode 4.5):
Click on the UIView
in the Size inspector drag content hugging to 1 (both horizontal and vertical)
drag compression resistance to 1000 (for both)
under the UIView's constraints click on Width and change priority to 250
Do the same for Height
You can use the UIView's inset to control padding for left/right/top/bottom
self.errorMessageLabel.text = someNewMessage;
// We don't know how long the given error message might be, so let's resize the label + containing view accordingly
CGFloat heightBeforeResize = self.errorMessageLabel.frame.size.height;
[self.errorMessageLabel sizeToFit];
CGFloat differenceInHeightAfterResize = self.errorMessageLabel.frame.size.height - heightBeforeResize;
self.errorViewHeightContstraint.constant = kErrorViewHeightConstraintConstant + differenceInHeightAfterResize;
This worked for me.

hitTest returns wrong UIView

I have a view hierarchy which contains smaller views on a scroll view. Each view can have subviews in it such as buttons etc.
For some reason, buttons on the view aren't clicked; exploring this further showed that while the scroll view receives the touchBegan event, the button does not. Calling the hitTest:event: message shows that the button is not returned, even though it is within the limits.
I've included a log output describing the touch's location on the scroll view, the item returned from hitTest, the touch's location if I called locationInView: using the expected item, and the hierarchy of the expected item (with frames printed). From this output I can deduce that the button should have been called...
Can anyone explain this? Am I missing something?
touched ({451, 309}) on <VCViewContainersView: 0x4b31ee0; frame = (0 0; 748 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x4b32130>> (location in expected item: {17, 7.5})
expected touched item is:
view: <UIButtonLabel: 0x482b920; frame = (32 5; 36 19); text = 'Click'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x4831370>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIRoundedRectButton: 0x482c100; frame = (50 50; 100 30); opaque = NO; layer = <CALayer: 0x482c450>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIImageView: 0x480f290; frame = (0 0; 320 255); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x480e840>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <VCViewContainer: 0x4b333c0; frame = (352 246.5; 320 471.75); layer = <CALayer: 0x4b33d50>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <UIScrollView: 0x4b32600; frame = (0 0; 1024 748); clipsToBounds = YES; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x4b32780>>, layer transform: [1, 0, 0, 1, 0, 0]
view: <VCViewsContainerView: 0x4b31ee0; frame = (0 0; 748 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x4b32130>>, layer transform: [0, 1, -1, 0, 0, 0]
view: <UIWindow: 0x4b1d590; frame = (0 0; 768 1024); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x4b1d6d0>>, layer transform: [1, 0, 0, 1, 0, 0]
Update: Other than the UIWindow and VCViewsContainerView, all views are created programmatically using initWithFrame: or in the case of the button, buttonWithType:. The VCViewContainer is initialized using CGRectZero and when the UIImageView is created, its frame is set to the image's size + additional space for labels on the bottom of it.
Update 2: When calling [self.layer hitTest:location] with the same location, I get the layer of the correct view! What's going on here...?
hitTest:withEvent: starts at the window. Each view tests its subviews before testing itself, and so on, recursively. If a view's userInteractionEnabled is NO, however, it returns nil from hitTest:withEvent:, without testing its subviews. Such a view is certainly hit-tested, but it immediately replies that neither it nor any of its subviews is the hit view.
You UIScrollView has its userInteractinonEnabled set to NO. Thus, when the VCViewContainersView tests its subview the UIScrollView, and the UIScrollView returns nil because its userInteractionEnabled is NO, the VCViewContainersView uses pointInside:withEvent: on itself, finds that the touch is within itself, and returns itself as the hit view (and the search ends). This explains the result you are getting.
The reason this doesn't happen when you do the hit-test by way of the layers is that layers are not touchable and know nothing about the rules for touches, so they ignore userInteractionEnabled, which is a view feature, not a layer feature. Layer hit-testing is sort of a kludge, intended only for when a view contains a whole layer hierarchy (without a view hierarchy) and you want to simulate that a particular layer is touchable. The docs do tell you that the logic is different for a layer's hitTest: than it is for a view's hitTest:withEvent:, though they fail to explain exactly how.
I don't know why you have set your scroll view to be non-touchable, but if that's important to you, you can override hitTest:withEvent: in a UIScrollView subclass so that it tests its subviews but returns nil if all of them return nil.
You might want to try subclassing your UIScrollView to receive touches on objects within it.
By default your touches will be received by your UIScrollView, so if you want to receive touches on objects inside of it, you need to set that up explicitly.
Here's a working subclass to do just that. Thanks to the SO community for this, because I believe I originally found something like this here, just can't dig up the original post now.
Here's the .h:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface TouchScroller : UIScrollView
{
}
#end
And the .m:
#import "TouchScroller.h"
#implementation TouchScroller
- (id)initWithFrame:(CGRect)frame
{
return [super initWithFrame:frame];
}
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
// If not dragging, send event to next responder
if (!self.dragging)
[self.nextResponder touchesEnded: touches withEvent:event];
else
[super touchesEnded: touches withEvent: event];
}
#end
That should do it.
If I understand view stack correctly then your button is subview of some UIImageView - it has userInteractionEnabled property set to NO (by default) - so the image view and all its subviews won't receive any touch events.
Setting image view's userInteractionEnabled property to YES must solve the problem