Swift 項目總結(jié) 07 - 視圖樣式可配置化

需求由來

在項目開發(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 的 decodeencode 代碼一樣,都是必須但又是無腦的(體力活)睡腿,我就想搞個東西方便配置視圖樣式语御,從這個過程中解脫出來

方案思考

全局配置樣式

通過全局變量進行配置(之前的做法):

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)注求贊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市爵憎,隨后出現(xiàn)的幾起案子慨亲,更是在濱河造成了極大的恐慌婚瓜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刑棵,死亡現(xiàn)場離奇詭異巴刻,居然都是意外死亡,警方通過查閱死者的電腦和手機蛉签,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門胡陪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碍舍,你說我怎么就攤上這事柠座。” “怎么了片橡?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵妈经,是天一觀的道長。 經(jīng)常有香客問我捧书,道長吹泡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任鳄厌,我火速辦了婚禮荞胡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘了嚎。我一直安慰自己泪漂,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布歪泳。 她就那樣靜靜地躺著萝勤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呐伞。 梳的紋絲不亂的頭發(fā)上敌卓,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音伶氢,去河邊找鬼趟径。 笑死,一個胖子當著我的面吹牛癣防,可吹牛的內(nèi)容都是我干的蜗巧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蕾盯,長吁一口氣:“原來是場噩夢啊……” “哼幕屹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤望拖,失蹤者是張志新(化名)和其女友劉穎渺尘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體说敏,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡鸥跟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了像云。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锌雀。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖迅诬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婿牍,我是刑警寧澤侈贷,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站等脂,受9級特大地震影響俏蛮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜上遥,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一搏屑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粉楚,春花似錦辣恋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至燃异,卻和暖如春携狭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背回俐。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工逛腿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仅颇。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓单默,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灵莲。 傳聞我的和親對象是個殘疾皇子雕凹,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容