Merge branch 'feature/atomic-vds-radioButton' into 'develop'

VDS Brand 3.0 Radiobutton

### Summary
VDS Brand 3.0 Radiobutton, RadiobuttonItem for IOS
* RadiobuttonGroup will be a later integration since this is not in the app at this time.

### JIRA Ticket
https://onejira.verizon.com/browse/ONEAPP-7004

Co-authored-by: Matt Bruce <matt.bruce@verizon.com>

See merge request https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui/-/merge_requests/1153
This commit is contained in:
Pfeil, Scott Robert 2024-07-30 20:07:46 +00:00
commit fa650bafb5
8 changed files with 245 additions and 233 deletions

View File

@ -7,37 +7,35 @@
//
import UIKit
import VDSCoreTokens
import VDS
@objcMembers open class RadioButton: Control, MFButtonProtocol {
//--------------------------------------------------
@objcMembers open class RadioButton: VDS.RadioButton, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol, MVMCoreUIViewConstrainingProtocol {
//------------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var diameter: CGFloat = 20 {
didSet { widthConstraint?.constant = diameter }
//------------------------------------------------------
open var viewModel: RadioButtonModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
open var radioButtonModel: RadioButtonModel {
viewModel
}
@objc public override var isSelected: Bool {
// Form Validation
open var fieldKey: String?
open var fieldValue: JSONValue?
open var groupName: String?
open override var isSelected: Bool {
didSet {
radioModel?.state = isSelected
updateAccessibilityLabel()
viewModel.state = isSelected
if oldValue != isSelected {
sendActions(for: .valueChanged)
}
}
}
public var enabledColor: UIColor {
return radioModel?.inverted ?? false ? VDSColor.elementsPrimaryOndark : VDSColor.elementsPrimaryOnlight
}
public var disabledColor: UIColor {
return radioModel?.inverted ?? false ? VDSColor.interactiveDisabledOndark : VDSColor.interactiveDisabledOnlight
}
public var delegateObject: MVMCoreUIDelegateObject?
var additionalData: [AnyHashable: Any]?
public var radioModel: RadioButtonModel? {
model as? RadioButtonModel
}
lazy public var radioGroupName: String? = { radioModel?.fieldKey }()
lazy public var radioGroupName: String? = { viewModel.fieldKey }()
lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
@ -48,132 +46,108 @@ import VDSCoreTokens
return radioButtonModel
}()
public override var isEnabled: Bool {
didSet {
isUserInteractionEnabled = isEnabled
setNeedsDisplay()
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
override public init(frame: CGRect) {
super.init(frame: frame)
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
public var widthConstraint: NSLayoutConstraint?
public var heightConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let color = isEnabled == false ? disabledColor.cgColor : enabledColor.cgColor
layer.cornerRadius = bounds.width * 0.5
layer.borderColor = color
layer.borderWidth = bounds.width * 0.0333
if isSelected {
// Space around inner circle is 1/5 the size
context.addEllipse(in: CGRect(x: bounds.width * 0.2,
y: bounds.height * 0.2,
width: bounds.width * 0.6,
height: bounds.height * 0.6))
context.setFillColor(color)
context.fillPath()
}
/// There is currently no intention on using xib files.
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("xib file is not implemented for Checkbox.")
}
public convenience required init() {
self.init(frame:.zero)
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
/// The action performed when tapped.
func tapAction() {
if !isEnabled {
return
}
public func isValidField() -> Bool { isSelected }
public func formFieldName() -> String? {
viewModel.fieldKey
}
public func formFieldGroupName() -> String? {
viewModel.fieldKey
}
public func formFieldValue() -> AnyHashable? {
guard let radioModel = viewModel, radioModel.enabled else { return nil }
return radioModel.fieldValue
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
public func viewModelDidUpdate() {
//events
viewModel.updateUI = {
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
guard let self = self else { return }
let isValid = viewModel.isValid ?? true
showError = !isValid
isEnabled = viewModel.enabled
})
}
isSelected = viewModel.state
isEnabled = viewModel.enabled && !viewModel.readOnly
RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel, self, delegateObject: delegateObject)
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func toggle() {
guard !isSelected, isEnabled else { return }
//removed error
if showError && isSelected == false {
showError.toggle()
}
let wasPreviouslySelected = isSelected
if let radioButtonModel = radioButtonSelectionHelper {
radioButtonModel.selected(self)
if let radioButtonSelectionHelper {
radioButtonSelectionHelper.selected(self)
} else {
isSelected = !isSelected
}
if let radioModel = radioModel, let actionModel = radioModel.action, isSelected, !wasPreviouslySelected {
if let actionModel = viewModel.action, isSelected, !wasPreviouslySelected {
Task(priority: .userInitiated) {
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioModel)
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: viewModel)
}
}
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
setNeedsDisplay()
}
public func isValidField() -> Bool { isSelected }
public func formFieldName() -> String? {
radioModel?.fieldKey
}
public func formFieldGroupName() -> String? {
radioModel?.fieldKey
}
public func formFieldValue() -> AnyHashable? {
guard let radioModel = radioModel, radioModel.enabled else { return nil }
return radioModel.fieldValue
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Methods
// MARK: - Actions
//--------------------------------------------------
/// Adjust accessibility label based on state of RadioButton.
func updateAccessibilityLabel() {
if let message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button"),
let selectedState = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "radio_selected_state" : "radio_not_selected_state") {
accessibilityLabel = message + selectedState
}
/// This will toggle the state of the Checkbox and execute the actionBlock if provided.
public func tapAction() {
toggle()
}
//--------------------------------------------------
// MARK: - MVMViewProtocol
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open override func setupView() {
super.setupView()
open func needsToBeConstrained() -> Bool { true }
backgroundColor = .clear
clipsToBounds = true
widthConstraint = widthAnchor.constraint(equalToConstant: 20)
widthConstraint?.isActive = true
heightConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1)
heightConstraint?.isActive = true
addTarget(self, action: #selector(tapAction), for: .touchUpInside)
isAccessibilityElement = true
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "radio_action_hint")
accessibilityTraits = .button
updateAccessibilityLabel()
}
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
public func updateView(_ size: CGFloat) {}
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
self.additionalData = additionalData
guard let model = model as? RadioButtonModel else { return }
isSelected = model.state
isEnabled = model.enabled && !model.readOnly
RadioButtonSelectionHelper.setupForRadioButtonGroup(model, self, delegateObject: delegateObject)
}
public override func reset() {
super.reset()
backgroundColor = .clear
}
}

View File

@ -7,48 +7,29 @@
//
import MVMCore
import VDS
open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
open class RadioButtonModel: FormFieldModel {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "radioButton"
public var id: String = UUID().uuidString
public var backgroundColor: Color?
public var accessibilityIdentifier: String?
public static override var identifier: String { "radioButton" }
public var state: Bool = false
public var enabled: Bool = true
public var readOnly: Bool = false
/// The specific value to send to server. TODO: update this to be more generic.
public var fieldValue: String?
public var baseValue: AnyHashable?
public var groupName: String = FormValidator.defaultGroupName
public var fieldKey: String?
public var action: ActionModelProtocol?
public var inverted: Bool = false
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case id
case moleculeName
case backgroundColor
case accessibilityIdentifier
case state
case enabled
case fieldValue
case fieldKey
case groupName
case action
case readOnly
case inverted
}
//--------------------------------------------------
@ -56,69 +37,50 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
//--------------------------------------------------
public init(_ state: Bool) {
super.init()
self.state = state
baseValue = state
self.baseValue = state
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
public func formFieldValue() -> AnyHashable? {
public override func formFieldValue() -> AnyHashable? {
guard enabled else { return nil }
return fieldValue
}
//--------------------------------------------------
// MARK: - Server Value
//--------------------------------------------------
open func formFieldServerValue() -> AnyHashable? {
return formFieldValue()
open override func setValidity(_ valid: Bool, errorMessage: String?) {
if let ruleErrorMessage = errorMessage, fieldKey != nil {
self.errorMessage = ruleErrorMessage
}
isValid = valid
updateUI?()
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
try super.init(from: decoder)
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) {
self.state = state
}
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
baseValue = state
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
self.groupName = groupName
}
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
}
}
public func encode(to encoder: Encoder) throws {
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
try container.encode(id, forKey: .id)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(state, forKey: .state)
try container.encode(enabled, forKey: .enabled)
try container.encode(readOnly, forKey: .readOnly)
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
try container.encodeIfPresent(groupName, forKey: .groupName)
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
try container.encodeModelIfPresent(action, forKey: .action)
try container.encodeIfPresent(inverted, forKey: .inverted)
}
}

View File

@ -6,6 +6,10 @@
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
public protocol RadioButtonSelectionHelperProtocol: AnyObject {
var isSelected: Bool { get set }
var radioButtonModel: RadioButtonModel { get }
}
@objcMembers public class RadioButtonSelectionHelper: FormFieldProtocol {
//--------------------------------------------------
@ -14,7 +18,7 @@
public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName
private var selectedRadioButton: RadioButton?
private var selectedRadioButton: RadioButtonSelectionHelperProtocol?
private var selectedRadioButtonModel: RadioButtonModel?
public var baseValue: AnyHashable?
public var enabled: Bool = true
@ -24,7 +28,7 @@
// MARK: - Initializer
//--------------------------------------------------
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) {
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol) {
self.fieldKey = radioButtonModel.fieldKey
self.groupName = radioButtonModel.groupName
@ -49,7 +53,7 @@
// MARK: - Functions
//--------------------------------------------------
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton, delegateObject: MVMCoreUIDelegateObject?) {
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButtonSelectionHelperProtocol, delegateObject: MVMCoreUIDelegateObject?) {
guard let groupName = radioButtonModel.fieldKey,
let formValidator = delegateObject?.formHolderDelegate?.formValidator
@ -61,10 +65,10 @@
FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate)
}
public func selected(_ radioButton: RadioButton) {
public func selected(_ radioButton: RadioButtonSelectionHelperProtocol) {
// Checks because the view could be reused
if selectedRadioButton?.radioModel === selectedRadioButtonModel {
if selectedRadioButton?.radioButtonModel === selectedRadioButtonModel {
selectedRadioButton?.isSelected = false
} else {
selectedRadioButtonModel?.state = false
@ -72,7 +76,7 @@
selectedRadioButton = radioButton
selectedRadioButton?.isSelected = true
selectedRadioButtonModel = selectedRadioButton?.radioModel
selectedRadioButtonModel = selectedRadioButton?.radioButtonModel
}
}

View File

@ -81,9 +81,7 @@
func updateAccessibilityLabel() {
var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", "
}

View File

@ -98,9 +98,7 @@ import UIKit
func updateAccessibilityLabel() {
var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", "
}

View File

@ -85,7 +85,6 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell {
var message = ""
radioButton.updateAccessibilityLabel()
if let radioButtonLabel = radioButton.accessibilityLabel {
message += radioButtonLabel + ", "
}

View File

@ -7,54 +7,121 @@
//
import UIKit
import VDS
@objcMembers public class RadioButtonLabel: VDS.RadioButtonItem, RadioButtonSelectionHelperProtocol, VDSMoleculeViewProtocol, MFButtonProtocol {
@objcMembers public class RadioButtonLabel: View {
public let radioButton = RadioButton()
var delegateObject: MVMCoreUIDelegateObject?
let label = Label()
public override func updateView(_ size: CGFloat) {
super.updateView(size)
radioButton.updateView(size)
label.updateView(size)
}
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
open var viewModel: RadioButtonLabelModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
open override func setupView() {
super.setupView()
open var radioButtonModel: RadioButtonModel {
viewModel.radioButton
}
addSubview(radioButton)
radioButton.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: 0).isActive = true
radioButton.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: PaddingOne).isActive = true
layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: radioButton.bottomAnchor, constant: PaddingOne).isActive = true
radioButton.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor).isActive = true
// Form Validation
var fieldKey: String?
var fieldValue: JSONValue?
var groupName: String?
if let rightView = createRightView() {
addSubview(rightView)
rightView.leftAnchor.constraint(equalTo: radioButton.rightAnchor, constant: Padding.Component.gutterForApplicationWidth).isActive = true
rightView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: 0).isActive = true
var constraint = rightView.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: PaddingOne)
constraint.priority = .defaultHigh
constraint.isActive = true
constraint = layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: rightView.bottomAnchor, constant: PaddingOne)
constraint.priority = .defaultHigh
constraint.isActive = true
layoutMarginsGuide.centerYAnchor.constraint(equalTo: rightView.centerYAnchor).isActive = true
open override var isSelected: Bool {
didSet {
radioButtonModel.state = isSelected
if oldValue != isSelected {
sendActions(for: .valueChanged)
}
}
}
func createRightView() -> Container? {
let rightView = Container(andContain: label)
return rightView
lazy public var radioGroupName: String? = { viewModel.radioButton.fieldKey }()
lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = {
guard let radioGroupName = radioGroupName,
let radioButtonModel = delegateObject?.formHolderDelegate?.formValidator?.radioButtonsModelByGroup[radioGroupName]
else { return nil }
return radioButtonModel
}()
//--------------------------------------------------
// MARK: - Life Cycle
//--------------------------------------------------
@objc open func updateView(_ size: CGFloat) {}
open override func toggle() {
guard !isSelected, isEnabled else { return }
//removed error
if showError && isSelected == false {
showError.toggle()
}
let wasPreviouslySelected = isSelected
if let radioButtonSelectionHelper {
radioButtonSelectionHelper.selected(self)
} else {
isSelected = !isSelected
}
if let actionModel = viewModel.radioButton.action, isSelected, !wasPreviouslySelected {
Task(priority: .userInitiated) {
try await Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: viewModel)
}
}
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let radioButtonLabelModel = model as? RadioButtonLabelModel else { return }
radioButton.set(with: radioButtonLabelModel.radioButton, delegateObject, additionalData)
label.set(with: radioButtonLabelModel.label, delegateObject, additionalData)
//--------------------------------------------------
// MARK: - Atomic
//--------------------------------------------------
public func viewModelDidUpdate() {
surface = viewModel.surface
updateRadioButton()
//primary label
labelText = viewModel.label.text
if let attributes = viewModel.label.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
labelTextAttributes = attributes
}
//secondary label
if let subTitleModel = viewModel.subTitle {
childText = subTitleModel.text
if let attributes = subTitleModel.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
childTextAttributes = attributes
}
}
}
public func updateRadioButton() {
if let fieldKey = viewModel.radioButton.fieldKey {
self.fieldKey = fieldKey
}
//properties
isEnabled = viewModel.radioButton.enabled && !viewModel.radioButton.readOnly
isSelected = viewModel.radioButton.state
//forms
RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel.radioButton, self, delegateObject: delegateObject)
//events
viewModel.radioButton.updateUI = {
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
guard let self = self else { return }
let isValid = viewModel.radioButton.isValid ?? true
showError = !isValid
errorText = viewModel.radioButton.errorMessage
isEnabled = viewModel.radioButton.enabled
})
}
}
}

View File

@ -8,8 +8,9 @@
import Foundation
import MVMCore
import VDS
@objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol {
@objcMembers public class RadioButtonLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -21,14 +22,23 @@ import MVMCore
public var moleculeName: String = RadioButtonLabelModel.identifier
public var radioButton: RadioButtonModel
public var label: LabelModel
public var subTitle: LabelModel?
public var inverted: Bool? = false
public var surface: Surface { inverted ?? false ? .dark : .light }
public var children: [MoleculeModelProtocol] {
guard let subTitle else { return [radioButton, label] }
return [radioButton, label, subTitle]
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(radioButton: RadioButtonModel, label: LabelModel) {
public init(radioButton: RadioButtonModel, label: LabelModel, subTitle: LabelModel?) {
self.radioButton = radioButton
self.label = label
self.subTitle = subTitle
}
}