iOS开发--Swift:布局库——SnapKit
如果你是只从事过iOS开发,觉得使用SnapKit(OC中的Masonry)很方便,甚至xib拖拉也不错。
可以说,这些都是iOS开发稀疏平常的日常。
但一旦你学过Flutter/Vue写过UI组件,那么iOS的UI编写真的是有种不忍直视的感觉,可以说是原始社会。
虽然隔壁Android的UI写起来也不会特别友好,但是还是比iOS好。
为啥,因为其他的UI编写基本上都可以既见既所得了,就算犯了错,边看边边调试就行了
只有iOS的需要编译调试。。。编译调试。。。编译调试。。。
而且其他家的UI编写基本上都是一脉相承,前端里面的CSS,在Flutter中可以找到一些命名相同的组件,我们来举个例子来说明一下隔壁构建UI的简单和同化:
下面这个UI如何Flutter、H5、iOS来实现:
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
Flutter:
Wrap(
children: model.children
.map(
(topic) => Padding(
padding: EdgeInsets.all(3.0),
child: Chip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
key: ValueKey<String>(topic.name),
backgroundColor: _getChipBgColor(topic.name),
label: Text(
topic.name,
style: TextStyle(fontSize: 14.0),
),
),
),
)
.toList(),
);
H5:
<view class="flex-wrap" v-for="(item, index) in list" :key="index">
<u-tag :text="item.name" :index="item.name" @click="click" />
</view>
<style scoped lang="scss">
.flex-wrap {
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
width: auto;
height: auto;
margin: 16rpx;
}
</style>
iOS:
我需要的代码太多了,这里就贴出来了 T_T
Flutter和H5都是用的wrap作为布局思路,Flutter有wrap组件,而H5直接用flex-wrap的CSS就可以了,而iOS中却没有,虽说很多iOS中的组件Flutter和H5需要自定义,而iOS开箱即用。
不过现在这个情况是,就算设计稿与UI元素都和iOS靠拢,但实际上Flutter和H5在组件上已经有非常成熟的官方组件或者第三方,而iOS却没有。。。加上没有热重载。。。
不过虽然说iOS的布局不好用,甚至不好使,但是SnapKit至少给你一点点曙光,此话怎讲,下面我们来看看同一个布局,使用iOS原生的布局和SnapKit的布局代码量吧。
iOS原生布局 VS SnapKit布局
ios原生布局:
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .leading,
relatedBy: .equal,
toItem: contentView,
attribute: .leading,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .top,
relatedBy: .equal,
toItem: contentView,
attribute: .top,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .trailing,
relatedBy: .equal,
toItem: contentView,
attribute: .trailing,
multiplier: 1,
constant: 0))
addConstraint(NSLayoutConstraint(item: imageView,
attribute: .bottom,
relatedBy: .equal,
toItem: contentView,
attribute: .bottom,
multiplier: 1,
constant: 0))
SnapKit布局:
contentView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalTo(contentView)
}
使用原生布局还是SnapKit,不用我多说了。
SnapKit的使用、注意事项与疑惑
就我目前做过的项目,就算里面包含着大量的xib,也基本上会使用SnapKit或者Masonry,虽然说它只是原生Api的一层封装,但其链式调用、函数式编程的思想非常值得我们学习和借鉴,下面说几个使用SnapKit的经验谈:
使用SnapKit前,一定要先将子控件添加到父视图中
parentView.addSubview(subview)
一定一定一定在布局前将子控件添加到父视图中,否则会直接崩溃!!!
leading和left、trailing和right
其实在目前国内App中使用leading与left,trailing与right在正常情况下是等价的,这是因为国内的阅读习惯是从左到右的,不过如果你的App需要在阿拉伯国家上架,他们的布局是从右至左时(比如阿拉伯文) 则会对调。
我个人的习惯是使用leading和trailing,这也可能和我做过国际化有关。
给控件添加、更新约束、引用约束、停用、启用
- 添加新的约束
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
}
- 删除控件以前所有约束,添加新约束
由于imageView已经添加到contentView上了,所以remake的时候不需要调用addSubview的方法
imageView.snp.remakeConstraints { (make) in
}
- 更新约束,写哪条更新哪条,其他约束不变
不过有的时候更新一条约束的时候可能会崩溃,我在cell中进行update的时候就遇到过,至于原因正在排查,这个时候如果崩溃了,可能考虑使用remakeConstraints已经控件的重新布局,虽然这样会消耗一点性能,但总比崩溃好。
imageView.snp.updateConstraints { (make) in
}
- 引用约束,声明一个局部变量或者类属性来引用要修改的约束
值得注意的是,topConstraint使用的是可选类型,这样保证了在空时,调用deactivate()和activate()方法不会崩溃。同时设置为全局变量,方便在各种情况进行调用。
var topConstraint: Constraint? = nil
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView()
view.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
self.topConstraint = make.left.equalToSuperview().offset(100).constraint
make.right.equalToSuperview().offset(-100)
make.top.equalToSuperview().offset(100)
make.bottom.equalToSuperview().offset(-100)
}
}
停用
topConstraint?.deactivate()
启用
topConstraint?.activate()
设置约束关系
约束关系 | 说明 |
---|---|
equalTo() | 设置属性等于某个数值 |
greaterThanOrEqualTo() | 设置属性大于或等于某个数值 |
lessThanOrEqualTo() | 设置属性小于或等于某个数值 |
multipliedBy() | 设置属性乘以因子后的值 |
multipliedBy() | 设置属性除以因子后的值 |
设置控件布局属性
约束关系 | 说明 |
---|---|
size | 尺寸 CGSize |
width、height | 宽、高 |
left | 边距左边 |
top | 边距顶部 |
right | 边距右边 |
bottom | 边距底部 |
center、centerX、centerY | x,y轴的交汇中心点、 x轴的中心点、y轴的中心点 |
leading | 阅读习惯的起始的边距 |
trailing | 阅读习惯的终末的边距 |
设置约束偏移
方法 | 参数 | 说明 |
---|---|---|
offset | CGFloat | 控件属性相对于参照物偏移多少 |
insets(MASEdgeInsets insets) | UIEdgeInsets | 控件四边相对于参照物偏移多少 |
举两个例子:
imageView.snp.makeConstraints { (make) in
make.left.equalToSuperview().offset(20)
make.right.equalToSuperview().offset(-20)
make.top.equalToSuperview().offset(20)
make.bottom.equalToSuperview().offset(-20)
}
/// 具体父控件四周都是20间距
imageView.snp.makeConstraints { (make) in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20))
}
第2种形式与第1种形式通过left、right、top、bottom写出来的效果一模一样,只是组织语言的形式不一样,SnapKit的编程方式虽然是Swift进行组织的,但是更多是靠其自身构建的DSL进行编码。
设置约束优先级
- SnapKit为我们提供了三个默认的方法,required、high、medium、low,优先级最大数值是1000
public static var required: ConstraintPriority {
return 1000.0
}
public static var high: ConstraintPriority {
return 750.0
}
public static var medium: ConstraintPriority {
#if os(OSX)
return 501.0
#else
return 500.0
#endif
}
public static var low: ConstraintPriority {
return 250.0
}
自己设置优先级的值,可以通过priority()方法来设置
imageView.snp.makeConstraints { (make) in
make.center.equalToSuperview()
make.width.equalTo(100).priority(ConstraintPriority.low)
make.height.equalTo(50).priority(800)
}
复制代码
其实这个优先级的使用,我自己在开发过程中很少使用到,还需要更多的学习和探索。
SnapKit与UIScrollView
很多新手将在将子控件放入UIScrollView进行布局的时候,经常会发现ScrollView滑不动了。
首先要理解一点核心思想:UIScrollView是依靠与其子视图(subview)之间的约束来确定ContentSize的大小。为什么这么说呢?
这是因为UIScrollView是个非常特殊的UIView,对于UIScrollView的subview来说,它的leading/trailing/top/bottom的space是相对于UIScrollView的contentSize而不是bounds来确定的,换句话说:UIScrollView与其subview之间相对位置的约束并不会直接用于frame的计算,而是会转化为对contentSize的计算。当UIScrollView知道了上下左右的约束分别指向subview的什么位置之后,只要subview的位置固定下来了,那么UIScrollView的contentSize的大小就确定下来了。
但是当我们尝试使用UIScrollView和它subview的leading/trailing/top/bottom来互相决定大小的时候,会出现Has ambiguous scrollable content width/height的 warning。
根据经验,习惯的做法是在UIScrollView和它原来的subviews之间增加一个contentView,依靠contentView来确定contentSize。
代码如下图所示:
class ViewController: UIViewController {
lazy let scrollView = UIScrollView()
lazy let contentView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(view)
}
/// 添加容器视图
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { (make) in
make.top.bottom.equalTo(scrollView)
make.left.right.equalTo(view) // 确定的宽度,因为垂直滚动
}
let label1 = UILabel()
/// 注意将组件添加到contentView上,而不是scrollView上面
contentView.addSubview(label1)
label1.numberOfLines = 0
label1.backgroundColor = .yellow
label1.snp.makeConstraints { (make) in
make.left.right.equalTo(contentView).inset(20)
make.top.equalTo(contentView).offset(20)
}
let label2 = UILabel()
/// 注意将组件添加到contentView上,而不是scrollView上面
contentView.addSubview(label2)
label2.numberOfLines = 0
label2.backgroundColor = .red
label2.snp.makeConstraints { (make) in
make.left.right.equalTo(label1)
make.top.equalTo(label1.snp.bottom).offset(20)
/// 底部约束一定要添加,用于告诉contentView在哪里,不然不能够确定contentSize。
make.bottom.equalToSuperview()
}
label1.text = """
hi,掘友们!
你们发现没,为什么程序员的技术水平差不多,有的升职加薪机会多,有的不去找工作,总有工作找他们?同等“硬件”条件下,为啥竞争力不一样呢?
很主要的一个原因是,大家的影响力不一样。写博客、做分享、写开源…… 这些都是提高影响力的方式,很多技术达人,甚至靠输出自己的技术知识和经验,把副业做成了主业。当然,这样的影响力不是一天一夕养成的,需要持续输出,积点成势。
很多人也立了Flag要每天写点东西,可总是坚持不了两天,Flag立了倒,倒了立,一直得不到正反馈,最后自己都气馁了。这一次,我们帮你培养长期坚持且正向激励的好习惯,扶稳Flag!
"""
label2.text = """
小王1人更文了28天,小王将获得3千元以内等值奖品,比如Gopro hero8。如果小王同时被幸运大奖砸中,小王将在幸运大奖和一等奖之内选择其一。
小王和小李共2人完成了27天更文挑战,小王和小李每人获得1500元等值奖品,比如AirPos2代。
小李更满30天,除了一等奖之外,还将获得满勤奖奖励。
小王等共6人完成了27天更文挑战,小王等6人每人可选833元以内等值奖品。
小王更文了23天,可选择二等奖/三等奖中的一种。
"""
}
}
如果文本还不够使得scrollView进行滑动,请在label1和label2中的text中多随意写些字符串。
其他选择
随着移动端App向着大前端的思路进化,目前iOS和Android的UI开发工具都在向声明式演进,iOS有SwiftUI,Android有ComposeUI,加上跨平台的Flutter,其实基本上都是一个模子刻出来的,只是语言不同而已。
另外iOS的OC中还有一个FlexLib 库。
该布局框架基于flexbox模型,这个模型是web端的布局标准。基于flexbox模型,FlexLib提供了强大的布局能力,并且易于使用。
之前有同事弄过,用过的都说好,我也准备探索一下。
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
明日继续
终于挨过了周末更文,最近工作上因为iOS与H5的交互非常多,所以考虑下周会插播一些工作上的问题,进行存档。还望大家海涵。
作者:season_zhu
链接:https://juejin.cn/post/6970515865003360263