From 48bcd87bc1c16dfa63de2001bfab51f302ea622f Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Tue, 12 Nov 2019 14:29:37 -0500 Subject: [PATCH] Refactored UI drawing to new class. Reflected change. --- MVMCoreUI.xcodeproj/project.pbxproj | 12 ++ MVMCoreUI/Atoms/TextFields/DigitBox.swift | 99 ++++------ .../Atoms/TextFields/DigitEntryField.swift | 101 +++------- MVMCoreUI/Atoms/TextFields/EntryField.swift | 182 ++++-------------- .../Atoms/TextFields/TextEntryField.swift | 9 +- MVMCoreUI/BaseClasses/View.swift | 20 +- MVMCoreUI/Containers/views/FormView.swift | 171 ++++++++++++++++ 7 files changed, 311 insertions(+), 283 deletions(-) create mode 100644 MVMCoreUI/Containers/views/FormView.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 9978a4c0..fcbd1272 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 0A6BF4722360C56C0028F841 /* DropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6BF4712360C56C0028F841 /* DropdownEntryField.swift */; }; 0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; }; + 0ABD136B237B193A0081388D /* FormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABD136A237B193A0081388D /* FormView.swift */; }; 943784F5236B77BB006A1E82 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F3236B77BB006A1E82 /* GraphView.swift */; }; 943784F6236B77BB006A1E82 /* GraphViewAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */; }; 9455B19C234F8A0400A574DB /* MVMAnimationFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */; }; @@ -237,6 +238,7 @@ 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxWithLabelView.swift; sourceTree = ""; }; 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitBox.swift; sourceTree = ""; }; + 0ABD136A237B193A0081388D /* FormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormView.swift; sourceTree = ""; }; 943784F3236B77BB006A1E82 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphViewAnimationHandler.swift; sourceTree = ""; }; 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MVMAnimationFramework.framework; path = ../SharedFrameworks/MVMAnimationFramework.framework; sourceTree = ""; }; @@ -441,6 +443,14 @@ path = FormUIHelpers; sourceTree = ""; }; + 0ABD1369237B18EE0081388D /* views */ = { + isa = PBXGroup; + children = ( + 0ABD136A237B193A0081388D /* FormView.swift */, + ); + path = views; + sourceTree = ""; + }; D224798823142BF2003FCCF9 /* SwitchMolecules */ = { isa = PBXGroup; children = ( @@ -645,6 +655,7 @@ D29DF11921E68467003B2FB9 /* Containers */ = { isa = PBXGroup; children = ( + 0ABD1369237B18EE0081388D /* views */, D29DF2B721E7BE79003B2FB9 /* TabBarController */, D29DF2B621E7BE66003B2FB9 /* SplitViewController */, D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */, @@ -1171,6 +1182,7 @@ 0105618E224BBE7700E1557D /* FormValidator+TextFields.swift in Sources */, 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */, D29DF2BE21E7BEA4003B2FB9 /* TopTabbar.m in Sources */, + 0ABD136B237B193A0081388D /* FormView.swift in Sources */, D2A514632213643100345BFB /* MoleculeStackCenteredTemplate.swift in Sources */, D29DF32421ED0DA2003B2FB9 /* TextButtonView.m in Sources */, D29DF29E21E7AE3B003B2FB9 /* MFStyler.m in Sources */, diff --git a/MVMCoreUI/Atoms/TextFields/DigitBox.swift b/MVMCoreUI/Atoms/TextFields/DigitBox.swift index 09ecee72..40972d5a 100644 --- a/MVMCoreUI/Atoms/TextFields/DigitBox.swift +++ b/MVMCoreUI/Atoms/TextFields/DigitBox.swift @@ -13,28 +13,19 @@ import UIKit } -@objcMembers open class DigitBox: UITextField, MVMCoreViewProtocol { +@objcMembers open class DigitBox: FormView, UITextFieldDelegate { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + let digitField = UITextField(frame: .zero) + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var bottomBar: CAShapeLayer = { - let layer = CAShapeLayer() - layer.backgroundColor = UIColor.black.cgColor - layer.drawsAsynchronously = true - layer.anchorPoint = CGPoint(x: 0.5, y: 1.0); - return layer - }() - private var previousSize: CGFloat = 0.0 - /// Determines if a border should be drawn. - var hideBorder = false - var showError = false - - var borderStrokeColor: UIColor = .mfSilver() - private var borderPath: UIBezierPath = UIBezierPath() - //-------------------------------------------------- // MARK: - Delegate //-------------------------------------------------- @@ -76,8 +67,9 @@ import UIKit translatesAutoresizingMaskIntoConstraints = false backgroundColor = .white - textAlignment = .center - keyboardType = .numberPad + digitField.delegate = self + digitField.textAlignment = .center + digitField.keyboardType = .numberPad showErrorState(false) widthConstraint = widthAnchor.constraint(equalToConstant: 39) @@ -90,30 +82,6 @@ import UIKit updateView(MVMCoreUISplitViewController.getDetailViewWidth()) } - open override func draw(_ rect: CGRect) { - super.draw(rect) - - borderPath.removeAllPoints() - - if !hideBorder { - // Brings the other half of the line inside the view to prevent cropping. - let origin = frame.origin - let size = frame.size - let insetLean: CGFloat = 0.5 - borderPath.lineWidth = 1 - - borderPath.move(to: CGPoint(x: origin.x + insetLean, y: origin.y + size.height)) - borderPath.addLine(to: CGPoint(x: origin.x + insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + size.height)) - - borderStrokeColor.setStroke() - borderPath.stroke() - } - - layoutIfNeeded() - } - open override func layoutSubviews() { super.layoutSubviews() @@ -125,40 +93,44 @@ import UIKit // MARK: - Methods //-------------------------------------------------- - override open func deleteBackward() { - super.deleteBackward() - - textBoxDelegate?.textFieldDidDelete?(self) + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + + if string.isBackspace { + textBoxDelegate?.textFieldDidDelete?(self.digitField) + } + + return true } - public func updateView(_ size: CGFloat) { + public override func updateView(_ size: CGFloat) { + super.updateView(size) DispatchQueue.main.async { [weak self] in guard let self = self else { return } if !MVMCoreGetterUtility.fequal(a: Float(size), b: Float(self.previousSize)) { - MFStyler.styleTextField(self) - self.font = MFFonts.mfFont55Rg(28) + MFStyler.styleTextField(self.digitField) + self.digitField.font = MFFonts.mfFont55Rg(28) - var digitWidth: CGFloat = 0 - var digitHeight: CGFloat = 0 + var width: CGFloat = 0 + var height: CGFloat = 0 let sizeObject = MFSizeObject(standardBlock: { - digitWidth = 39 - digitHeight = 44 + width = 39 + height = 44 }, smalliPhone: { - digitWidth = 35 - digitHeight = 38 + width = 35 + height = 38 }, standardiPadPortraitBlock: { - digitWidth = 59 - digitHeight = 74 + width = 59 + height = 74 }) sizeObject?.performBlockBase(onSize: size) - self.widthConstraint?.constant = digitWidth - self.heightConstraint?.constant = digitHeight + self.widthConstraint?.constant = width + self.heightConstraint?.constant = height self.previousSize = size } @@ -178,3 +150,12 @@ import UIKit layoutIfNeeded() } } + + +// TODO: Move if working properly. +extension String { + var isBackspace: Bool { + let char = self.cString(using: String.Encoding.utf8)! + return strcmp(char, "\\b") == -92 + } +} diff --git a/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift b/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift index 9417fc27..c455294a 100644 --- a/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift @@ -23,12 +23,14 @@ import UIKit public override var isEnabled: Bool { didSet { - titleLabel.textColor = isEnabled ? .black : .mfBattleshipGrey() + titleLabel.textColor = self.isEnabled ? .mfBattleshipGrey() : .mfSilver() - for digitBox in digitFields { - digitBox.isUserInteractionEnabled = isEnabled - digitBox.isEnabled = isEnabled - digitBox.textColor = isEnabled ? .black : .mfBattleshipGrey() + digitFields.forEach { + $0.isEnabled = self.isEnabled + + $0.isUserInteractionEnabled = isEnabled + $0.digitField.isEnabled = isEnabled + $0.digitField.textColor = isEnabled ? .black : .mfBattleshipGrey() } } } @@ -37,7 +39,7 @@ import UIKit get { var string = "" - digitFields.forEach { string += $0.attributedPlaceholder?.string ?? "" } + digitFields.forEach { string += $0.digitField.attributedPlaceholder?.string ?? "" } return !string.isEmpty ? string : nil } @@ -47,7 +49,7 @@ import UIKit for (index, field) in digitFields.enumerated() { if index < newValue.count { let indexChar = newValue.index(newValue.startIndex, offsetBy: index) - field.attributedPlaceholder = NSAttributedString(string: String(newValue[indexChar]), attributes: [ + field.digitField.attributedPlaceholder = NSAttributedString(string: String(newValue[indexChar]), attributes: [ NSAttributedString.Key.foregroundColor: UIColor.mfBattleshipGrey()]) } } @@ -59,12 +61,6 @@ import UIKit feedback = "" } -// if let feedback = feedback, !feedback.isEmpty { -// feedbackContainerDistance?.constant = 10 -// } else { -// feedbackContainerDistance?.constant = 0 -// } - /* * adding missing accessibilityLabel value * if we have some value in accessibilityLabel, @@ -79,7 +75,7 @@ import UIKit get { var string = "" - digitFields.forEach { string += $0.text ?? "" } + digitFields.forEach { string += $0.digitField.text ?? "" } return string } @@ -89,7 +85,7 @@ import UIKit for (index, field) in digitFields.enumerated() { if index < newValue.count { let indexChar = newValue.index(newValue.startIndex, offsetBy: index) - field.text = String(newValue[indexChar]) + field.digitField.text = String(newValue[indexChar]) } } @@ -97,46 +93,6 @@ import UIKit } } - /// Updates the visual appearance of the container, with some logical laterations as well. - public override func updateUI(appearance: Appearance) { - - self.appearance = appearance - isUserInteractionEnabled = true - titleLabel.textColor = .mfBattleshipGrey() - feedback = showError ? errorMessage : nil - - switch appearance { - case .original: - digitFields.forEach { - $0.borderStrokeColor = .mfSilver() - $0.bottomBar.backgroundColor = UIColor.black.cgColor - } - case .error: - digitFields.forEach { - $0.borderStrokeColor = .mfPumpkin() - $0.bottomBar.backgroundColor = UIColor.mfPumpkin().cgColor - $0.showErrorState(true) - } - case .lock: - digitFields.forEach { - $0.isUserInteractionEnabled = false - $0.bottomBar.backgroundColor = UIColor.clear.cgColor - } - case .select: - digitFields.forEach { - $0.borderStrokeColor = .black - $0.bottomBar.backgroundColor = UIColor.black.cgColor - } - case .disable: - digitFields.forEach { - $0.isUserInteractionEnabled = false - $0.borderStrokeColor = .mfSilver() - $0.bottomBar.backgroundColor = self.isEnabled ? UIColor.black.cgColor : UIColor.mfSilver().cgColor - } - titleLabel.textColor = self.isEnabled ? .mfBattleshipGrey() : .mfSilver() - } - } - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -171,13 +127,10 @@ import UIKit public override func setupFieldContainerContent(_ container: UIView) { -// textField.removeFromSuperview() - alignCenterHorizontal() isAccessibilityElement = false - hideBorder = true - bottomBar.backgroundColor = UIColor.clear.cgColor - bottomBar.frame = CGRect(x: 0, y: fieldContainer.bounds.height, width: 0, height: 0) + entryContainer.bottomBar.backgroundColor = UIColor.clear.cgColor + entryContainer.bottomBar.frame = CGRect(x: 0, y: entryContainer.bounds.height, width: 0, height: 0) setupDigitFieldsView(size: MVMCoreUISplitViewController.getDetailViewWidth()) textField.removeFromSuperview() @@ -185,11 +138,11 @@ import UIKit private func createDigitField() -> DigitBox { - let textField = DigitBox() - textField.isAccessibilityElement = true - textField.delegate = self - textField.textBoxDelegate = self - return textField + let digitBox = DigitBox() + digitBox.isAccessibilityElement = true + digitBox.digitField.delegate = self + digitBox.textBoxDelegate = self + return digitBox } func setupDigitFieldsView(size: CGFloat) { @@ -205,18 +158,18 @@ import UIKit for (index, box) in digitFields.enumerated() { accessibleElements.append(box) - fieldContainer.addSubview(box) + entryContainer.addSubview(box) - box.topAnchor.constraint(equalTo: fieldContainer.topAnchor).isActive = true - fieldContainer.bottomAnchor.constraint(equalTo: box.bottomAnchor).isActive = true - box.centerYAnchor.constraint(equalTo: fieldContainer.centerYAnchor).isActive = true + box.topAnchor.constraint(equalTo: entryContainer.topAnchor).isActive = true + entryContainer.bottomAnchor.constraint(equalTo: box.bottomAnchor).isActive = true + box.centerYAnchor.constraint(equalTo: entryContainer.centerYAnchor).isActive = true if index == 0 { - box.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor).isActive = true + box.leadingAnchor.constraint(equalTo: entryContainer.leadingAnchor).isActive = true } else if index == digitFields.count - 1 { box.leadingAnchor.constraint(equalTo: prevBox!.trailingAnchor, constant: space).isActive = true - fieldContainer.trailingAnchor.constraint(greaterThanOrEqualTo: box.trailingAnchor).isActive = true + entryContainer.trailingAnchor.constraint(greaterThanOrEqualTo: box.trailingAnchor).isActive = true } else { box.leadingAnchor.constraint(equalTo: prevBox!.trailingAnchor, constant: space).isActive = true @@ -279,7 +232,7 @@ import UIKit guard let self = self else { return } for (index, field) in self.digitFields.enumerated() { - field.isSecureTextEntry = secureEntry + field.digitField.isSecureTextEntry = secureEntry // Accessibility - 33704 fix voice over will read what pin user is filling field.accessibilityLabel = String(format: "PIN %lu of %lu", UInt(index) + 1, UInt(self.digitFields.count)) @@ -352,8 +305,8 @@ import UIKit } if !dictionary.isEmpty{ - for textField in digitFields { - MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: delegateObject as? UITextFieldDelegate) + for digitBox in digitFields { + MVMCoreUICommonViewsUtility.addDismissToolbar(digitBox.digitField, delegate: delegateObject as? UITextFieldDelegate) } } diff --git a/MVMCoreUI/Atoms/TextFields/EntryField.swift b/MVMCoreUI/Atoms/TextFields/EntryField.swift index 9b437901..8cb48268 100644 --- a/MVMCoreUI/Atoms/TextFields/EntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/EntryField.swift @@ -28,10 +28,9 @@ import UIKit return label }() - public private(set) var fieldContainer: UIView = { - let view = UIView(frame: .zero) + public private(set) var entryContainer: FormView = { + let view = FormView() view.isAccessibilityElement = false - view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -57,23 +56,11 @@ import UIKit public var isValid = false public var fieldKey: String? - /// Determines if a border should be drawn. - var hideBorder = false - - private var borderStrokeColor: UIColor = .mfSilver() - private var borderPath: UIBezierPath = UIBezierPath() - - public var bottomBar: CAShapeLayer = { - let layer = CAShapeLayer() - layer.backgroundColor = UIColor.black.cgColor - layer.drawsAsynchronously = true - layer.anchorPoint = CGPoint(x: 0.5, y: 1.0); - return layer - }() - - public var appearance: Appearance = .original - - public var showError = false + public var showError = false { + didSet { + entryContainer.showError = showError + } + } public var errorMessage: String? @@ -83,7 +70,21 @@ import UIKit DispatchQueue.main.async { [weak self] in guard let self = self else { return } - self.updateUI(appearance: self.isEnabled ? .original : .disable) + self.isUserInteractionEnabled = self.isEnabled + self.entryContainer.isEnabled = self.isEnabled + self.feedback = self.showError ? self.errorMessage : nil + self.titleLabel.textColor = self.isEnabled ? .mfBattleshipGrey() : .mfSilver() + } + } + } + + public var isLocked = false { + didSet { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.isUserInteractionEnabled = !self.isLocked + self.entryContainer.lockedUI() } } } @@ -109,7 +110,7 @@ import UIKit set { feedbackLabel.text = newValue setAccessibilityString(newValue) - refreshBorderUI() + entryContainer.refreshUI() } } @@ -129,8 +130,8 @@ import UIKit // MARK: - Constraints //-------------------------------------------------- - public var fieldContainerLeading: NSLayoutConstraint? - public var fieldContainerTrailing: NSLayoutConstraint? + public var entryContainerLeading: NSLayoutConstraint? + public var entryContainerTrailing: NSLayoutConstraint? public var feedbackLabelTrailing: NSLayoutConstraint? public var feedbackLabelLeading: NSLayoutConstraint? @@ -175,6 +176,7 @@ import UIKit guard subviews.isEmpty else { return } translatesAutoresizingMaskIntoConstraints = false + isAccessibilityElement = false setContentCompressionResistancePriority(.required, for: .vertical) accessibilityElements = [titleLabel, feedbackLabel] backgroundColor = .clear @@ -187,19 +189,19 @@ import UIKit titleLabelTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor) titleLabelLeading?.isActive = true - addSubview(fieldContainer) - setupFieldContainerContent(fieldContainer) + addSubview(entryContainer) + setupFieldContainerContent(entryContainer) - titleContainerDistance = fieldContainer.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4) + titleContainerDistance = entryContainer.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4) titleContainerDistance?.isActive = true - fieldContainerLeading = fieldContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) - fieldContainerLeading?.isActive = true - fieldContainerTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor) - fieldContainerTrailing?.isActive = true + entryContainerLeading = entryContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + entryContainerLeading?.isActive = true + entryContainerTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: entryContainer.trailingAnchor) + entryContainerTrailing?.isActive = true addSubview(feedbackLabel) - feedbackContainerDistance = feedbackLabel.topAnchor.constraint(equalTo: fieldContainer.bottomAnchor, constant: PaddingOne) + feedbackContainerDistance = feedbackLabel.topAnchor.constraint(equalTo: entryContainer.bottomAnchor, constant: PaddingOne) feedbackContainerDistance?.isActive = true feedbackLabelLeading = feedbackLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) feedbackLabelLeading?.isActive = true @@ -207,14 +209,12 @@ import UIKit feedbackLabelTrailing?.isActive = true layoutMarginsGuide.bottomAnchor.constraint(equalTo: feedbackLabel.bottomAnchor).isActive = true - fieldContainer.layer.addSublayer(bottomBar) - setNeedsDisplay() layoutIfNeeded() } /// Method to override. - /// Intended to add the interactive content (i.e. textField) to the fieldContainer. + /// Intended to add the interactive content (i.e. textField) to the entryContainer. open func setupFieldContainerContent(_ container: UIView) { // To be overridden by subclass. } @@ -224,7 +224,7 @@ import UIKit titleLabel.updateView(size) feedbackLabel.updateView(size) - refreshBorderUI() + entryContainer.updateView(size) } open override func reset() { @@ -233,53 +233,8 @@ import UIKit backgroundColor = .clear titleLabel.reset() feedbackLabel.reset() - fieldContainer.subviews.forEach { $0.removeFromSuperview() } - updateUI(appearance: .original) - } - - open override func layoutSubviews() { - super.layoutSubviews() - - refreshBorderUI(bottomBarSize: 1) - } - - open func refreshBorderUI(bottomBarSize: CGFloat? = nil) { - - let size: CGFloat = appearance == .error ? 4 : 1 - bottomBar.frame = CGRect(x: 0, y: fieldContainer.bounds.height - size, width: fieldContainer.bounds.width, height: size) - - delegateObject?.moleculeDelegate?.moleculeLayoutUpdated?(self) - setNeedsDisplay() - layoutIfNeeded() - } - - //-------------------------------------------------- - // MARK: - Drawing - //-------------------------------------------------- - - open override func draw(_ rect: CGRect) { - super.draw(rect) - - borderPath.removeAllPoints() - - if !hideBorder { - - // Brings the other half of the line inside the view to prevent thinness from cropping. - let origin = fieldContainer.frame.origin - let size = fieldContainer.frame.size - let insetLean: CGFloat = 0.5 - borderPath.lineWidth = 1 - - borderPath.move(to: CGPoint(x: origin.x + insetLean, y: origin.y + size.height)) - borderPath.addLine(to: CGPoint(x: origin.x + insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + insetLean)) - borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + size.height)) - - borderStrokeColor.setStroke() - borderPath.stroke() - } - - layoutIfNeeded() + entryContainer.subviews.forEach { $0.removeFromSuperview() } + titleLabel.textColor = .mfBattleshipGrey() } //-------------------------------------------------- @@ -288,66 +243,17 @@ import UIKit open override func setLeftPinConstant(_ constant: CGFloat) { - fieldContainerLeading?.constant = constant + entryContainerLeading?.constant = constant feedbackLabelLeading?.constant = constant titleLabelLeading?.constant = constant } open override func setRightPinConstant(_ constant: CGFloat) { - fieldContainerTrailing?.constant = constant + entryContainerTrailing?.constant = constant feedbackLabelTrailing?.constant = constant titleLabelTrailing?.constant = constant } - - //-------------------------------------------------- - // MARK: - Form UI Base Appearance - //-------------------------------------------------- - - public enum Appearance: String { - case original - case error - case lock - case select - case disable - } - - /// Updates the visual appearance of the container, with some logical laterations as well. - public func updateUI(appearance: Appearance) { - - self.appearance = appearance - isUserInteractionEnabled = true - titleLabel.textColor = .mfBattleshipGrey() - hideBorder = false - feedback = showError ? errorMessage : nil - - switch appearance { - case .original: - borderStrokeColor = .mfSilver() - bottomBar.backgroundColor = UIColor.black.cgColor - - case .error: - borderStrokeColor = .mfPumpkin() - bottomBar.backgroundColor = UIColor.mfPumpkin().cgColor - - case .lock: - isUserInteractionEnabled = false - hideBorder = true - bottomBar.backgroundColor = UIColor.clear.cgColor - - case .select: - borderStrokeColor = .black - bottomBar.backgroundColor = UIColor.black.cgColor - - case .disable: - isUserInteractionEnabled = false - borderStrokeColor = .mfSilver() - titleLabel.textColor = self.isEnabled ? .mfBattleshipGrey() : .mfSilver() - bottomBar.backgroundColor = self.isEnabled ? UIColor.black.cgColor : UIColor.mfSilver().cgColor - } - - refreshBorderUI(bottomBarSize: appearance == .error ? 4 : 1) - } } // MARK: - Molecular @@ -371,12 +277,8 @@ extension EntryField { errorMessage = errMessage } - if let hideBorder = dictionary["hideBorder"] as? Bool { - self.hideBorder = hideBorder - } - - if let appearance = dictionary["appearance"] as? String, let kind = Appearance(rawValue: appearance) { - updateUI(appearance: kind) + if let isLocked = dictionary["isLocked"] as? Bool { + self.isLocked = isLocked } // Key used to send text value to server diff --git a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift index 63d3d0e1..ff243339 100644 --- a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift @@ -174,7 +174,7 @@ import UIKit open func clearErrorState() { textField.accessibilityValue = nil - updateUI(appearance: .original) + entryContainer.originalUI() } public func setBothTextDelegates(to delegate: (UITextFieldDelegate & TextFieldDelegate)?) { @@ -224,7 +224,7 @@ import UIKit if isValid { clearErrorState() - bottomBar.backgroundColor = UIColor.black.cgColor + entryContainer.bottomBar.backgroundColor = UIColor.black.cgColor } else if let errMessage = errorMessage { feedback = errMessage @@ -233,10 +233,7 @@ import UIKit @objc func startEditing() { - if appearance != .original { - updateUI(appearance: .original) - } - + entryContainer.originalUI() textField.becomeFirstResponder() } } diff --git a/MVMCoreUI/BaseClasses/View.swift b/MVMCoreUI/BaseClasses/View.swift index bc6e7634..8ae475b9 100644 --- a/MVMCoreUI/BaseClasses/View.swift +++ b/MVMCoreUI/BaseClasses/View.swift @@ -9,18 +9,25 @@ import UIKit @objcMembers open class View: UIView { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + open var json: [AnyHashable: Any]? private var initialSetupPerformed = false + //-------------------------------------------------- + // MARK: - Initialization + //-------------------------------------------------- + public override init(frame: CGRect) { super.init(frame: .zero) initialSetup() } - public init() { - super.init(frame: .zero) - initialSetup() + public convenience init() { + self.init(frame: .zero) } public required init?(coder: NSCoder) { @@ -29,6 +36,7 @@ import UIKit } public func initialSetup() { + if !initialSetupPerformed { initialSetupPerformed = true setupView() @@ -36,7 +44,9 @@ import UIKit } } +/// MARK:- MVMCoreViewProtocol extension View: MVMCoreViewProtocol { + open func updateView(_ size: CGFloat) {} /// Will be called only once. @@ -46,8 +56,10 @@ extension View: MVMCoreViewProtocol { } } +/// MARK:- MVMCoreUIMoleculeViewProtocol extension View: MVMCoreUIMoleculeViewProtocol { - open func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { + + open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { self.json = json if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) { diff --git a/MVMCoreUI/Containers/views/FormView.swift b/MVMCoreUI/Containers/views/FormView.swift new file mode 100644 index 00000000..3e09bb12 --- /dev/null +++ b/MVMCoreUI/Containers/views/FormView.swift @@ -0,0 +1,171 @@ +// +// FormView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 11/12/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import UIKit + + +@objcMembers open class FormView: View { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public var bottomBar: CAShapeLayer = { + let layer = CAShapeLayer() + layer.backgroundColor = UIColor.black.cgColor + layer.drawsAsynchronously = true + layer.anchorPoint = CGPoint(x: 0.5, y: 1.0); + return layer + }() + + public var borderStrokeColor: UIColor = .mfSilver() + public var borderPath: UIBezierPath = UIBezierPath() + + /// Determines if a border should be drawn. + public var hideBorder = false + public var showError = false { + didSet { + _ = showError ? errorUI() : originalUI() + } + } + + public var isEnabled = true { + didSet { + isEnabled ? originalUI() : disabledUI() + } + } + + //-------------------------------------------------- + // MARK: - Delegate + //-------------------------------------------------- + + weak var delegateObject: MVMCoreUIDelegateObject? + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func layoutSubviews() { + super.layoutSubviews() + + refreshUI(bottomBarSize: showError ? 4 : 1) + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + + refreshUI() + } + + open override func draw(_ rect: CGRect) { + super.draw(rect) + + borderPath.removeAllPoints() + + if !hideBorder { + // Brings the other half of the line inside the view to prevent cropping. + let origin = frame.origin + let size = frame.size + let insetLean: CGFloat = 0.5 + borderPath.lineWidth = 1 + + borderPath.move(to: CGPoint(x: origin.x + insetLean, y: origin.y + size.height)) + borderPath.addLine(to: CGPoint(x: origin.x + insetLean, y: origin.y + insetLean)) + borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + insetLean)) + borderPath.addLine(to: CGPoint(x: origin.x + size.width - insetLean, y: origin.y + size.height)) + + borderStrokeColor.setStroke() + borderPath.stroke() + } + + layoutIfNeeded() + } + + override open func setupView() { + super.setupView() + + layer.addSublayer(bottomBar) + } + + //-------------------------------------------------- + // MARK: - Draw States + //-------------------------------------------------- + + open func originalUI() { + + isUserInteractionEnabled = true + hideBorder = false + borderStrokeColor = .mfSilver() + bottomBar.backgroundColor = UIColor.black.cgColor + + refreshUI(bottomBarSize: 1) + } + + open func errorUI() { + + isUserInteractionEnabled = true + hideBorder = false + borderStrokeColor = .mfPumpkin() + bottomBar.backgroundColor = UIColor.mfPumpkin().cgColor + + refreshUI(bottomBarSize: 4) + } + + open func selectedUI() { + + isUserInteractionEnabled = true + hideBorder = false + borderStrokeColor = .black + bottomBar.backgroundColor = UIColor.black.cgColor + + refreshUI(bottomBarSize: 1) + } + + open func lockedUI() { + + isUserInteractionEnabled = false + hideBorder = true + bottomBar.backgroundColor = UIColor.clear.cgColor + + refreshUI(bottomBarSize: 1) + } + + open func disabledUI() { + + isUserInteractionEnabled = false + borderStrokeColor = .mfSilver() + hideBorder = false + bottomBar.backgroundColor = self.isEnabled ? UIColor.black.cgColor : UIColor.mfSilver().cgColor + + refreshUI(bottomBarSize: 1) + } + + open func refreshUI(bottomBarSize: CGFloat? = nil) { + + let size: CGFloat = showError ? 4 : 1 + bottomBar.frame = CGRect(x: 0, y: bounds.height - size, width: bounds.width, height: size) + + delegateObject?.moleculeDelegate?.moleculeLayoutUpdated?(self) + setNeedsDisplay() + layoutIfNeeded() + } + + //-------------------------------------------------- + // MARK: - Molecular + //-------------------------------------------------- + + override open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + self.delegateObject = delegateObject + + guard let dictionary = json, !dictionary.isEmpty else { return } + + if let hideBorder = dictionary["hideBorder"] as? Bool { + self.hideBorder = hideBorder + } + } +}