使用协议和结构体让一个view适配不同的布局 | LiJun's Blog
在实际项目中,我们会碰到一个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 | 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