细数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的变化。在大多数情况下,我们可以在这个方法里触发drawinglayout的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这个方法可以满足我们的工作。如果需要更加个性化的修改,我们可以重写alignmentRectForFrameframeForAlignmentRect这两个方法。比如我们不想减去视图固定的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的变化是从Super view往Subview传递的,如果当前View没有指定traitCollection,就采用super view的traitCollection。

总结

UIView到目前为止(iOS 8.1),所有增加的关于AutoLayout的API请参考上述文章。进一步对这些API理解可以让我们写出更健壮的布局代码。

转载自:http://chun.tips/blog/2014/10/23/xi-shu-autolayoutyi-lai-uiviewhe-uiviewcontrollerxin-zeng-de-xiang-guan-api-uiviewpian/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值