iOS:支持iOS7+的StackView们

StackView们

  • UIStackView是iOS9新引入的控件。它支持垂直和水平排列多个子视图(SubView)。如:水平放置三个按钮,等宽,并且按钮间的间隙为10。如果自己实现的话,会分麻烦(无论计算frame还是设置约束,都需要很多代买),而 UIStackView很容易实现。当然还有很多其他强大功能。如,隐藏UIstackView中的某个视图后,其他视图的位置自动计算并变化。完全不用自己去实现约束值。省时省力。可是只支持iOS9+,我想在iOS7/8中使用怎么办?下面两个第三方库,支持iOS7/8
  • OAStackView,基于OC的StackView库,支持iOS7+以上的系统。同时支持代码和IB视图。功能强大,无需质疑。
  • TZStackView,基于Swift的StackView库,同样支持iOS7+以上的系统,但是不支持storyboard。

教程

raywenderlic中关于UIStackView的教程在这里,简单易懂,通过这个教程可以了解UIStackView的基本特性和使用方法。

对于TZStackView和OAStackView,官方事例就是最好的教程了。

OAStackView关于子视图等分和添加间隙功能的实现简析

官方示例:

iOS:支持iOS7+的StackView们_第1张图片
示例:效果图

在viewDidLoad中添加如下代码:

 UILabel *l1 = [[UILabel alloc] init];
 l1.text = @"Label 1";
 UILabel *l2 = [[UILabel alloc] init];
 l2.text = @"Label 2";
 OAStackView *stackView = [[OAStackView alloc] initWithArrangedSubviews:@[l1, l2]];
 stackView.translatesAutoresizingMaskIntoConstraints = NO;
 [self.view addSubview:stackView];
 [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-30-[stackView]"
                                                                  options:0
                                                                  metrics:0
                                                                    views:NSDictionaryOfVariableBindings(stackView)]];
 [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[stackView]"
                                                                  options:0
                                                                  metrics:0
                                                                    views:NSDictionaryOfVariableBindings(stackView)]];

很方便的实现了,两个按钮的垂直排列。默认是垂直排列的。如果想要水平排列,修改stackView.axis值为UILayoutConstraintAxisHorizontal即可。这里要注意,因为不是用IB创建的View,所以要设定View的translatesAutoresizingMaskIntoConstraints属性为NO,否则排列属性不生效。当非IB创建时,属性默认为YES;当IB创建View时,属性默认为NO。

原理
那么是如何实现的呢?感到非常好奇。之前的想法是在subView之间添加空白视图实现等宽间隔排列。不过OAStackView不是这样实现的。OAStackView是添加subViews之间的约束设置他们之间的间距为0,然后再将View与subView的约束设置为0,来实现初始化布局。然后再设定distribution属性值为:OAStackViewDistributionFillEqually,设定几个view的(Equal Width、 Equal Height)等width/height约束,来实现等宽分布。下面来具体分析,等等,先来介绍下VFL。

VFL
啥是VFL?VFL(Virsual Format Language)是一种虚拟的格式化语言,主要用来创建AutoLayout的约束字符串。上面constraintsWithVisualFormat的第一个参数就是VFL创建的。
示例,如:V: |-(0)-Label1-(0)-Label2-(0)-| 方向:从左到右,从上到下
V:表示方向为垂直方向,也就是竖向;H为横向。
|:竖线表示为边界(当前所在View的边界),这里紧邻方向表示符V,方向是从上到下,因此表示上面界。
0:NSNumber 0 表示约束值为0。这里是Label1距离上边界的约束为0。
Label1:表示对象Label1。
0:表示Label1和Label2的约束为0.
Label2:表示对象Label2。
0:表示Label2和下边界的约束为0.
|:表示下边界。
想要继续了解的,请移步这里.

分析
继续刚刚的话题,上面代码片段创建的两个Label之间的约束关系,用VFL表示,见下方:
垂直方向,两个Label距离彼此和边界的距离(space)为0,用VFL标定垂直方向的约束,则实现为:
V: |-(0)-Label1-(0)-Label2-(0)-|
水平方向,两个Label分别距离边界距离(space)为0,水平方向约束,实现为:
H: |-(0)-Label1-(0)-|
H: |-(0)-Label2-(0)-|
这样就标定了Label在StackView中的约束(4个方向:上下左右),从而达到了排列的目的。

设定distribution属性值为OAStackViewDistributionFillEqually后的约束呢?这里设定axis值为UILayoutConstraintAxisHorizontal,水平排列。效果图:


iOS:支持iOS7+的StackView们_第2张图片
没有设定OAStackViewDistributionFillEqually属性

iOS:支持iOS7+的StackView们_第3张图片
设定了OAStackViewDistributionFillEqually属性

VFL表示:
水平方向:H: |-0-(Label 1)-0-(Label 2-3-4-5-6)-0-|
垂直方向:V: |-(0)-(Label 1)-(0)-|H: |-(0)-(Label 2-3-4-5-6)-(0)-|
其他(等宽):label1.width == label2-3-4-5-6.width
这里多了一个等宽的约束。其实用IB实现等宽分布视图的方式也是这样设定的。最后会有storyboard中连线添加约束的截图。
viewDidLoad中添加如下代码:

    UILabel *l1 = [[UILabel alloc] init];
    l1.text = @"Label 1";
    l1.backgroundColor = [UIColor orangeColor];
    UILabel *l2 = [[UILabel alloc] init];
    l2.text = @"Label 2-3-4-5-6";
    l2.backgroundColor = [UIColor lightGrayColor];
    OAStackView *stackView = [[OAStackView alloc] initWithArrangedSubviews:@[l1, l2]];
    stackView.translatesAutoresizingMaskIntoConstraints = NO;
    NSLog(@"stackView Constraints: %@",stackView.constraints);
    stackView.axis = UILayoutConstraintAxisHorizontal;
    stackView.distribution = OAStackViewDistributionFillEqually;
    [self.view addSubview:stackView];
    
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-30-[stackView]"
                                                                      options:0
                                                                      metrics:0
                                                                        views:NSDictionaryOfVariableBindings(stackView)]];
    
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[stackView]"
                                                                      options:0
                                                                      metrics:0
                                                                        views:NSDictionaryOfVariableBindings(stackView)]];

看看Log中输出的内容,约束刚刚好是上面初始化后两个Label的约束形式。

   "",
   "",
   "",
   "",
   "",
   "",
   ""

让我们继续,在stackView.distribution = OAStackViewDistributionFillEqually;设置断点:

运行程序,点击下一步

进入到方法内部。我们发现,进入程序此刻进入到OAStackView.m的 setDistribution:(OAStackViewDistribution)distribution方法中了。
iOS:支持iOS7+的StackView们_第4张图片

这时我们在控制台中打印下stackView的约束。


然后,这里和distribution有关的,只有[self.distributionStrategy alignView:view afterView:previousView]这一个方法了,进入到这个方法里:
在OAStackViewDistributionStrategy.m中:

- (void)alignView:(UIView*)view afterView:(UIView*)previousView {
  if (!previousView && !view) { return; }
  if (!previousView) {
    return [self alignFirstView:view];
  }
  if(!view) {
    return [self alignLastView:previousView];
  }
  if (previousView && view) {
    [self alignMiddleView:view afterView:previousView];
  }
}

胜利就在眼前,这里添加相等约束的方法是[self alignMiddleView:view afterView:previousView]的操作(怎么知道的,断点一步一步走到这里的)command+左键点击该方法,弹出框,选择alignMiddleView:(UIView*)view afterView:(UIView*)previousView方法,因为之前已经设定了distribution属性值为OAStackViewDistributionFillEqually。

iOS:支持iOS7+的StackView们_第5张图片

我们发现这里刚刚好是实现两个视图相等约束的方法。


iOS:支持iOS7+的StackView们_第6张图片

如何验证呢?回到下断点后第一次跳转到得函数里,在[self.alignmentStrategy alignView:view withPreviousView:previousView];下一个新断点,然后点击运行

两次(因为有两个Label,要循环运行两次,才会运行到这),然后在控制台输出stackView的约束。


iOS:支持iOS7+的StackView们_第7张图片

iOS:支持iOS7+的StackView们_第8张图片

相等的约束确实已经添加上来了,这样视图就可以等分了。那么,间隔是如何实现的呢?其实是设定space属性,改变视图见的约束值来实现的。默认为0,如果有新值传进来,则改成新值。其实这些都可以在storyboard中拖拽视图,添加约束来验证。

最后,说下OAStackView初始化后,是如何给子视图添加约束的。其实,主要就是下面这个回调,轮询给子视图添加约束。
在OAStackView+Traversal.h中声明

- (void)iterateVisibleViews:(void (^) (UIView *view, UIView *previousView))block;

在OAStackView+Traversal.m中定义

- (void)iterateVisibleViews:(void (^) (UIView *view, UIView *previousView))block {
  
  id previousView;
  for (UIView *view in self.subviews) {
    if (view.isHidden) { continue; }
    
    block(view, previousView);
    previousView = view;
  }
}

在在OAStackView.m中实施

- (void)layoutArrangedViews {
  [self removeDecendentConstraints];
  
  [self iterateVisibleViews:^(UIView *view, UIView *previousView) {
    [self.distributionStrategy alignView:view afterView:previousView];
    [self.alignmentStrategy addConstraintsOnOtherAxis:view];
  }];
  
  [self.distributionStrategy alignView:nil afterView:[self lastVisibleItem]];
}

你可能感兴趣的:(iOS:支持iOS7+的StackView们)