// // CommonButtonAction.swift // List // // Created by Igor Danich on 20.07.2020. // Copyright © 2020 Igor Danich. All rights reserved. // import UIKit extension UIButton { enum Style: String { case primary case secondary case tertiary case outline } enum Size: CGFloat { case small = 28 case large = 48 } } @IBDesignable class CommonButtonAction: UIButton, CommonMenuSender { private let indicator = UIActivityIndicatorView(style: .large) @IBInspectable var styleName: String = UIButton.Style.primary.rawValue { didSet { update() } } @IBInspectable var image: UIImage? { didSet { update() } } var style: Style { get { Style(rawValue: self.styleName) ?? .primary } set { self.styleName = newValue.rawValue self.update() } } override var lzTitle: String? { didSet { update(force: true) } } var cornerRadius: CGFloat = 10 { didSet { update() } } var action: CommonMenuAction? { didSet { update(animated: false) } } init(frame: CGRect = .zero, action: CommonMenuAction? = nil, height: CGFloat? = 0, isEnabled: Bool = true) { super.init(frame: frame) self.action = action self.isEnabled = isEnabled if let height = height { let viewHeightConstraint = heightAnchor.constraint(equalToConstant: height) viewHeightConstraint.priority = .defaultHigh viewHeightConstraint.isActive = true } addTarget(self, action: #selector(onTap), for: .touchUpInside) if action == nil { update() } else { update(animated: false) } } override init(frame: CGRect) { super.init(frame: frame) addTarget(self, action: #selector(onTap), for: .touchUpInside) update() } required init?(coder: NSCoder) { super.init(coder: coder) addTarget(self, action: #selector(onTap), for: .touchUpInside) update() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() update() } var isLoading: Bool = false { didSet { update() } } private var lastSize: CGSize? func update(animated: Bool) { update(force: true) } private func update(force: Bool = true) { var title = (action?.menu?.title ?? lzTitle)?.localized let image = action?.menu?.image ?? self.image if !force, lastSize == frame.size { return } if contentEdgeInsets == .zero { contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12) } isUserInteractionEnabled = !isLoading borderColor = _border().color layer.borderWidth = _border().width layer.cornerRadius = cornerRadius layer.masksToBounds = true lastSize = frame.size setTitle(nil, for: .normal) setImage(isLoading ? nil : image, for: .normal) if !isLoading, image != nil { titleEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: 0) imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: (title?.isEmpty ?? true) ? 0 : 4) if !(title?.isEmpty ?? true) { title = "\(title ?? "") " } } else { titleEdgeInsets = .zero imageEdgeInsets = .zero } let states: [UIControl.State] = [.normal, .highlighted, .selected, .disabled] states.forEach({ setTitle(nil, for: $0) }) if isLoading { indicator.startAnimating() indicator.center = .init(x: frame.width/2, y: frame.height/2) states.forEach({ setAttributedTitle(nil, for: $0) }) } else { indicator.stopAnimating() states.forEach({ setAttributedTitle(title?.attributed(style: .medium, size: 14, color: _color(for: $0)), for: $0) }) } states.forEach({ self.backgroundColor = _background(for: $0) }) layoutIfNeeded() } override var isEnabled: Bool { willSet { self.alpha = newValue ? 1 : 0.2 } } @objc private func onTap() { action?.perform(sender: self) } func setAttributedTitle(_ title: AttributedString) { let states: [UIControl.State] = [.normal, .highlighted, .selected, .disabled] states.forEach({ self.setAttributedTitle(title, for: $0) }) } } extension CommonButtonAction { fileprivate func _background(for state: State) -> UIColor? { let styles = ([ .primary: [ .normal: Asset.deepWater.color, .highlighted: Asset.granite.color, .selected: Asset.granite.color, .disabled: Asset.deepWater.color ], .secondary: [ .normal: Asset.fog.color, .highlighted: Asset.pebble.color, .selected: Asset.pebble.color, .disabled: Asset.fog.color ], .tertiary: [ .highlighted: Asset.marble.color, .selected: Asset.marble.color ], .outline: [ .highlighted: Asset.deepWater.color, .selected: Asset.deepWater.color ] ] as [Style: [State: UIColor]]) return styles[self.style]?[state] } fileprivate func _image(for state: State) -> UIImage? { ([ .primary: [ .normal: Asset.deepWater.color, .highlighted: Asset.granite.color, .selected: Asset.granite.color, .disabled: Asset.deepWater.color.withAlphaComponent(0.2) ], .secondary: [ .normal: Asset.fog.color, .highlighted: Asset.pebble.color, .selected: Asset.pebble.color, .disabled: Asset.fog.color ], .tertiary: [ .highlighted: Asset.marble.color, .selected: Asset.marble.color ], .outline: [ .highlighted: Asset.deepWater.color.withAlphaComponent(0.1), .selected: Asset.deepWater.color.withAlphaComponent(0.1) ] ] as [Style: [State: UIColor]])[style]?[state].map({ .colored($0, size: frame.size, cornerRadius: cornerRadius) }) } fileprivate func _color(for state: State) -> UIColor { let color: [Style: [State: UIColor]] = [ .primary: [ .normal: Asset.textSnow.color, .highlighted: Asset.textSnow.color, .selected: Asset.textSnow.color, .disabled: Asset.textSnow.color ], .secondary: [ .normal: Asset.textGranite.color, .highlighted: Asset.textGranite.color, .selected: Asset.textGranite.color, .disabled: Asset.textGranite.color.withAlphaComponent(0.3) ], .tertiary: [ .normal: Asset.deepWater.color, .highlighted: Asset.deepWater.color, .selected: Asset.deepWater.color, .disabled: Asset.deepWater.color.withAlphaComponent(0.4) ], .outline: [ .normal: Asset.textDeepWater.color, .highlighted: Asset.textDeepWater.color, .selected: Asset.textDeepWater.color, .disabled: Asset.textDeepWater.color.withAlphaComponent(0.4) ] ] return color[style]?[state] ?? .clear } fileprivate func _border() -> (width: CGFloat, color: UIColor?) { [Style.outline: (1, Asset.deepWater.color.withAlphaComponent(0.2))][style] ?? (0, nil) } } extension UIControl.State: Hashable {} extension UIButton { @IBInspectable var borderColor: UIColor? { get { UIColor(cgColor: layer.borderColor ?? UIColor.clear.cgColor) } set { layer.borderColor = newValue?.cgColor } } } extension UIView { static func button( style: UIButton.Style = .primary, title: String? = nil, image: UIImage? = nil, size: UIButton.Size = .large, isEnabled: Bool = true, action: @escaping () -> Void ) -> CommonButtonAction { .view(frame: .init(x: 0, y: 0, width: 1, height: size.rawValue)) { $0.style = style $0.lzTitle = title $0.image = image $0.isEnabled = isEnabled let heightConstraint = $0.heightAnchor.constraint(equalToConstant: size.rawValue) heightConstraint.priority = .defaultHigh heightConstraint.isActive = true $0.action = .action() { _ in action() } } } }