Are your projects portrait only? Most of my friends (including bloggers, of course) have never done horizontal screen development, so this project just has this demand, so I will write an article about horizontal and vertical screen for your reference.

Review 01.

Most company projects only support portrait, and only one or two interfaces need to support landscape, just like video apps, which only need landscape when video is playing, and all other times only portrait is allowed. The demo presented deals with two scenarios that require landscape:

  • The first is to record a video in landscape
  • The second is to play the video landscape

Please go to Youku video for a specific demonstration: BLLandscape Demo.

02. Record video landscape

Generally, it may only need to play the video in landscape, but recording landscape is generally not used, but if a friend needs to do landscape video recording, then it needs to record landscape processing, like the following.

Here’s the idea:

  • To landscape, first remove the view to landscape from the original superView and add it to the current keyWindow, then animate the frame, set the height of the window to the width of the view, set the width of the window to the height of the View, and then rotate the view 90°. Execute the animation to get the current effect.

  • In portrait mode, the process is reversed by animating the window and then inserting the view into the superView before landscape.

2.1. Landscape switching

I’m going to split all of these implementations into a UIView category, and look at the implementation:

/ / frame conversion. - (void) landscapeExecute {self. Transform = CGAffineTransformMakeRotation (M_PI_2); CGRect bounds = CGRectMake(0, 0, CGRectGetHeight(self.superview.bounds), CGRectGetWidth(self.superview.bounds)); CGPoint center = CGPointMake(CGRectGetMidX(self.superview.bounds), CGRectGetMidY(self.superview.bounds)); self.bounds = bounds; self.center = center; } - (void)bl_landscapeAnimated:(BOOL)animated animations:(BLScreenEventsAnimations)animations complete:(BLScreenEventsComplete)complete{ if (self.viewStatus ! = BLLandscapeViewStatusPortrait) { return; } self.viewStatus = BLLandscapeViewStatusAnimating; self.parentViewBeforeFullScreenSubstitute.anyObject = self.superview; self.frame_beforeFullScreen = [NSValue valueWithCGRect:self.frame]; NSArray *subviews = self.superview.subviews; if (subviews.count == 1) { self.indexBeforeAnimation = 0; } else{ for (int i = 0; i < subviews.count; i++) { id object = subviews[i]; if (object == self) { self.indexBeforeAnimation = i; break; } } } CGRect rectInWindow = [self.superview convertRect:self.frame toView:nil]; [self removeFromSuperview]; self.frame = rectInWindow; [[UIApplication sharedApplication].keyWindow addSubview:self]; Animations :^{[self landscapeExecute]; if (animated) {animations();  } } completion:^(BOOL finished) { [self landscpeFinishedComplete:complete]; }]; } else{ [self landscapeExecute]; [self landscpeFinishedComplete:complete]; } self.viewStatus = BLLandscapeViewStatusLandscape; [self refreshStatusBarOrientation:UIInterfaceOrientationLandscapeRight]; }Copy the code

2.2. Portrait switch

Portrait and landscape is a reverse process, and I won’t post the code or explain it here. Do not understand to see the source code will know.

2.3. Pay attention to the point

2.3.1. Implementation in classificationweak

The source code is not too difficult, but there is one detail that needs to be noted. We need to refer to the superView before the animation in a weak memory management strategy in the classification, so that we can come back and do portrait animation and add the current view to the superView before the animation. However, there is no weak attribute in the memory management policy that adds attributes to the category. However, there is an OBJC_ASSOCIATION_ASSIGN policy, which is similar to the common assign policy. The assign policy does not actively set the applied object to nil after the object is released. There is a risk that accessing zombie objects will cause the application to crash.

To solve this problem, we can create a surrogate object. We can use the OBJC_ASSOCIATION_RETAIN_NONATOMIC policy to strongly refer to the surrogate object in the classification, and then use the weak policy to refer to the object we really need to save in the surrogate object. That would solve the problem that could lead to a meltdown.

We can create a block that references the object that we want to use weak memory management, and then we can strongly reference the block, like the following:

#import <Foundation/Foundation.h>

@interface NSObject (Weak)

/**
 * weak
 */
@property(nonatomic) id weakObject;

@end

#import "NSObject+Weak.h"
#import <objc/runtime.h>

@implementation NSObject (Weak)

- (void)setWeakObject:(id)weakObject {
    id __weak __weak_object = weakObject;
    id (^__weak_block)() = ^{
        return __weak_object;
    };
    objc_setAssociatedObject(self, @selector(weakObject),   __weak_block, OBJC_ASSOCIATION_COPY);
}

- (id)weakObject {
    id (^__weak_block)() = objc_getAssociatedObject(self, _cmd);
    return __weak_block();
}

@end
Copy the code
2.3.2. Layout

Since we are doing frame animation, we must use frame layout when adding child controls to the view. The Autolayout layout will not be updated on the current view, resulting in a UI mess.

03. Play video landscape

Most of the scenes are landscape while the video is playing, like this one:

If you go on the Internet and you search for the iOS landscape switch, what you get is the landscape of the video, and these articles seem to be copied from one article, and they all say the same thing. Although people copy back and forth, it seems that what they write in their articles can solve the problem, but in fact their articles can not solve the actual problem.

3.1. Play video landscape

Let’s look at two ways to control screen rotation:

@interface UIViewController (UIViewControllerRotation) ... - (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED; - (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED; . @endCopy the code

So you can see that the method that controls the orientation of the screen is defined in UIViewController, and the first one is shouldAutorotate, which is asking if the current controller supports rotation, The second method – supportedInterfaceOrientations tell the system the current controller supports that a few of the direction of rotation.

In a real project, our UI architecture might look like this:

So in our project we start with a window, then a root controller, then UITabbarController then UINavigationController, and then finally our UIViewController, we have some interface that needs landscape, Therefore, system queries must be refined to the methods of each controller.

Combined with the above figure, let’s look at the transmission process of the next vertical and horizontal events:

  • First the gyroscope picks up a landscape event
  • The system then finds the APP that the current user is operating on
  • The APP will find the current window window
  • The window window will find the root controller, at which point the event will finally reach us developers
  • For our custom root controller, it needs to pass this event toUITabbarController
  • forUITabbarController, the event needs to be passed toUINavigationController
  • forUINavigationControllerWe need to pass events to our own controller
  • Finally, we decide in our own controller whether or not an interface needs landscape

Wait, your project and mine is a big project with potentially thousands of controllers. If we follow this logic, we’ll have to write these two methods in each controller. I can’t imagine.

At this time we should consider the first is to use classification to achieve, simple and elegant, and maintain clean, why not?

#import <UIKit/ uikit. h> @interface UIViewController (Landscape) / */ @property(nonatomic) BOOL bl_shouldAutoLandscape; @end #import "UIViewController+Landscape.h" #import <objc/runtime.h> #import <JRSwizzle.h> @implementation UIViewController (Landscape) + (void)load{ [self jr_swizzleMethod:@selector(shouldAutorotate) withMethod:@selector(bl_shouldAutorotate) error:nil]; [self jr_swizzleMethod:@selector(supportedInterfaceOrientations) withMethod:@selector(bl_supportedInterfaceOrientations)  error:nil]; Bl_shouldAutorotate {} - (BOOL) / / support. The if ([self isKindOfClass: NSClassFromString (@ "BLAppRootViewController")]) { return self.childViewControllers.firstObject.shouldAutorotate; } if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) { return ((UITabBarController *)self).selectedViewController.shouldAutorotate; } if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) { return ((UINavigationController *)self).viewControllers.lastObject.shouldAutorotate; } if ([self checkSelfNeedLandscape]) { return YES; } if (self.bl_shouldAutoLandscape) { return YES; } return NO; } - (UIInterfaceOrientationMask bl_supportedInterfaceOrientations) {/ / support in the direction of rotation. If ([the self isKindOfClass:NSClassFromString(@"BLAppRootViewController")]) { return [self.childViewControllers.firstObject supportedInterfaceOrientations]; } if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) { return [((UITabBarController *)self).selectedViewController supportedInterfaceOrientations]; } if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) { return [((UINavigationController *)self).viewControllers.lastObject supportedInterfaceOrientations]; } if ([self checkSelfNeedLandscape]) { return UIInterfaceOrientationMaskAllButUpsideDown; } if (self.bl_shouldAutoLandscape) { return UIInterfaceOrientationMaskAllButUpsideDown; } return UIInterfaceOrientationMaskPortrait; } - (void)setBl_shouldAutoLandscape:(BOOL)bl_shouldAutoLandscape{ objc_setAssociatedObject(self, @selector(bl_shouldAutoLandscape), @(bl_shouldAutoLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)bl_shouldAutoLandscape{ return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (BOOL)checkSelfNeedLandscape{ NSProcessInfo *processInfo = [NSProcessInfo processInfo]; NSOperatingSystemVersion operatingSytemVersion = processInfo.operatingSystemVersion; if (operatingSytemVersion.majorVersion == 8) { NSString *className = NSStringFromClass(self.class); if ([@[@"AVPlayerViewController", @"AVFullScreenViewController", @"AVFullScreenPlaybackControlsViewController" ] containsObject:className]) { return YES; } if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) { return YES; } } else if (operatingSytemVersion.majorVersion == 9){ NSString *className = NSStringFromClass(self.class); if ([@[@"WebFullScreenVideoRootViewController", @"AVPlayerViewController", @"AVFullScreenViewController" ] containsObject:className]) { return YES; } if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) { return YES; } } else if (operatingSytemVersion.majorVersion == 10){ if ([self isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) { return YES; } } return NO; } @endCopy the code

3.2. Pay attention to the point

3.2.1. JPWarpViewController

JPNavigationController is used in the current demo. Because of the structure of JPNavigationController, one is added here

if ([self isKindOfClass:NSClassFromString(@"JPWarpViewController")]) {
    return [self.childViewControllers.firstObject supportedInterfaceOrientations];
 }
Copy the code

If you have a requirement to customize the navigation bar for each interface in your project, you might want to check it out on my GitHub.

3.2.2.AVFullScreenViewController

When there is a video label in the web page, iPhone will replace the video label with the corresponding system player when opening the web page. When we click the video, the system will enter a video playing interface in full screen. By printing the controller, we can see the controller class name is AVFullScreenViewController, so, this interface need to landscape, landscape corresponding returns this controller landscape attributes can be achieved.

3.2.3. Horizontal screen is required to realize web pages with videos

Not all web pages need landscape, but if the page has video, it often does, so how do we know if a page needs landscape, if it has video?

One way is to agree an event with H5 that tells the native APP to flag if there is a video, and set bl_shouldAutoLandscape to YES.

But I’m here to provide a more simple and elegant way, our UIWebView is can pass – stringByEvaluatingJavaScriptFromString: methods and our interaction, so we can try the following method:

#pragma mark - UIWebViewDelegate - (void)webViewDidFinishLoad:(UIWebView *)webView{ NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"if(document.getElementsByTagName('video').length>0)document.getElementsByTagNam e('video').length;"] ; if (result.length && result.integerValue ! = 0) { self.bl_shouldAutoLandscape = YES; }}Copy the code

When the WebView is loaded, we will check whether there is a Video label in the current H5 page. If there is, we can get the result and do the corresponding landscape processing.

3.2.4. Here comes the big hole

Originally our UITableViewController didn’t support vertical or horizontal, like this, but notice that the UISwitch button is off at this point.

Next, we turn this switch on. The code for this switch looks like this:

- (void)switchValueChanged:(UISwitch *)aswitch{ if (aswitch.isOn) { BLAnotherWindowViewController *vc = [BLAnotherWindowViewController new]; self.anotherWindow.rootViewController = vc; [self.anotherWindow insertSubview:vc.view atIndex:0]; } else{ self.anotherWindow.rootViewController = nil; }}Copy the code

Add a root controller to another window and the code for the root controller looks like this:

#import "BLAnotherWindowViewController.h"

@interface BLAnotherWindowViewController ()

@end

@implementation BLAnotherWindowViewController

- (BOOL)shouldAutorotate{
    return YES;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
    return UIInterfaceOrientationMaskAll;
}

@end
Copy the code

With that in mind, let’s look at how the controller behaves:

It does look like our home screen still doesn’t support vertical screens, which is fine, but it looks like our status bar has been hijacked by the blue window, and they’re both doing this vertical thing together.

Let’s imagine that now this blue window is at the front, and we can quickly observe that this blue window has hijacked the status bar. What if the blue window was behind our main window, we wouldn’t notice this detail at all, and we would see something like this:

The first time I encountered this bug, MY heart was broken.

Let’s analyze how this problem is caused. When we have multiple Windows, each window is equal. The blue window also receives the query from the system.

  • Remember the way the previous two systems asked wasUIViewControllerIf the window does notrootViewControllerBlue Windows do not hijack status bars and landscape events.
  • If the blue window hasrootViewControllerThe return value of the control determines the direction of the device. And that’s what caused this bug.

04. Supplementary updates

A new bug has been discovered, and it should be added that if our application is portrait, but some interfaces require landscape, then if we turn on all the vertical and horizontal screens of the project’s info-plist, it would look like this:

For plus phones, if you open the APP horizontally, the UI of the first interface will definitely be confused, because the APP has not been started at this time, and the system will be initialized according to our info.plist configuration, which will lead to this bug.

Now the solution is this, info.plist and we’re still going to write it this way, just to make sure we don’t mess up the UI when we first start the APP.

Next, we’ll go to the AppDelegate and return the supported landscape orientation, as written below, just as we did in info.plist.

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow  *)window{ return self.window.rootViewController.supportedInterfaceOrientations; }Copy the code

5. The last

The last GitHub address is here BLLandscape.

NewPan’s collection of articles

The link below is a catalog of all my articles. These articles are in every article that deals with implementationGithubThe address,GithubHave source code on.

Index of NewPan’s collection of articles

If you have a question, in addition to leaving a comment at the end of the article, you can also leave a comment on Weibo@ hope _HKbuyLeave me a message and visit myGithub.