惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
I
Intezer
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
Cloudbric
Cloudbric
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LINUX DO - 热门话题
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Latest news
Latest news
B
Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
L
LangChain Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
S
Security Affairs
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Security Latest
Security Latest
Vercel News
Vercel News
Y
Y Combinator Blog
G
GRAHAM CLULEY
S
Securelist
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网

土土哥的Blog

好久没有更新博客=。= 阿里巴巴国际无线技术部 - 招人啦~求iOS、Android、Java 转-阿里巴巴国际无线技术部 - 在这里遇见最好的自己 反编译分析并模拟实现methodSignatureForSelector方法 反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash 有趣的Autolayout示例5-Masonry实现 在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper 用QuartzCode快速实现一个收藏动画 开源项目-拼图验证控件TTGPuzzleVerify的实现 Swift开源Mac App - BingWallPaper 有趣的Autolayout示例4-Masonry实现 翻译-为什么objc_msgSend必须用汇编实现 API返回结果设计经验与总结 结合访问Out Parameters出现EXC_BAD_ACCESS的例子,反编译汇编解读__autoreleasing 总结一些iOS项目中组织代码的方法 对组件化与模块化的思考与总结 开源项目-TTGTagCollectionView Swift开源项目: TTGEmojiRate的实现 Swift写的库-TTGSnackbar 有趣的Autolayout示例2-Masonry实现 解决iOS项目的版本兼容问题-结合宏、Category和Runtime 用Runtime的手段填充任意NSObject对象的nil属性 有趣的Autolayout示例-Masonry实现 UITextView编辑时插入自定义表情-续-自定义表情图片的大小 RPC框架Thrift例子-PHP调用C++后端程序 GCD使用经验与技巧浅谈 为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权 Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5 @autoreleasepool-内存的分配与释放 有关宏定义的经验与技巧-简化代码-增强Log Effective-Objective-C-读书笔记-Item-4-如何正确定义常量 UITextView编辑时插入自定义表情-简单的图文混编 关于评论不见了=。= Entity和Model的不同-关于代码的数据层 一次审核被拒的经历-关于iCloud到底应该备份什么数据 Block类型变量-缓存Http请求与回调 提升UITableView性能-复杂页面的优化 NSString的Copy与内存分配 利用NSProxy实现消息转发-模块化的网络接口层设计-原创 Effective-Objective-C-读书笔记-Item-3 Effective-Objective-C-读书笔记-Item-2 Effective-Objective-C-读书笔记-Item-1 iOS项目的目录结构-原创 Android开源库-LinkTextView-原创 第一篇Blog
有趣的Autolayout示例3-Masonry实现
土土哥 · 2015-12-14 · via 土土哥的Blog

更新

  • 2016-06-02: 感谢我不是蜡笔小新但是我有小白,发现了Case 2的最后两个cell展开动画问题,原来是tableView的estimatedRowHeight会跟展开动画冲突的缘故。
  • 2015-12-23: 感谢XVXVXXX的PR,Case 1不再需要Fake Header View,直接用contentInset就好~
  • 2015-12-17:Case3的等间距,用UIStackView实现最优雅,但是无奈只有iOS9以上支持。兼容方案如OAStackView也可以,但是在UITableViewCell里面用,或者需要频繁的增减内部View的数量时,性能损耗有点厉害,会卡。所以,还是要看需求=。=

前言

第三篇来了。
依然是3个小例子,主要部分用Masonry手写代码实现,其它的约束在storyboard里面直接拖拽搭建。
至于为啥不用VFL,主要是因为它的“描述性”的写法很容易出错,没有补全、不好调试,写起来没有“代码”的感觉=。=。当然,这个仁者见仁智者见智~
三个例子分别是:Parallax Header动态变高度的UITableViewCell,以及两种方式实现等间距。原理其实都很简单,例子也都是平时积累起来的。

前两篇:

Github地址:
https://github.com/zekunyan/AutolayoutExampleWithMasonry

Gif示例

Parallax翻译过来就是“视差”,我个人觉得就是一种“联动”的效果,在许多应用里面都能见到。当前这个例子,就是最简单的一种。

原理

原理其实就是根据UITableView当前下拉的位移值,同步改变Parallax Header的高度,即NSLayoutConstraintconstant属性,对应到Masonry里面就是重新让约束equalTo()一次。

主要步骤

主要的步骤如下:

  1. 设置UITableView背景透明。
  2. 在UITableView正下方放置一个UIImageView,作为我们的Parallax Header,设置contentModeUIViewContentModeScaleAspectFill,并加上上左右的固定约束,使其与UITableView对其,然后加上一个固定高度的约束,并在代码里面保存。
  3. 设置UITableView的contentInset跟Parallax Header等高,使UITableView的头部“撑开”,让后面的Parallax Header露出来。
  4. 在代码里面监听UITableView的contentOffset属性,当y小于0时,增加Parallax Header高度,使其产生联动效果。

约束示意图如下:
Parallax Header

代码

创建Parallax Header的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_parallaxHeaderView = [UIImageView new];

[self.view insertSubview:_parallaxHeaderView belowSubview:_tableView];


_parallaxHeaderView.contentMode = UIViewContentModeScaleAspectFill;
_parallaxHeaderView.image = [UIImage imageNamed:@"parallax_header_back"];


[_parallaxHeaderView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.and.right.equalTo(self.view);
make.top.equalTo(self.mas_topLayoutGuideBottom);

_parallaxHeaderHeightConstraint = make.height.equalTo(@(ParallaxHeaderHeight));
}];

设置UITableView的contentInset

1
_tableView.contentInset = UIEdgeInsetsMake(ParallaxHeaderHeight, 0, 0, 0)

监听contentOffset属性的两种方法

方法1:直接实现scrollViewDidScroll:

这种方法应该是最直接的:

1
2
3
4
5
6
7
8
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y < 0) {

_parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - scrollView.contentOffset.y));
} else {
_parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight));
}
}

方法2:KVO监听contentOffset变化

用KVO的好处就是不用要求当前类实现UITableView的delegate,对于代码的拆分有好处。

增加KVO:

1
[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

实现监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {

CGPoint contentOffset = ((NSValue *)change[NSKeyValueChangeNewKey]).CGPointValue;


if (contentOffset.y < 0) {
_parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - contentOffset.y));
} else {
_parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight));
}
}
}

最后别忘了取消KVO:

1
2
3
- (void)dealloc {
[_tableView removeObserver:self forKeyPath:@"contentOffset"];
}

小节

NSLayoutConstraintconstant属性非常有用,既可以做动画,也可以方便的调整现有布局,大家多多挖掘哈~

Case 2: 动态变高度的UITableViewCell

嗯,又是UITableViewCell=。=
只不过这次的是“动态改变高度”,就是类似于微信朋友圈里面的“全文”那种效果。

单纯的不定高UITableViewCell不是本例子的重点,详细请看有趣的Autolayout示例2-Masonry实现里面的Case1。

先说一点我觉得在代码设计上比较重要的地方:Cell只负责显示内容,不应该保存具体的状态信息

我们都知道,UITableViewCell是会被重用的,也就是说,不能保证UITableView里面的哪一行一定由哪一个UITableViewCell实例展示。
动态展开、收回Cell的时候,我们需要一个BOOL变量,用于保存当前Cell的展开、收回的状态。这个BOOL变量就是所谓的“状态”,这个状态应该保存在当前Cell的数据里面,如Entity、ViewModel里面。对Cell填充数据的时候,再根据这个“状态”,修改对应的约束。

主要步骤

先看看大致的步骤:

  1. 通过点击的Cell找到对应的数据Entity。
  2. 改变这一行数据Entity用于保存状态的BOOL变量的值。
  3. 让UITableView刷新这一行。
  4. 刷新的时候,Cell根据这个BOOL变量重新调整约束、填充数据,得到新的高度。
  5. 最后就是Cell的高度变化。(此时的这一行的Cell实例并不一定是之前的那个实例)

布局

为了尽量简单,例子里面的Cell只有三个子控件,第一个UILabel是标题等调试信息,第二个UILabel用来显示多行文本,最后一个UIButton用来切换展开、收回的状态。大致的布局如下:

Case2 UITableViewCell布局示意

布局的代码

标题UIlabel

1
2
3
4
5
6
7
_titleLabel = [UILabel new]
[self.contentView addSubview:_titleLabel]

[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(@21);
make.left.and.right.and.top.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8));
}]

底部“More”按钮

1
2
3
4
5
6
7
8
9
_moreButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_moreButton setTitle:@"More" forState:UIControlStateNormal];
[_moreButton addTarget:self action:@selector(switchExpandedState:) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:_moreButton];

[_moreButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(@32);
make.left.and.right.and.bottom.equalTo(self.contentView);
}];

正文UIlabel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CGFloat preferredMaxWidth = [UIScreen mainScreen].bounds.size.width - 16;


_contentLabel = [UILabel new];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByCharWrapping;
_contentLabel.clipsToBounds = YES;
_contentLabel.preferredMaxLayoutWidth = preferredMaxWidth;
[self.contentView addSubview:_contentLabel];

[_contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.and.right.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8));
make.top.equalTo(_titleLabel.mas_bottom).with.offset(4);
make.bottom.equalTo(_moreButton.mas_top).with.offset(-4);

_contentHeightConstraint = make.height.equalTo(@64).with.priorityHigh();
}];

为什么要加正文UIlabel高度约束

有趣的Autolayout示例2-Masonry实现里面的Case1也讲过,获取Cell的高度的方法是systemLayoutSizeFittingSize:,如果不对正文UILabel加上高度约束,获取的高度就是根据正文的内容计算出来的,这与之前的例子里面一致。

为了使高度固定,就需要加上一个高度约束,使得systemLayoutSizeFittingSize:计算时按照这个约束去计算。

为什么正文UILabel的高度约束的优先级要调整为High

在UITableView刷新时,会先计算高度,即先调用tableView: heightForRowAtIndexPath:方法,如果高度约束为默认的1000最高的话,会产生冲突。

因为在计算的时候,我们的高度是由一个“template cell”填充内容后计算得来,这个时候的高度已经是展开以后的高度,当前的Cell还来不及调整约束(甚至不会调整,如果只用beginUpdates和endUpdates更新的话,Cell不会reload),所以降低这个高度约束的优先级,去掉冲突。

使用install和uninstall控制约束

为了能得正确高度,Cell需要根据具体的数据、状态更新约束。
这里可以使用installuninstall来控制正文UILabel高度约束是否生效。在填充Cell的数据时,就可以根据状态BOOL值来选择调用:

1
2
3
4
5
6
7
8
9
10
- (void)setEntity:(Case8DataEntity *)entity indexPath:(NSIndexPath *)indexPath {



if (_entity.expanded) {
[_contentHeightConstraint uninstall];
} else {
[_contentHeightConstraint install];
}
}

创建Delegate,使得Cell的事件得以回传到ViewController

在点击Cell的“More”按钮时,需要改变当前的展开收回状态BOOL值,还需要让UITableView刷新。
直接在Cell里面修改Entity数据,或者持有UITableView实例都是不恰当的,这个时候可以用Delegate模式实现。

Delegate如下:

1
2
3
@protocol Case8CellDelegate <NSObject>
- (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index;
@end

然后ViewController实现这个Protocol

1
2
3
4
5
6
7
8
9
- (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index {

Case8DataEntity *case8DataEntity = _data[(NSUInteger) index.row];

case8DataEntity.expanded = !case8DataEntity.expanded;
case8DataEntity.cellHeight = 0;


}

Cell保存ViewController这个delegate,然后在按钮点击时回调

1
2
3
4
5
6
7

@property (weak, nonatomic) id <Case8CellDelegate> delegate;


- (void)switchExpandedState:(UIButton *)button {
[_delegate case8Cell:self switchExpandedStateWithIndexPath:_indexPath];
}

刷新的方式

UITableView的刷新可以用以下几种方法:

1. reloadData

reloadData刷新,其实就是把所有Cell都刷新了一次,代价有点大,不推荐。

2. reloadRowsAtIndexPaths:withRowAnimation:

这个方法的好处就是可以指定要刷新的哪几行,而且可以指定刷新时的动画形式,一般来说用UITableViewRowAnimationFade就不错。
刷新的时候,tableView:cellForRowAtIndexPath:会被调用,原来的Cell实例会被替换。

3. beginUpdates和endUpdates

这两个方法一般都是成对使用的,在中间可以执行插入、删除等调整Cell的操作,改变Cell的高度也可以用它。
不过要注意的是,这两个方法并不会重新加载Cell,只是单纯的改变了高度,所以如果Cell原来的约束里面有高度约束这种,而又保持默认的优先级,就会产生约束冲突。

从效果上来讲,我个人觉得用reloadRowsAtIndexPaths:withRowAnimation:会更好一些~

小节

想用好Autolayout不容易啊,要仔细研究UITableView的机制=。=

Case 3: 两种方式实现等间距

等间距,也就是View之间的X或Y轴上的坐标等差,在这里我只举出水平方向上的等间距,垂直方向上一个道理。

方法1:利用透明等宽的占位View填充空白处,实现等间距

步骤很简单,就是循环创建真正要展示的View和占位View,布局如下:

利用占位View实现等间距

说明一下:

  • 占位View的宽度不能定死,这样外部的父级View宽度变化时,内部的View仍然可以保持等间距。
  • 既然占位View宽度不定,总得有个宽度的参照,这个参照就是其它的占位View,也就是说,要给占位View加上两两宽度相等的约束。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

UIView *lastSpaceView = [UIView new];
lastSpaceView.backgroundColor = [UIColor greenColor];
[_containerView1 addSubview:lastSpaceView];


[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.and.top.and.bottom.equalTo(_containerView1);
}];


for (NSUInteger i = 0; i < ITEM_COUNT; i++) {

UIView *itemView = [self getItemViewWithIndex:i];
[_containerView1 addSubview:itemView];


[itemView mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.and.width.equalTo(@(ITEM_SIZE));
make.left.equalTo(lastSpaceView.mas_right);
make.centerY.equalTo(_containerView1.mas_centerY);
}];


UIView *spaceView = [UIView new];
spaceView.backgroundColor = [UIColor greenColor];
[_containerView1 addSubview:spaceView];



[spaceView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(itemView.mas_right).with.priorityHigh();
make.top.and.bottom.equalTo(_containerView1);
make.width.equalTo(lastSpaceView.mas_width);
}];


lastSpaceView = spaceView;
}


[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(_containerView1.mas_right);
}];

像这种重复添加View和约束,合理使用变量保存“上一次循环”创建的占位View,就可以大大简化代码,而且可以任意调整数量~

方法2:直接按比例设置multiplier

等间距,其实就是按比例,再进一步就是x坐标是按比例的。
延伸到View上,可以理解为centerX的值与父级View的宽度按比例增减。

但是,读者可以尝试一下,直接设置一个View的边界、位置属性,如centerX,等于其父级View的宽度是会报错的。

难道就没有办法了?当然不是。

始终在坐标系上考虑约束

在上一篇有趣的Autolayout示例2-Masonry实现的开头我也提到过,Autolayout最终都是体现在坐标系上,一切都会按照viewA-attribute = viewB-attribute * multiplier + constant这种公式去计算,既然centerX不能跟父级的Width宽度一起加约束,那就换一个,如父级的右边界,父级View的右边界在父级本身的参照系下的Y坐标值不就等于其宽度吗~

所以,可以按照如下方式加约束:

按比例设置multiplier实现等间距

对应的代码也会异常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

for (NSUInteger i = 0; i < ITEM_COUNT; i++) {
UIView *itemView = [self getItemViewWithIndex:i];
[_containerView2 addSubview:itemView];

[itemView mas_makeConstraints:^(MASConstraintMaker *make) {

make.width.and.height.equalTo(@(ITEM_SIZE));

make.centerY.equalTo(_containerView2.mas_centerY);

make.centerX.equalTo(_containerView2.mas_right).multipliedBy(((CGFloat)i + 1) / ((CGFloat)ITEM_COUNT + 1));
}];
}

小节

很多时候,灵活的使用multiplier能大大简化开发~

总结

三个例子说完了,你有啥收获呢~?:-D