使用协议和结构体让一个view适配不同的布局

在实际项目中,我们会碰到一个view需要有不同的布局样式的情况,这种情况,我们可能会分成几个view来写,或者在一个view中通过if...else..来适配不同的布局。前一种方式会造成代码浪费,后一种方式造成一堆if...else...,降低代码质量。在Swift中,通过使用协议和结构体,可以将一个view中的视图和布局功能解耦,让一个view可以便捷地使用不同的布局,一个布局可以便捷地运用到不同的view上。

假如我们现在有这样一个需求,需要在一个view上展示一段文字和一张图片,于是这个view就有一个UIImageView和一个UILabel,但是它有四种展示样式,分别是左字右图,左图右字,上图下字,上字下图。

首先我们创建一个枚举LayoutType

1
2
3
4
5
6
enum LayoutType {
case topImage
case leftImage
case bottomImage
case rightImage
}

然后创建一个ContentView,它有一个UIImageView,一个UILabel,然后在sizeThatFits方法中计算size,在layoutSubviews方法中设置subviews的frame。一般的做法,就是直接在这两个方法里,写一堆if...else...或者写switch...case...,来对不同的布局分别处理,但这样带来的问题是不同的布局方式写在一起,会造成代码的可读性和维护性降低,而且布局方式的代码不能复用,也会造成一定代码重复。这种代码结构见下图:

屏幕快照 2017-02-19 10.31.37

在这里可以引入协议和结构体,首先通过协议来抽象一个布局的接口,再由不同的结构体来实现不同的布局,具体的视图和布局实现了解耦,一个布局可以运用于不同的view,一个view也可以轻松拥有不同的布局方式。代码结构见下图:

屏幕快照 2017-02-19 10.37.28

那么接下来我们来定义一个Layout协议,因为这里我们采用全手动布局,只有实现两个方法即可,一个用来计算size,一个用来设置frame,因此这个协议可以这样定义:

1
2
3
4
5
6
7
8
9

protocol Layout {

// 计算size
func sizeThatFits(_ size: CGSize) -> CGSize

// 设置frame
mutating func layoutViews(in rect: CGRect)
}

有了这个协议,我们在ContentView中,就只需要设置一个Layout变量,通过这个变量来布局即可,这样就完全把ContentView的具体视图和布局解耦了,代码就会非常简洁。具体代码如下:

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

class ContentView: UIView {

let imageView = UIImageView()
let textLabel = UILabel()
var layoutType = LayoutType.rightImage
var layout: Layout?

override init(frame: CGRect) {
super.init(frame: frame)
addSubview(imageView)
addSubview(textLabel)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func sizeThatFits(_ size: CGSize) -> CGSize {
return layout?.sizeThatFits(size) ?? .zero
}

override func layoutSubviews() {
super.layoutSubviews()
layout?.layoutViews(in: bounds)
}
}

接下来,我们就可以用结构体来创建具体的布局方式,因为这里只有两个view,所以结构体中只需要有两个view,并实现Layout协议即可。比如第一个topImage样式,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private let padding: CGFloat = 20
private let imageHeight: CGFloat = 50
private let imageTextMargin: CGFloat = 20

struct TopImageLayout: Layout {
let imageView: UIView
let textView: UIView

func sizeThatFits(_ size: CGSize) -> CGSize {
let textMaxWidth = size.width - padding * 2
let textSize = textView.sizeThatFits(CGSize(width: textMaxWidth, height: size.height))
let height = padding + imageHeight + imageTextMargin + textSize.height
return CGSize(width: size.width, height: height)
}

mutating func layoutViews(in rect: CGRect) {
imageView.frame = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: imageHeight)

let textMaxWidth = rect.size.width - padding * 2
let textSize = textView.sizeThatFits(CGSize(width: textMaxWidth, height: rect.size.height))
textView.frame = CGRect(x: rect.minX + padding, y: imageView.frame.maxY + imageTextMargin, width: textSize.width, height: textSize.height)
}
}

类似的,我们可以创建LeftImageLayout,BottomImageLayout,RightImageLayout,对于不同的layoutType,使用不同的layout,为了方便我们可以将layout变成计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
var layout: Layout {
switch layoutType {
case .topImage:
return TopImageLayout(imageView: imageView, textView: textLabel)
case .leftImage:
return LeftImageLayout(imageView: imageView, textView: textLabel)
case .bottomImage:
return BottomImageLayout(imageView: imageView, textView: textLabel)
case .rightImage:
return RightImageLayout(imageView: imageView, textView: textLabel)
}
}

这样外部调用者只需要改变layoutType即可。

总结:使用协议和结构体,将UIView的布局功能拆分出去,可以很好地将具体视图和布局功能解耦,实现视图和布局的灵活搭配,一个视图可以便捷地使用不同的布局,一个布局可以便捷地运用到不同的view上。

完整的demo可以到我的github上下载:Protocol-UIView
protocol-uivie

坚持原创技术分享,您的支持将鼓励我继续创作!