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" } }