diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index dd3d8e50..f48fbe28 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 */; }; @@ -100,6 +103,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 = ""; }; @@ -209,6 +215,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 = ( @@ -283,6 +298,7 @@ EA33619D288B1E330071C351 /* Components */ = { isa = PBXGroup; children = ( + 5FC35BE128D513EB004EBEAC /* Button */, EAF7F092289985E200B287F5 /* Checkbox */, EA3362412892EF700071C351 /* Label */, EA89200B28B530F0006B9984 /* RadioBox */, @@ -334,6 +350,7 @@ EA3361C8289054C50071C351 /* Surfaceable.swift */, EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */, EAB1D2CC28ABE76000DAE764 /* Withable.swift */, + 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */, ); path = Protocols; sourceTree = ""; @@ -630,6 +647,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 */, @@ -647,6 +665,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 */, @@ -659,6 +678,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..622c5b2b --- /dev/null +++ b/VDS/Components/Button/Button.swift @@ -0,0 +1,219 @@ +// +// 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 mainStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.alignment = .top + $0.axis = .vertical + $0.spacing = 0 + } + }() + + private var buttonWidthConstraint: NSLayoutConstraint? + private var buttonHeightConstraint: NSLayoutConstraint? + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + @Proxy(\.model.surface) + open var surface: Surface + + @Proxy(\.model.use) + open var use: Use + + @Proxy(\.model.disabled) + open var disabled: Bool { + didSet { + self.isEnabled = !disabled + } + } + + open var buttonWidth: CGFloat = 200 { + didSet { + self.updateView(viewModel: model) + } + } + + 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 + } + } + + @Proxy(\.model.attributes) + open var attributes: [any LabelAttributeModel]? + + //-------------------------------------------------- + // 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() { + self.translatesAutoresizingMaskIntoConstraints = false + mainStackView.addArrangedSubview(self) + mainStackView.spacing = 12 + } + + open func reset() { +// text = nil + accessibilityCustomActions = [] + accessibilityTraits = .staticText + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open func updateView(viewModel: ModelType) { + if let text = viewModel.text { + self.setTitle(text, for: .normal) + } else { + self.setTitle("No ViewModel Text", for: .normal) + } + let backgroundColor = buttonBackgroundColorConfiguration.getColor(viewModel) + let borderColor = buttonBorderColorConfiguration.getColor(viewModel) + let borderWidth = viewModel.use == .secondary ? 1.0 : 0.0 + let titleColor = buttonTitleColorConfiguration.getColor(viewModel) + + self.titleLabel?.font = TypographicalStyle.BoldBodyLarge.font + self.backgroundColor = backgroundColor + self.setTitleColor(titleColor, for: .normal) + self.layer.borderColor = borderColor.cgColor + self.layer.cornerRadius = self.frame.size.height / 2 + self.layer.borderWidth = borderWidth + let buttonHeight : CGFloat = 44.0 + + if buttonWidthConstraint == nil { + buttonWidthConstraint = self.widthAnchor.constraint(equalToConstant: buttonWidth) + } else { + buttonWidthConstraint?.constant = buttonWidth + } + buttonWidthConstraint?.isActive = true + + if buttonHeightConstraint == nil { + buttonHeightConstraint = self.heightAnchor.constraint(equalToConstant: buttonHeight) + } else { + buttonHeightConstraint?.constant = buttonHeight + } + buttonHeightConstraint?.isActive = true + + } + + 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) + } + } +} diff --git a/VDS/Components/Button/ButtonModel.swift b/VDS/Components/Button/ButtonModel.swift new file mode 100644 index 00000000..3a766043 --- /dev/null +++ b/VDS/Components/Button/ButtonModel.swift @@ -0,0 +1,42 @@ +// +// ButtonModel.swift +// VDS +// +// Created by Jarrod Courtney on 9/16/22. +// + +import Foundation +import UIKit + +public protocol ButtonModel: Modelable, Useable, Equatable, AnyEquatable { + var text: String? { get set } + var attributes: [any LabelAttributeModel]? { get set } +} + +public struct DefaultButtonModel: ButtonModel { + + public static func == (lhs: DefaultButtonModel, rhs: DefaultButtonModel) -> Bool { + lhs.isEqual(rhs) + } + + public func isEqual(_ equatable: DefaultButtonModel) -> Bool { + return id == equatable.id + && attributes == equatable.attributes + && text == equatable.text + && surface == equatable.surface + && use == equatable.use + && typograpicalStyle == equatable.typograpicalStyle + && disabled == equatable.disabled + && buttonWidth == equatable.buttonWidth + } + + public var id = UUID() + public var text: String? + public var attributes: [any LabelAttributeModel]? + public var typograpicalStyle: TypographicalStyle = .BoldBodyLarge + public var surface: Surface = .light + public var use: Use = .primary + public var disabled: Bool = false + public var buttonWidth: CGFloat? + public init(){} +} diff --git a/VDS/Protocols/Useable.swift b/VDS/Protocols/Useable.swift new file mode 100644 index 00000000..077ceb02 --- /dev/null +++ b/VDS/Protocols/Useable.swift @@ -0,0 +1,21 @@ +// +// 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 var color: UIColor { + return self == .primary ? VDSColor.backgroundPrimaryDark : .clear + } +} + +public protocol Useable { + var use: Use { get set } +}