From 0c7a2940d985e170604b5763a789ee6f1a823beb Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Thu, 23 Jan 2020 14:26:04 -0500 Subject: [PATCH] Fixing issues for model reuse. --- .../TextFields/DateDropdownEntryField.swift | 36 +-- .../DateDropdownEntryFieldModel.swift | 4 +- .../Atoms/TextFields/DigitEntryField.swift | 6 +- .../TextFields/DigitEntryFieldModel.swift | 8 +- MVMCoreUI/Atoms/TextFields/EntryField.swift | 29 +- .../Atoms/TextFields/EntryFieldModel.swift | 21 +- .../TextFields/ItemDropdownEntryField.swift | 4 + .../ItemDropdownEntryFieldModel.swift | 24 -- .../Atoms/TextFields/TextEntryField.swift | 8 +- .../views/EntryFieldContainer.swift | 293 ++++++++++++++++++ .../Items/DropDownFilterTableViewCell.swift | 10 +- .../Items/DropDownListItemModel.swift | 3 + 12 files changed, 349 insertions(+), 97 deletions(-) create mode 100644 MVMCoreUI/Containers/views/EntryFieldContainer.swift diff --git a/MVMCoreUI/Atoms/TextFields/DateDropdownEntryField.swift b/MVMCoreUI/Atoms/TextFields/DateDropdownEntryField.swift index 6fde6b45..7051239c 100644 --- a/MVMCoreUI/Atoms/TextFields/DateDropdownEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/DateDropdownEntryField.swift @@ -22,7 +22,20 @@ import UIKit return calendar }() - public var dateFormat: String? + public var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.system + formatter.locale = .current + formatter.formatterBehavior = .default + return formatter + }() + + public var dateFormat: String = "MMM d, y" { + didSet { + dateFormatter.dateFormat = dateFormat + } + } //-------------------------------------------------- // MARK: - Initializers @@ -83,7 +96,7 @@ import UIKit if calendar.isDate(date, inSameDayAs: Date()) { text = MVMCoreUIUtility.hardcodedString(withKey: "textfield_today_string") } else { - text = dateFormatter().string(from: date) + text = dateFormatter.string(from: date) } } @@ -98,29 +111,12 @@ import UIKit setTextWith(date: datePicker?.date) } - public func dateFormatter() -> DateFormatter { - - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeZone = NSTimeZone.system - formatter.locale = .current - formatter.formatterBehavior = .default - - if let dateFormat = dateFormat { - formatter.dateFormat = dateFormat - } - - return formatter - } - public override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.setWithModel(model, delegateObject, additionalData) guard let model = model as? DateDropdownEntryFieldModel else { return } - if let dateFormat = model.dateFormat { - self.dateFormat = dateFormat - } + self.dateFormat = model.dateFormat } } diff --git a/MVMCoreUI/Atoms/TextFields/DateDropdownEntryFieldModel.swift b/MVMCoreUI/Atoms/TextFields/DateDropdownEntryFieldModel.swift index ebccffcd..c226660b 100644 --- a/MVMCoreUI/Atoms/TextFields/DateDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atoms/TextFields/DateDropdownEntryFieldModel.swift @@ -15,7 +15,7 @@ return "dateDropdownEntryField" } - public var dateFormat: String? + public var dateFormat: String = "MMM d, y" //-------------------------------------------------- // MARK: - Keys @@ -32,7 +32,7 @@ required public init(from decoder: Decoder) throws { try super.init(from: decoder) let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - dateFormat = try typeContainer.decodeIfPresent(String.self, forKey: .dateFormat) + dateFormat = try typeContainer.decodeIfPresent(String.self, forKey: .dateFormat) ?? "MMM d, y" } public override func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift b/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift index ce741346..c7888041 100644 --- a/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/DigitEntryField.swift @@ -333,11 +333,9 @@ import UIKit guard let model = model as? DigitEntryFieldModel else { return } - numberOfDigits = model.digits ?? 4 + numberOfDigits = model.digits - if let secureEntry = model.secureEntry, secureEntry { - setAsSecureTextEntry(true) - } + setAsSecureTextEntry(model.secureEntry) for digitBox in digitBoxes { MVMCoreUICommonViewsUtility.addDismissToolbar(digitBox.digitField, delegate: delegateObject as? UITextFieldDelegate) diff --git a/MVMCoreUI/Atoms/TextFields/DigitEntryFieldModel.swift b/MVMCoreUI/Atoms/TextFields/DigitEntryFieldModel.swift index a798eebb..3b139e9b 100644 --- a/MVMCoreUI/Atoms/TextFields/DigitEntryFieldModel.swift +++ b/MVMCoreUI/Atoms/TextFields/DigitEntryFieldModel.swift @@ -16,8 +16,8 @@ return "digitTextField" } - public var digits: Int? - public var secureEntry: Bool? + public var digits: Int = 4 + public var secureEntry: Bool = false //-------------------------------------------------- // MARK: - Keys @@ -35,8 +35,8 @@ required public init(from decoder: Decoder) throws { try super.init(from: decoder) let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - digits = try typeContainer.decodeIfPresent(Int.self, forKey: .digits) - secureEntry = try typeContainer.decodeIfPresent(Bool.self, forKey: .secureEntry) + digits = try typeContainer.decodeIfPresent(Int.self, forKey: .digits) ?? 4 + secureEntry = try typeContainer.decodeIfPresent(Bool.self, forKey: .secureEntry) ?? false } public override func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/Atoms/TextFields/EntryField.swift b/MVMCoreUI/Atoms/TextFields/EntryField.swift index 57d1b0bf..e70a2df0 100644 --- a/MVMCoreUI/Atoms/TextFields/EntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/EntryField.swift @@ -248,29 +248,12 @@ import UIKit entryFieldContainer.setWithModel(model, delegateObject, additionalData) - if let title = model.title { - self.title = title - } - - if let isDisabled = model.isDisabled { - self.isEnabled = isDisabled - } - - if let feedback = model.feedback { - self.feedback = feedback - } - - if let errorMessage = model.errorMessage { - self.errorMessage = errorMessage - } - - if let isLocked = model.isLocked { - self.isLocked = isLocked - } - - if let isSelected = model.isSelected { - self.isSelected = isSelected - } + title = model.title + isEnabled = model.isEnabled + feedback = model.feedback + errorMessage = model.errorMessage + isLocked = model.isLocked + isSelected = model.isSelected if let fieldKey = model.fieldKey { self.fieldKey = fieldKey diff --git a/MVMCoreUI/Atoms/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atoms/TextFields/EntryFieldModel.swift index dbf240ee..adb2b58c 100644 --- a/MVMCoreUI/Atoms/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atoms/TextFields/EntryFieldModel.swift @@ -21,11 +21,10 @@ import Foundation public var backgroundColor: Color? public var title: String? public var feedback: String? - public var errorMessage: String? - public var isDisabled: Bool? - public var isLocked: Bool? - public var isSelected: Bool? - + public var errorMessage: String = "" + public var isEnabled: Bool = true + public var isLocked: Bool = false + public var isSelected: Bool = false public var fieldKey: String? public var isValid: Bool? public var isRequired: Bool? @@ -37,7 +36,7 @@ import Foundation private enum CodingKeys: String, CodingKey { case backgroundColor case title - case isDisabled + case isEnabled case feedback case errorMessage = "errorMsg" case isLocked @@ -56,10 +55,10 @@ import Foundation backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback) - errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) - isDisabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .isDisabled) - isLocked = try typeContainer.decodeIfPresent(Bool.self, forKey: .isLocked) - isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .isSelected) + errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) ?? "" + isEnabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true + isLocked = try typeContainer.decodeIfPresent(Bool.self, forKey: .isLocked) ?? false + isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .isSelected) ?? false fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) isValid = try typeContainer.decodeIfPresent(Bool.self, forKey: .isValid) } @@ -70,7 +69,7 @@ import Foundation try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(feedback, forKey: .feedback) try container.encodeIfPresent(errorMessage, forKey: .errorMessage) - try container.encodeIfPresent(isDisabled, forKey: .isDisabled) + try container.encodeIfPresent(isEnabled, forKey: .isEnabled) try container.encodeIfPresent(isLocked, forKey: .isLocked) try container.encodeIfPresent(isSelected, forKey: .isSelected) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) diff --git a/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryField.swift b/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryField.swift index 440ad5fc..962ff89b 100644 --- a/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryField.swift @@ -115,10 +115,14 @@ extension ItemDropdownEntryField: UIPickerViewDelegate, UIPickerViewDataSource { } @objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + guard !pickerData.isEmpty else { return nil } + return pickerData[row] } @objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + guard !pickerData.isEmpty else { return } + observeDropdownChange?(text ?? "", pickerData[row]) text = pickerData[row] } diff --git a/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryFieldModel.swift index e1313086..a12d6b80 100644 --- a/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atoms/TextFields/ItemDropdownEntryFieldModel.swift @@ -16,28 +16,4 @@ } public var options: [String]? - - //-------------------------------------------------- - // MARK: - Keys - //-------------------------------------------------- - - private enum CodingKeys: String, CodingKey { - case options - } - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - - required public init(from decoder: Decoder) throws { - try super.init(from: decoder) - let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - options = try typeContainer.decodeIfPresent([String].self, forKey: .options) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(options, forKey: .options) - } } diff --git a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift index f4381faf..1230a485 100644 --- a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift @@ -286,13 +286,9 @@ import UIKit textColor.disabled = disabledTextColor.uiColor } - if let text = model.text { - self.text = text - } + text = model.text + placeholder = model.placeholder - if let placeholder = model.placeholder { - self.placeholder = placeholder - } switch model.type { case "password": diff --git a/MVMCoreUI/Containers/views/EntryFieldContainer.swift b/MVMCoreUI/Containers/views/EntryFieldContainer.swift new file mode 100644 index 00000000..3f80006a --- /dev/null +++ b/MVMCoreUI/Containers/views/EntryFieldContainer.swift @@ -0,0 +1,293 @@ +// +// EntryFieldContainer.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 11/12/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import UIKit + + +@objcMembers open class EntryFieldContainer: View { + //-------------------------------------------------- + // MARK: - Drawing Properties + //-------------------------------------------------- + + /// The bottom border line. Height is dynamic based on scenario. + 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 + }() + + /// Total control over the drawn top, bottom, left and right borders. + public var disableAllBorders = false { + didSet { + bottomBar?.isHidden = disableAllBorders + } + } + + private(set) var fieldState: FieldState = .original { + didSet (oldState) { + // Will not update if new state is the same as old. + if fieldState != oldState { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.fieldState.setStateUI(for: self) + } + } + } + } + + /// Determines if the top, left, and right borders should be drawn. + private var hideBorders = false + + public var borderStrokeColor: UIColor = .mfSilver() + private var borderPath: UIBezierPath = UIBezierPath() + + //-------------------------------------------------- + // MARK: - Property Observers + //-------------------------------------------------- + + private var _isEnabled: Bool = true + private var _showError: Bool = false + private var _isLocked: Bool = false + private var _isSelected: Bool = false + + public var isEnabled: Bool { + get { return _isEnabled } + set (enabled) { + + _isEnabled = enabled + _isLocked = false + _isSelected = false + _showError = false + + fieldState = enabled ? .original : .disabled + } + } + + public var showError: Bool { + get { return _showError } + set (error) { + + _showError = error + _isEnabled = true + _isLocked = false + _isSelected = false + + fieldState = error ? .error : .original + } + } + + public var isLocked: Bool { + get { return _isLocked } + set (locked) { + + _isLocked = locked + _isEnabled = true + _isSelected = false + _showError = false + + fieldState = locked ? .locked : .original + } + } + + public var isSelected: Bool { + get { return _isSelected } + set (selected) { + + _isSelected = selected + _isLocked = false + _isEnabled = true + + if _showError { + fieldState = selected ? .selectedError : .error + } else { + fieldState = selected ? .selected : .original + } + } + } + + //-------------------------------------------------- + // MARK: - Delegate + //-------------------------------------------------- + + /// Holds reference to delegateObject to inform molecular tableView of an update. + 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() + } + + /// This handles the top, left, and right border lines. + open override func draw(_ rect: CGRect) { + super.draw(rect) + + borderPath.removeAllPoints() + + if !disableAllBorders && !hideBorders { + // Brings the other half of the line inside the view to prevent cropping. + let origin = bounds.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() + } + } + + override open func setupView() { + super.setupView() + + isAccessibilityElement = false + isOpaque = false + + if let bottomBar = bottomBar { + layer.addSublayer(bottomBar) + } + } + + //-------------------------------------------------- + // MARK: - Draw States + //-------------------------------------------------- + + public enum FieldState { + case original + case error + case selectedError + case selected + case locked + case disabled + + public func setStateUI(for formField: EntryFieldContainer) { + + switch self { + case .original: + formField.originalUI() + + case .error: + formField.errorUI() + + case .selectedError: + formField.selectedErrorUI() + + case .selected: + formField.selectedUI() + + case .locked: + formField.lockedUI() + + case .disabled: + formField.disabledUI() + } + } + } + + open func originalUI() { + + isUserInteractionEnabled = true + hideBorders = false + borderStrokeColor = .mfSilver() + bottomBar?.backgroundColor = UIColor.black.cgColor + refreshUI(bottomBarSize: 1) + } + + open func errorUI() { + + isUserInteractionEnabled = true + hideBorders = false + borderStrokeColor = .mfPumpkin() + bottomBar?.backgroundColor = UIColor.mfPumpkin().cgColor + refreshUI(bottomBarSize: 4) + } + + open func selectedErrorUI() { + + isUserInteractionEnabled = true + hideBorders = false + borderStrokeColor = .black + bottomBar?.backgroundColor = UIColor.mfPumpkin().cgColor + refreshUI(bottomBarSize: 4) + } + + open func selectedUI() { + + isUserInteractionEnabled = true + hideBorders = false + borderStrokeColor = .black + bottomBar?.backgroundColor = UIColor.black.cgColor + refreshUI(bottomBarSize: 1) + } + + open func lockedUI() { + + isUserInteractionEnabled = false + hideBorders = true + borderStrokeColor = .clear + bottomBar?.backgroundColor = UIColor.clear.cgColor + refreshUI(bottomBarSize: 1) + } + + open func disabledUI() { + + isUserInteractionEnabled = false + hideBorders = false + borderStrokeColor = .mfSilver() + bottomBar?.backgroundColor = UIColor.mfSilver().cgColor + refreshUI(bottomBarSize: 1) + } + + open func refreshUI(bottomBarSize: CGFloat? = nil) { + + if !disableAllBorders { + let size: CGFloat = bottomBarSize ?? (showError ? 4 : 1) + bottomBar?.frame = CGRect(x: 0, y: bounds.height - size, width: bounds.width, height: size) + + delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self) + setNeedsDisplay() + layoutIfNeeded() + } + } + + //-------------------------------------------------- + // MARK: - MVMCoreUIMoleculeViewProtocol + //-------------------------------------------------- + + open override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.setWithModel(model, delegateObject, additionalData) + self.delegateObject = delegateObject + } +} + +extension EntryFieldContainer { + + 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 } + } +} diff --git a/MVMCoreUI/Molecules/Items/DropDownFilterTableViewCell.swift b/MVMCoreUI/Molecules/Items/DropDownFilterTableViewCell.swift index abf6f711..bf67a5bc 100644 --- a/MVMCoreUI/Molecules/Items/DropDownFilterTableViewCell.swift +++ b/MVMCoreUI/Molecules/Items/DropDownFilterTableViewCell.swift @@ -9,21 +9,25 @@ import UIKit @objcMembers public class DropDownFilterTableViewCell: TableViewCell { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- var dropDownListItemModel: DropDownListItemModel? let dropDown = ItemDropdownEntryField() var delegateObject: MVMCoreUIDelegateObject? var previousIndex = NSNotFound - var dropDownSelectionObservation: NSKeyValueObservation? + //-------------------------------------------------- // MARK: - MFViewProtocol + //-------------------------------------------------- + override public func setupView() { super.setupView() + guard dropDown.superview == nil else { return } - dropDown.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(dropDown) - NSLayoutConstraint.activate(Array(NSLayoutConstraint.pinView(toSuperview: dropDown, useMargins: true).values)) dropDown.observeDropdownChange = { [weak self] oldValue, newValue in diff --git a/MVMCoreUI/Molecules/Items/DropDownListItemModel.swift b/MVMCoreUI/Molecules/Items/DropDownListItemModel.swift index 28bc373b..9b9a66f1 100644 --- a/MVMCoreUI/Molecules/Items/DropDownListItemModel.swift +++ b/MVMCoreUI/Molecules/Items/DropDownListItemModel.swift @@ -9,6 +9,7 @@ import Foundation @objcMembers public class DropDownListItemModel: ContainerModel, ListItemModelProtocol { + public static var identifier: String = "dropDownListItem" public var molecules: [[ListItemModelProtocol]] public var dropDown: ItemDropdownEntryFieldModel @@ -33,9 +34,11 @@ import Foundation let typeContainer = try decoder.container(keyedBy: CodingKeys.self) molecules = try typeContainer.decodeMolecules2D(codingKey: .molecules) as! [[ListItemModelProtocol]] dropDown = try typeContainer.decode(ItemDropdownEntryFieldModel.self, forKey: .dropDown) + if let lineModel = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) { line = lineModel } + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) try super.init(from: decoder) }