文字罗嗦,篇幅较长,只需营养可直接看红字部分。

一个viewController的初始化大概涉及到如下几个方法的调用:

initWithNibName:bundle:

viewDidLoad

viewWillAppear:animated:

viewDidAppear:animated:

viewWillLayoutSubviews

viewDidLayoutSubviews

通常情况这几个方法是依次被调用的,我们会在init方法中初始化一些成员变量,做一些与view无关的事情。而后在viewDidLoad中进行view布局相关的属性调整,比如改变一下背景颜色,增加一些subview之类的。不知道大家有没有想过,这样不在init中写view相关代码是为了什么?难道仅仅是为了代码结构清晰?如果我非要在init做一些与view相关的初始化工作,能不能实现?有什么问题?

@implementation testViewController

- (void)printFrame:(CGRect)frame name:(NSString *)name
{
   NSLog(@"%@ :(%f, %f, %f, %f)", name, frame.origin.x, frame.origin.y,frame.size.width, frame.size.height);
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
   self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
   if (self) {
       // Custom initialization
       self.view.backgroundColor = [UIColor yellowColor];      
       [self printFrame:self.view.frame name:@"initFrame"];
   }
   return self;
}

- (void)viewDidLoad
{
   [super viewDidLoad];
   // Do any additional setup after loading the view.
   [self printFrame:self.view.frame name:@"didloadFrame"];
}

- (void)viewWillLayoutSubviews
{
   [super viewWillLayoutSubviews];
   [self printFrame:self.view.frame name:@"willLayoutFrame"];   
}

-(void)viewDidLayoutSubviews
{
   [super viewDidLayoutSubviews];
   [self printFrame:self.view.frame name:@"didLayoutFrame"]; 
}

- (void)viewWillAppear:(BOOL)animated
{
   [super viewWillAppear:animated];
   [self printFrame:self.view.frame name:@"willAppearFrame"];
}

-(void)viewDidAppear:(BOOL)animated
{
   [super viewDidAppear:animated];
   [self printFrame:self.view.frame name:@"didappearFrame"];
}

这段代码在init方法中设置了一下view的backgroundColor。运行结果很正常,view的背景色被成功地设定为黄色,但是看控制台的log输出,出现了一个不符合预期的现象:

didloadFrame :(0.000000, 20.000000, 768.000000, 1004.000000)
initFrame :(0.000000, 20.000000, 768.000000, 1004.000000)
willAppearFrame :(0.000000, 0.000000, 768.000000, 960.000000)
didappearFrame :(0.000000, 0.000000, 768.000000, 960.000000)
willLayoutFrame :(0.000000, 0.000000, 768.000000, 960.000000)
didLayoutFrame :(0.000000, 0.000000, 768.000000, 960.000000)

viewDidLoad竟然先于init给出了输出,经过跟踪发现,原来当程序第一次调用self.view的时候,viewDidLoad方法就会被执行,而不一定非要等到init之后willAppear之前。这给我们敲响了警钟,这样的代码就隐藏了问题:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
   self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
   if (self) {
       // Custom initialization
       self.view.backgroundColor = [UIColor yellowColor];
       aInstanceVariable_= 0; // Custom initialization of an instance variable
   }
   return self;
}

- (void)viewDidLoad
{
   [super viewDidLoad];
   // Do any additional setup after loading the view.
   aInstanceVariable_ = 10086;
}

这段代码执行完后的aInstanceVariable_是0而不是10086,可能会为一些bug深深地埋下一颗种子。

搞清楚了代码执行顺序,下面我们来关注一下frame和bounds的问题。frame和bounds的定义和区别在这篇blog里讲的很清楚,总结起来要点就是,frame是相对于父view参照系(是父view而不是父viewController)的,bounds是本地参照系,改frame的时候center和bounds联动,但改bounds的时候center不动。

把上面的程序稍微修改一下,来看一组值得研究一下的结果(此viewController由带导航条的navigationController推送),实际上不用navigationController而直接加载这个vc,结果又不一样,viewDidAppear会在最后viewDidLayoutSubviews之后才调用,其他顺序不变,乱吧……

didLoadFrame :(0.000000, 20.000000, 768.000000, 1004.000000) direction:(1, 1)
didLoadBounds :(0.000000, 0.000000, 768.000000, 1004.000000) direction:(1, 1)
initFrame :(0.000000, 20.000000, 768.000000, 1004.000000) direction:(1, 1)
initBounds :(0.000000, 0.000000, 768.000000, 1004.000000) direction:(1, 1)
willAppearFrame :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
willAppearBounds :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
didAppearFrame :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
didAppearBounds :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
willLayoutFrame :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
willLayoutBounds :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
didLayoutFrame :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)
didLayoutBounds :(0.000000, 0.000000, 768.000000, 960.000000) direction:(1, 1)

刚才这个是竖屏的,再来个横屏的:

didLoadFrame :(0.000000, 0.000000, 748.000000, 1024.000000) direction:(3, 3)
didLoadBounds :(0.000000, 0.000000, 748.000000, 1024.000000) direction:(3, 3)
initFrame :(0.000000, 0.000000, 748.000000, 1024.000000) direction:(3, 3)
initBounds :(0.000000, 0.000000, 748.000000, 1024.000000) direction:(3, 3)
willAppearFrame :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
willAppearBounds :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
didAppearFrame :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
didAppearBounds :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
willLayoutFrame :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
willLayoutBounds :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
didLayoutFrame :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)
didLayoutBounds :(0.000000, 0.000000, 1024.000000, 704.000000) direction:(3, 3)

总结一下不难发现其特征:1. 在viewWillAppear之前,无论横屏还是竖屏,view的frame和bounds都是按竖屏方式计算的;2. 在viewWillAppear之前,navigationController(而非父view,实际上这个vc的superview是navigationController的view的一个subview)的导航条并没有计算在frame和bounds中,但电池条的宽度是一直计算了的;3. 在转屏时,触发的是viewWillLayoutSubview及viewDidLayoutSubview(data not shown)。

由此结论,我们继续往下想,如果我们要改变self.view的frame值,我们应当在哪个方法中修改呢?很容易想到的是,init和viewDidLoad中是不行的,实践证明,在viewWillAppear中也是不行的,要在viewDidAppear/viewWillLayoutSubviews/viewDidLayoutSubviews方法中修改才能产生效果。

看起来越来越复杂了……对了,以上的结论对iOS5和6是通用的。下面开始研究转屏,转屏对iOS5和6来说,差别就大了。

先看iOS5

iOS5的时候,转屏函数主要是这几个:(补:其实还有一个willAnimationRotationToInterfaceOrientation:duration:,调用时机在viewDidLayoutSubviews之后,didRotation之前)

-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
   NSLog(@"shouldRotate");
   return YES;
} //以下简称shouldRotate

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientationduration:(NSTimeInterval)duration
{
   NSLog(@"willRotate");
}

-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
   NSLog(@"didRotate");
}

初始化一个正常viewController时转屏函数的调用过程如下:

2012-11-18 16:40:58.090 testRotation[1874:c07] init
2012-11-18 16:40:58.091 testRotation[1874:c07] shouldRotate
2012-11-18 16:40:58.092 testRotation[1874:c07] didLoad
2012-11-18 16:40:58.092 testRotation[1874:c07] shouldRotate
2012-11-18 16:40:58.093 testRotation[1874:c07] willappear
2012-11-18 16:40:58.093 testRotation[1874:c07] shouldRotate
2012-11-18 16:40:58.094 testRotation[1874:c07] willlayout
2012-11-18 16:40:58.095 testRotation[1874:c07] didlayout
2012-11-18 16:40:58.096 testRotation[1874:c07] didappear

我的妈呀,初始化一个vc怎么调用了三次shouldRotate方法……(别着急,三次算什么,这种情况下调用几次都有可能……)

如果初始化vc是在一个navigationController下,看起来还比较正常:

2012-11-19 20:42:42.037 testRotation[462:c07] init
2012-11-19 20:42:42.039 testRotation[462:c07] didload
2012-11-19 20:42:42.040 testRotation[462:c07] willappear
2012-11-19 20:42:42.041 testRotation[462:c07] shouldRotate
2012-11-19 20:42:42.042 testRotation[462:c07] didappear
2012-11-19 20:42:42.042 testRotation[462:c07] willlayout
2012-11-19 20:42:42.043 testRotation[462:c07] didlayout

shouldRotate在willAppear之后调用一次。

无论有navigationController与否,再转一下屏后,方法调用过程是一样的:

2012-11-19 20:51:00.729 testRotation[527:c07] shouldRotate
2012-11-19 20:51:00.730 testRotation[527:c07] willRotate
2012-11-19 20:51:00.731 testRotation[527:c07] willlayout
2012-11-19 20:51:00.731 testRotation[527:c07] didlayout
2012-11-19 20:51:00.732 testRotation[527:c07] shouldRotate
2012-11-19 20:51:01.133 testRotation[527:c07] didRotate

注意,shouldRotate方法依然被调用了两次。

为了把shouldRotate方法的调用次数以及这几次调用的返回值有什么用搞明白,我做了个实验,详细过程不赘述,只说结论。结论是一个坏消息和一个好消息:坏消息是,shouldRotate方法可能调用很多次(只出现在非navigationController方式直接将vc作为rootViewController的情况),我最多遇到过连续调用6次的,弄的我一头雾水,具体原因尚不详;好消息是,无论在哪个阶段调用多少次,起决定作用的只有willAppear调用后,willLayoutSubviews调用前shouldRotate的最后一次调用,其余阶段返回yes还是no都不重要。

再看iOS6

iOS6对转屏逻辑做了修改,去掉了原来的shouldRotate方法,代之以新的几个方法,具体可看这篇blog,介绍很详细,不再赘述,做一些补充:

-(BOOL)shouldAutorotate
{
   return YES;
}
-(NSUInteger)supportedInterfaceOrientations
{
   return UIInterfaceOrientationMaskAll;
}
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
   return UIInterfaceOrientationLandscapeRight;
}

这3个方法代替了原来的shouldRotate方法,但并不是换汤不换药。

iOS6把转屏的逻辑判断放到了rootViewController里,也就是说,无论当前显示的是那个子vc,都是rootViewController响应转屏事件(或者present出来的modalViewController也可以,总之是最根部的vc才行),而且不向下传递。直接在一个childViewController里写这几个方法,是根本不会被调用到的。这就带来一个问题,根vc的转屏逻辑直接决定了子vc的转屏逻辑,如果老子说这个方向支持,那儿子只好支持,而且所有的儿子还都得支持。解决这个专制问题,可以在根vc里这样写:

-(BOOL)shouldAutorotate
{
   return [self.topViewController shouldAutorotate];
}
-(NSUInteger)supportedInterfaceOrientations
{
   return [self.topViewController supportedInterfaceOrientations];
}

让老子每次转屏被问到的时候,都亲自问下他现在正在活跃的子孙。

转屏时调用顺序跟iOS5一样,不过shouldRotate被顺序拆分为shouldAutoRotate和supported。并且如果shouldAutoRotate返回了NO,则转屏过程中断,不再继续执行supported。

最后说到强制横屏。

iOS5和6都有这个问题,如果我们采用presentViewController的方式展示一个vc,那么我们是可以在进入vc的时候控制present的方向的。但是如果我们采用的是pushViewController的方式,问题就出现了,无论我们用何种方式设置这个vc支持的屏幕方向,都只能在转屏的时候进行调整,而无法在第一次进入这个vc的时候调整。也就是说,竖屏push进入一个只支持横屏的vc,显示依然是竖屏,但当转横屏之后,就转不回竖屏了。

这显然不对,解决这个问题,要么用私有API setOrientation: 这个显然是风险太大的。比较好的解决方式就是检测屏幕方向,然后用view.transform去人工转view,setStatusBarOrientation。这里面要注意几个要点:

1. view.transform的makeRotation方式转view是中心点center不动,view旋转。

2. 旋转过后view的frame会改变,所以要人工调整,这里计算frame的新位置和尺寸是重点。由于是人工转屏,改变电池条的方向并不会改变view的坐标系,所以一切要在原坐标系里算。

3. view转屏退出后要记得用identity恢复之前view转过的状态。

4. 最坑爹的一点是,用setStatusBarOrientation:animated:方法来设置电池条方向时,在iOS5下没有问题,但在iOS6下,这个方法会调用rootvc的shouldAutoRotate(相当于一次转屏判断),如果shouldAutoRotate返回YES(无论supported返回什么),电池条方向都不会被设定!非常坑,所以逻辑要想好,比如可以通过一个bool值判断是在改变电池条方向还是系统转屏,如果是前者,返回个NO骗骗它……

5. 在哪个方法里处理转屏,设置电池条方向,以及在哪个方法里调整view的frame,都是很重要的,要视你的view是怎么push进来的(有rootvc还是本身就是),要具体情况具体分析。中心思想是:比如强制要求横屏,则在横屏进入的时候,直接用系统转屏逻辑限制方向即可;而在竖屏进入时,禁用系统转屏逻辑,人工将view旋转至需要的方向,而后再转为横屏时,可采用两种方式,一是恢复原本view方向后重新开启系统转屏逻辑,二是继续根据方向人工转屏。设计这个过程代码时,明确之前研究的frame尺寸应该什么时候重设以及各个view方法的执行顺序,是必须的。

5. iOS5和6要区分处理。总之,强制横屏绝对不是网上随处可见的transform一下然后重设一下bound就ok了的事情。

附上一种强制横屏实现的代码:

// 强制横屏的一种实现
// 使用方法:
// 在vc的init方法中调用initLogic
// 在vc关闭之前调用cleanRotateTrace方法

-(void)initLogic
{
   isPortraitIn_ = NO;
   isSettingStatusBar_ = NO;
}
-(BOOL)shouldAutorotate
{
   if (isSettingStatusBar_)
   {
       return NO;
   }
   return YES;
}

-(NSUInteger)supportedInterfaceOrientations
{
   return UIInterfaceOrientationMaskLandscape;
}

-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
   return ((toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft)||(toInterfaceOrientation == UIInterfaceOrientationLandscapeRight));
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientationduration:(NSTimeInterval)duration
{
   if (isPortraitIn_)
   {
       self.view.transform = CGAffineTransformIdentity;
       isPortraitIn_ = NO;
   }
}

- (void)cleanRotationTrace
{
   if (isPortraitIn_)
   {
       self.view.transform = CGAffineTransformIdentity;
       isPortraitIn_ = NO;
       UIInterfaceOrientation orientation = [UIApplicationsharedApplication].statusBarOrientation;
       if (orientation == UIInterfaceOrientationLandscapeRight)
       {
           isSettingStatusBar_ = YES;
           [[UIApplication sharedApplication]setStatusBarOrientation:UIInterfaceOrientationPortrait animated:NO];
           isSettingStatusBar_ = NO;
       }
       else
       {
           isSettingStatusBar_ = YES;
           [[UIApplication sharedApplication]setStatusBarOrientation:UIInterfaceOrientationPortraitUpsideDown animated:NO];
           isSettingStatusBar_ = NO;
       }
       [self.view setFrame:CGRectMake(0, 0, self.view.frame.size.height + 20,self.view.frame.size.width - 20)];
   }
}

-(void)viewDidAppear:(BOOL)animated
{
   [super viewDidAppear:animated];
   UIInterfaceOrientation orientation = [UIApplicationsharedApplication].statusBarOrientation;
   if (UIInterfaceOrientationIsPortrait(orientation))
   {
       isPortraitIn_ = YES;
       self.view.transform = CGAffineTransformMakeRotation(M_PI_2);
       if (orientation == UIInterfaceOrientationPortrait)
       {
           isSettingStatusBar_ = YES;
           [[UIApplication sharedApplication]setStatusBarOrientation:UIInterfaceOrientationLandscapeRight animated:NO];
           isSettingStatusBar_ = NO;
       }
       else
       {
           isSettingStatusBar_ = YES;
           [[UIApplication sharedApplication]setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft animated:NO];
           isSettingStatusBar_ = NO;
       }
       [self.view setFrame:CGRectMake(0, -20, self.view.frame.size.height - 20,self.view.frame.size.width + 20)];
   }
}

虽然强制横屏的中心思想都差不多,但具体实现方式可以有很多种,我自己写过两种,效果都差不多,代码简洁程度不同。这些实现目前我都没有解决的问题是转屏的动画,用系统逻辑的部分没有问题,但如果是竖屏进入强制横屏的,在第一次转到真正横屏的时候,电池条的转动与view的转动是不同步的,动画很难看,之后再转就又是系统转屏没有问题了。

这个动画问题我至今能够想到的唯一解决方法是完全不用系统转屏,而是所有的转屏都自己写。求更好解决方案。

到此为止。

【转】从viewController讲到强制横屏,附IOS5强制横屏的有效办法的更多相关文章

  1. Android9.0 MTK 平板横屏方案修改(强制app横屏 + 开机logo/动画+关机充电横屏 + RecoveryUI 横屏)

    文章较长建议先收藏再看 拆解步骤 1.app 强制横屏显示,无视 android:screenOrientation="portrait" 属性 2.屏幕触摸坐标修改为横屏 3.开 ...

  2. css实现强制不换行/自动换行/强制换行

    在我们日常的编码中经常会遇到这段文字不可以换行,或者自动换行的需求.虽然这个功能在我们平时很常见但是我相信大家一定不会可以的去记住它吧(至少小月是很懒惰的从来是用什么查什么 ♦ 嘻嘻...).今天我们 ...

  3. 转:css实现强制不换行/自动换行/强制换行

    css实现强制不换行/自动换行/强制换行 [日期:2007-08-22] 来源:  作者: [字体:大 中 小] 强制不换行 div{ white-space:nowrap;} 自动换行 div{  ...

  4. 张小龙在2017微信公开课PRO版讲了什么(附演讲实录和2016微信数据报告)

    今天2017微信公开课PRO版在广州亚运城综合体育馆举行,这次2017微信公开课大会以“下一站”为主题,而此次的微信公开课的看点大家可能就集中在腾讯公司高级副总裁.微信之父——张小龙的演讲上了!今天中 ...

  5. 用友ERP-U8最新破解(再次更新版本,附安装过程中的解决办法)

    新版用友u8.70下载地址:http://ftp.shangyuchem.com/应用软件/用友ERP-U8管理软件(8.70版).rar 准备好安装环境,因为需要SQLSERVER和IIS支持,而个 ...

  6. HTML+CSS 对于英文单词强制换行但不截断单词的解决办法

    如何处理长的单词和链接(强制换行,连接符,省略号等) 我们在前端开发中经常会遇到一些很长的文本串从它的容器中溢出,例如: 通过这样一段css可以有效解决这种问题: .dont-break-out { ...

  7. Number 强制类型转换 int 强制转换整型 float 强制转换浮点型 complex 强制转换成复数 bool 强制转换成布尔类型,结果只有两种,要么True 要么 False """bool 可以转换所有的数据类型 everything"""

    # ###Number 强制类型转换 var1 = 5 var2 = 4.85 var3 = True var3_2 = False var4 = 3+9j var5 = "888777&q ...

  8. CSS自己主动换行、强制不换行、强制断行、超出显示省略号

    P标签是默认是自己主动换行的,因此设置好宽度之后,可以较好的实现效果,可是近期的项目中发现,使用ajax载入数据之后.p标签内的内容没有换行,导致布局错乱,于是尝试着使用换行样式,尽管攻克了问题.可是 ...

  9. CSS自动换行、强制不换行、强制断行、超出显示省略号

    转自:https://blog.csdn.net/liuyan19891230/article/details/50969393 P标签是默认是自动换行的,因此设置好宽度之后,能够较好的实现效果,但是 ...

随机推荐

  1. Bower 手册

    安装 Bower 使用 npm 安装 Bower.(Bower 依赖于 Node, npm 和 Git.) $ npm install -g bower 基本用法 安装程序包 程序包安装命令 bowe ...

  2. 【WPF】控件使用-宽度自动适应窗口大小

    <Grid MinWidth="1260" HorizontalAlignment="Stretch" />

  3. Java Garbage Collection Basics--转载

    原文地址:http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html Overview Purpose ...

  4. Java后端书架

    本书架主要针对Java后端开发与架构. 更新记录:4.0版把第五部份-具体技术的书整块拿掉了.<TCP/IP详解 卷1:协议>出到了第二版,增加<SRE:Google运维解密> ...

  5. mac ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

    好久不用mysql,今天突然想用的时候, mysql -uroot -p 直接报了下面的错误 ERROR 2002 (HY000): Can't connect to local MySQL serv ...

  6. WPF 程序自删除(自毁)|卸载程序删除

    一般是在MainWindow_Closed 事件中调用批处理命令删除. 在借鉴别人的想法的基础上的算是改进. 自删除步骤: 1.删除文件 2.删除存放文件夹. 实现代码: private static ...

  7. NFS客户端访问行为相关的几个参数解释

    soft / hard Determines the recovery behavior of the NFS client after an NFS request times out. If ne ...

  8. PUT 还是 POST ?

    http://www.oschina.net/translate/put-or-post http://my.oschina.net/u/1263964/blog/268932 这两个方法咋一看都可以 ...

  9. kFreeBsd 国内开源镜像站汇总

    从http://bbs.chinaunix.net/archiver/tid-3756178.html这里抽取了debian源中支撑kfreebsd架构的源. 中科大: http://debian.u ...

  10. 面向企业客户的制造业CRM系统的不成熟思考

    CRM就是客户关系管理(Customer Relationship Management),一直一知半解,最近有涉及这方面的需求,所以稍作研究,并思考一些相关问题. CRM是什么? CRM具体如何定义 ...