iOS開發中的橫屏問題

作者:代培
地址:http://blog.csdn.net/dp948080952/article/details/53701726
轉載請註明出處

前言

最近項目中遇到一個問題,需要使某些界面強制橫屏,某些界面只能豎屏,比較難的地方是在於需要橫屏的界面的VC是拿不到的,只能通過一些非正常的手段來做到,雖然能夠解決,不過卻不是非常優雅的方法,但是在探索的過程中卻對iOS中的屏幕方向問題有所瞭解,在這裏寫篇博客記錄下來。

正文

橫屏概況

首先說說怎麼讓界面橫屏顯示,我覺得可以分爲兩個方面,一個是主動,一個是被動,主動就是你沒有改變手機的方向通過代碼改變屏幕的顯示方向,被動就是當你彈出或是啓動應用時屏幕的方向。

  • 主動
[[UIDevice currentDevice] setOrientation: UIInterfaceOrientationLandscapeRight]

這是方法以前是公開的,現在已經是私有API了,不過我們仍然可以去使用這個方法。

if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
        [[UIDevice currentDevice] performSelector:@selector(setOrientation:)
                                       withObject:(id)UIInterfaceOrientationLandscapeLeft];
}

還有一個方法有些類似,是改變狀態欄的方向,這個方法已經不推薦使用了,不過暫時還可以用。

[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft animated:YES];
  • 被動

上面的主動的方式如果要能實現必須要有被動的支持,如果沒有被動的支持,那些方法調用以後一點效果都沒有。

被動的有分爲兩個層級,一個是應用級,一個是VC級,我們看一下具體的代碼:

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    return UIInterfaceOrientationMaskAll;
}

這是應用級,如果你這個地方返回的是UIInterfaceOrientationMaskLandscape那麼整個應用都只支持橫屏,無法豎屏(當然也有例外,這個後面再說),注意這個是UIApplicationDelegate協議中定義的方法,所以這些代碼應該寫在AppDelegate中。

當然除了代碼我們也可以在工程裏進行設置,如下圖所示,勾選了全部相當於前面代碼中返回UIInterfaceOrientationMaskAll,當然如果一個也不勾那麼效果和現在也是相同的,需要注意的是這裏的勾選會被上面的代碼覆蓋,如果兩個同時都有那麼以代碼中的返回值爲準。

對於VC級的代碼如下:

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

這是UIViewControllerUIViewControllerRotation分類中定義的方法,我們需要在VC中重寫該方法,返回你需要的方向,上面的代碼表明該VC只會橫屏顯示。

全局屏幕方向控制

說完關於屏幕方向的基礎知識來說一下工程中如何優雅的控制屏幕的方向,首先說一下全局的控制方案,就是在AppDelegate中進行控制,通過控制- application: supportedInterfaceOrientationsForWindow:的返回值來控制屏幕的方向。

如果應用需要在某幾個頁面強制橫屏,在某些頁面只要豎屏,那可以使用類似下面的代碼:

//UIViewController+TopVC.m

@implementation UIViewController (TopVC)

+ (UIViewController *)topMostViewController {
    UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;

    return [self topViewControllerWithRootViewController:rootViewController];
}

+ (UIViewController *)topViewControllerWithRootViewController:(UIViewController *)rootViewController {

    if ([rootViewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController* tabBarController = (UITabBarController*)rootViewController;
        return [self topViewControllerWithRootViewController:tabBarController.selectedViewController];
    } else if ([rootViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController* nav = (UINavigationController*)rootViewController;
        return [self topViewControllerWithRootViewController:nav.visibleViewController];
    } else if (rootViewController.presentedViewController) {
        UIViewController* presentedViewController = rootViewController.presentedViewController;
        return [self topViewControllerWithRootViewController:presentedViewController];
    } else {
        return rootViewController;
    }
}

@end


//AppDelegate.m

@interface AppDelegate ()

@end

@implementation AppDelegate

...

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    if ([[UIViewController topMostViewController] isKindOfClass:[DPViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}
@end

就是獲取最上層的VC,然後根據VC的類名來控制屏幕的方向。這個方法適合應用中有極少數界面的方向不同於其他界面。

局部屏幕方向控制

  • 繼承

這裏局部的意思是每個VC控制自己的方向,這其實很好理解,如果一個VC只能豎屏,那麼-supportedInterfaceOrientations方法就返回豎屏,只能橫屏那麼就返回橫屏,有人覺得寫太累了,每個VC都要重寫這個方法,不過這樣有一個好處,那就是能夠應付應用界面方向很多種的情況,可以有一個簡化的方式那就是建幾個基礎的基類:

// DPLandscapeViewController.m

@interface DPLandscapeViewController ()

@end

@implementation DPLandscapeViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

@end


// DPPortraitViewController.m

@interface DPPortraitViewController ()

@end

@implementation DPPortraitViewController

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

@end

例如建一個豎屏和一個橫屏的基類,後面需要豎屏顯示的VC都繼承第一個類,需要橫屏顯示的都繼承第二個類,這樣就不用在每個VC中都重寫這個方法了。

這個方法適用於屏幕方向有許多種的應用,不過這樣的應用應該比較少吧:)

  • 分類

除了使用繼承的方式,還可以使用分類的方法,這樣感覺會更好些,不需要全部都繼承一個類,也更加靈活,不需要在應用開發的初期就考慮好全部,在開發過程中也可以改變VC的屏幕方向,可以用類似下面的代碼來實現:

@implementation UIViewController (Orientation)

-(NSUInteger) supportedInterfaceOrientations
{
    if ([[self class] isSubclassOfClass:[XXXViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}

在Category重寫這個方法,根據不同的VC來設置不同的屏幕方向,這樣很方便也很容易實現,但是如果這樣寫就不要在VC裏再重寫這個方法,否則這個方法就不會被調用。

這個方法適用於應用中少數界面的方向與其他界面不同,當然即使應用裏界面方向各異用這個方法也不是不行,個人覺得這是最優雅的方案,不用繼承,不用在每個VC中都重寫一個方法,靈活自由,後期修改也很方便。

注意

在介紹完上面兩種方法後,有一個需要注意的點,這裏有一個坑,十分的坑,那就是這兩個方法千萬不要同時使用,什麼意思呢,如果在VC中重寫了-supportedInterfaceOrientations方法,那麼在AppDelegate中千萬不要重寫關於方向支持的方法,也不要在工程設置裏勾選任意一個方向,否則你可能會遇到下面這個錯誤:

*** Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', 
reason: 'Supported orientations has no common orientation with the application, 
and [DPViewController shouldAutorotate] is returning YES'

什麼原因呢?意思大致就是你的應用不支持某個方向,但是你的VC非要在那個方向上顯示,就產生了這個錯誤,比如應用只支持豎向,而你的VC只支持橫向,那麼這個時候就衝突了。

避免的方法很簡單那就是不要同時重寫兩個方法,因爲你很難去控制不衝突,至少說是會有這個隱患的,而我的建議是儘量不要使用全局的方法,因爲如果別人添加一個VC的時候萬一不知道而重寫了-supportedInterfaceOrientations方法,那麼就極其容易引起崩潰,所以這是個隱患,要儘量去避免。

這裏爲什麼說極其容易引起崩潰呢?這裏給大家舉個例子,在這個例子中,看似不會崩潰的代碼實際上卻會崩潰,大家準備好張大嘴吧!

在AppDelegate中我們這樣重寫

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    if ([[UIViewController topMostViewController] isKindOfClass:[DPViewController class]]) {
        return UIInterfaceOrientationMaskLandscape;
    }
    return UIInterfaceOrientationMaskPortrait;
}

在DPViewController中我們這樣重寫

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

在其他ViewController中我們都這樣重寫

(UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

從代碼的邏輯來看這樣是沒有問題的,在DPViewController下都設爲橫屏,其他都設爲豎屏,但實際上這樣是會崩潰的,崩潰的原因就是上面的原因,是不是很驚訝,爲啥呢?這就跟它底層的實現有關了。

-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[ViewController supportedInterfaceOrientations]
-[AppDelegate application:supportedInterfaceOrientationsForWindow:]
-[DPViewController supportedInterfaceOrientations]

我們來看從ViewController present DPViewController時的打印情況,首先會調用ViewController的supportedInterfaceOrientations方法,然後是AppDelegate,然後反覆進行了3次,3次可能是經歷了一個確認階段,這時DPViewController還沒被present出來,然後就是調用DPViewController和AppDelegate的方法,這時候最上層的VC已經變爲DPViewController,所以AppDelegate的返回值已經是橫屏了,這時比較有趣的是系統用調用了一次ViewController的supportedInterfaceOrientations方法,就是這樣引起了衝突,導致了崩潰,所以這個崩潰的原因是很奇怪的,是因爲這些方法古怪的調用順序造成的,但是這個既然是這樣肯定有他的道理,存在即合理,何況是蘋果的實現。

綜上我想說,全局的方向控制還是少用,即使要用也要完全理解了,儘量減少應用崩潰的風險。

UINavigationController的屏幕方向

UINavigationController的情況是比較特殊的,對於UINavigationController來說,他的子VC的屏幕方向是跟隨UINavigationController的,就是說即使你的子VC重寫了supportedInterfaceOrientations方法,但當這個VC被push出來時,其方向也不會是你設定的方向。

這個情況該如何解決呢,如果你想讓push出來的VC具有present出的VC同樣的屬性,即返回啥方向就按照啥方向來顯示,那麼你需要繼承UINavigationController在寫一個類重寫其supportedInterfaceOrientations方法,可以寫成類似下面的樣子:

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return [self.topViewController supportedInterfaceOrientations];
}

讓UINavigationController的方向取決於最上層的子VC,不過這樣還是有一個問題,假設開始是豎屏的,你push一個設置爲橫屏的VC,可是push出來後仍然是豎屏,這時你要把手機橫過來,VC才能變爲橫屏,這就比較尷尬了,不符合我們之前的預期!不要急,這個問題肯定能夠解決的。

還記得文章開頭時所寫的主動轉屏的方法嘛,這裏就能派上用場了,雖然是私有API,但是能解決問題,在push方法結束後讓設備變爲橫屏,這樣就沒問題了。

其實這裏調用這個方法就相當於你模擬了將手機橫過來的操作,我猜測蘋果在檢測到手機角度發生變化時就是調用那個私有API,完成屏幕的跟隨旋轉,所以這也就能解釋通爲何這個方法會變成私有API,因爲這個API對於開發者來說應該是沒有應用場景的,因爲你是沒有權利去模擬一個旋轉手機的操作,所以這個方法對於開發者不應該開放。

當然除了這個解決方案,stackoverflow上也給出了不同的解決方案:UINavigationController Force Rotate,大家可以參考一下,覺得哪個好就用哪個!

總結

以上是在解決問題的過程中學習到的和自己的總結思考,如果有錯誤希望大家能夠指出。其實我真正要解決的問題並沒有解決,前面還說到一個問題:當你把應用設爲只能豎屏也不能保證應用裏不出現橫屏的界面。我就遇到過,當我在接入一家視頻廣告的時我發現在一個只許橫屏的應用中,這個視頻仍然能夠橫屏播放卻不會出錯,這讓我很好奇他是怎麼做到的,如果有人知道希望大家留言告訴我,不勝感激!不過我也會持續研究,如果有什麼新發現,還會寫一篇博客分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章