This commit is contained in:
Kevin G Christiano 2020-04-28 08:37:03 -04:00
commit a97e6ae7a1
7 changed files with 373 additions and 29 deletions

View File

@ -188,6 +188,7 @@
94CA227F24058534002D6750 /* VerizonNHGeTX-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 94CA227B24058533002D6750 /* VerizonNHGeTX-Regular.otf */; };
94F217B623E0BF6100A47C06 /* PrimaryButtonView.h in Headers */ = {isa = PBXBuildFile; fileRef = 94F217B423E0BF6100A47C06 /* PrimaryButtonView.h */; settings = {ATTRIBUTES = (Public, ); }; };
94F217B723E0BF6100A47C06 /* PrimaryButtonView.m in Sources */ = {isa = PBXBuildFile; fileRef = 94F217B523E0BF6100A47C06 /* PrimaryButtonView.m */; };
94F6516D2437954100631BF9 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F6516C2437954100631BF9 /* Tabs.swift */; };
94FB966223D797DA003D482B /* MFTextButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 94FB966023D797DA003D482B /* MFTextButton.h */; settings = {ATTRIBUTES = (Public, ); }; };
94FB966323D797DA003D482B /* MFTextButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 94FB966123D797DA003D482B /* MFTextButton.m */; };
AA11A41F23F15D3100D7962F /* ListRightVariablePayments.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11A41E23F15D3100D7962F /* ListRightVariablePayments.swift */; };
@ -641,6 +642,7 @@
94CA227B24058533002D6750 /* VerizonNHGeTX-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGeTX-Regular.otf"; sourceTree = "<group>"; };
94F217B423E0BF6100A47C06 /* PrimaryButtonView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PrimaryButtonView.h; sourceTree = "<group>"; };
94F217B523E0BF6100A47C06 /* PrimaryButtonView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrimaryButtonView.m; sourceTree = "<group>"; };
94F6516C2437954100631BF9 /* Tabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = "<group>"; };
94FB966023D797DA003D482B /* MFTextButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MFTextButton.h; sourceTree = "<group>"; };
94FB966123D797DA003D482B /* MFTextButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MFTextButton.m; sourceTree = "<group>"; };
AA11A41E23F15D3100D7962F /* ListRightVariablePayments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariablePayments.swift; sourceTree = "<group>"; };
@ -1334,6 +1336,7 @@
D28A838E23CCDEDE00DFE4FC /* TwoButtonViewModel.swift */,
D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */,
D28A837E23CCA96400DFE4FC /* TabsModel.swift */,
94F6516C2437954100631BF9 /* Tabs.swift */,
011D9625240EBB16000E3791 /* RadioButtonLabelModel.swift */,
017BEB372360C6AC0024EF95 /* RadioButtonLabel.swift */,
);
@ -2276,6 +2279,7 @@
01509D952327ED1900EF99AA /* HeadlineBodyLinkToggle.swift in Sources */,
31BE15CB23D8924D00452370 /* CheckboxLabelModel.swift in Sources */,
D29DF13021E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m in Sources */,
94F6516D2437954100631BF9 /* Tabs.swift in Sources */,
5248BFEC23F12E350059236A /* ListThreeColumnPlanDataDivider.swift in Sources */,
0ABD136D237CAD1E0081388D /* DateDropdownEntryField.swift in Sources */,
D264FA8E243BCD9A00D98315 /* CollectionTemplate.swift in Sources */,

View File

@ -41,7 +41,7 @@ import UIKit
// MARK: - Delegate
//--------------------------------------------------
weak var delegateObject: MVMCoreUIDelegateObject?
var delegateObject: MVMCoreUIDelegateObject?
//--------------------------------------------------
// MARK: - Stored Properties

View File

@ -88,6 +88,7 @@ import Foundation
// Horizontal Combination Molecules
MoleculeObjectMapping.shared()?.register(viewClass: StringAndMoleculeView.self, viewModelClass: StringAndMoleculeModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: ImageHeadlineBody.self, viewModelClass: ImageHeadlineBodyModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: Tabs.self, viewModelClass: TabsModel.self)
// Vertical Combination Molecules
MoleculeObjectMapping.shared()?.register(viewClass: HeadlineBody.self, viewModelClass: HeadlineBodyModel.self)

View File

@ -0,0 +1,320 @@
//
// Tabs.swift
// MVMCoreUI
//
// Created by Ryan on 2/7/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
@objc public protocol TabsDelegate {
func shouldSelectItem(_ indexPath: IndexPath, tabs: Tabs) -> Bool
func didSelectItem(_ indexPath: IndexPath, tabs: Tabs)
}
@objcMembers open class Tabs: View, MVMCoreUIViewConstrainingProtocol {
public var tabsModel: TabsModel? {
get { return model as? TabsModel }
}
var delegateObject: MVMCoreUIDelegateObject?
var additionalData: [AnyHashable: Any]?
let layout = UICollectionViewFlowLayout()
public var collectionView: UICollectionView?
let bottomScrollView = UIScrollView(frame: .zero)
let bottomContentView = View()
let bottomLine = View()
var bottomLineLeftConstraint: NSLayoutConstraint?
var bottomLineWidthConstraint: NSLayoutConstraint?
private var widthLabel = Label()
//delegate
weak public var delegate: TabsDelegate?
//control var
public var heightConstraint: NSLayoutConstraint?
public var selectedIndex: Int = 0
public var paddingBeforeFirstTab: Bool = true
//constant
let TabCellId = "TabCell"
public let sectionPadding: CGFloat = 20.0
public let cellSpacing: CGFloat = 34.0
public let cellHeight: CGFloat = 34.0
public let bottomLineHeight: CGFloat = 4.0
public let bottomLineWidth: CGFloat = 32.0
public let tabsHeight: CGFloat = 38.0
public let bottomLineMovingTime: TimeInterval = 0.2
//-------------------------------------------------
// MARK:- Layout Views
//-------------------------------------------------
open override func reset() {
super.reset()
heightConstraint?.constant = tabsHeight
selectedIndex = 0
paddingBeforeFirstTab = true
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
}
open override func setupView() {
super.setupView()
backgroundColor = .white
setupCollectionView()
setupBottomLine()
setupConstraints()
}
func setupCollectionView () {
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = cellSpacing
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(TabItemCell.self, forCellWithReuseIdentifier: TabCellId)
collectionView.backgroundColor = .clear
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.dataSource = self
collectionView.delegate = self
addSubview(collectionView)
self.collectionView = collectionView
}
func setupBottomLine() {
bottomScrollView.translatesAutoresizingMaskIntoConstraints = false
bottomScrollView.delegate = self
addSubview(bottomScrollView)
bottomScrollView.addSubview(bottomContentView)
bottomLine.backgroundColor = .mvmRed
bottomContentView.addSubview(bottomLine)
bringSubviewToFront(bottomScrollView)
}
func setupConstraints() {
//collection view
NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)
//bottom lines
NSLayoutConstraint.constraintPinSubview(bottomScrollView, pinTop: false, pinBottom: true, pinLeft: true, pinRight: true)
bottomScrollView.heightAnchor.constraint(equalToConstant: bottomLineHeight).isActive = true
NSLayoutConstraint.constraintPinSubview(bottomLine, pinTop: true, pinBottom: true, pinLeft: false, pinRight: false)
bottomLine.heightAnchor.constraint(equalToConstant: bottomLineHeight).isActive = true
bottomLineLeftConstraint = bottomLine.leftAnchor.constraint(equalTo: bottomContentView.leftAnchor)
bottomLineLeftConstraint?.isActive = true
bottomLineWidthConstraint = bottomLine.widthAnchor.constraint(equalToConstant: bottomLineWidth)
bottomLineWidthConstraint?.isActive = true
NSLayoutConstraint.constraintPinSubview(toSuperview: bottomContentView)
//height
heightConstraint = heightAnchor.constraint(equalToConstant: tabsHeight)
heightConstraint?.isActive = true
}
//-------------------------------------------------
// MARK:- Control Methods
//-------------------------------------------------
public func pinHeight(_ height: CGFloat) {
heightConstraint?.constant = height
setNeedsLayout()
layoutIfNeeded()
}
public func selectIndex(_ index: Int, animated: Bool) {
guard let _ = collectionView, tabsModel?.tabs.count ?? 0 > 0 else {
selectedIndex = index
return
}
MVMCoreDispatchUtility.performBlock(onMainThread: {
let currentIndex = self.selectedIndex
self.selectedIndex = index
self.deselect(indexPath: IndexPath(row: currentIndex, section: 0))
self.selectItem(atIndexPath: IndexPath(row: index, section: 0), animated: animated)
})
}
public func reloadData() {
collectionView?.reloadData()
}
//-------------------------------------------------
// MARK:- Molecule Setup
//-------------------------------------------------
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
self.additionalData = additionalData
self.selectedIndex = tabsModel?.selectedIndex ?? 0
self.bottomLine.backgroundColor = tabsModel?.selectedColor.uiColor
reloadData()
}
}
//-------------------------------------------------
// MARK:- Collection View Methods
//-------------------------------------------------
extension Tabs: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tabsModel?.tabs.count ?? 0
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let labelModel = tabsModel?.tabs[indexPath.row].label, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCellId, for: indexPath) as? TabItemCell else {
return UICollectionViewCell()
}
cell.updateCell(labelModel: labelModel, indexPath: indexPath, delegateObject: delegateObject, additionalData: additionalData, selected: indexPath.row == selectedIndex, tabsModel: tabsModel)
return cell
}
}
extension Tabs: UICollectionViewDelegateFlowLayout {
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
guard let labelModel = tabsModel?.tabs[indexPath.row].label else {
return .zero
}
return CGSize(width: getLabelWidth(labelModel).width, height: cellHeight)
}
//pre calculate the width of the collection cell
//when user select tabs, it will reload related collectionview, if we use autosize, it would relayout the width, need to keep the cell width constant.
func getLabelWidth(_ labelModel: LabelModel?) -> CGSize {
guard let labelModel = labelModel else { return .zero}
widthLabel.set(with: labelModel, nil, nil)
let cgSize = widthLabel.intrinsicContentSize
widthLabel.reset()
return cgSize
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if !paddingBeforeFirstTab && section == 0 {
return .zero
} else {
return UIEdgeInsets(top: 0, left: sectionPadding, bottom: 0, right: 0)
}
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return sectionPadding
}
public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return delegate?.shouldSelectItem(indexPath, tabs: self) ?? true
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectIndex(indexPath.row, animated: true)
}
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let tabCell = cell as? TabItemCell else { return }
if indexPath.row == selectedIndex {
moveBottomLine(toIndex: indexPath, animated: false, cell: tabCell)
}
}
func deselect(indexPath:IndexPath) {
collectionView?.deselectItem(at: indexPath, animated: false)
collectionView?.reloadItems(at: [indexPath])
}
func selectItem(atIndexPath indexPath: IndexPath, animated: Bool) {
guard let collect = collectionView, tabsModel?.tabs.count ?? 0 > 0 else { return }
collect.selectItem(at: indexPath, animated: animated, scrollPosition: .centeredHorizontally)
guard let tabCell = collect.cellForItem(at: indexPath) as? TabItemCell, let tabsModel = self.tabsModel else { return }
self.moveBottomLine(toIndex: indexPath, animated: animated, cell: tabCell)
tabCell.label.textColor = tabsModel.selectedColor.uiColor
tabCell.updateAccessibility(indexPath: indexPath, selected: true, tabsModel: tabsModel)
tabCell.setNeedsDisplay()
tabCell.setNeedsLayout()
tabCell.layoutIfNeeded()
self.delegate?.didSelectItem(indexPath, tabs: self)
}
}
extension Tabs: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
/*bottomScrollview is subview of self, it's not belongs to collectionview.
When collectionview is scrolling, bottomScrollView will stay without moving
Adding collectionview's offset to bottomScrollView, will make the bottomScrollview looks like scrolling with the selected tab item.
*/
guard let offsetX = collectionView?.contentOffset.x else { return }
bottomScrollView.setContentOffset(CGPoint(x: offsetX, y: bottomScrollView.contentOffset.y), animated: false)
}
}
//-------------------------------------------------
// MARK:- Bottom Line Methods
//-------------------------------------------------
extension Tabs {
func moveBottomLine(toIndex indexPath: IndexPath, animated: Bool, cell: TabItemCell) {
guard let collect = self.collectionView else {return}
let size = collectionView(collect, layout: layout, sizeForItemAt: indexPath)
let barWidth = max(size.width, bottomLineWidth)
let animationBlock = {
[weak self] in
let x = cell.frame.origin.x
self?.bottomLineWidthConstraint?.constant = barWidth
self?.bottomLineLeftConstraint?.constant = x + (size.width - barWidth) / 2.0
self?.bottomContentView.layoutIfNeeded()
}
if animated {
UIView.animate(withDuration: bottomLineMovingTime, animations: animationBlock)
} else {
animationBlock()
}
}
}
@objcMembers public class TabItemCell: CollectionViewCell {
public let label = Label()
public var labelModel: LabelModel?
public override func setupView() {
super.setupView()
contentView.addSubview(label)
NSLayoutConstraint.constraintPinSubview(label, pinTop: false, pinBottom: false, pinLeft: true, pinRight: true)
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
label.baselineAdjustment = .alignCenters
}
public func updateCell(labelModel: LabelModel, indexPath: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?, selected: Bool, tabsModel: TabsModel?) {
label.reset()
label.set(with: labelModel, delegateObject, additionalData)
self.labelModel = labelModel
if selected, let selectedColor = tabsModel?.selectedColor {
label.textColor = selectedColor.uiColor
}
updateAccessibility(indexPath: indexPath, selected: selected, tabsModel: tabsModel)
}
public func updateAccessibility(indexPath: IndexPath, selected: Bool, tabsModel: TabsModel?) {
//Accessibility
isAccessibilityElement = false
contentView.isAccessibilityElement = true
let accKey = selected ? "toptabbar_tab_selected" : "AccTab"
let accLabel = "\(label.text ?? "") \(MVMCoreUIUtility.hardcodedString(withKey: accKey) ?? "")"
let accOrder = String(format: MVMCoreUIUtility.hardcodedString(withKey: "AccTabIndex") ?? "", indexPath.row + 1, tabsModel?.tabs.count ?? 0)
contentView.accessibilityLabel = "\(accLabel) \(accOrder)"
contentView.accessibilityHint = selected ? nil : MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
}
}

View File

@ -11,7 +11,7 @@ import UIKit
public class TabsModel: MoleculeModelProtocol {
public static var identifier: String = "tabs"
public var backgroundColor: Color?
public var tabs: [LabelModel]
public var tabs: [TabItemModel]
public var selectedColor = Color(uiColor: .mfTomatoRed())
// Must be capped to 0...(tabs.count - 1)
@ -25,13 +25,13 @@ public class TabsModel: MoleculeModelProtocol {
case moleculeName
}
public init(with tabs: [LabelModel]) {
public init(with tabs: [TabItemModel]) {
self.tabs = tabs
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
tabs = try typeContainer.decode([LabelModel].self, forKey: .tabs)
tabs = try typeContainer.decode([TabItemModel].self, forKey: .tabs)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedColor) {
selectedColor = color
@ -50,3 +50,33 @@ public class TabsModel: MoleculeModelProtocol {
try container.encode(selectedIndex, forKey: .selectedIndex)
}
}
public class TabItemModel: Codable {
var label: LabelModel
var action: ActionModelProtocol?
init(label: LabelModel) {
self.label = label
}
private enum CodingKeys: String, CodingKey {
case label
case action
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
label = try typeContainer.decode(LabelModel.self, forKey: .label)
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeModel(label, forKey: .label)
try container.encodeModelIfPresent(action, forKey: .action)
}
}

View File

@ -12,7 +12,7 @@ import UIKit
var tabsListItemModel: TabsListItemModel? {
return listItemModel as? TabsListItemModel
}
let tabs = TopTabbar(frame: .zero)
let tabs = Tabs(frame: .zero)
var delegateObject: MVMCoreUIDelegateObject?
var previousTabIndex = 0
@ -22,7 +22,6 @@ import UIKit
tabs.paddingBeforeFirstTab = false
tabs.translatesAutoresizingMaskIntoConstraints = false
tabs.delegate = self
tabs.datasource = self
contentView.addSubview(tabs)
NSLayoutConstraint.activate(Array(NSLayoutConstraint.pinView(toSuperview: tabs, useMargins: true).values))
@ -39,7 +38,9 @@ import UIKit
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
tabs.reloadData()
if let tabsModel = tabsListItemModel?.tabs {
tabs.set(with: tabsModel, delegateObject, additionalData)
}
}
public override func reset() {
@ -53,33 +54,22 @@ import UIKit
}
}
extension TabsTableViewCell: TopTabbarDelegate {
public func shouldSelectItem(at index: Int, topTabbar: TopTabbar) -> Bool {
extension TabsTableViewCell: TabsDelegate {
public func shouldSelectItem(_ indexPath: IndexPath, tabs: Tabs) -> Bool {
if let model = tabsListItemModel {
let molecules = model.molecules[topTabbar.selectedIndex]
delegateObject?.moleculeDelegate?.removeMolecules(molecules, animation: index < tabs.selectedIndex ? .right : .left)
let molecules = model.molecules[tabs.selectedIndex]
delegateObject?.moleculeDelegate?.removeMolecules(molecules, animation: indexPath.row < tabs.selectedIndex ? .right : .left)
}
previousTabIndex = tabs.selectedIndex
return true
}
public func topTabbar(_ topTabbar: TopTabbar, didSelectItemAt index: Int) {
guard let model = tabsListItemModel,
let indexPath = delegateObject?.moleculeDelegate?.getIndexPath(for: model) else { return }
let molecules = model.molecules[index]
delegateObject?.moleculeDelegate?.addMolecules(molecules, indexPath: indexPath, animation: index < previousTabIndex ? .left : .right)
}
}
extension TabsTableViewCell: TopTabbarDataSource {
public func number(ofTopTabbarItems topTabbar: TopTabbar) -> Int {
return tabsListItemModel?.tabs.tabs.count ?? 0
}
public func topTabbar(_ topTabbar: TopTabbar, titleForItemAt index: Int) -> String? {
guard let title = tabsListItemModel?.tabs.tabs[index].text else {
return "Select"
public func didSelectItem(_ indexPath: IndexPath, tabs: Tabs) {
let index = indexPath.row
if let model = tabsListItemModel, index < model.molecules.count {
let molecules = model.molecules[index]
delegateObject?.moleculeDelegate?.addMolecules(molecules, indexPath: indexPath, animation: index < previousTabIndex ? .left : .right)
}
return title
}
}

View File

@ -307,7 +307,6 @@
weakSelf.buttonView.label.accessibilityLabel = [NSString stringWithFormat:@"%@ - %@", [MVMCoreUIUtility hardcodedStringWithKey:@"top_alert_notification"],weakSelf.buttonView.label.accessibilityLabel];
void(^completion)(void) = ^(void) {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, weakSelf.buttonView.label);
[operation markAsFinished];
};