iOS

FDStackView —— Downward Compatible UIStackView (Part 2)

Part 2

Posted by Alpaca on April 22, 2016

原文来自wtlucky,在此只研究了实际使用,完整内容请看原PO

加入百度知道团队也有一段时间了,能跟@我就叫Sunny怎么了@sinojerk等小伙伴一起工作生活是一种极赞的体验。在完成日常业务开发之余,我们也会进行一些技术研究项目,并将研究结果以开源的方式公布出来,自然我也成为了forkingdog开源小组的一员。

近期我们的研究项目是FDStackView,现如今已经完成了Alpha版本的开发工作,并将其开源在了Github上,项目地址。虽然现在已经完成所有的基本功能,但是仍需要在真实的环境中测试试用,欢迎大家将试用之后的问题反馈给我们,提issue给我们,使我们更好的修复和完善FDStackView,以便于更好的方便开发者们使用。

Introduce

FDStackView究竟是什么呢?在介绍FDStackView之前,首先你需要知道UIStackView是什么?UIStackView是苹果在WWDC上发布iOS9的时候新推出的一个UIKit的视图,现在网上可以搜索到很多关于它的资料,关于介绍,如何使用等。简单来说就是可以使用它来做一些流式布局,开发者只需要将需要的视图丢到UIStackView中,然后设置它的一些属性来展现所需要的布局,因此无需自己再去添加各种约束,所有约束不在由开发者自己去管理,这对于一些还不会使用AutoLayout的开发者来说是一个福音。复杂来说,因为UIStackView是可以嵌套使用的,那么再结合上一些简单的约束,那么就可以完成任何复杂的界面了。想想之前需要各种管理约束,而现在有了它只需要将视图丢给它,改几个属性然后界面就做好了,是不是爽到爆,开发效率又提升一个档次啊。下面提供几个介绍UIStackView的文章,使还不太了解的同学可以了解一下,传送门在此:

iOS 9: Getting Started with UIStackView

中文翻译版

An Introduction to Stack Views in iOS 9 and Xcode 7

中文翻译版

介绍完UIStackView的优势想必大家都已经跃跃欲试了,我自身对于这个控件都是十分的期待,因为在开发中你可以不用去写大段的创建constraints的代码了,如果你使用xib或者storyboard的话,那么在IB中你也不需要去连接各种约束了,这是多么棒的一种体验,而且在Xcode7IB中右下角往常用来增加约束,修正视图的位置又新增加了一个stack按钮,可以快速的将所选视图加入到UIStackView中,可见苹果也是推荐开发者使用UIStackView的。但是UIStackView是在iOS9才推出的,最低支持的系统也是iOS9,这就蛋疼了,现在能有几个APP是从iOS9开始支持的,如此一来这个控件就成了鸡肋般的存在,再低版本下根本无法使用。自己在业务开发中经常会想这个需求用UIStackView简直就是妙解,而我却还在这里痛苦的连约束……鉴于这个强烈的需求,FDStackView出现了,它就是为了解决UIStackView在低于iOS9的系统下无法使用的问题。在FDStackView之前也已经有了一些类似的开源项目,比如OAStackViewTZStackView,然而他们都不能满足我们的需求,局限性还是比较大的,比如不支持IB,某些功能还没有实现,类名需要使用非UIStackView,在我们看来这些对开发者来说都是不友好的,开发者需要的是一款功能完善,支持IB,使用时完全无感,在Xcode7上直接使用UIStackView即可,接下来的事情交给FDStackView就好,它负责将UIStackView在低于iOS9的系统上运行。需要注意的是如果使用IB的话,那么IBBuilds for属性需要设置为iOS 9.0 and later。如图所示:

https://raw.githubusercontent.com/forkingdog/FDStackView/master/Snapshots/snapshot0.png

Research

这个技术项目有一大部分的时间,我们都是在做调研工作,首先我们需要把UIStackView玩的很熟练,它的各种属性,各种状态以及他们的组合关系分别是什么样的,其次我们需要解决的问题有:

  1. 使用低系统版本的API和控件创建一个和UIStackView一模一样的控件FDStackView;
  2. 在低系统版本运行UIStackView的时候使用我们的FDStackView;
  3. 使FDStackView获得Interface Builder的支持。

解决了以上三个问题后,那么这个项目基本上也就算是完成了,第一个是工作量最大的工程,它又可以拆分为以下几个技术点:

  • alignmentdistribution的约束如何添加和管理;
  • spacingdistribution的关系及约束的创建;
  • 子视图的隐藏显示如何处理;
  • 子视图的intrinsicContentSize发生变化时如何处理。

首先我们假设在第一个难点已经解决的前提下去攻克其他的难点,毕竟有其他开源方案的存在,说明这个不是不可行的。

至于第二个难点,UIStackView在低系统版本编译时会报找不到符号的error,那么解决的思路就是在低系统版本将UIStackView的符号写进去,然后在runtime将符号与我们的FDStackView做关联,从而使低系统版本也能够运行UIStackView,而实际上在起作用的是我们的FDStackView。这里使用到的黑魔法就是汇编语言,网上已经有大神给出了类似的解决方案,对其进行优化和修改之后应该就能满足我们的需求。

最后一个难点就是使FDStackView获得Interface Builder的支持,因为我们是IB的重度使用者,一个不能在IB上使用的控件一定不是一个好控件。所以一定要让FDStackView能够在IB上使用,有一个方案就是直接使用UIView然后把他的Class指定为FDStackView,将AxisAlignmenDistribution等属性通过IBInspectable使其可以在IB中编辑和设置,但是这样一个是IBInspectableIB中的显示效果很烂,说实话就是不好用,再一个就是用了UIView没有办法像UIStackView那样在IB中可以直接预览布局效果,这就是很差的一种体验了。最好的方案就是在IB中仍然使用UIStackView,使其在IB中有最佳的体验,然后借助上一难点的解决方案,在低系统版本中使用FDStackView代替UIStackView。这样就会带来两个其他问题:

  1. IB的构建版本是根据Project的部署版本来的,如果项目不是支持iOS9的话那么会报这样一个error:”UIStackView before iOS 9.0”
  2. 如何使IB构建出来的FDStackView获得在IB中给UIStackView所设置的各种属性。 这两个问题,第一个只需要将IB的构建版本设置为iOS9及以后即可,目前来看是没有问题的,但是还不知道其他的控件被IB搞成iOS9的版本,在低系统版本上会不会有问题,这个还需要后续的验证。第二个问题,由于使用IB创建的UIKit控件都会由initWithCoder:进行初始化,因此弄清楚NSCoderdecode过程就能将IB设置的属性赋值给所创建的对象了。

解决完以上两个难点,就可以回过头来研究第一个了,就是创建一个和UIStackView一模一样的FDStackView。这里我们对UIStackView进行了详细的研究,包括dump出所有UIStackView的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView的约束constraints的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB中借助UIView手动连接约束的方式,连出每一个UIStackView所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView的工作原理与实现方式。

与此同时我们还发现了两个UIStackViewbug,本以为在Xcode7正式发布之后会得到修复,可是遗憾的是从我们开始研究的时候的beta5到后来的beta6GM和正式版这两个bug依然存在,后面我会介绍一下这两个bug。 省略文章一…


写完了Part 1就被接踵而至的新项目和新版本忙的不可开交,转眼间一个季度就已经过去了,而这篇Part 2却迟迟还没有出现。实在是抱歉没有及时更新。不过有一个好消息就是FDStackView已经被使用在我们自己的项目中,并且我们的项目也已经经过了两个版本的迭代,FDStackView可以说还是相当稳定的,并且可以顺利的通过苹果的审核机制,对这方面有顾虑的小伙伴们可以放心大胆的使用了。同时我们也将它的版本号从1.0-alpha升级到1.0。在此感谢一下各位热心的小伙伴们在Github上提出的issue,以及着重感谢下@里脊串FDStackView的重度使用及提出的各种隐晦的bug。后续我们将会对性能的优化做出改进,以及对Layout Margins的支持。

回到主题,这篇文章主要介绍StackView的实现,即如何通过现有AutoLayout技术实现StackView这样的一个控件。这里说明一下,当初我们编写FDStackView的时候,UIStackView还没有支持Layout Margins,所以我们也没有添加Layout Margins的支持,不过目前的iOS SDK已经增加了这一部分的支持,所以在打开layoutMarginsRelativeArrangement属性的情况下,StackView创建出的约束会与我后面所介绍的内容有一些出入,不过问题不大,仅仅是部分约束的firstItemStackView本身变成UILayoutGuide的区别。

实现StackView主要包括这几个技术点:

  • alignmentdistribution的约束如何添加和管理;
  • spacingdistribution的关系及约束的创建;
  • 子视图的隐藏显示如何处理;
  • 子视图的intrinsicContentSize发生变化时如何处理。

我们对UIStackView进行了详细的研究,包括dump出所有UIStackView的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView的约束constraints的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB中借助UIView手动连接约束的方式,连出每一个UIStackView所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView的工作原理与实现方式。

如上篇文章所说,在进行了详尽的研究之后,总结出大概需要攻克的是这几个技术点,以尽可能的与UIStackView的实现保持一致,在难以完成的地方通过自己的方式实现。在这之前先介绍一下我们使用到的几个私有类。

CATransformLayer

StackView是一个透明不可见的容器,主要就是因为这个layer,我们继承了它并重载了两个方法,setOpaque:setOpaque:,用于避免产生警告⚠️。也就是项目中的FDTransformLayer

_UILayoutSpacer

这是一个私有类,它的主要作用是用了辅助StackView创建alignment方向上的约束,它的父类是UILayoutGuide,并不是一个UIView的子类,所以我们并不能以熟悉的方式对它添加约束。但是在知道了它的作用之后,我们完全可以使用一个UIView来代替它,同时它也是不可见的,所以它的layer自然也是FDTransformLayer。这是项目中的FDLayoutSpacer

_UIOLAGapGuide

_UILayoutSpacer相同是UILayoutGuide的子类,用来辅助distribution方向上的约束创建,并且只有UIStackViewDistributionEqualSpacingUIStackViewDistributionEqualCentering两种模式下它才会出现。在项目中我们通过UIView的子类FDGapLayoutGuide来实现它。

_UILayoutArrangement

同样是一个私有类,用来管理StackView及其子视图的约束的创建。它是一个父类,在FDStackView中我们使用FDStackViewLayoutArrangement来与之对应。

_UIAlignedLayoutArrangement

该类是_UILayoutArrangement的子类,用来控制alignment方向上的约束的创建及管理,它维护了一个_UILayoutSpacer并负责它的生命周期。在FDStackView中我们以更直接的FDStackViewAlignmentLayoutArrangement来对它命名。

_UIOrderedLayoutArrangement

_UIAlignedLayoutArrangement相对,用来控制distribution方向上的约束创建及管理,它维护了一组_UIOLAGapGuide。在FDStackView中我们以更直接的FDStackViewDistributionLayoutArrangement来对它命名。

先提前解释几个后面会提到的名词:

  • canvascanvas是什么?翻译过来是画布的意思,其实就是容器也就是StackView本身
  • Ambiguity Suppression:经常DebugAutoLayout的同学可能对这个词并木陌生,一般约束产生冲突或者模棱两可的时候,控制台就会输出一组信息,其中就会包含这个词。这里就是抵制模棱两可的约束的意思。StackView中会创建一些低优先级的约束来完成这件事儿,以防止控制台打出AutoLayout异常的log
  • minAttribute:是NSLayoutAttribute一个便捷获取方式,针对不同的axis会对应不同的NSLayoutAttribute,可能是NSLayoutAttributeTop也可能是NSLayoutAttributeLeading
  • centerAttribute:同样针对不同的axis可能是NSLayoutAttributeCenterY或者NSLayoutAttributeCenterX
  • maxAttribute:同样针对不同的axis可能是NSLayoutAttributeBottom或者NSLayoutAttributeTrailing
  • dimensionAttribute:同样针对不同的axis可能是NSLayoutAttributeHeight或者NSLayoutAttributeWidth

FDStackViewAlignmentLayoutArrangement

- (NSLayoutAttribute)minAttributeForCanvasConnections {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeTop : NSLayoutAttributeLeading;
}

- (NSLayoutAttribute)centerAttributeForCanvasConnections {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeCenterY : NSLayoutAttributeCenterX;
}

- (NSLayoutAttribute)maxAttributeForCanvasConnections {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeBottom : NSLayoutAttributeTrailing;
}

- (NSLayoutAttribute)dimensionAttributeForCurrentAxis {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeHeight : NSLayoutAttributeWidth;
}

FDStackViewAlignmentLayoutArrangement

- (NSLayoutAttribute)minAttributeForCanvasConnections - {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeLeading : NSLayoutAttributeTop;
}

- (NSLayoutAttribute)centerAttributeForCanvasConnections {
  return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeCenterY : NSLayoutAttributeCenterX;
}

- (NSLayoutAttribute)dimensionAttributeForCurrentAxis {
  return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeWidth : NSLayoutAttributeHeight;
}

- (NSLayoutAttribute)minAttributeForGapConstraint {
return self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeLeading : NSLayoutAttributeTop;
}

此外UIStackView的约束的管理方式也十分的奇妙。除了一个例外的Ambiguity Suppression的约束,其余不管约束何种关系的约束都是add在canvas上的。既然约束都加在了canvas上,那这么多的约束如何区分何管理呢?

这里有个小技巧,那就是用weakToWeakNSMapTable来管理,key是约束的firstItem,value是约束,而且因为NSMapTableweakToWeak的,所以keyvalue所对应的object并不会增加引用计数,不会带来内存上的管理困难。若要找一个view所关联约束,直接取view作为keyvalue就可以了。_UILayoutArrangement维护了多个这样的NSMapTable,分别来管理不同作用的约束。不得不说这样的设计真的是太巧妙了。


alignmentdistribution的约束如何添加和管理

先给一张图看一下什么是alignmentdistribution以及Spacing:

image

在介绍实现之前,我先介绍一下StackView的各种alignment模式都是什么效果的:

  • UIStackViewAlignmentFill:这种就是填充满整个StackView了,用得比较多。

image

  • UIStackViewAlignmentLeading:这种是左对齐。

image

  • UIStackViewAlignmentTop:这种是上部对齐。

image

  • UIStackViewAlignmentFirstBaseline:这种是让arrangedSubviews按照firstBaseline对齐。只能出现在水平的StackView中。

image

  • UIStackViewAlignmentCenter:这种是居中对齐。

image

  • UIStackViewAlignmentTrailing:这种是右部对齐。

image

  • UIStackViewAlignmentBottom:这种是底部对齐。

image

  • UIStackViewAlignmentLastBaseline:这种是让arrangedSubviews按照lastBaseline对齐。同样只能出现在水平的StackView中。

image

下面介绍实现,首先是alignment方向,alignment方向的约束主要包括4种

@interface FDStackViewAlignmentLayoutArrangement : FDStackViewLayoutArrangement
@property (nonatomic, strong) NSMutableArray<NSLayoutConstraint *> *canvasConnectionConstraints;
@property (nonatomic, strong) NSMapTable<UIView *, NSLayoutConstraint *> *hiddingDimensionConstraints;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMapTable *> *alignmentConstraints;
@end

@interface FDLayoutSpacer : UIView
@property (nonatomic, strong, readonly) NSMutableArray<NSLayoutConstraint *> *systemConstraints;
@end
  • canvasConnectionConstraints:它管理的是arrangedSubviewscanvas之间的约束;
  • hiddingDimensionConstraints:它管理的是当arrangedSubviewshidden的时候,该arrangedSubview的有关dimensionAttribute的约束;
  • systemConstraints:它是由_UILayoutSpacer来管理的,它管理了spacer与arrangedSubviews之间的约束,因为这些约束的firstItem都是spacer自身,所以就不需要使用NSMapTable而直接是NSArray。另外spacer只有在alignment不是UIStackViewAlignmentFill的时候才会被创建,所以当alignmentUIStackViewAlignmentFill时,是没有systemConstraints的
  • alignmentConstraints:它管理的是arrangedSubviews之间的约束,它包括两组NSMapTable,根据alignment的不同具体的约束也不同,具体的NSMapTablekeyalignmentaxis的关系如下表:

image

可以看到除了UIStackViewAlignmentFill模式以外,都会有一个Ambiguity Suppression的key,这个key对应的NSMapTable的就管理了前面提到的那些低优先级防止布局时出现模棱两可状态的约束。此外Baseline相关的约束是只有在axisHorizontal时才会有的,并且UIStackViewAlignmentFirstBaselineUIStackViewAlignmentTopUIStackViewAlignmentLastBaselineUIStackViewAlignmentBottom的key值是相同的。

这个key的名字之所以这么取也是有讲究的,它代表着它所对应的NSMapTable管理的约束关系。举个例子:axisHorizontalalignmentUIStackViewAlignmentFill时,key为TopBottom,那么Top对应的NSMapTable管理的约束就是arrangedSubviews之间NSLayoutAttributeTop相等的约束。同理Bottom就是NSLayoutAttributeBottom相等的约束。

这样结合alignment的效果来看就很容易理解,UIStackViewAlignmentFill模式需要arrangedSubviews都充满容器,那么自然他们的NSLayoutAttributeTopNSLayoutAttributeBottom需要都相等,而UIStackViewAlignmentTop模式需要top对齐那么只需要NSLayoutAttributeTop相等就OK了。

这里还有一个点就是arrangedSubviews之间的约束不是迭代添加的,而是都与第一个arrangedSubview创建关系。假设有3个view,那就是view2view1建立约束,view3同样与view1建立约束而不是与view2迭代建立约束。

这4种约束的创建顺序是:

  1. FDLayoutSpacer的systemConstraints
  2. canvasConnectionConstraints
  3. alignmentConstraints
  4. hiddingDimensionConstraints

FDLayoutSpacer的systemConstraintsFDStackViewAlignmentLayoutArrangement中被称为spanningLayoutGuideConstraints,创建方法是

- (void)updateSpanningLayoutGuideConstraintsIfNecessary {
if (self.mutableItems.count == 0) {
    return;
}

if (self.spanningLayoutGuide && self.spanningGuideConstraintsNeedUpdate) {
    [self.canvas removeConstraints:self.spanningLayoutGuide.systemConstraints];
    [self.spanningLayoutGuide.systemConstraints removeAllObjects];

    //FDSV-spanning-fit
    NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.spanningLayoutGuide attribute:self.spanningLayoutGuide.isHorizontal ? NSLayoutAttributeWidth : NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:0];
    constraint.priority = 51;
    constraint.identifier = @"FDSV-spanning-fit";
    [self.canvas addConstraint:constraint];
    [self.spanningLayoutGuide.systemConstraints addObject:constraint];

    //FDSV-spanning-boundary
    [self.mutableItems enumerateObjectsUsingBlock:^(UIView *item, NSUInteger idx, BOOL *stop) {
        NSLayoutConstraint *minConstraint = [NSLayoutConstraint constraintWithItem:self.spanningLayoutGuide attribute:self.minAttributeForCanvasConnections relatedBy:[self layoutRelationForItemConnectionForAttribute:self.minAttributeForCanvasConnections] toItem:item attribute:self.minAttributeForCanvasConnections multiplier:1 constant:0];
        minConstraint.identifier = @"FDSV-spanning-boundary";
        minConstraint.priority = 999.5;
        [self.canvas addConstraint:minConstraint];
        [self.spanningLayoutGuide.systemConstraints addObject:minConstraint];

        NSLayoutConstraint *maxConstraint = [NSLayoutConstraint constraintWithItem:self.spanningLayoutGuide attribute:self.maxAttributeForCanvasConnections relatedBy:[self layoutRelationForItemConnectionForAttribute:self.maxAttributeForCanvasConnections] toItem:item attribute:self.maxAttributeForCanvasConnections multiplier:1 constant:0];
        maxConstraint.identifier = @"FDSV-spanning-boundary";
        maxConstraint.priority = 999.5;
        [self.canvas addConstraint:maxConstraint];
        [self.spanningLayoutGuide.systemConstraints addObject:maxConstraint];
    }];
}
}

首先判断一些不需要创建或者不需要更新这组约束的情况,比如之前提到的alignmentUIStackViewAlignmentFill或者没有arrangedSubview的时候。接下来创建一个宽或高为0的约束给spacer,因为对于后面添加的约束而言,spacer是缺少这样的一个约束以保证它能够正确布局。最后就是把每一个arrangedSubview与spacer分别建立minAttributemaxAttribute的约束,这些约束的constant都是0,但是关系却不一定都是等于,需要根据alignment的属性不同来动态调整,有可能是大于等于,也有可能是小于等于。这需要查表来得到。

下一步创建canvasConnectionConstraints

- (void)updateCanvasConnectionConstraintsIfNecessary {
if (self.mutableItems.count == 0) {
    return;
}

[self.canvas removeConstraints:self.canvasConnectionConstraints];
[self.canvasConnectionConstraints removeAllObjects];

NSArray<NSNumber *> *canvasAttributes = @[@(self.minAttributeForCanvasConnections), @(self.maxAttributeForCanvasConnections)];
if (self.alignment == UIStackViewAlignmentCenter) {
    canvasAttributes = [canvasAttributes arrayByAddingObject:@(self.centerAttributeForCanvasConnections)];
} else if (self.isBaselineAlignment) {
    NSLayoutConstraint *canvasFitConstraint = [NSLayoutConstraint constraintWithItem:self.canvas attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:0];
    canvasFitConstraint.identifier = @"FDSV-canvas-fit";
    canvasFitConstraint.priority = 49;
    [self.canvas addConstraint:canvasFitConstraint];
    [self.canvasConnectionConstraints addObject:canvasFitConstraint];
}

[canvasAttributes enumerateObjectsUsingBlock:^(NSNumber *canvasAttribute, NSUInteger idx, BOOL *stop) {
    NSLayoutAttribute attribute = canvasAttribute.integerValue;
    NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:[self viewOrGuideForLocationAttribute:attribute] attribute:attribute relatedBy:[self layoutRelationForCanvasConnectionForAttribute:attribute] toItem:self.canvas attribute:attribute multiplier:1 constant:0];
    constraint.identifier = @"FDSV-canvas-connection";
    [self.canvas addConstraint:constraint];
    [self.canvasConnectionConstraints addObject:constraint];
}];
}

因为这是alignmentcanvasConnectionConstraints,所以只需关注它自己的minAttributemaxAttribute两个方向与canvas的约束即可,其余两个方向会在distributionLayoutArrangement中创建。

特别的是如果alignmentUIStackViewAlignmentCenter的话需要加上一个centerAttribute的约束。如果是alignmentbaseline相关的话还要给canvas添加一个高为0的低优先级约束,用来满足某些特殊情况下canvas约束不满足的情况。

具体与canvas建立约束关系的firstItemrelation关系是根据alignment类型以及NSLayoutAttribute的不同而不同的,情况比较多我就不一一列举了,同样是根据查表得到,具体可以看代码去查。

最后是alignmentConstraintshiddingDimensionConstraints,虽然前面说它们两个的顺序是一前一后创建,但其实并不是,它们可以说是一起创建的,首先取出第一个arrangedSubview作为guardView,然后循环遍历其余arrangedSubview,先添加alignmentConstraint,如果这个arrangedSubviewhidden的那么就会再添加一个hiddingDimensionConstraint

- (void)updateAlignmentItemsConstraintsIfNecessary {
if (self.mutableItems.count == 0) {
    return;
}

[self.alignmentConstraints setObject:[NSMapTable weakToWeakObjectsMapTable] forKey:self.alignmentConstraintsFirstKey];
[self.alignmentConstraints setObject:[NSMapTable weakToWeakObjectsMapTable] forKey:self.alignmentConstraintsSecondKey];
[self.canvas removeConstraints:self.hiddingDimensionConstraints.fd_allObjects];
[self.hiddingDimensionConstraints removeAllObjects];

UIView *guardView = self.mutableItems.firstObject;
[self.mutableItems enumerateObjectsUsingBlock:^(UIView *item, NSUInteger idx, BOOL *stop) {
    if (self.alignment != UIStackViewAlignmentFill) {
        NSLayoutConstraint *ambiguitySuppressionConstraint = [NSLayoutConstraint constraintWithItem:item attribute:self.alignmentConstraintsFirstAttribute relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:0];
        ambiguitySuppressionConstraint.identifier = @"FDSV-ambiguity-suppression";
        ambiguitySuppressionConstraint.priority = 25;
        [item addConstraint:ambiguitySuppressionConstraint];
        [self.alignmentConstraints[self.alignmentConstraintsFirstKey] setObject:ambiguitySuppressionConstraint forKey:item];
    } else {
        if (item != guardView) {
            NSLayoutConstraint *firstConstraint = [NSLayoutConstraint constraintWithItem:guardView attribute:self.alignmentConstraintsFirstAttribute relatedBy:NSLayoutRelationEqual toItem:item attribute:self.alignmentConstraintsFirstAttribute multiplier:1 constant:0];
            firstConstraint.identifier = @"FDSV-alignment";
            [self.canvas addConstraint:firstConstraint];
            [self.alignmentConstraints[self.alignmentConstraintsFirstKey] setObject:firstConstraint forKey:item];
        }
    }
    if (item != guardView) {
        NSLayoutConstraint *secondConstraint = [NSLayoutConstraint constraintWithItem:guardView attribute:self.alignmentConstraintsSecondAttribute relatedBy:NSLayoutRelationEqual toItem:item attribute:self.alignmentConstraintsSecondAttribute multiplier:1 constant:0];
        secondConstraint.identifier = @"FDSV-alignment";
        [self.canvas addConstraint:secondConstraint];
        [self.alignmentConstraints[self.alignmentConstraintsSecondKey] setObject:secondConstraint forKey:item];
    }
    if (item.hidden) {
        NSLayoutConstraint *hiddenConstraint = [NSLayoutConstraint constraintWithItem:item attribute:self.axis == UILayoutConstraintAxisHorizontal ? NSLayoutAttributeHeight : NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:0];
        hiddenConstraint.priority = [item contentCompressionResistancePriorityForAxis:self.axis == UILayoutConstraintAxisHorizontal ? UILayoutConstraintAxisVertical : UILayoutConstraintAxisHorizontal];
        hiddenConstraint.identifier = @"FDSV-hiding";
        [self.canvas addConstraint:hiddenConstraint];
        [self.hiddingDimensionConstraints setObject:hiddenConstraint forKey:item];
    }
}];
}

这里的alignmentConstraint的创建都是guardView与其余的arrangedSubview创建relation关系为相等的约束,而NSLayoutAttribute的选择仍然是查表法,根据axisalignment的不同而选择不同的NSLayoutAttribute

如果alignment不是UIStackViewAlignmentFill模式的话,就会给arrangedSubview创建一个dimensionAttribute0的低优先级约束,称为ambiguitySuppressionConstraint放在上图中keyAmbiguity SuppressionNSMapTable中。


现在解释一下本文章Part 1中最后提到的UIStackViewalignmentUIStackViewAlignmentFill时,最高视图隐藏掉,而其余视图没有变成第二个的视图的高度的bug。原因就是在UIStackView的中实现中AlignmentLayoutArrangement是没有管理hiddingDimensionConstraints的,所以当视图被隐藏了后,那个视图被添加了一个宽为0的约束,视觉上看不到了,但是高方向的约束仍然存在,所以仍然会撑开StackView,所以在FDStackView中我们在alignment方向上同时增加了hiddingDimensionConstraints,视图被hidden后,会在高度方向上也给他加上一个高0为的约束,而且这个优先级也很有讲究需要跟它的contentCompressionResistancePriority设为一样,这样才不会在AutoLayout布局系统中当用户人为添加一个高度约束后产生冲突。

写了这么多,才写完第一个技术点的第一部分,内容确实比较多,我写的也比较乱,时间比较紧所以写作时间是间断的,所以思维也是间断跳跃的,还麻烦各位看官多多包涵。本来打算一篇写完的,但是这么长,还是有必要在分一下的,Part 2就到这吧,其余的内容就在Part 3吧。



分享文章到微博:
对您有点帮助? 您的支持将鼓励我继续创作!