add new tabs

This commit is contained in:
Xinlei(Ryan) Pan 2020-04-03 12:03:13 -04:00
parent 7440a3c665
commit 1e67404748
5 changed files with 395 additions and 27 deletions

View File

@ -173,6 +173,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 */; };
@ -566,6 +567,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>"; };
@ -1169,6 +1171,7 @@
D28A838E23CCDEDE00DFE4FC /* TwoButtonViewModel.swift */,
D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */,
D28A837E23CCA96400DFE4FC /* TabsModel.swift */,
94F6516C2437954100631BF9 /* Tabs.swift */,
011D9625240EBB16000E3791 /* RadioButtonLabelModel.swift */,
017BEB372360C6AC0024EF95 /* RadioButtonLabel.swift */,
);
@ -2042,6 +2045,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 */,
0A7EF85B23D8A52800B2AAD1 /* EntryFieldModel.swift in Sources */,

View File

@ -85,6 +85,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,343 @@
//
// 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?
//delegate
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 = .mfTomatoRed()
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)
}
func getLabelWidth(_ labelModel: LabelModel?) -> CGSize {
guard let labelModel = labelModel else { return .zero}
let label = Label()
label.set(with: labelModel, nil, nil)
return label.intrinsicContentSize
}
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) {
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()
}
}
public func progress(fromIndex: Int, toIndex: Int, progressPercentage percent: CGFloat) {
guard let _ = collectionView else { return }
MVMCoreDispatchUtility.performBlock(onMainThread: {
self.setBottomLine(fromIndex: IndexPath(row: fromIndex, section: 0), toIndex: IndexPath(row: toIndex, section: 0), percent: percent)
})
}
func setBottomLine(fromIndex: IndexPath, toIndex: IndexPath, percent: CGFloat) {
guard let fromCell = collectionView?.cellForItem(at: fromIndex) as? TabItemCell, let toCell = collectionView?.cellForItem(at: toIndex) as? TabItemCell else {
return
}
let fromWidth = getLabelWidth(fromCell.labelModel).width
let toWidth = getLabelWidth(toCell.labelModel).width
let finalWidth = (toWidth - fromWidth) * percent + fromWidth
bottomLineWidthConstraint?.constant = finalWidth
let xDifference = toCell.frame.origin.x - fromCell.frame.origin.x
let finalX = xDifference * percent + fromCell.frame.origin.x
bottomLineLeftConstraint?.constant = finalX
bottomContentView.layoutIfNeeded()
}
}
@objcMembers public class TabItemCell: UICollectionViewCell {
public let label = Label()
public var labelModel: LabelModel?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
func 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
}
}