Swift:UIButton+Extension

JKSwiftExtension,测试用例在 UIButtonExtensionViewController.swift 里面
目录:
1、基本的扩展
2、链式调用
3、UIButton 图片 与 title 位置关系(提示:title和image要在设置布局关系之前设置)
4、自带倒计时功能的 Button

一、基本的扩展
// MARK:- 一、基本的扩展
public extension UIButton {
    enum SmallButtonType {
        case red
        case pink
    }
    
    // MARK: 1.1、创建一个带颜色的 Button
    /// 创建一个带颜色的 Button
    /// - Parameters:
    ///   - type: 类型
    ///   - height: 高度
    /// - Returns: 返回自身
    @discardableResult
    static func small(type: SmallButtonType = .red, height: CGFloat = 45) -> UIButton {
        let normalColor: UIColor
        let disabledColor: UIColor
        let lineTypeNormal: LineType
        let lineTypeDisable: LineType
        let titleColorNormal: UIColor
        let titleColorDisable: UIColor
    
        switch type {
        case .red:
            normalColor = .hexStringColor(hexString: "#E54749")
            disabledColor = .hexStringColor(hexString: "#CCCCCC")
            lineTypeNormal = .none
            lineTypeDisable = .none
            titleColorNormal = .white
            titleColorDisable = .white
        case .pink:
            normalColor = .hexStringColor(hexString: "#FFE8E8")
            disabledColor = .hexStringColor(hexString: "#CCCCCC")
            lineTypeNormal = .color(.hexStringColor(hexString: "#F6CDCD"))
            lineTypeDisable = .color(.hexStringColor(hexString: "#9C9C9C"))
            titleColorNormal = .hexStringColor(hexString: "#E54749")
            titleColorDisable = .white
        }
    
        let btn = UIButton(type: .custom).font(.systemFont(ofSize: 16))
        btn.setTitleColor(titleColorNormal, for: .normal)
        btn.setTitleColor(titleColorDisable, for: .disabled)
        btn.setBackgroundImage(drawSmallBtn(color: normalColor, height: height, lineType: lineTypeNormal), for: .normal)
        btn.setBackgroundImage(drawSmallBtn(color: disabledColor, height: height, lineType: lineTypeDisable), for: .disabled)
        btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 13, right: 0)
        return btn
    }

    // MARK: 1.2、创建一个常规的 Button
    /// 创建一个常规的 Button
    /// - Returns: 返回自身
    static func normal() -> UIButton {
        let btn = UIButton(type: .custom).font(.boldSystemFont(ofSize: 18))
        btn.setTitleColor(.white, for: .normal)
        btn.setTitleColor(.white, for: .disabled)
        btn.setBackgroundImage(drawNormalBtn(color: .hexStringColor(hexString: "#E54749"))?.resizableImage(withCapInsets: UIEdgeInsets(top: 10, left: 15, bottom: 15, right: 15)), for: .normal)
        btn.setBackgroundImage(drawNormalBtn(color: .hexStringColor(hexString: "#CCCCCC"))?.resizableImage(withCapInsets: UIEdgeInsets(top: 10, left: 15, bottom: 15, right: 15)), for: .disabled)
        btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 4, right: 0)
        return btn
    }

    private static func drawSmallBtn(color: UIColor, height: CGFloat, lineType: LineType) -> UIImage? {
        let rect = CGRect(x: 0, y: 0, width: 200, height: height + 20)
        let path = UIBezierPath(roundedRect: CGRect(x: 10, y: 3, width: 180, height: height), cornerRadius: height / 2)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
        let context = UIGraphicsGetCurrentContext()
        context?.setFillColor(color.cgColor)
        context?.addPath(path.cgPath)
        context?.setShadow(offset: CGSize(width: 1, height: 4), blur: 10, color: color.withAlphaComponent(0.5).cgColor)
        context?.fillPath()
        switch lineType {
        case let .color(color):
            color.setStroke()
            path.lineWidth = kPixel
            path.stroke()
        default:
            break
        }
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img
    }

    private static func drawNormalBtn(color: UIColor) -> UIImage? {
        let rect = CGRect(x: 0, y: 0, width: 260, height: 50)
        let path = UIBezierPath(roundedRect: CGRect(x: 10, y: 3, width: 240, height: 40), cornerRadius: 3)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
        let context = UIGraphicsGetCurrentContext()
        context?.setFillColor(color.cgColor)
        context?.addPath(path.cgPath)
        context?.setShadow(offset: CGSize(width: 1, height: 2), blur: 6, color: color.withAlphaComponent(0.5).cgColor)
        context?.fillPath()
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img
    }
}
二、链式调用
// MARK:- 二、链式调用
public extension UIButton {

    // MARK: 2.1、设置title
    /// 设置title
    /// - Parameters:
    ///   - text: 文字
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func title(_ text: String, _ state: UIControl.State = .normal) -> Self {
        setTitle(text, for: state)
        return self
    }

    // MARK: 2.2、设置文字颜色
    /// 设置文字颜色
    /// - Parameters:
    ///   - color: 文字颜色
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func textColor(_ color: UIColor, _ state: UIControl.State = .normal) -> Self {
        setTitleColor(color, for: state)
        return self
    }

    // MARK: 2.3、设置字体大小(UIFont)
    /// 设置字体大小
    /// - Parameter font: 字体 UIFont
    /// - Returns: 返回自身
    @discardableResult
    func font(_ font: UIFont) -> Self {
        titleLabel?.font = font
        return self
    }

    // MARK: 2.4、设置字体大小(CGFloat)
    /// 设置字体大小(CGFloat)
    /// - Parameter fontSize: 字体的大小
    /// - Returns: 返回自身
    @discardableResult
    func font(_ fontSize: CGFloat) -> Self {
        titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
        return self
    }

    // MARK: 2.5、设置字体粗体
    /// 设置粗体
    /// - Parameter fontSize: 设置字体粗体
    /// - Returns: 返回自身
    @discardableResult
    func boldFont(_ fontSize: CGFloat) -> Self {
        titleLabel?.font = UIFont.boldSystemFont(ofSize: fontSize)
        return self
    }

    // MARK: 2.6、设置图片
    /// 设置图片
    /// - Parameters:
    ///   - image: 图片
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func image(_ image: UIImage?, _ state: UIControl.State = .normal) -> Self {
        setImage(image, for: state)
        return self
    }

    // MARK: 2.7、设置图片(通过Bundle加载)
    /// 设置图片(通过Bundle加载)
    /// - Parameters:
    ///   - bundle: Bundle
    ///   - imageName: 图片名字
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func image(in bundle: Bundle? = nil, _ imageName: String, _ state: UIControl.State = .normal) -> Self {
        let image = UIImage(named: imageName, in: bundle, compatibleWith: nil)
        setImage(image, for: state)
        return self
    }

    // MARK: 2.8、设置图片(通过Bundle加载)
    /// 设置图片(通过Bundle加载)
    /// - Parameters:
    ///   - aClass: className bundle所在的类的类名
    ///   - bundleName: bundle 的名字
    ///   - imageName: 图片的名字
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func image(forParent aClass: AnyClass, bundleName: String, _ imageName: String, _ state: UIControl.State = .normal) -> Self {
        guard let path = Bundle(for: aClass).path(forResource: bundleName, ofType: "bundle") else {
            return self
        }
        let image = UIImage(named: imageName, in: Bundle(path: path), compatibleWith: nil)
        setImage(image, for: state)
        return self
    }

    // MARK: 2.9、设置图片(纯颜色的图片)
    /// 设置图片(纯颜色的图片)
    /// - Parameters:
    ///   - color: 图片颜色
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func image(_ color: UIColor, _ size: CGSize = CGSize(width: 20.0, height: 20.0), _ state: UIControl.State = .normal) -> Self {
        let image = UIImage.image(color: color, size: size)
        setImage(image, for: state)
        return self
    }

    // MARK: 2.10、设置背景图片
    /// 设置背景图片
    /// - Parameters:
    ///   - image: 图片
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func bgImage(_ image: UIImage?, _ state: UIControl.State = .normal) -> Self {
        setBackgroundImage(image, for: state)
        return self
    }

    // MARK: 2.11、设置背景图片(通过Bundle加载)
    /// 设置背景图片(通过Bundle加载)
    /// - Parameters:
    ///   - aClass: className bundle所在的类的类名
    ///   - bundleName: bundle 的名字
    ///   - imageName: 图片的名字
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func bgImage(forParent aClass: AnyClass, bundleName: String, _ imageName: String, _: UIControl.State = .normal) -> Self {
        guard let path = Bundle(for: aClass).path(forResource: bundleName, ofType: "bundle") else {
            return self
        }
        let image = UIImage(named: imageName, in: Bundle(path: path), compatibleWith: nil)
        setBackgroundImage(image, for: state)
        return self
    }

    // MARK: 2.12、设置背景图片(通过Bundle加载)
    /// 设置背景图片(通过Bundle加载)
    /// - Parameters:
    ///   - bundle: Bundle
    ///   - imageName: 图片的名字
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func bgImage(in bundle: Bundle? = nil, _ imageName: String, _ state: UIControl.State = .normal) -> Self {
        let image = UIImage(named: imageName, in: bundle, compatibleWith: nil)
        setBackgroundImage(image, for: state)
        return self
    }

    // MARK: 2.13、设置背景图片(纯颜色的图片)
    /// 设置背景图片(纯颜色的图片)
    /// - Parameters:
    ///   - color: 背景色
    ///   - state: 状态
    /// - Returns: 返回自身
    @discardableResult
    func bgImage(_ color: UIColor, _ state: UIControl.State = .normal) -> Self {
        let image = UIImage.image(color: color)
        setBackgroundImage(image, for: state)
        return self
    }

    // MARK: 2.14、按钮点击的变化
    /// 按钮点击的变化
    /// - Returns: 返回自身
    @discardableResult
    func confirmButton() -> Self {
        let normalImage = UIImage.image(color: UIColor.hexStringColor(hexString: "#E54749"), size: CGSize(width: 10, height: 10), round: 4)?.resizableImage(withCapInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
        let disableImg = UIImage.image(color: UIColor.hexStringColor(hexString: "#E6E6E6"), size: CGSize(width: 10, height: 10), round: 4)?.resizableImage(withCapInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
        setBackgroundImage(normalImage, for: .normal)
        setBackgroundImage(disableImg, for: .disabled)
        return self
    }
}
三、UIButton 图片 与 title 位置关系(提示:title和image要在设置布局关系之前设置)
// MARK:- 三、UIButton 图片 与 title 位置关系
/// UIButton 图片与title位置关系 https://www.jianshu.com/p/0f34c1b52604
public extension UIButton {

    /// 图片 和 title 的布局样式
    enum ImageTitleLayout {
        case imgTop
        case imgBottom
        case imgLeft
        case imgRight
    }

    // MARK: 3.1、设置图片和 title 的位置关系(提示:title和image要在设置布局关系之前设置)
    /// 设置图片和 title 的位置关系(提示:title和image要在设置布局关系之前设置)
    /// - Parameters:
    ///   - layout: 布局
    ///   - spacing: 间距
    /// - Returns: 返回自身
    @discardableResult
    func setImageTitleLayout(_ layout: ImageTitleLayout, spacing: CGFloat = 0) -> Self {
        switch layout {
        case .imgLeft:
            alignHorizontal(spacing: spacing, imageFirst: true)
        case .imgRight:
            alignHorizontal(spacing: spacing, imageFirst: false)
        case .imgTop:
            alignVertical(spacing: spacing, imageTop: true)
        case .imgBottom:
            alignVertical(spacing: spacing, imageTop: false)
        }
        return self
    }

    /// 水平方向
    /// - Parameters:
    ///   - spacing: 间距
    ///   - imageFirst: 图片是否优先
    private func alignHorizontal(spacing: CGFloat, imageFirst: Bool) {
        let edgeOffset = spacing / 2
        imageEdgeInsets = UIEdgeInsets(top: 0,
                                       left: -edgeOffset,
                                       bottom: 0,
                                       right: edgeOffset)
        titleEdgeInsets = UIEdgeInsets(top: 0,
                                       left: edgeOffset,
                                       bottom: 0,
                                       right: -edgeOffset)
        if !imageFirst {
            self.transform = CGAffineTransform(scaleX: -1, y: 1)
            imageView?.transform = CGAffineTransform(scaleX: -1, y: 1)
            titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1)
        }
        contentEdgeInsets = UIEdgeInsets(top: 0, left: edgeOffset, bottom: 0, right: edgeOffset)
    }

    /// 垂直方向
    /// - Parameters:
    ///   - spacing: 间距
    ///   - imageTop: 图片是不是在顶部
    private func alignVertical(spacing: CGFloat, imageTop: Bool) {
        guard let imageSize = self.imageView?.image?.size,
            let text = self.titleLabel?.text,
            let font = self.titleLabel?.font
            else {
                return
        }
        let labelString = NSString(string: text)
        let titleSize = labelString.size(withAttributes: [NSAttributedString.Key.font: font])
    
        let imageVerticalOffset = (titleSize.height + spacing) / 2
        let titleVerticalOffset = (imageSize.height + spacing) / 2
        let imageHorizontalOffset = (titleSize.width) / 2
        let titleHorizontalOffset = (imageSize.width) / 2
        let sign: CGFloat = imageTop ? 1 : -1
    
        imageEdgeInsets = UIEdgeInsets(top: -imageVerticalOffset * sign,
                                       left: imageHorizontalOffset,
                                       bottom: imageVerticalOffset * sign,
                                       right: -imageHorizontalOffset)
        titleEdgeInsets = UIEdgeInsets(top: titleVerticalOffset * sign,
                                       left: -titleHorizontalOffset,
                                       bottom: -titleVerticalOffset * sign,
                                       right: titleHorizontalOffset)
        // increase content height to avoid clipping
        let edgeOffset = (min(imageSize.height, titleSize.height) + spacing)/2
        contentEdgeInsets = UIEdgeInsets(top: edgeOffset, left: 0, bottom: edgeOffset, right: 0)
    }
}
四、自带倒计时功能的 Button"
// MARK:- 四、自带倒计时功能的 Button(有待改进)
/// 自带倒计时功能的Button
/// - 状态分为 [倒计时中,倒计时完成],分别提供回调
/// - 需要和业务结合时,后期再考虑
public extension UIButton {

    // MARK: 4.1、设置 Button 倒计时
    /// 设置 Button 倒计时
    /// - Parameters:
    ///   - count: 最初的倒计时数字
    ///   - timering: 倒计时中的 Block
    ///   - complete: 倒计时完成的 Block
    ///   - timeringPrefix: 倒计时文字的:前缀
    ///   - completeText: 倒计时完成后的文字
    func countDown(_ count: Int, timering: TimeringBlock? = nil, complete: CompletionBlock? = nil, timeringPrefix: String = "再次获取", completeText: String = "重新获取") {
        isEnabled = false
        let begin = ProcessInfo().systemUptime
        let c_default = UIColor.hexStringColor(hexString: "#2798fd")
        let c_default_disable = UIColor.hexStringColor(hexString: "#999999")
    
        self.textColor(titleColor(for: .normal) ?? c_default)
        self.textColor(titleColor(for: .disabled) ?? c_default_disable, .disabled)
        var remainingCount: Int = count {
            willSet {
                setTitle(timeringPrefix + "(\(newValue)s)", for: .normal)
                if newValue <= 0 {
                    setTitle(completeText, for: .normal)
                }
            }
        }
        self.invalidate()
        self.timer = DispatchSource.makeTimerSource(queue:DispatchQueue.global())
        self.timer?.schedule(deadline: .now(), repeating: .seconds(1))
        self.isTiming = true
        self.timer?.setEventHandler(handler: {
            DispatchQueue.main.async {
                remainingCount = count - Int(ProcessInfo().systemUptime - begin)
                if remainingCount <= 0 {
                    if let cb = complete {
                        cb()
                    }
                    // 计时结束后,enable的条件
                    self.isEnabled = self.reEnableCond ?? true
                    self.isTiming = false
                    self.invalidate()
                } else {
                    if let tb = timering {
                        tb(remainingCount)
                    }
                }
            }
        })
        self.timer?.resume()
    }

    // MARK: 4.2、是否可以点击
    /// 是否可以点击
    var reEnableCond: Bool? {
        get {
            if let value = objc_getAssociatedObject(self, &TimerKey.reEnableCond_key) {
                return value as? Bool
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &TimerKey.reEnableCond_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    // MARK: 4.3、是否正在倒计时
    /// 是否正在倒计时
    var isTiming: Bool {
        get {
            if let value = objc_getAssociatedObject(self, &TimerKey.running_key) {
                return value as! Bool
            }
            // 默认状态
            return false
        }
        set {
            objc_setAssociatedObject(self, &TimerKey.running_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    // MARK: 4.3、处于倒计时时,前缀文案,如:「再次获取」 + (xxxs)
    /// 处于倒计时时,前缀文案,如:「再次获取」 + (xxxs)
    var timeringPrefix: String? {
        get {
            if let value = objc_getAssociatedObject(self, &TimerKey.timeringPrefix_key) {
                return value as? String
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &TimerKey.timeringPrefix_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    // MARK: 销毁定时器
    /// 销毁定时器
    func invalidate() {
        if self.timer != nil {
            self.timer?.cancel()
            self.timer = nil
        }
    }

    // MARK: 时间对象
    /// 时间对象
    var timer: DispatchSourceTimer? {
        get {
            if let value = objc_getAssociatedObject(self, &TimerKey.timer_key) {
                return value as? DispatchSourceTimer
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &TimerKey.timer_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    typealias TimeringBlock = (Int) -> ()
    typealias CompletionBlock = () -> ()
    private struct TimerKey {
        static var timer_key = "timer_key"
        static var running_key = "running_key"
        static var timeringPrefix_key = "timering_prefix_key"
        static var reEnableCond_key = "re_enable_cond_key"
   }
}

推荐阅读更多精彩内容