美文网首页iOS开发iOS干货iOS Developer
Swift 项目总结 07 - 视图样式可配置化

Swift 项目总结 07 - 视图样式可配置化

作者: 执着丶执念 | 来源:发表于2018-04-30 14:36 被阅读51次

    需求由来

    在项目开发过程中,设计师调整设计稿是正常的,但如果调整频率一高,就让我们开发十分抓狂。

    我们来进行一个情景模拟(以 AutoLayout 为例):

    设计师:这个左边距调多 2 px,这个上边距调少 2 px,这 2 个 view 之间间距调大点,多 2 px 吧,这个文本字体调大一号。

    开发:好的,我马上调。(我一顿操作,调整约束值,...)

    ======== 过了 1 天 ==========

    设计师:这个样式有点问题,整体样式我重新设计了一下,你调一下(给了我最新的设计稿)

    开发:这个样式调整有点大啊,各种约束都不一样了,你确定要改吗?

    设计师:确定。(我一顿操作,删除旧约束代码,添加新约束代码,...)

    ======== 又过了 1 天 ==========

    设计师:这个样式,老板看后和之前对比,觉得还是之前样式好,你换回来吧。

    开发:.......

    还有一种情况,一个视图在不同地方显示的布局样式是不一样的,这种视图样式配置是非常繁琐的,就像我们使用 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) {
            // 样式配置代码
        }
    }
    

    属性配置样式

    初始化配置样式在大部分情况下已经满足需求了,但因为初始化方法有很多,尤其是使用 xib 加载的时候,不好处理。

    因为我那段时间正在学习 RxSwift + ReactorKit 框架使用,发现 ReactorKit 框架中 Reactor 协议抽离视图内的业务逻辑处理非常巧妙,让每个视图绑定各自的处理器处理业务逻辑,我就想视图的配置不是也可以和 Reactor 协议一样,每个视图都绑定一个视图样式配置

    // MARK: - 视图可配置协议
    public protocol ViewConfigurable: class {
        associatedtype ViewStyle
        var viewStyle: ViewStyle? { get set }
        func bind(viewStyle: ViewStyle)
    }
    
    /// 为实现该协议的类添加一个伪存储属性(利用 objc 的关联方法实现),用来保存样式配置表
    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) {
            // 样式配置代码
        }
    }
    

    最终方案

    我构造了一些常用视图配置项来辅助样式配置,可自己看情况自定义配置项:

    // 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)
        }
        
        /// 更新视图样式,不要直接调用,通过赋值 self.viewStyle 属性间接调用
        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
            
            // 更新视图布局,不同布局约束关系直接切换
            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

    有什么问题可以在下方评论区提出,写得不好可以提出你的意见,我会合理采纳的,O(∩_∩)O哈哈~,求关注求赞

    相关文章

      网友评论

        本文标题:Swift 项目总结 07 - 视图样式可配置化

        本文链接:https://www.haomeiwen.com/subject/abjzlftx.html