在实际项目中,我们会碰到一个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...
,来对不同的布局分别处理,但这样带来的问题是不同的布局方式写在一起,会造成代码的可读性和维护性降低,而且布局方式的代码不能复用,也会造成一定代码重复。这种代码结构见下图:

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

那么接下来我们来定义一个Layout
协议,因为这里我们采用全手动布局,只有实现两个方法即可,一个用来计算size,一个用来设置frame,因此这个协议可以这样定义:
1 2 3 4 5 6 7 8 9 10
| protocol Layout {
func sizeThatFits(_ size: CGSize) -> CGSize
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 28
| 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 25
| 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
