diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 2f010251..a34f8806 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; }; + 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; + 5FC35BE528D51414004EBEAC /* ButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE428D51413004EBEAC /* ButtonModel.swift */; }; EA1F265D28B944F00033E859 /* CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F265B28B944F00033E859 /* CollectionView.swift */; }; EA1F265E28B944F00033E859 /* CollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F265C28B944F00033E859 /* CollectionViewCell.swift */; }; EA1F266428B945070033E859 /* RadioSwatchGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266028B945070033E859 /* RadioSwatchGroupModel.swift */; }; @@ -102,6 +105,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = ""; }; + 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; + 5FC35BE428D51413004EBEAC /* ButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonModel.swift; sourceTree = ""; }; EA1F265B28B944F00033E859 /* CollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionView.swift; sourceTree = ""; }; EA1F265C28B944F00033E859 /* CollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewCell.swift; sourceTree = ""; }; EA1F266028B945070033E859 /* RadioSwatchGroupModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatchGroupModel.swift; sourceTree = ""; }; @@ -213,6 +219,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5FC35BE128D513EB004EBEAC /* Button */ = { + isa = PBXGroup; + children = ( + 5FC35BE228D51405004EBEAC /* Button.swift */, + 5FC35BE428D51413004EBEAC /* ButtonModel.swift */, + ); + path = Button; + sourceTree = ""; + }; EA1F265F28B945070033E859 /* RadioSwatch */ = { isa = PBXGroup; children = ( @@ -288,6 +303,7 @@ isa = PBXGroup; children = ( EA4DB2FE28DCBC1900103EE3 /* Badge */, + 5FC35BE128D513EB004EBEAC /* Button */, EAF7F092289985E200B287F5 /* Checkbox */, EA3362412892EF700071C351 /* Label */, EA89200B28B530F0006B9984 /* RadioBox */, @@ -339,6 +355,7 @@ EA3361C8289054C50071C351 /* Surfaceable.swift */, EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */, EAB1D2CC28ABE76000DAE764 /* Withable.swift */, + 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */, ); path = Protocols; sourceTree = ""; @@ -645,6 +662,7 @@ EAB1D29E28A5619500DAE764 /* RadioButtonGroupModel.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA3C3B4C2894823E000CA526 /* AnyProxy.swift in Sources */, + 5FC35BE528D51414004EBEAC /* ButtonModel.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, EAB1D29A28A5611D00DAE764 /* SelectorGroupModelable.swift in Sources */, EAF7F0BB289D80ED00B287F5 /* Modelable.swift in Sources */, @@ -663,6 +681,7 @@ EA89200828B526E0006B9984 /* CheckboxGroupModel.swift in Sources */, EA3361B6288B2A410071C351 /* Control.swift in Sources */, EAB1D2A328A5994800DAE764 /* Debuggable.swift in Sources */, + 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, EA3362452892F9130071C351 /* Labelable.swift in Sources */, EA3361AD288B26190071C351 /* DataTrackable.swift in Sources */, @@ -675,6 +694,7 @@ EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA1F266428B945070033E859 /* RadioSwatchGroupModel.swift in Sources */, EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */, + 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/VDS/Components/Button/Button.swift b/VDS/Components/Button/Button.swift new file mode 100644 index 00000000..bdddb0a9 --- /dev/null +++ b/VDS/Components/Button/Button.swift @@ -0,0 +1,273 @@ +// +// Button.swift +// VDS +// +// Created by Jarrod Courtney on 9/16/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +public class Button:ButtonBase{} + +open class ButtonBase: UIButton, ModelHandlerable, ViewProtocol, Resettable { + + //-------------------------------------------------- + // MARK: - Combine Properties + //-------------------------------------------------- + @Published public var model: ModelType = ModelType() + public var modelPublisher: Published.Publisher { $model } + public var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var minWidthConstraint: NSLayoutConstraint? + private var widthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + @Proxy(\.model.surface) + open var surface: Surface + + @Proxy(\.model.disabled) + open var disabled: Bool { + didSet { + isEnabled = !disabled + } + } + + @Proxy(\.model.text) + open var text: String? + + @Proxy(\.model.use) + open var use: Use + + @Proxy(\.model.size) + open var size: ButtonSize + + @Proxy(\.model.width) + open var width: CGFloat? + + open override var isEnabled: Bool { + get { !model.disabled } + set { + //create local vars for clear coding + let disabled = !newValue + if model.disabled != disabled { + model.disabled = disabled + } + isUserInteractionEnabled = isEnabled + } + } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + private var buttonBackgroundColorConfiguration: UseableColorConfiguration = { + return UseableColorConfiguration().with { + $0.primary.enabled.lightColor = VDSColor.backgroundPrimaryDark + $0.primary.enabled.darkColor = VDSColor.backgroundPrimaryLight + $0.primary.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.primary.disabled.darkColor = VDSColor.interactiveDisabledOndark + + $0.secondary.enabled.lightColor = UIColor.clear + $0.secondary.enabled.darkColor = UIColor.clear + $0.secondary.disabled.lightColor = UIColor.clear + $0.secondary.disabled.darkColor = UIColor.clear + } + }() + + private var buttonBorderColorConfiguration: UseableColorConfiguration = { + return UseableColorConfiguration().with { + $0.primary.enabled.lightColor = VDSColor.elementsPrimaryOndark + $0.primary.enabled.darkColor = VDSColor.elementsPrimaryOnlight + $0.primary.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.primary.disabled.darkColor = VDSColor.interactiveDisabledOndark + + $0.secondary.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.secondary.enabled.darkColor = VDSColor.elementsPrimaryOndark + $0.secondary.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.secondary.disabled.darkColor = VDSColor.interactiveDisabledOndark + } + }() + + private var buttonTitleColorConfiguration: UseableColorConfiguration = { + return UseableColorConfiguration().with { + $0.primary.enabled.lightColor = VDSColor.elementsPrimaryOndark + $0.primary.enabled.darkColor = VDSColor.elementsPrimaryOnlight + $0.primary.disabled.lightColor = VDSColor.elementsPrimaryOndark + $0.primary.disabled.darkColor = VDSColor.elementsPrimaryOnlight + + $0.secondary.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.secondary.enabled.darkColor = VDSColor.elementsPrimaryOndark + $0.secondary.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.secondary.disabled.darkColor = VDSColor.interactiveDisabledOndark + } + }() + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + initialSetup() + } + + public required init(with model: ModelType) { + super.init(frame: .zero) + initialSetup() + set(with: model) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + set(with: model) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + //-------------------------------------------------- + // MARK: - Public Functions + //-------------------------------------------------- + open func initialSetup() { + backgroundColor = .clear + translatesAutoresizingMaskIntoConstraints = false + accessibilityCustomActions = [] + accessibilityTraits = .staticText + setupUpdateView() + setup() + } + + open func setup() { + translatesAutoresizingMaskIntoConstraints = false + titleLabel?.adjustsFontSizeToFitWidth = false + titleLabel?.lineBreakMode = .byTruncatingTail + + //only 1 of the 2 widths can be on at the same time + widthConstraint = widthAnchor.constraint(equalToConstant: 0) + minWidthConstraint = widthAnchor.constraint(greaterThanOrEqualToConstant: model.size.minimumWidth) + + //height + heightConstraint = heightAnchor.constraint(equalToConstant: model.size.height) + heightConstraint?.isActive = true + } + + open func reset() { + model = ModelType() + accessibilityCustomActions = [] + accessibilityTraits = .staticText + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open func updateView(viewModel: ModelType) { + + let bgColor = buttonBackgroundColorConfiguration.getColor(viewModel) + let borderColor = buttonBorderColorConfiguration.getColor(viewModel) + let titleColor = buttonTitleColorConfiguration.getColor(viewModel) + let borderWidth = viewModel.use == .secondary ? 1.0 : 0.0 + let buttonHeight = viewModel.size.height + let cornerRadius = buttonHeight / 2 + let minWidth = viewModel.size.minimumWidth + let font = viewModel.size == .large ? TypographicalStyle.BoldBodyLarge.font : TypographicalStyle.BoldBodySmall.font + let edgeInsets = viewModel.size.edgeInsets + + if let text = viewModel.text { + setTitle(text, for: .normal) + } else { + setTitle("No ViewModel Text", for: .normal) + } + titleLabel?.font = font + backgroundColor = bgColor + setTitleColor(titleColor, for: .normal) + layer.borderColor = borderColor.cgColor + layer.cornerRadius = cornerRadius + layer.borderWidth = borderWidth + contentEdgeInsets = edgeInsets + + minWidthConstraint?.constant = minWidth + heightConstraint?.constant = buttonHeight + + if let width = viewModel.width, width > minWidth { + widthConstraint?.constant = width + widthConstraint?.isActive = true + minWidthConstraint?.isActive = false + } else { + widthConstraint?.isActive = false + minWidthConstraint?.isActive = true + } + } + + //-------------------------------------------------- + // MARK: - PRIVATE + //-------------------------------------------------- + + private class UseableColorConfiguration : Colorable { + public var primary = DisabledSurfaceColorConfiguration() + public var secondary = DisabledSurfaceColorConfiguration() + + required public init(){} + + public func getColor(_ viewModel: ModelType) -> UIColor { + return viewModel.use == .primary ? primary.getColor(viewModel) : secondary.getColor(viewModel) + } + } + +} + +extension ButtonSize { + + public var height: CGFloat { + switch self { + case .large: + return 44 + case .small: + return 32 + } + } + + public var minimumWidth: CGFloat { + switch self { + case .large: + return 76 + case .small: + return 60 + } + } + + public var edgeInsets: UIEdgeInsets { + var verticalPadding = 0.0 + var horizontalPadding = 0.0 + switch self { + case .large: + verticalPadding = 12 + horizontalPadding = 24 + break + case .small: + verticalPadding = 8 + horizontalPadding = 16 + break + } + return UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding) + } + +} + +extension Use { + + public var color: UIColor { + return self == .primary ? VDSColor.backgroundPrimaryDark : .clear + } + +} diff --git a/VDS/Components/Button/ButtonModel.swift b/VDS/Components/Button/ButtonModel.swift new file mode 100644 index 00000000..d5b7f0d9 --- /dev/null +++ b/VDS/Components/Button/ButtonModel.swift @@ -0,0 +1,34 @@ +// +// ButtonModel.swift +// VDS +// +// Created by Jarrod Courtney on 9/16/22. +// + +import Foundation +import UIKit + +public enum ButtonSize: String, Codable, CaseIterable { + case large + case small +} + +public protocol ButtonModel: Modelable, Useable { + var text: String? { get set } + var width: CGFloat? { get set } + var size: ButtonSize { get set } + var use: Use { get set } +} + +public struct DefaultButtonModel: ButtonModel { + + public var id = UUID() + public var text: String? + public var typograpicalStyle: TypographicalStyle = .BoldBodyLarge + public var surface: Surface = .light + public var use: Use = .primary + public var disabled: Bool = false + public var width: CGFloat? + public var size: ButtonSize = .large + public init(){} +} diff --git a/VDS/Protocols/Useable.swift b/VDS/Protocols/Useable.swift new file mode 100644 index 00000000..3f60ae18 --- /dev/null +++ b/VDS/Protocols/Useable.swift @@ -0,0 +1,18 @@ +// +// Useable.swift +// VDS +// +// Created by Jarrod Courtney on 9/22/22. +// + +import Foundation +import UIKit +import VDSColorTokens + +public enum Use: String, Codable, Equatable { + case primary, secondary +} + +public protocol Useable { + var use: Use { get set } +}