博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从ViewController初始化一直谈到强制横屏
阅读量:4115 次
发布时间:2019-05-25

本文共 13111 字,大约阅读时间需要 43 分钟。

文字罗嗦,篇幅较长,只需营养可直接看红字部分。转自:http://blog.sina.com.cn/s/blog_76264a170101e5lb.html

一个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的定义和区别在里讲的很清楚,总结起来要点就是,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)
toInterfaceOrientation
duration:(
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方法,代之以新的几个方法,具体可看,介绍很详细,不再赘述,做一些补充:

-(
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)
toInterfaceOrientation
duration:(
NSTimeInterval)
duration
{
 
  
if (
isPortraitIn_)
 
  
{
 
  
  
  
self
.
view
.
transform 
= 
CGAffineTransformIdentity;
 
  
  
  
isPortraitIn_ 
= 
NO;
 
  
}
}
- (
void)
cleanRotationTrace
{
 
  
if (
isPortraitIn_)
 
  
{
 
  
  
  
self
.
view
.
transform 
= 
CGAffineTransformIdentity;
 
  
  
  
isPortraitIn_ 
= 
NO;
 
  
  
  
UIInterfaceOrientation 
orientation 
= 
[
UIApplication
sharedApplication
].
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 
= 
[
UIApplication
sharedApplication
].
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的转动是不同步的,动画很难看,之后再转就又是系统转屏没有问题了。

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

到此为止。

转载地址:http://xxwpi.baihongyu.com/

你可能感兴趣的文章
Linux 中必须要了解的命令操作
查看>>
查找在线主机的 IP 地址,让对方无处遁形!
查看>>
专注于云软件安全的8种方法
查看>>
女生可不可以进入IT行业做Linux运维工程师?
查看>>
Linux也有全功能杀毒软件啦!
查看>>
linux基础命令——文本编辑vim
查看>>
技能包!Linux 下清空或删除大文件内容的 5 种方法
查看>>
开源强大、超凡,那能用来做设计吗?
查看>>
大神教你如何在 Linux 中启用 Shell 脚本的调试模式
查看>>
想要学习Linux技术,先好好的读一本Linux书籍吧。
查看>>
Linux运维工程师真实的工作状态到底是怎么样的?
查看>>
不常见但是很有用的 gcc 命令行选项发布啦!
查看>>
Linux以外的7种开源操作系统
查看>>
带发行版 Logo 的系统信息显示工具
查看>>
最好的6个大数据处理分析工具
查看>>
你对Linux窗口管理程序Tmux了解吗
查看>>
Linux下有趣的命令
查看>>
Gooligan木马威胁谷歌的安全
查看>>
关于国产项目Apache Kylin 发展历程及背后的那些事
查看>>
如何在 Linux 中启用 Shell 脚本的调试模式
查看>>