细数AutoLayout以来UIView和UIViewController新增的相关API – UIViewController篇
UILayoutSupport
- @property(nonatomic,readonly,retain) id<UILayoutSupport> topLayoutGuide NS_AVAILABLE_IOS(7_0);
- @property(nonatomic,readonly,retain) id<UILayoutSupport> bottomLayoutGuide NS_AVAILABLE_IOS(7_0);
- @protocol UILayoutSupport <NSObject>
- @property(nonatomic,readonly) CGFloat length;
- @end
从iOS 7以来,当我们的视图控制器结构中有NavigationBar,TabBar或者ToolBar的时候,它们的translucent属性的默认值改为了YES,并且当前的ViewController的高度会是整个屏幕的高度。(比如一个场景:拖动TableView的时候,上面的NavigationBar能够透过去看到TableView的内容。)
为了确保我们的视图不被这些Bar覆盖,我们可以在我们AutoLayout布局中使用topLayoutGuide和bottomLayoutGuide这两个属性。像这样:
- NSDictionary *views = @{"topLayoutGuide" : self.topLayoutGuide, @"myLabel" : myLabel};
- [NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-[myView]" options:0 metrics:nil views:views]
这个时候我们的视图就不会被Bar所覆盖,显示在了Bar下方:
并且使用这个属性布局时,在traitCollection改变时(旋转屏幕),它的值也会动态变化。上述代码,在横屏情况下,navigationbar高度变了之后,仍然能够正确显示。
这两个guides的计算方式如下:
topLayoutGuide是通过计算 View Controller->View->Top 到 覆盖这个View最下层的那个Bar(像Navigation Bar) -> Bottom 的距离
bottomLayoutGuide是通过计算 View Controller->View->Bottom 到 覆盖这个View上层那个Bar(像Tab bar) -> Top 的距离
如果我们不使用AutoLayout布局,我们也可以通过Guide的length属性获得相应的距离。我们应该在-viewDidLayoutSubviews或者-layoutSubviews调用super之后,再去获得length这个值,以确保正确。
UIConstraintBasedLayoutCoreMethods
- - (void)updateViewConstraints NS_AVAILABLE_IOS(6_0);
UIViewController中也新增了一个更新布局约束的方法,在AutoLayout UIView相关API的笔记中,详细讲述了UIView的一组更新布局约束的方法。
这个方法默认的实现是调用对应View的 -updateConstraints 。ViewController的View在更新视图布局时,会先调用ViewController的updateViewConstraints 方法。我们可以通过重写这个方法去更新当前View的内部布局,而不用再继承这个View去重写-updateConstraints方法。我们在重写这个方法时,务必要调用 super 或者 调用当前View的 -updateConstraints 方法。
UITraitEnvironment
又一次看到了UITraitEnvironment协议,在UIKit Framework中,有四个类支持这个协议,分别是UIScreen, UIViewController,UIView 和 UIPresentationController。所以当视图的traitCollection改变时,UIViewController能够捕获到这个消息,并做对应处理的。 更多解释可以参考上一篇文章详解UICoordinateSpace和UIScreen在iOS 8上的坐标问题。
关于Size Class和UITraitCollection的概念可参考如下链接:
WWDC 2014 Session笔记 - iOS界面开发的大一统 From: onecat’s Blog
iOS8 Size Classes的理解与使用 From: Joywii’s Blog
另外,UIViewController还另外提供了以下两个方法:
- - (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- - (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
我们可以通过调用ViewController的setOverrideTraitCollection方法为它的ChildViewController重新设置traitCollection的值。一般情况下traitCollection值从父controller传到子controller是不做修改的。当我们自己实现一个容器Controller的时候,我们可以使用这个方法进行调整。
相对的,我们可以通过overrideTraitCollectionForChildViewController方法获得ChildViewController的traitCollection值。
UIContentContainer
iOS 8上随着Size Class概念的提出,UIViewController支持了UIContentContainer这样一组新的协议:
- - (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(id <UIContentContainer>)container NS_AVAILABLE_IOS(8_0);
- - (CGSize)sizeForChildContentContainer:(id <UIContentContainer>)container withParentContainerSize:(CGSize)parentSize NS_AVAILABLE_IOS(8_0);
- - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);
- - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);
UIViewController对这组协议提供了默认的实现。我们自定义ViewController的时候可以重写这些方法来调整视图布局,比如我们可以在这些方法里调整ChildViewControler的位置。当我们重写这些协议方法时,我们通常都去调用 super。
viewWillTransitionToSize: ViewController的View的size被他的Parent Controller改变时,会触发这个方法。(比如rootViewController在它的window旋转的时候)。我们在重写这个方法时,确保要调用super,来保证size改变的这条消息能够正常传递给它的Views或者ChildViewControllers。
willTransitionToTraitCollection: 当ViewController的traitCollection的值将要改变时会调用这个方法。这个方法是在 UITraitEnvironment协议方法 traitCollectionDidChange:之前被调用。我们在重写这个方法时,也要确保要调用super来保证消息的传递。比如,我们可以像这样在traitCollection值改变时,对视图做对应的动画进行调整:
- - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
- withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
- {
- [super willTransitionToTraitCollection:newCollection
- withTransitionCoordinator:coordinator];
- [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
- } else {
- }
- [self.view setNeedsLayout];
- } completion:nil];
- }
sizeForChildContentContainer:一个容器ViewController可以使用这个方法设置ChildViewController的size。当容器ViewControllerviewWillTransitionToSize:withTransitionCoordinator:被调用时(我们重写这个方法时要调用Super),sizeForChildContentContainer方法将会被调用。然后我们可以把需要设置的size发送给ChildViewController。当我们设置的这个size和当前ChildViewController的size一样,那么ChildViewController的viewWillTransitionToSize方法将不会被调用。
sizeForChildContentContainer默认的实现是返回 parentSize。
systemLayoutFittingSizeDidChangeForChildContentContainer:当满足如下情况,这个方法会被调用:
当前ViewController没有使用AutoLayout布局
ChildrenViewController的View使用了AutoLayout布局
ChildrenViewController View -systemLayoutSizeFittingSize:方法返回的值改变(View由于内容的变化,size也出现了变化)
preferredContentSize
- // From UIContentContainer Protocol
- @property (nonatomic, readonly) CGSize preferredContentSize NS_AVAILABLE_IOS(8_0);
- - (void)preferredContentSizeDidChangeForChildContentContainer:(id <UIContentContainer>)container NS_AVAILABLE_IOS(8_0);
- // From UIViewController
- @property (nonatomic) CGSize preferredContentSize NS_AVAILABLE_IOS(7_0);
preferredContentSize在UIContentContainer协议中是只读的,对应的UIViewController有可写的版本。我们可以使用preferredContentSize来设置我们期望的ChildViewController的界面大小。举个例子,如果应用中使用的popOver大小会发生变化,iOS7之前我们可以用contentSizeForViewInPopover来调整。iOS7开始这个API被废弃,我们可以使用preferredContentSize来设置。
当一个容器ViewController的ChildViewController的这个值改变时,UIKit会调用preferredContentSizeDidChangeForChildContentContainer这个方法告诉当前容器ViewController。我们可以在这个方法里根据新的Size对界面进行调整。
总结
UIViewController到目前为止(iOS 8.1), 关于布局的API最大的变化是iOS8中新增支持的两组协议:UITraitEnvironment 和 UIContentContainer。我们可以在学习中通过Demo实现这些协议,来观察ViewController中这些方法最终被调用的时机。
细数AutoLayout以来UIView和UIViewController新增的相关API--UIView篇
iOS8上关于UIView的Margin新增了3个APIs:
- @property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
- @property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
- - (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
在iOS 8中,可以使用layoutMargins去定义view之间的间距,该属性只对AutoLayout布局生效。
因此AutoLayout中NSLayoutAttribute的枚举值有了相应的更新:
- NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
通过在Xcode中测试打印,发现UIView默认的layoutMargins的值为 {8, 8, 8, 8},我们可以通过修改这个值来改变View之间的距离。
在我们改变View的layoutMargins这个属性时,会触发- (void)layoutMarginsDidChange这个方法。我们在自己的View里面可以重写这个方法来捕获layoutMargins的变化。在大多数情况下,我们可以在这个方法里触发drawing和layout的Update。
preservesSuperviewLayoutMargins这个属性默认是NO。如果把它设为YES,layoutMargins会根据屏幕中相关View的布局而改变。举个例子:
如上图,有三个View,其中蓝色View的layoutMargins设为 UIEdgeInsetsMake(50, 50, 50, 50),黄色View的layoutMargins设为 UIEdgeInsetsMake(10, 10, 10, 10)。对黄色View的布局约束代码如下:
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeHeight multiplier:0.5 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
对黑色View的布局代码如下:
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeTrailingMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeLeadingMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeTopMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeBottomMargin multiplier:1.0 constant:0.0];
在preservesSuperviewLayoutMargins默认为NO的情况下,显示效果就和上图一样(间距为10)。当设置黄色View的preservesSuperviewLayoutMargins为YES时,将会获得如下效果(间距为50):
UIConstraintBasedLayoutInstallingConstraints
- @interface UIView (UIConstraintBasedLayoutInstallingConstraints)
- - (NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- - (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
- - (void)addConstraints:(NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- - (void)removeConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
- - (void)removeConstraints:(NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- @end
以上这五个API中,第一个是返回当前View中所有的constraints。后面四个方法即将被废弃,应该使用NSLayoutConstraint类中activateConstraint相关方法替代。
UIConstraintBasedLayoutCoreMethods
- @interface UIView (UIConstraintBasedLayoutCoreMethods)
- - (void)updateConstraintsIfNeeded NS_AVAILABLE_IOS(6_0);
- - (void)updateConstraints NS_AVAILABLE_IOS(6_0);
- - (BOOL)needsUpdateConstraints NS_AVAILABLE_IOS(6_0);
- - (void)setNeedsUpdateConstraints NS_AVAILABLE_IOS(6_0);
- @end
setNeedsUpdateConstraints : 当一个自定义的View某一个属性的改变可能影响到界面布局,我们应该调用这个方法来告诉布局系统在未来某个时刻需要更新。系统会调用updateConstraints去更新布局。
updateConstraints :自定义View时,我们应该重写这个方法来设置当前view局部的布局约束。重写这个方法时,一定要调用[super updateConstraints]。
needsUpdateConstraints :布局系统使用这个返回值来确定是否调用updateConstraints
updateConstraintsIfNeeded :我们可以调用这个方法触发update Constraints的操作。在needsUpdateConstraints返回YES时,才能成功触发update Constraints的操作。我们不应该重写这个方法。
Auto Layout的布局过程是 update constraints(updateConstraints)-> layout Subviews(layoutSubViews)-> display(drawRect) 这三步不是单向的,如果layout的过程中改变了constrait, 就会触发update constraints,进行新的一轮迭代。我们在实际代码中,应该避免在此造成死循环。
UIConstraintBasedCompatibility
- @interface UIView (UIConstraintBasedCompatibility)
- - (BOOL)translatesAutoresizingMaskIntoConstraints NS_AVAILABLE_IOS(6_0);
- - (void)setTranslatesAutoresizingMaskIntoConstraints:(BOOL)flag NS_AVAILABLE_IOS(6_0);
- + (BOOL)requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
- @end
默认情况下,View的autoresizing工作会根据当前位置自动设置约束。我们在使用代码写自己的约束布局代码时,必须设置当前View的translatesAutoresizingMaskIntoConstraints为NO,否则无法正常运作。IB默认是NO。
requiresConstraintBasedLayout :我们应该在自定义View中重写这个方法。如果我们要使用Auto Layout布局当前视图,应该设置为返回YES。
UIConstraintBasedLayoutLayering
- - (CGRect)alignmentRectForFrame:(CGRect)frame NS_AVAILABLE_IOS(6_0);
- - (CGRect)frameForAlignmentRect:(CGRect)alignmentRect NS_AVAILABLE_IOS(6_0);
- - (UIEdgeInsets)alignmentRectInsets NS_AVAILABLE_IOS(6_0);
AutoLayout并不会直接操作View的Frame,但是视图的alignment rect是起作用的。视图的默认alignmentRectInsets值就是(0,0,0,0)。
我们可以简单的对当前View设置用来布局的矩形,比如:
我们有一个自定义icon类型的Button,但是icon的大小比我们期望点击的Button区域要小。这个时候我们可以重写alignmentRectInsets,把icon放在适当的位置。
大多数情况下重写alignmentRectInsets这个方法可以满足我们的工作。如果需要更加个性化的修改,我们可以重写alignmentRectForFrame和frameForAlignmentRect这两个方法。比如我们不想减去视图固定的Insets,而是需要基于当前frame修改alignment rect。在重写这两个方法时,我们应该确保是互为可逆的。
Base line
- - (UIView *)viewForBaselineLayout NS_AVAILABLE_IOS(6_0);
当我们在使用布局约束中NSLayoutAttributeBaseline属性时,系统会默认返回当前视图的底部作为baseline。我们可以重写上述方法,但必须返回的是当前视图中的子视图。
Intrinsic Content Size
- UIKIT_EXTERN const CGFloat UIViewNoIntrinsicMetric NS_AVAILABLE_IOS(6_0);
- - (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
- - (void)invalidateIntrinsicContentSize NS_AVAILABLE_IOS(6_0);
- - (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
通过重写intrinsicContentSize可以设置当前视图显示特定内容时的大小。比如我们设置一个自定义View,View里面包含一个Label显示文字,为了设置当前View在不同Size Class下内容的大小,我们可以这样:
- - (CGSize)intrinsicContentSize
- {
- CGSize size = [label intrinsicContentSize];
- if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
- size.width += 4.0f;
- } else {
- size.width += 40.0f;
- }
- if (self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
- size.height += 4.0;
- } else {
- size.height += 40.0;
- }
- return size;
- }
当有任何会影响这个Label内容大小的事件发生时,我们应该调用invalidateIntrinsicContentSize:
- label.text = @"content update"
- [self invalidateIntrinsicContentSize];
- // 或者比如当前视图Size Class改变的时候
- - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- {
- [super traitCollectionDidChange:previousTraitCollection];
- if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
- || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) {
- [self invalidateIntrinsicContentSize];
- }
- }
不是所有的视图都有 intrinsicContentSize, UIView默认情况下就返回的是 UIViewNoIntrinsicMetric。只有当视图中需要根据内部内容进行调整大小时,我们才需要用到 intrinsicContentSize。
当视图大小在变化时,我们可以使用上面最后四个API来设置视图的压缩或者放大的方式。
- typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
- UILayoutConstraintAxisHorizontal = 0,
- UILayoutConstraintAxisVertical = 1
- };
上面最后四个API主要是通过修改水平或者垂直方向的优先级来实现视图是基于水平缩小(放大)还是垂直缩小(放大)。当我们的视图需要根据内部内容进行调整大小时,我们应该使用上述方法为当前视图设置初始值。而不应该重写这几个方法。
UIConstraintBasedLayoutFittingSize
- UIKIT_EXTERN const CGSize UILayoutFittingCompressedSize NS_AVAILABLE_IOS(6_0);
- UIKIT_EXTERN const CGSize UILayoutFittingExpandedSize NS_AVAILABLE_IOS(6_0);
- @interface UIView (UIConstraintBasedLayoutFittingSize)
- - (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);
- - (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);
- @end
上面两个API可以获得当前使用AutoLayout视图的size。其中targetSize可以传入UILayoutFittingCompressedSize或者UILayoutFittingExpandedSize,分别对应的是最小情况下可能的Size和最大情况下可能的Size。
UIConstraintBasedLayoutDebugging
- - (NSArray *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
- - (void)exerciseAmbiguityInLayout NS_AVAILABLE_IOS(6_0);
第一个API可以获得视图在不同方向上所有的布局约束。
hasAmbiguousLayout :可以知道当前视图的布局是否会有歧义。这里有一个私有API _autolayoutTrace可以获得整个视图树的字符串。
- #ifdef DEBUG
- NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
- #endif
exerciseAmbiguityInLayout :这个方法会随机改变视图的layout到另外一个有效的layout。这样我们就可以很清楚的看到哪一个layout导致了整体的布局约束出现了错误,或者我们应该增加更多的布局约束。
我们应该让上面的四个方法只在DEBUG环境下被调用。
新增支持 UITraitEnvironment Protocol
- @protocol UITraitEnvironment <NSObject>
- @property (nonatomic, readonly) UITraitCollection *traitCollection;
- - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- @end
iOS 8 上新增了Size Class的概念,其中UITraitCollection类用来描述不同Size大小。关于Size Class和UITraitCollection的概念可参考如下链接:http://joywii.github.io/blog/2014/09/24/ios8-size-classesde-li-jie-yu-shi-yong/
UIView实现了这个协议,我们可以获得当前View的traitCollection,从而得知当前View处于什么样的Size Class下。并且当traitCollection有变化时,我们可以通过重写traitCollectionDidChange知道该事件的触发。默认情况下,这个方法什么都不执行。
traitCollection的变化是从UIScreen开始被触发,并且逐层往下传递的,具体如下:
UIScreen -> UIWindow -> UIViewController -> ChildViewControllers -> View -> Subviews
关于这一点,我在详解UICoordinateSpace和UIScreen在iOS 8上的坐标问题一文中有做详细解释。
总结
UIView到目前为止(iOS 8.1),所有增加的关于AutoLayout的API请参考上述文章。进一步对这些API理解可以让我们写出更健壮的布局代码。
本文转自ChunTips