需求由來
在項目開發(fā)過程中腿短,設(shè)計師調(diào)整設(shè)計稿是正常的,但如果調(diào)整頻率一高,就讓我們開發(fā)十分抓狂。
我們來進行一個情景模擬(以 AutoLayout 為例):
設(shè)計師:這個左邊距調(diào)多 2 px背零,這個上邊距調(diào)少 2 px,這 2 個 view 之間間距調(diào)大點无埃,多 2 px 吧徙瓶,這個文本字體調(diào)大一號。
開發(fā):好的嫉称,我馬上調(diào)侦镇。(我一頓操作,調(diào)整約束值织阅,...)
======== 過了 1 天 ==========
設(shè)計師:這個樣式有點問題壳繁,整體樣式我重新設(shè)計了一下,你調(diào)一下(給了我最新的設(shè)計稿)
開發(fā):這個樣式調(diào)整有點大啊,各種約束都不一樣了氮趋,你確定要改嗎伍派?
設(shè)計師:確定。(我一頓操作剩胁,刪除舊約束代碼,添加新約束代碼祥国,...)
======== 又過了 1 天 ==========
設(shè)計師:這個樣式昵观,老板看后和之前對比,覺得還是之前樣式好舌稀,你換回來吧啊犬。
開發(fā):.......
還有一種情況,一個視圖在不同地方顯示的布局樣式是不一樣的壁查,這種視圖樣式配置是非常繁瑣的觉至,就像我們使用 ObjC 的 decode
和 encode
代碼一樣,都是必須但又是無腦的(體力活)睡腿,我就想搞個東西方便配置視圖樣式语御,從這個過程中解脫出來
方案思考
全局配置樣式
通過全局變量進行配置(之前的做法):
extension View {
// 約束值
struct Constraint {
static let topPadding: CGFloat = 30
static let bottomPadding: CGFloat = 10
static let leftPadding: CGFloat = 43
static let rightPadding: CGFloat = 41
}
// 顏色
struct Color {
static let title = UIColor.red
static let date = UIColor.white
static let source = UIColor.black
}
// 字體
struct Font {
static let title = UIFont.systemFont(ofSize: 16)
static let date = UIFont.systemFont(ofSize: 13)
static let source = UIFont.systemFont(ofSize: 13)
}
}
初始化配置樣式
全局配置很不方便,沒法在外部修改樣式配置席怪,后來想到可以通過初始化傳入樣式進行配置的:
class ViewStyle {
// 約束值
var topPadding: CGFloat = 30
var bottomPadding: CGFloat = 10
var leftPadding: CGFloat = 43
var rightPadding: CGFloat = 41
// 顏色
var titleColor = UIColor.red
var dateColor = UIColor.white
var sourceColor = UIColor.black
// 字體
var titleFont = UIFont.systemFont(ofSize: 16)
var dateFont = UIFont.systemFont(ofSize: 13)
var sourceFont = UIFont.systemFont(ofSize: 13)
}
class View: UIView {
var style: ViewStyle?
override init(frame: CGRect, style: ViewStyle) {
super.init(frame: frame)
self. style = style
setupSubviews(with: style)
}
fileprivate func setupSubviews(with style: ViewStyle) {
// 樣式配置代碼
}
}
屬性配置樣式
初始化配置樣式在大部分情況下已經(jīng)滿足需求了应闯,但因為初始化方法有很多,尤其是使用 xib 加載的時候挂捻,不好處理碉纺。
因為我那段時間正在學(xué)習 RxSwift + ReactorKit
框架使用,發(fā)現(xiàn) ReactorKit
框架中 Reactor 協(xié)議抽離視圖內(nèi)的業(yè)務(wù)邏輯處理非常巧妙刻撒,讓每個視圖綁定各自的處理器處理業(yè)務(wù)邏輯骨田,我就想視圖的配置不是也可以和 Reactor
協(xié)議一樣,每個視圖都綁定一個視圖樣式配置
// MARK: - 視圖可配置協(xié)議
public protocol ViewConfigurable: class {
associatedtype ViewStyle
var viewStyle: ViewStyle? { get set }
func bind(viewStyle: ViewStyle)
}
/// 為實現(xiàn)該協(xié)議的類添加一個偽存儲屬性(利用 objc 的關(guān)聯(lián)方法實現(xiàn))声怔,用來保存樣式配置表
fileprivate var viewStyleKey: String = "viewStyleKey"
extension ViewConfigurable {
var viewStyle: ViewStyle? {
get {
return objc_getAssociatedObject(self, &viewStyleKey) as? ViewStyle
}
set {
objc_setAssociatedObject(self, &viewStyleKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if let style = newValue {
self.bind(viewStyle: style)
}
}
}
}
class View: UIView, ViewConfigurable {
func bind(viewStyle: ViewStyle) {
// 樣式配置代碼
}
}
最終方案
我構(gòu)造了一些常用視圖配置項來輔助樣式配置态贤,可自己看情況自定義配置項:
// MARK: - 以下是一些常用配置項
/// View 配置項
class ViewConfiguration {
lazy var backgroundColor: UIColor = UIColor.clear
lazy var borderWidth: CGFloat = 0
lazy var borderColor: UIColor = UIColor.clear
lazy var cornerRadius: CGFloat = 0
lazy var clipsToBounds: Bool = false
lazy var contentMode: UIViewContentMode = .scaleToFill
// 下面屬性用于約束值配置
lazy var padding: UIEdgeInsets = .zero
lazy var size: CGSize = .zero
}
/// Label 配置項
class LabelConfiguration: ViewConfiguration {
lazy var numberOfLines: Int = 1
lazy var textColor: UIColor = UIColor.black
lazy var textBackgroundColor: UIColor = UIColor.clear
lazy var font: UIFont = UIFont.systemFont(ofSize: 14)
lazy var textAlignment: NSTextAlignment = .left
lazy var lineBreakMode: NSLineBreakMode = .byTruncatingTail
lazy var lineSpacing: CGFloat = 0
lazy var characterSpacing: CGFloat = 0
// 屬性表,用于屬性字符串使用
var attributes: [String: Any] {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = self.lineSpacing
paragraphStyle.lineBreakMode = self.lineBreakMode
paragraphStyle.alignment = self.textAlignment
let attributes: [String: Any] = [
NSParagraphStyleAttributeName: paragraphStyle,
NSKernAttributeName: self.characterSpacing,
NSFontAttributeName: self.font,
NSForegroundColorAttributeName: self.textColor,
NSBackgroundColorAttributeName: self.textBackgroundColor
]
return attributes
}
}
/// Button 配置項
class ButtonConfiguration: ViewConfiguration {
class StateStyle<T> {
var normal: T?
var highlighted: T?
var selected: T?
var disabled: T?
}
lazy var titleFont: UIFont = UIFont.systemFont(ofSize: 14)
lazy var titleColor = StateStyle<UIColor>()
lazy var image = StateStyle<UIImage>()
lazy var title = StateStyle<String>()
lazy var backgroundImage = StateStyle<UIImage>()
lazy var contentEdgeInsets: UIEdgeInsets = .zero
lazy var imageEdgeInsets: UIEdgeInsets = .zero
lazy var titleEdgeInsets: UIEdgeInsets = .zero
}
/// ImageView 配置項
class ImageConfiguration: ViewConfiguration {
var image: UIImage?
}
配置樣式大概類似這樣:
/// 樣式配置基類
class TestViewStyle {
lazy var nameLabel = LabelConfiguration()
lazy var introLabel = LabelConfiguration()
lazy var subscribeButton = ButtonConfiguration()
lazy var imageView = ImageConfiguration()
}
/// 樣式一
class TestViewStyle1: TestViewStyle {
override init() {
super.init()
// 樣式
nameLabel.padding.left = 10
nameLabel.padding.right = -14
nameLabel.textColor = UIColor.black
nameLabel.font = UIFont.systemFont(ofSize: 15)
introLabel.lineSpacing = 10
introLabel.padding.top = 10
introLabel.numberOfLines = 0
introLabel.textColor = UIColor.gray
introLabel.font = UIFont.systemFont(ofSize: 13)
introLabel.lineBreakMode = .byCharWrapping
subscribeButton.padding.top = 10
subscribeButton.size.height = 30
subscribeButton.image.normal = UIImage(named: "subscribe")
subscribeButton.image.selected = UIImage(named: "subscribed")
subscribeButton.title.normal = "訂閱"
subscribeButton.title.selected = "已訂"
subscribeButton.titleColor.normal = UIColor.black
subscribeButton.titleColor.selected = UIColor.yellow
subscribeButton.titleFont = UIFont.systemFont(ofSize: 12)
imageView.padding.left = 14
imageView.padding.top = 20
imageView.size.width = 60
imageView.contentMode = .scaleAspectFill
imageView.borderColor = UIColor.red
imageView.borderWidth = 3
imageView.cornerRadius = imageView.size.width * 0.5
imageView.clipsToBounds = true
}
}
/// 樣式二
class TestViewStyle2: TestViewStyle {
override init() {
super.init()
// 樣式
nameLabel.padding = UIEdgeInsets(top: 10, left: 14, bottom: 0, right: -14)
nameLabel.textColor = UIColor.red
nameLabel.font = UIFont.systemFont(ofSize: 17)
introLabel.padding.top = 10
introLabel.numberOfLines = 0
introLabel.textColor = UIColor.purple
introLabel.font = UIFont.systemFont(ofSize: 15)
introLabel.lineBreakMode = .byCharWrapping
introLabel.lineSpacing = 4
subscribeButton.padding.top = 10
subscribeButton.size.height = 30
subscribeButton.image.normal = UIImage(named: "subscribe")
subscribeButton.image.selected = UIImage(named: "subscribed")
subscribeButton.title.normal = "訂閱"
subscribeButton.title.selected = "已訂"
subscribeButton.titleColor.normal = UIColor.black
subscribeButton.titleColor.selected = UIColor.yellow
subscribeButton.titleFont = UIFont.systemFont(ofSize: 12)
imageView.padding.top = 20
imageView.size.width = 60
imageView.contentMode = .scaleAspectFill
imageView.borderColor = UIColor.red
imageView.borderWidth = 3
imageView.clipsToBounds = true
imageView.cornerRadius = imageView.size.width * 0.5
}
}
在視圖中配置大概這樣:
import UIKit
import SnapKit
class TestView: UIView, ViewConfigurable {
fileprivate var nameLabel: UILabel!
fileprivate var introLabel: UILabel!
fileprivate var subscribeButton: UIButton!
fileprivate var imageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSubviews()
}
fileprivate func setupSubviews() {
nameLabel = UILabel(frame: self.bounds)
self.addSubview(nameLabel)
introLabel = UILabel(frame: self.bounds)
self.addSubview(introLabel)
subscribeButton = UIButton(type: .custom)
self.addSubview(subscribeButton)
imageView = UIImageView(frame: self.bounds)
self.addSubview(imageView)
}
/// 更新視圖樣式捧搞,不要直接調(diào)用抵卫,通過賦值 self.viewStyle 屬性間接調(diào)用
func bind(viewStyle: TestViewStyle) {
/* 對外可配置屬性 */
// 名字
nameLabel.textColor = viewStyle.nameLabel.textColor
nameLabel.font = viewStyle.nameLabel.font
// 介紹
introLabel.numberOfLines = viewStyle.introLabel.numberOfLines
if let text = introLabel.text {
introLabel.attributedText = NSAttributedString(string: text, attributes: viewStyle.introLabel.attributes)
}
// 訂閱按鈕
subscribeButton.setTitleColor(viewStyle.subscribeButton.titleColor.normal, for: .normal)
subscribeButton.setTitleColor(viewStyle.subscribeButton.titleColor.selected, for: .selected)
subscribeButton.setImage(viewStyle.subscribeButton.image.normal, for: .normal)
subscribeButton.setImage(viewStyle.subscribeButton.image.selected, for: .selected)
subscribeButton.setTitle(viewStyle.subscribeButton.title.normal, for: .normal)
subscribeButton.setTitle(viewStyle.subscribeButton.title.selected, for: .selected)
subscribeButton.titleLabel?.font = viewStyle.subscribeButton.titleFont
// 頭像
imageView.layer.borderColor = viewStyle.imageView.borderColor.cgColor
imageView.layer.borderWidth = viewStyle.imageView.borderWidth
imageView.layer.cornerRadius = viewStyle.imageView.cornerRadius
imageView.clipsToBounds = viewStyle.imageView.clipsToBounds
imageView.contentMode = viewStyle.imageView.contentMode
// 更新視圖布局,不同布局約束關(guān)系直接切換
if let viewStyle1 = viewStyle as? TestViewStyle1 {
updateLayoutForStyle1(viewStyle1)
} else if let viewStyle2 = viewStyle as? TestViewStyle2 {
updateLayoutForStyle2(viewStyle2)
}
}
fileprivate func updateLayoutForStyle1(_ viewStyle: TestViewStyle1) {
imageView.snp.remakeConstraints { (make) in
make.left.equalTo(self.snp.left).offset(viewStyle.imageView.padding.left)
make.top.equalTo(self.snp.top).offset(viewStyle.imageView.padding.top)
make.width.equalTo(viewStyle.imageView.size.width)
make.height.equalTo(self.imageView.snp.width)
}
nameLabel.snp.remakeConstraints { (make) in
make.top.equalTo(self.imageView.snp.top)
make.left.equalTo(self.imageView.snp.right).offset(viewStyle.nameLabel.padding.left)
make.right.equalTo(self.snp.right).offset(viewStyle.nameLabel.padding.right)
}
introLabel.snp.remakeConstraints { (make) in
make.top.equalTo(self.nameLabel.snp.bottom).offset(viewStyle.introLabel.padding.top)
make.left.equalTo(self.nameLabel.snp.left)
make.right.equalTo(self.nameLabel.snp.right)
}
subscribeButton.snp.remakeConstraints { (make) in
make.top.equalTo(self.imageView.snp.bottom).offset(viewStyle.subscribeButton.padding.top)
make.left.equalTo(self.imageView.snp.left)
make.right.equalTo(self.imageView.snp.right)
make.height.equalTo(viewStyle.subscribeButton.size.height)
}
}
fileprivate func updateLayoutForStyle2(_ viewStyle: TestViewStyle2) {
imageView.snp.remakeConstraints { (make) in
make.centerX.equalTo(self.snp.centerX)
make.top.equalTo(self.snp.top).offset(viewStyle.imageView.padding.top)
make.width.equalTo(viewStyle.imageView.size.width)
make.height.equalTo(self.imageView.snp.width)
}
subscribeButton.snp.remakeConstraints { (make) in
make.left.equalTo(self.imageView.snp.left)
make.right.equalTo(self.imageView.snp.right)
make.centerX.equalTo(self.imageView.snp.centerX)
make.top.equalTo(self.imageView.snp.bottom).offset(viewStyle.subscribeButton.padding.top)
make.height.equalTo(viewStyle.subscribeButton.size.height)
}
nameLabel.snp.remakeConstraints { (make) in
make.top.equalTo(self.subscribeButton.snp.bottom).offset(viewStyle.nameLabel.padding.top)
make.left.equalTo(self.snp.left).offset(viewStyle.nameLabel.padding.left)
make.right.equalTo(self.snp.right).offset(viewStyle.nameLabel.padding.right)
}
introLabel.snp.remakeConstraints { (make) in
make.top.equalTo(self.nameLabel.snp.bottom).offset(viewStyle.introLabel.padding.top)
make.left.equalTo(self.nameLabel.snp.left)
make.right.equalTo(self.nameLabel.snp.right)
}
}
}
外面使用起來就很簡單胎撇,切換不同布局快捷方便:
class ViewController: UIViewController {
fileprivate var testView: TestView!
override func viewDidLoad() {
super.viewDidLoad()
// 初始化
testView = TestView(frame: CGRect(x: 0, y: 100, width: self.view.frame.size.width, height: 200))
// 配置樣式
testView.viewStyle = TestViewStyle1()
self.view.addSubview(testView)
// 更換樣式配置
testView.viewStyle = TestViewStyle2()
}
}
Demo 源代碼在這:ViewStyleProtocolDemo
有什么問題可以在下方評論區(qū)提出介粘,寫得不好可以提出你的意見,我會合理采納的晚树,O(∩_∩)O哈哈~姻采,求關(guān)注求贊