260 lines
8.5 KiB
Swift
260 lines
8.5 KiB
Swift
//
|
|
// 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 {
|
|
heightAnchor.constraint(equalToConstant: height).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() }
|
|
}
|
|
}
|
|
}
|