389 lines
14 KiB
Swift
389 lines
14 KiB
Swift
//
|
|
// ButtonIcon.swift
|
|
// VDS
|
|
//
|
|
// Created by Matt Bruce on 5/12/23.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSColorTokens
|
|
|
|
@objc(VDSButtonIcon)
|
|
open class ButtonIcon: Control {
|
|
//--------------------------------------------------
|
|
// MARK: - Models
|
|
//--------------------------------------------------
|
|
//--------------------------------------------------
|
|
// MARK: - Enums
|
|
//--------------------------------------------------
|
|
public enum Kind: String, CaseIterable {
|
|
case ghost, lowContrast, highContrast
|
|
}
|
|
|
|
public enum SurfaceType: String, CaseIterable {
|
|
case colorFill, media
|
|
}
|
|
|
|
public enum Size: String, EnumSubset {
|
|
case large
|
|
case small
|
|
|
|
public var defaultValue: Icon.Size { .large }
|
|
|
|
public var containerSize: CGFloat {
|
|
switch self {
|
|
case .large:
|
|
return 44.0
|
|
case .small:
|
|
return 32.0
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Properties
|
|
//--------------------------------------------------
|
|
private var centerXConstraint: NSLayoutConstraint?
|
|
private var centerYConstraint: NSLayoutConstraint?
|
|
private var layoutGuideWidthConstraint: NSLayoutConstraint?
|
|
private var layoutGuideHeightConstraint: NSLayoutConstraint?
|
|
private var currentIconName: Icon.Name? {
|
|
if let selectedIconName {
|
|
return selectedIconName
|
|
} else {
|
|
return iconName
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Properties
|
|
//--------------------------------------------------
|
|
open var icon = Icon()
|
|
|
|
open var kind: Kind = .ghost { didSet { setNeedsUpdate() } }
|
|
open var surfaceType: SurfaceType = .colorFill { didSet { setNeedsUpdate() } }
|
|
open var iconName: Icon.Name? { didSet { setNeedsUpdate() } }
|
|
open var selectedIconName: Icon.Name? { didSet { setNeedsUpdate() } }
|
|
open var size: Size = .large { didSet { setNeedsUpdate() } }
|
|
open var customSize: Int? { didSet { setNeedsUpdate() }}
|
|
open var floating: Bool = false { didSet { setNeedsUpdate() } }
|
|
open var fitToIcon: Bool = false { didSet { setNeedsUpdate() } }
|
|
open var hideBorder: Bool = true { didSet { setNeedsUpdate() } }
|
|
open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } }
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Configuration
|
|
//--------------------------------------------------
|
|
private var iconColorConfiguration: AnyColorable {
|
|
if selectedIconName != nil {
|
|
return selectedIconColorConfiguration
|
|
} else {
|
|
if kind == .highContrast {
|
|
return highContrastIconColorConfiguration
|
|
} else {
|
|
return standardIconColorConfiguration
|
|
}
|
|
}
|
|
}
|
|
|
|
private var currentConfiguration: any Configuration {
|
|
switch kind {
|
|
case .ghost:
|
|
return GhostConfiguration()
|
|
|
|
case .lowContrast:
|
|
if surfaceType == .colorFill {
|
|
return floating ? LowContrastColorFillFloatingConfiguration() : LowContrastColorFillConfiguration()
|
|
} else {
|
|
return floating ? LowContrastMediaFloatingConfiguration() : LowContrastMediaConfiguration()
|
|
}
|
|
|
|
case .highContrast:
|
|
return floating ? HighContrastFloatingConfiguration() : HighContrastConfiguration()
|
|
}
|
|
}
|
|
|
|
private var standardIconColorConfiguration: AnyColorable = {
|
|
return ControlColorConfiguration().with {
|
|
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
|
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
|
|
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
|
}.eraseToAnyColorable()
|
|
}()
|
|
|
|
private var highContrastIconColorConfiguration: AnyColorable = {
|
|
return SurfaceColorConfiguration(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight).eraseToAnyColorable()
|
|
}()
|
|
|
|
private var selectedIconColorConfiguration: AnyColorable = {
|
|
return ControlColorConfiguration().with {
|
|
$0.setSurfaceColors(VDSColor.elementsBrandhighlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
|
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
|
|
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
|
}.eraseToAnyColorable()
|
|
}()
|
|
|
|
private struct GhostConfiguration: Configuration {
|
|
var kind: Kind = .ghost
|
|
var surfaceType: SurfaceType = .colorFill
|
|
var floating: Bool = false
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(.clear, .clear).eraseToAnyColorable()
|
|
}()
|
|
}
|
|
|
|
private struct LowContrastColorFillConfiguration: Configuration {
|
|
var kind: Kind = .lowContrast
|
|
var surfaceType: SurfaceType = .colorFill
|
|
var floating: Bool = false
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.paletteGray44.withAlphaComponent(0.06), VDSColor.paletteGray44.withAlphaComponent(0.26)).eraseToAnyColorable()
|
|
}()
|
|
}
|
|
|
|
private struct LowContrastColorFillFloatingConfiguration: Configuration {
|
|
var kind: Kind = .lowContrast
|
|
var surfaceType: SurfaceType = .colorFill
|
|
var floating: Bool = true
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable()
|
|
}()
|
|
}
|
|
|
|
private struct LowContrastMediaConfiguration: Configuration, Borderable {
|
|
var kind: Kind = .lowContrast
|
|
var surfaceType: SurfaceType = .media
|
|
var floating: Bool = false
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable()
|
|
}()
|
|
var borderWidth: CGFloat = 1.0
|
|
var borderColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, .clear).eraseToAnyColorable()
|
|
}()
|
|
}
|
|
|
|
private struct LowContrastMediaFloatingConfiguration: Configuration, Dropshadowable {
|
|
var kind: Kind = .lowContrast
|
|
var surfaceType: SurfaceType = .media
|
|
var floating: Bool = true
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable()
|
|
}()
|
|
var shadowColorConfiguration: AnyColorable = {
|
|
SurfaceColorConfiguration(VDSColor.paletteBlack, .clear).eraseToAnyColorable()
|
|
}()
|
|
var shadowOpacity: CGFloat = 0.16
|
|
var shadowOffset: CGSize = .init(width: 0, height: 2)
|
|
var shadowRadius: CGFloat = 2
|
|
}
|
|
|
|
private struct HighContrastConfiguration: Configuration {
|
|
var kind: Kind = .highContrast
|
|
var surfaceType: SurfaceType = .colorFill
|
|
var floating: Bool = false
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
return ControlColorConfiguration().with {
|
|
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
|
|
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
|
|
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
|
}.eraseToAnyColorable()
|
|
|
|
}()
|
|
}
|
|
|
|
private struct HighContrastFloatingConfiguration: Configuration {
|
|
var kind: Kind = .highContrast
|
|
var surfaceType: SurfaceType = .colorFill
|
|
var floating: Bool = true
|
|
var backgroundColorConfiguration: AnyColorable = {
|
|
return ControlColorConfiguration().with {
|
|
$0.setSurfaceColors(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryLight, forState: .normal)
|
|
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
|
|
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
|
}.eraseToAnyColorable()
|
|
}()
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Initializers
|
|
//--------------------------------------------------
|
|
required public init() {
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Lifecycle
|
|
//--------------------------------------------------
|
|
open override func setup() {
|
|
super.setup()
|
|
|
|
//create a layoutGuide for the icon to key off of
|
|
let iconLayoutGuide = UILayoutGuide()
|
|
addLayoutGuide(iconLayoutGuide)
|
|
|
|
//add the icon
|
|
addSubview(icon)
|
|
|
|
//determines the height/width of the icon
|
|
layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize)
|
|
layoutGuideHeightConstraint = iconLayoutGuide.height(constant: size.containerSize)
|
|
|
|
//pin layout guide
|
|
iconLayoutGuide.pinToSuperView()
|
|
|
|
//determines the center point of the icon
|
|
centerXConstraint = icon.centerXAnchor.constraint(equalTo: iconLayoutGuide.centerXAnchor, constant: 0)
|
|
centerXConstraint?.activate()
|
|
centerYConstraint = icon.centerYAnchor.constraint(equalTo: iconLayoutGuide.centerYAnchor, constant: 0)
|
|
centerYConstraint?.activate()
|
|
}
|
|
|
|
/// Resets to default settings.
|
|
open override func reset() {
|
|
super.reset()
|
|
shouldUpdateView = false
|
|
kind = .ghost
|
|
surfaceType = .colorFill
|
|
size = .large
|
|
floating = false
|
|
hideBorder = true
|
|
iconOffset = .init(x: 0, y: 0)
|
|
iconName = nil
|
|
shouldUpdateView = true
|
|
setNeedsUpdate()
|
|
}
|
|
|
|
/// Used to make changes to the View based off a change events or from local properties.
|
|
open override func updateView() {
|
|
super.updateView()
|
|
|
|
//ensure there is an icon to set
|
|
if let currentIconName {
|
|
icon.name = currentIconName
|
|
let color = iconColorConfiguration.getColor(self)
|
|
icon.color = color
|
|
icon.size = size.value
|
|
icon.customSize = customSize
|
|
} else {
|
|
icon.reset()
|
|
}
|
|
setNeedsLayout()
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
|
|
open override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
let currentConfig = currentConfiguration
|
|
|
|
backgroundColor = currentConfig.backgroundColorConfiguration.getColor(self)
|
|
|
|
// calculate center point for child view with offset
|
|
let childCenter = CGPoint(x: center.x + iconOffset.x, y: center.y + iconOffset.y)
|
|
centerXConstraint?.constant = childCenter.x - center.x
|
|
centerYConstraint?.constant = childCenter.y - center.y
|
|
|
|
//updating current container size
|
|
var iconLayoutSize = size.containerSize
|
|
if let customSize {
|
|
iconLayoutSize = CGFloat(customSize)
|
|
}
|
|
// check to see if this is fitToIcon
|
|
if fitToIcon && kind == .ghost {
|
|
iconLayoutSize = size.value.dimensions.width
|
|
layer.cornerRadius = 0
|
|
} else {
|
|
layer.cornerRadius = min(frame.width, frame.height) / 2.0
|
|
}
|
|
|
|
layoutGuideWidthConstraint?.constant = iconLayoutSize
|
|
layoutGuideHeightConstraint?.constant = iconLayoutSize
|
|
|
|
//border
|
|
if let borderable = currentConfig as? Borderable {
|
|
layer.borderColor = borderable.borderColorConfiguration.getColor(self).cgColor
|
|
layer.borderWidth = borderable.borderWidth
|
|
} else {
|
|
layer.borderColor = nil
|
|
layer.borderWidth = 0
|
|
}
|
|
|
|
if let dropshadowable = currentConfig as? Dropshadowable {
|
|
addDropShadow(config: dropshadowable)
|
|
} else {
|
|
removeDropShadow()
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// MARK: AppleGuidlinesTouchable
|
|
extension ButtonIcon: AppleGuidlinesTouchable {
|
|
|
|
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
Self.acceptablyOutsideBounds(point: point, bounds: bounds)
|
|
}
|
|
|
|
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let view = super.hitTest(point, with: event)
|
|
|
|
if view == icon {
|
|
return self
|
|
}
|
|
|
|
return view
|
|
}
|
|
|
|
}
|
|
|
|
extension UIView {
|
|
fileprivate func addDropShadow(config: Dropshadowable) {
|
|
layer.masksToBounds = false
|
|
layer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
|
|
layer.shadowOpacity = Float(config.shadowOpacity)
|
|
layer.shadowOffset = config.shadowOffset
|
|
layer.shadowRadius = config.shadowRadius
|
|
layer.shouldRasterize = true
|
|
layer.rasterizationScale = UIScreen.main.scale
|
|
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
|
|
}
|
|
|
|
fileprivate func removeDropShadow() {
|
|
layer.shadowOpacity = 0
|
|
layer.shadowRadius = 0
|
|
layer.shadowPath = nil
|
|
}
|
|
}
|
|
|
|
private protocol Borderable {
|
|
var borderWidth: CGFloat { get set }
|
|
var borderColorConfiguration: AnyColorable { get set }
|
|
}
|
|
|
|
private protocol Dropshadowable {
|
|
var shadowColorConfiguration: AnyColorable { get set }
|
|
var shadowOpacity: CGFloat { get set }
|
|
var shadowOffset: CGSize { get set }
|
|
var shadowRadius: CGFloat { get set }
|
|
}
|
|
|
|
private protocol Configuration {
|
|
var kind: ButtonIcon.Kind { get set }
|
|
var surfaceType: ButtonIcon.SurfaceType { get set }
|
|
var floating: Bool { get set }
|
|
var backgroundColorConfiguration: AnyColorable { get set }
|
|
}
|