UIScrollView autorotation issue - subviews sticking to original positions and moving to new positions at the same time

StackOverflow https://stackoverflow.com/questions/21800882

Question

In my iOS app, part of it is a video player (using the YouTube API). I've decided to layout this view in code with a scroll view in order to make sure that the views are correctly laid out in the iPad version of the app in all rotations. Before I continue, here's my code for the laying out of the subviews. I call this whenever my view comes on the screen.

- (void)setupView {

// Set up Open in YouTube App button
self.openInYouTubeAppButton = [[UIBarButtonItem alloc] initWithTitle:@"Open in YouTube App" style:UIBarButtonItemStylePlain target:self action:@selector(openInYouTubeApp)];
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"youtube://"]]) {
    self.navigationItem.rightBarButtonItems = @[self.shareButton, self.openInYouTubeAppButton];
} else {
    self.navigationItem.rightBarButtonItems = @[self.shareButton];
}

self.scrollView.frame = self.view.frame;

// Set up video embed view
self.videoEmbedView = ({
    // Frame
    UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 768, 432)];
    webView.center = CGPointMake(self.scrollView.center.x, (webView.bounds.size.height / 2));

    // View Properties
    webView.scrollView.contentInset = UIEdgeInsetsMake((-72 + 64), -8, 0, 0); // makes sure the video is fully seen in the view at first glance
    webView.scrollView.scrollEnabled = NO; // doesn't allow the user to mistakenly scroll the video

    webView;
});

self.videoTitleLabel = ({
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, (self.videoEmbedView.bounds.size.height + 14), 0, 0)]; // initial frame

    label.text = self.videoTitle; // text is video title
    label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; // dynamic type style
    label.preferredMaxLayoutWidth = self.view.bounds.size.width; // makes sure label text does not extend outside the view.
    [label sizeToFit];
    label.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + (label.bounds.size.height / 2))); // set center

    label;
});

self.videoDateLabel = ({
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8), 0, 0)]; // initial frame

    label.text = [NSDateFormatter localizedStringFromDate:self.videoDate dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterNoStyle];
    label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; // dynamic type style
    label.preferredMaxLayoutWidth = self.view.bounds.size.width; // makes sure label text does not extend outside the view.
    [label sizeToFit];
    label.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + (label.bounds.size.height / 2))); // set center

    label;
});

self.videoDescriptionTextView = ({
    UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25, 768, 200) textContainer:nil]; // initial frame

    textView.text = self.videoDescription;
    textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    textView.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25 + (textView.bounds.size.height / 2))); // set center

    textView;
});

// Update scroll view content size
[self.scrollView setContentSize:CGSizeMake(self.view.bounds.size.width, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25 + self.videoDescriptionTextView.bounds.size.height))];

// Add views to scroll view
[self.scrollView addSubview:self.videoEmbedView];
[self.scrollView addSubview:self.videoTitleLabel];
[self.scrollView addSubview:self.videoDateLabel];
[self.scrollView addSubview:self.videoDescriptionTextView];

// Set description text view frame to content size
CGRect frame = self.videoDescriptionTextView.frame;
frame.size.height = self.videoDescriptionTextView.contentSize.height;
self.videoDescriptionTextView.frame = frame;

// Update scroll view content size
[self.scrollView setContentSize:CGSizeMake(self.view.bounds.size.width, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25 + self.videoDescriptionTextView.bounds.size.height))];

// Load video into embed view
NSString *embed = [NSString stringWithFormat:@"<iframe width='%f' height='%f' src='http://www.youtube.com/embed/%@' frameborder='0' allowfullscreen></iframe>", 768.0, 432.0, self.videoID];
[self.videoEmbedView loadHTMLString:embed baseURL:self.videoURL];
}

This code works fine in the portrait orientation (there is, however, an error with setting the text view for the description's size dynamically to fit the content; i could use help with that also). This is my screen in Portrait

Then in the rotation to landscape (or when the view is launched in landscape, for that matter), I figured I need to edit my subviews locations; like so (I was told that this was the best method to override for autorotation; it yields the same result as all the other ways of autorotation customization):

- (void)viewWillLayoutSubviews {
CGFloat screenHeight =[UIScreen mainScreen].bounds.size.height;
CGFloat screenWidth =[UIScreen mainScreen].bounds.size.width;
self.scrollView.frame = CGRectMake(0, 0, screenWidth, screenHeight);

[self.scrollView setContentSize:CGSizeMake(self.view.bounds.size.width, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25 + self.videoDescriptionTextView.bounds.size.height))];

// Scroll view subviews
self.videoEmbedView.center = CGPointMake(self.scrollView.center.x, (self.videoEmbedView.bounds.size.height / 2));
self.videoTitleLabel.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + (self.videoTitleLabel.bounds.size.height / 2)));
self.videoDateLabel.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + (self.videoDateLabel.bounds.size.height / 2)));
self.videoDescriptionTextView.center = CGPointMake(self.view.center.x, (self.videoEmbedView.bounds.size.height + 14 + self.videoTitleLabel.bounds.size.height + 8 + self.videoDateLabel.bounds.size.height + 25 + (self.videoDescriptionTextView.bounds.size.height / 2)));
}

But this is not working as well as I hoped, I wanted everything to be center-justified and even-ish in landscape, but I get this instead: Landscape autorotation is error

As you can see, the views stick to their previous positions as well as appear in their new positions; and the video embed view doesn't even move at all!

Can someone help me? I am stuck and I've looked everywhere for SOME guidance as to what generally to do with UIScrollView subviews in autorotation, so now I thought it was time to get some specific answers to my situation. Thanks in advance for your help.

Was it helpful?

Solution

There's not a simple 'fix this line of code' fix to this problem. In my experience, this kind of rotation sizing within a scroll view is best accomplished by using auto layout to order the views within the UIScrollView and using auto layout to size them according to the container of the scroll view.

From your code it looks like you've got this structure of views:

UIScrollView (a)
    UIWebView (b)
    UILabel (c)
    UILabel (d)
    UILabel (e)
    etc.

I would create a slightly different structure:

UIView (Z)
    UIScrollView (a)
       UIWebView (b)
       UILabel (c)
       UILabel (d)
       UILabel (e)
       etc.

Using the auto layout syntax I would do these constraints: H:|[Z]| and V:|[Z]| and H:|[a]| and V:|[a]| With those, the container UIView and the UIScrollView will always fill their space allocated. Then for spacing the content I would do: V:|[b][c][d][e]| H:|[b] H:|[c] H:|[d] H:|[e] You may want to set the size of 'a' to a fixed height do accommodate the video, but once you do that the rest of the views will flow height wise. The horizontal axis constraints, e.g. H:|[a], tells auto layout that all of the views are fixed to the leading edge of the UIScrollView (their parent).

The next part can't really be expressed in auto layout visual format language, but what you do is create constraints such that the width of each view within the UIScrollView (b,c,d, & e) are the same width as the container UIView (Z).

When auto layout does it's layout magic, then it will use the intrinsic height of the content to layout the views vertically, and the width of the container to size them horizontally.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top