From 6b77c2829adfa504834392e4657a30ab0e39f78d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 25 Jul 2022 14:51:12 -0500 Subject: [PATCH] first cut of Toggle Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 128 ++++++- .../xcschemes/xcschememanagement.plist | 2 +- VDS/BaseClasses/Control.swift | 62 ++++ .../Toggle/ToggleModelProtocol.swift | 15 + VDS/Components/Toggle/VDSToggle.swift | 347 ++++++++++++++++++ VDS/Extensions/UIColor.swift | 113 ++++++ VDS/Protocols/Changable.swift | 12 + VDS/Protocols/DataTrackable.swift | 14 + VDS/Protocols/Disabling.swift | 12 + VDS/Protocols/FormFieldable.swift | 13 + VDS/Protocols/Invertable.swift | 12 + VDS/Protocols/Modelable.swift | 14 + VDS/Protocols/ViewProtocol.swift | 19 + VDS/Utilities/TypeAlias.swift | 31 ++ VDS/Utilities/VDSHelper.swift | 14 + 15 files changed, 804 insertions(+), 4 deletions(-) create mode 100644 VDS/BaseClasses/Control.swift create mode 100644 VDS/Components/Toggle/ToggleModelProtocol.swift create mode 100644 VDS/Components/Toggle/VDSToggle.swift create mode 100644 VDS/Extensions/UIColor.swift create mode 100644 VDS/Protocols/Changable.swift create mode 100644 VDS/Protocols/DataTrackable.swift create mode 100644 VDS/Protocols/Disabling.swift create mode 100644 VDS/Protocols/FormFieldable.swift create mode 100644 VDS/Protocols/Invertable.swift create mode 100644 VDS/Protocols/Modelable.swift create mode 100644 VDS/Protocols/ViewProtocol.swift create mode 100644 VDS/Utilities/TypeAlias.swift create mode 100644 VDS/Utilities/VDSHelper.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index f844c974..4c13c71a 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -11,6 +11,21 @@ EA336177288B19210071C351 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33616C288B19200071C351 /* VDS.framework */; }; EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; }; EA33617D288B19210071C351 /* VDS.h in Headers */ = {isa = PBXBuildFile; fileRef = EA33616F288B19200071C351 /* VDS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EA336195288B1C420071C351 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33618E288B1C0C0071C351 /* VDSColorTokens.xcframework */; }; + EA336197288B1C420071C351 /* VDSFormControlsTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33618F288B1C0C0071C351 /* VDSFormControlsTokens.xcframework */; }; + EA33619F288B1E4D0071C351 /* VDSToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33619E288B1E4D0071C351 /* VDSToggle.swift */; }; + EA3361A2288B1E840071C351 /* ToggleModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361A1288B1E840071C351 /* ToggleModelProtocol.swift */; }; + EA3361A8288B23300071C351 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361A7288B23300071C351 /* UIColor.swift */; }; + EA3361AA288B25E40071C351 /* Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361A9288B25E40071C351 /* Disabling.swift */; }; + EA3361AD288B26190071C351 /* DataTrackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361AC288B26190071C351 /* DataTrackable.swift */; }; + EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361AE288B26310071C351 /* FormFieldable.swift */; }; + EA3361B1288B26490071C351 /* Invertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B0288B26490071C351 /* Invertable.swift */; }; + EA3361B3288B265D0071C351 /* Changable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B2288B265D0071C351 /* Changable.swift */; }; + EA3361B6288B2A410071C351 /* Control.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B5288B2A410071C351 /* Control.swift */; }; + EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */; }; + EA3361BB288B2C010071C351 /* VDSHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361BA288B2C010071C351 /* VDSHelper.swift */; }; + EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361BC288B2C760071C351 /* TypeAlias.swift */; }; + EA3361BF288B2EA60071C351 /* Modelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361BE288B2EA60071C351 /* Modelable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -29,6 +44,21 @@ EA336170288B19200071C351 /* VDS.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = VDS.docc; sourceTree = ""; }; EA336176288B19210071C351 /* VDSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VDSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA33617B288B19210071C351 /* VDSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSTests.swift; sourceTree = ""; }; + EA33618E288B1C0C0071C351 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = "/Users/mattbruce/Documents/Projects/iPhone/Frameworks/MVA-JSONCreator/JSONCreator_iOS/../SharedFrameworks/VDSColorTokens.xcframework"; sourceTree = ""; }; + EA33618F288B1C0C0071C351 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = "/Users/mattbruce/Documents/Projects/iPhone/Frameworks/MVA-JSONCreator/JSONCreator_iOS/../SharedFrameworks/VDSFormControlsTokens.xcframework"; sourceTree = ""; }; + EA33619E288B1E4D0071C351 /* VDSToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSToggle.swift; sourceTree = ""; }; + EA3361A1288B1E840071C351 /* ToggleModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleModelProtocol.swift; sourceTree = ""; }; + EA3361A7288B23300071C351 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; + EA3361A9288B25E40071C351 /* Disabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disabling.swift; sourceTree = ""; }; + EA3361AC288B26190071C351 /* DataTrackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTrackable.swift; sourceTree = ""; }; + EA3361AE288B26310071C351 /* FormFieldable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldable.swift; sourceTree = ""; }; + EA3361B0288B26490071C351 /* Invertable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Invertable.swift; sourceTree = ""; }; + EA3361B2288B265D0071C351 /* Changable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changable.swift; sourceTree = ""; }; + EA3361B5288B2A410071C351 /* Control.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Control.swift; sourceTree = ""; }; + EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewProtocol.swift; sourceTree = ""; }; + EA3361BA288B2C010071C351 /* VDSHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSHelper.swift; sourceTree = ""; }; + EA3361BC288B2C760071C351 /* TypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeAlias.swift; sourceTree = ""; }; + EA3361BE288B2EA60071C351 /* Modelable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modelable.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -36,6 +66,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EA336195288B1C420071C351 /* VDSColorTokens.xcframework in Frameworks */, + EA336197288B1C420071C351 /* VDSFormControlsTokens.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -56,6 +88,7 @@ EA33616E288B19200071C351 /* VDS */, EA33617A288B19210071C351 /* VDSTests */, EA33616D288B19200071C351 /* Products */, + EA33618D288B1C0C0071C351 /* Frameworks */, ); sourceTree = ""; }; @@ -71,8 +104,13 @@ EA33616E288B19200071C351 /* VDS */ = { isa = PBXGroup; children = ( - EA33616F288B19200071C351 /* VDS.h */, EA336170288B19200071C351 /* VDS.docc */, + EA33616F288B19200071C351 /* VDS.h */, + EA3361B4288B2A360071C351 /* BaseClasses */, + EA33619D288B1E330071C351 /* Components */, + EA3361A6288B23240071C351 /* Extensions */, + EA3361AB288B25EC0071C351 /* Protocols */, + EA3361B9288B2BE30071C351 /* Utilities */, ); path = VDS; sourceTree = ""; @@ -85,6 +123,71 @@ path = VDSTests; sourceTree = ""; }; + EA33618D288B1C0C0071C351 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EA33618E288B1C0C0071C351 /* VDSColorTokens.xcframework */, + EA33618F288B1C0C0071C351 /* VDSFormControlsTokens.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; + EA33619D288B1E330071C351 /* Components */ = { + isa = PBXGroup; + children = ( + EA3361A0288B1E6F0071C351 /* Toggle */, + ); + path = Components; + sourceTree = ""; + }; + EA3361A0288B1E6F0071C351 /* Toggle */ = { + isa = PBXGroup; + children = ( + EA33619E288B1E4D0071C351 /* VDSToggle.swift */, + EA3361A1288B1E840071C351 /* ToggleModelProtocol.swift */, + ); + path = Toggle; + sourceTree = ""; + }; + EA3361A6288B23240071C351 /* Extensions */ = { + isa = PBXGroup; + children = ( + EA3361A7288B23300071C351 /* UIColor.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + EA3361AB288B25EC0071C351 /* Protocols */ = { + isa = PBXGroup; + children = ( + EA3361B2288B265D0071C351 /* Changable.swift */, + EA3361AC288B26190071C351 /* DataTrackable.swift */, + EA3361A9288B25E40071C351 /* Disabling.swift */, + EA3361AE288B26310071C351 /* FormFieldable.swift */, + EA3361B0288B26490071C351 /* Invertable.swift */, + EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */, + EA3361BE288B2EA60071C351 /* Modelable.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + EA3361B4288B2A360071C351 /* BaseClasses */ = { + isa = PBXGroup; + children = ( + EA3361B5288B2A410071C351 /* Control.swift */, + ); + path = BaseClasses; + sourceTree = ""; + }; + EA3361B9288B2BE30071C351 /* Utilities */ = { + isa = PBXGroup; + children = ( + EA3361BA288B2C010071C351 /* VDSHelper.swift */, + EA3361BC288B2C760071C351 /* TypeAlias.swift */, + ); + path = Utilities; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -194,7 +297,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EA3361A2288B1E840071C351 /* ToggleModelProtocol.swift in Sources */, + EA3361BB288B2C010071C351 /* VDSHelper.swift in Sources */, + EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, + EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, + EA3361B3288B265D0071C351 /* Changable.swift in Sources */, EA336171288B19200071C351 /* VDS.docc in Sources */, + EA3361AA288B25E40071C351 /* Disabling.swift in Sources */, + EA3361B6288B2A410071C351 /* Control.swift in Sources */, + EA3361B1288B26490071C351 /* Invertable.swift in Sources */, + EA3361AD288B26190071C351 /* DataTrackable.swift in Sources */, + EA33619F288B1E4D0071C351 /* VDSToggle.swift in Sources */, + EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, + EA3361BF288B2EA60071C351 /* Modelable.swift in Sources */, + EA3361A8288B23300071C351 /* UIColor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -340,13 +456,15 @@ EA336181288B19210071C351 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BITCODE_GENERATION_MODE = bitcode; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = FCMA4QKS77; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -370,13 +488,15 @@ EA336182288B19210071C351 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BITCODE_GENERATION_MODE = bitcode; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = FCMA4QKS77; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -400,6 +520,7 @@ EA336184288B19210071C351 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = FCMA4QKS77; @@ -416,6 +537,7 @@ EA336185288B19210071C351 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = FCMA4QKS77; diff --git a/VDS.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/VDS.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index d30585e6..92988fd9 100644 --- a/VDS.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/VDS.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ VDS.xcscheme_^#shared#^_ orderHint - 0 + 5 diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift new file mode 100644 index 00000000..3cb6cbc2 --- /dev/null +++ b/VDS/BaseClasses/Control.swift @@ -0,0 +1,62 @@ +// +// Control.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit + +@objcMembers open class Control: UIControl { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + private var initialSetupPerformed = false + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public init() { + super.init(frame: .zero) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + fatalError("Control does not support xib.") + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + public func initialSetup() { + if !initialSetupPerformed { + initialSetupPerformed = true + setupView() + } + } + + open func reset() { + backgroundColor = .clear + } +} + +// MARK: - ViewProtocol +extension Control: ViewProtocol { + + open func updateView(_ size: CGFloat) { } + + /// Will be called only once. + open func setupView() { + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + } +} diff --git a/VDS/Components/Toggle/ToggleModelProtocol.swift b/VDS/Components/Toggle/ToggleModelProtocol.swift new file mode 100644 index 00000000..ec5d858a --- /dev/null +++ b/VDS/Components/Toggle/ToggleModelProtocol.swift @@ -0,0 +1,15 @@ +// +// ToggleModel.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit + +public protocol ToggleModelProtocol: Invertable, FormFieldable, DataTrackable, Disabling { + var id: String? { get set } + var hideText: Bool { get set } + var on: Bool { get set } +} diff --git a/VDS/Components/Toggle/VDSToggle.swift b/VDS/Components/Toggle/VDSToggle.swift new file mode 100644 index 00000000..9d448c8f --- /dev/null +++ b/VDS/Components/Toggle/VDSToggle.swift @@ -0,0 +1,347 @@ +// +// Toggle.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit +import VDSColorTokens +/** + A custom implementation of Apple's UISwitch. + + By default this class begins in the off state. + + Container: The background of the toggle control. + Knob: The circular indicator that slides on the container. + */ +@objcMembers open class VDSToggle: Control, Modelable, Changable { + public typealias ModelType = ToggleModelProtocol + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var model: ModelType? + + /// Holds the on and off colors for the container. + public var containerTintColor: (on: UIColor, off: UIColor) = (on: .green, off: .black) + + /// Holds the on and off colors for the knob. + public var knobTintColor: (on: UIColor, off: UIColor) = (on: .white, off: .white) + + /// Holds the on and off colors for the disabled state.. + public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .gray, knob: .white) + + /// Set this flag to false if you do not want to animate state changes. + public var isAnimated = true + + public var onChange: Blocks.ActionBlock? + + // Sizes are from InVision design specs. + static let containerSize = CGSize(width: 51, height: 31) + static let knobSize = CGSize(width: 28, height: 28) + + private var knobView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + view.layer.cornerRadius = VDSToggle.getKnobHeight() / 2.0 + return view + }() + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + open override var isEnabled: Bool { + didSet { + isUserInteractionEnabled = isEnabled + changeStateNoAnimation(isEnabled ? isOn : false) + setToggleAppearanceFromState() + accessibilityHint = VDSHelper.localizedString?(isEnabled ? "AccToggleHint" : "AccDisabled") + } + } + + /// Simple means to prevent user interaction with the toggle. + public var isLocked: Bool = false { + didSet { isUserInteractionEnabled = !isLocked } + } + + /// The state on the toggle. Default value: false. + open var isOn: Bool = false { + didSet { + if isAnimated { + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: { + if self.isOn { + self.knobView.backgroundColor = self.knobTintColor.on + self.backgroundColor = self.containerTintColor.on + + } else { + self.knobView.backgroundColor = self.knobTintColor.off + self.backgroundColor = self.containerTintColor.off + } + }, completion: nil) + + UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: { + self.constrainKnob() + self.knobWidthConstraint?.constant = Self.getKnobWidth() + self.layoutIfNeeded() + }, completion: nil) + + } else { + setToggleAppearanceFromState() + self.constrainKnob() + } + + model?.on = isOn + accessibilityValue = VDSHelper.localizedString?(isOn ? "AccOn" : "AccOff") + setNeedsLayout() + layoutIfNeeded() + } + } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + private var knobLeadingConstraint: NSLayoutConstraint? + private var knobTrailingConstraint: NSLayoutConstraint? + private var knobHeightConstraint: NSLayoutConstraint? + private var knobWidthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + private var widthConstraint: NSLayoutConstraint? + + private func constrainKnob() { + + knobLeadingConstraint?.isActive = false + knobTrailingConstraint?.isActive = false + + _ = isOn ? constrainKnobOn() : constrainKnobOff() + + knobTrailingConstraint?.isActive = true + knobLeadingConstraint?.isActive = true + } + + private func constrainKnobOn() { + + knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2) + knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor) + } + + private func constrainKnobOff() { + + knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor) + knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2) + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public required init(model: ToggleModelProtocol) { + self.model = model + super.init() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public convenience override init() { + self.init(frame: .zero) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + public override func updateView(_ size: CGFloat) { + super.updateView(size) + + heightConstraint?.constant = Self.getContainerHeight() + widthConstraint?.constant = Self.getContainerWidth() + + knobHeightConstraint?.constant = Self.getKnobHeight() + knobWidthConstraint?.constant = Self.getKnobWidth() + + layer.cornerRadius = Self.getContainerHeight() / 2.0 + knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0 + + changeStateNoAnimation(isOn) + } + + public override func setupView() { + super.setupView() + + isAccessibilityElement = true + accessibilityHint = VDSHelper.localizedString?( "AccToggleHint") + accessibilityLabel = VDSHelper.localizedString?( "Toggle_buttonlabel") + accessibilityTraits = .button + + heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height) + heightConstraint?.isActive = true + + widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width) + widthConstraint?.isActive = true + + layer.cornerRadius = Self.getContainerHeight() / 2.0 + backgroundColor = containerTintColor.off + + addSubview(knobView) + + knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height) + knobHeightConstraint?.isActive = true + knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width) + knobWidthConstraint?.isActive = true + knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true + bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true + + constrainKnobOff() + } + + public override func reset() { + super.reset() + + backgroundColor = containerTintColor.off + knobView.backgroundColor = knobTintColor.off + accessibilityLabel = VDSHelper.localizedString?( "Toggle_buttonlabel") + isAnimated = true + onChange = nil + } + + public static func getContainerWidth() -> CGFloat { + let size = Self.containerSize.width + guard let block = VDSHelper.sizeForDevice else { return size } + return block(size, .iPadPortrait, CGFloat(size * 1.5)) + } + + public static func getContainerHeight() -> CGFloat { + let size = Self.containerSize.height + guard let block = VDSHelper.sizeForDevice else { return size } + return block(size, .iPadPortrait, CGFloat(size * 1.5)) + } + + public static func getKnobWidth() -> CGFloat { + let size = Self.knobSize.width + guard let block = VDSHelper.sizeForDevice else { return size } + return block(size, .iPadPortrait, CGFloat(size * 1.5)) + } + + public static func getKnobHeight() -> CGFloat { + let size = Self.knobSize.width + guard let block = VDSHelper.sizeForDevice else { return size } + return block(size, .iPadPortrait, CGFloat(size * 1.5)) + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + + open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { + super.sendAction(action, to: target, for: event) + toggleAndAction() + } + + open override func sendActions(for controlEvents: UIControl.Event) { + super.sendActions(for: controlEvents) + toggleAndAction() + } + + /// This will toggle the state of the Toggle and execute the actionBlock if provided. + public func toggleAndAction() { + isOn.toggle() + onChange?() + } + + private func changeStateNoAnimation(_ state: Bool) { + + // Hold state in case User wanted isAnimated to remain off. + let isAnimatedState = isAnimated + + isAnimated = false + isOn = state + isAnimated = isAnimatedState + } + + override open func accessibilityActivate() -> Bool { + // Hold state in case User wanted isAnimated to remain off. + guard isUserInteractionEnabled else { return false } + let isAnimatedState = isAnimated + isAnimated = false + sendActions(for: .touchUpInside) + isAnimated = isAnimatedState + return true + } + + //-------------------------------------------------- + // MARK: - UIResponder + //-------------------------------------------------- + + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { + + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint?.constant += Constants.PaddingOne + self.layoutIfNeeded() + }) + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + knobReformAnimation() + + // Action only occurs of the user lifts up from withing acceptable region of the toggle. + guard let coordinates = touches.first?.location(in: self), + coordinates.x > -20, + coordinates.x < bounds.width + 20, + coordinates.y > -20, + coordinates.y < bounds.height + 20 + else { return } + + sendActions(for: .touchUpInside) + } + + public func touchesCancelled(_ touches: Set, with event: UIEvent) { + + knobReformAnimation() + sendActions(for: .touchCancel) + } + + //-------------------------------------------------- + // MARK: - Animations + //-------------------------------------------------- + + public func setToggleAppearanceFromState() { + + backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container + knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob + } + + public func knobReformAnimation() { + + if isAnimated { + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint?.constant = Self.getKnobWidth() + self.layoutIfNeeded() + }, completion: nil) + + } else { + knobWidthConstraint?.constant = Self.getKnobWidth() + layoutIfNeeded() + } + } + + // MARK:- MoleculeViewProtocol + open func set(with model: ModelType) { + self.model = model + isOn = model.on + changeStateNoAnimation(isOn) + isAnimated = true + isEnabled = !model.disabled + } + +} + diff --git a/VDS/Extensions/UIColor.swift b/VDS/Extensions/UIColor.swift new file mode 100644 index 00000000..c967caca --- /dev/null +++ b/VDS/Extensions/UIColor.swift @@ -0,0 +1,113 @@ +// +// UIColor.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit + +extension UIColor { + //-------------------------------------------------- + // MARK: - Functions + //-------------------------------------------------- + + /// Convenience to get a grayscale UIColor where the same value is used for red, green, and blue. + public class func grayscale(rgb: Int, alpha: CGFloat = 1.0) -> UIColor { + + let grayscale = CGFloat(rgb) / 255.0 + return UIColor(red: grayscale, green: grayscale, blue: grayscale, alpha: alpha) + } + + /// Convenience to get a UIColor. + public class func color8Bits(red: Int, green: Int, blue: Int, alpha: CGFloat = 1.0) -> UIColor { + + return UIColor(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: alpha) + } + + /// Convenience to get a UIColor. + public class func color8Bits(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) -> UIColor { + + return UIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: alpha) + } + + /// Gets UIColor via an 8 digit hex string. + public class func getColorBy(hex: String) -> UIColor { + + var hexint: UInt64 = 0 + + let scanner = Scanner(string: hex) + scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") + scanner.scanHexInt64(&hexint) + + return UIColor(red: (CGFloat((hexint & 0xFF0000) >> 16)) / 255, + green: (CGFloat((hexint & 0xFF00) >> 8)) / 255, + blue: (CGFloat(hexint & 0xFF)) / 255, + alpha: 1) + } + + /// Gets UIColor via an 8 digit hex string. The last two being the alpha channel. + public class func getColorWithTransparencyBy(hex: String) -> UIColor { + + var hexint: UInt64 = 0 + + let scanner = Scanner(string: hex) + scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") + scanner.scanHexInt64(&hexint) + + return UIColor(red: (CGFloat((hexint & 0xFF000000) >> 24)) / 255, + green: (CGFloat((hexint & 0xFF0000) >> 16)) / 255, + blue: (CGFloat((hexint & 0xFF00) >> 8)) / 255, + alpha: (CGFloat(hexint & 0xFF)) / 255) + } + + public class func gradientColor(_ color: UIColor?) -> UIColor { + + var h: CGFloat = 0 + var s: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + if color?.getHue(&h, saturation: &s, brightness: &b, alpha: &a) ?? false { + return UIColor(hue: h, saturation: max(s - 0.17, 0.0), brightness: min(b - 0.03, 1.0), alpha: a) + } + + return .white + } + + /// - parameter color: The UIColor intended to retrieve its hex value. + public class func hexString(for color: UIColor) -> String? { + + guard let components = color.cgColor.components else { return nil } + + let numberOfComponents = color.cgColor.numberOfComponents + if numberOfComponents >= 3 { + // RGB color space + let r = Int(CGFloat(components[0]) * 255) + let g = Int(CGFloat(components[1]) * 255) + let b = Int(CGFloat(components[2]) * 255) + + // If alpha of color is less than 1.0 then alpha hex is relevant. + if color.cgColor.numberOfComponents == 4 && components[3] < 1.0 { + let a = Int(CGFloat(components[3]) * 255) + return String(format: "%02X%02X%02X%02X", r, g, b, a) + } + + return String(format: "%02X%02X%02X", r, g, b) + } else if numberOfComponents == 2 { + // Monochromatic color space + let value = Int(CGFloat(components[0]) * 255) + + // If alpha of color is less than 1.0 then alpha hex is relevant. + if components[1] < 1.0 { + let alpha = Int(CGFloat(components[1]) * 255) + return String(format: "%02X%02X%02X%02X", value, value, value, alpha) + } + return String(format: "%02X%02X%02X", value, value, value) + } + + return nil + } + +} diff --git a/VDS/Protocols/Changable.swift b/VDS/Protocols/Changable.swift new file mode 100644 index 00000000..6df913c3 --- /dev/null +++ b/VDS/Protocols/Changable.swift @@ -0,0 +1,12 @@ +// +// Changable.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol Changable { + var onChange: Blocks.ActionBlock? { get set } +} diff --git a/VDS/Protocols/DataTrackable.swift b/VDS/Protocols/DataTrackable.swift new file mode 100644 index 00000000..5fb8f810 --- /dev/null +++ b/VDS/Protocols/DataTrackable.swift @@ -0,0 +1,14 @@ +// +// DataTrackable.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol DataTrackable { + var dataAnalyticsTrack: String? { get set } + var dataClickStream: String? { get set } + var dataTrack: String? { get set } +} diff --git a/VDS/Protocols/Disabling.swift b/VDS/Protocols/Disabling.swift new file mode 100644 index 00000000..c0be6d22 --- /dev/null +++ b/VDS/Protocols/Disabling.swift @@ -0,0 +1,12 @@ +// +// Disabling.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol Disabling { + var disabled: Bool { get set } +} diff --git a/VDS/Protocols/FormFieldable.swift b/VDS/Protocols/FormFieldable.swift new file mode 100644 index 00000000..41017204 --- /dev/null +++ b/VDS/Protocols/FormFieldable.swift @@ -0,0 +1,13 @@ +// +// FormFieldable.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol FormFieldable { + var inputId: String? { get set } + var value: AnyHashable? { get set } +} diff --git a/VDS/Protocols/Invertable.swift b/VDS/Protocols/Invertable.swift new file mode 100644 index 00000000..0e634fa2 --- /dev/null +++ b/VDS/Protocols/Invertable.swift @@ -0,0 +1,12 @@ +// +// Invertable.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol Invertable { + var inverted: Bool { get set } +} diff --git a/VDS/Protocols/Modelable.swift b/VDS/Protocols/Modelable.swift new file mode 100644 index 00000000..3920e9d9 --- /dev/null +++ b/VDS/Protocols/Modelable.swift @@ -0,0 +1,14 @@ +// +// Modelable.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public protocol Modelable { + associatedtype ModelType + var model: ModelType? { get set } + func set(with model: ModelType) +} diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift new file mode 100644 index 00000000..02d1d31b --- /dev/null +++ b/VDS/Protocols/ViewProtocol.swift @@ -0,0 +1,19 @@ +// +// ViewProtocol.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit + +public protocol ViewProtocol { + + // Updates the ui to fit the right size. + func updateView(_ size: CGFloat) + + // Can setup ui here. Should be called in the initialization functions. + func setupView() +} + diff --git a/VDS/Utilities/TypeAlias.swift b/VDS/Utilities/TypeAlias.swift new file mode 100644 index 00000000..3ae8a9f8 --- /dev/null +++ b/VDS/Utilities/TypeAlias.swift @@ -0,0 +1,31 @@ +// +// TypeAlias.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation +import UIKit + +public struct Blocks { + public typealias ActionBlock = () -> () + public typealias StringValueBlock = (_ key: String) -> (String) + public typealias SizeForDeviceTypeBlock = (_ size: CGFloat, _ deviceType: Enums.SizeDeviceType, _ deviceSize: CGFloat) -> (CGFloat) +} + +public struct Constants { + public static let PaddingOne = 10.0 +} + +public struct Enums { + public enum SizeDeviceType { + case iPhone4Height + case iPhone5Height + case iPhone + case largeiPhone + case iPadPortrait + case iPadLandscape + case iPadProLandscape + } +} diff --git a/VDS/Utilities/VDSHelper.swift b/VDS/Utilities/VDSHelper.swift new file mode 100644 index 00000000..30d5cd3c --- /dev/null +++ b/VDS/Utilities/VDSHelper.swift @@ -0,0 +1,14 @@ +// +// VDSHelper.swift +// VDS +// +// Created by Matt Bruce on 7/22/22. +// + +import Foundation + +public struct VDSHelper { + public static var localizedString: Blocks.StringValueBlock? + + public static var sizeForDevice: Blocks.SizeForDeviceTypeBlock? +}