Merge branch 'develop' into feature/tags_items_view

* develop:
  reuse
  add check for single hiding pager
  Add action to collection cells make paging configurable for carousel. add insets to carousel use button helper function for collection and table cell.
  allow server override
  move model update
  set model as well
  fix to anchor
  remove images from core ui
  Add line.
  Fix for defect CXTDT-83977: rightLabel not aligned with headline.
  Tabs update
  Setup listener for tab response
  view will appear tab change thread protection
  fix to bottom anchor
  tab bar selection logic
  change bar button item to convenience function
  Tab updates
  Demo
This commit is contained in:
Damodaram 2020-06-05 15:40:33 +05:30
commit c9775376d3
25 changed files with 564 additions and 145 deletions

View File

@ -337,6 +337,10 @@
D28A839123CD4FD400DFE4FC /* CornerLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28A839023CD4FD400DFE4FC /* CornerLabelsModel.swift */; };
D28A839323CE828900DFE4FC /* HeadlineBodyCaretLinkImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28A839223CE828900DFE4FC /* HeadlineBodyCaretLinkImageModel.swift */; };
D28BA730247EC2EB00B75CB8 /* NavigationButtomModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA72F247EC2EB00B75CB8 /* NavigationButtomModelProtocol.swift */; };
D28BA741248025A300B75CB8 /* TabBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA740248025A300B75CB8 /* TabBarModel.swift */; };
D28BA7432480284E00B75CB8 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA7422480284E00B75CB8 /* TabBar.swift */; };
D28BA7452481652D00B75CB8 /* TabBarProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */; };
D28BA74D248589C800B75CB8 /* TabPageModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */; };
D296E14722A5984C0051EBE7 /* MVMCoreUIViewConstrainingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */; };
D29C94D5242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */; };
@ -774,6 +778,10 @@
D28A839023CD4FD400DFE4FC /* CornerLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerLabelsModel.swift; sourceTree = "<group>"; };
D28A839223CE828900DFE4FC /* HeadlineBodyCaretLinkImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyCaretLinkImageModel.swift; sourceTree = "<group>"; };
D28BA72F247EC2EB00B75CB8 /* NavigationButtomModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtomModelProtocol.swift; sourceTree = "<group>"; };
D28BA740248025A300B75CB8 /* TabBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarModel.swift; sourceTree = "<group>"; };
D28BA7422480284E00B75CB8 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarProtocol.swift; sourceTree = "<group>"; };
D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageModelProtocol.swift; sourceTree = "<group>"; };
D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = "<group>"; };
D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = "<group>"; };
D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = "<group>"; };
@ -910,6 +918,7 @@
D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */,
D2092354244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift */,
D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */,
D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */,
);
path = ModelProtocols;
sourceTree = "<group>";
@ -1235,6 +1244,8 @@
017BEB372360C6AC0024EF95 /* RadioButtonLabel.swift */,
D28764FA245A33A500CB882D /* TwoLinkViewModel.swift */,
D28764F8245A327200CB882D /* TwoLinkView.swift */,
D28BA740248025A300B75CB8 /* TabBarModel.swift */,
D28BA7422480284E00B75CB8 /* TabBar.swift */,
);
path = HorizontalCombinationViews;
sourceTree = "<group>";
@ -1892,6 +1903,7 @@
012A88C7238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift */,
017BEB47236230DB0024EF95 /* MoleculeViewProtocol.swift */,
012A88AC238C418100FE3DA1 /* TemplateProtocol.swift */,
D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */,
011B58EE23A2AA850085F53C /* ModelProtocols */,
);
path = Protocols;
@ -2042,6 +2054,7 @@
BBC0C4FF24811DCA0087C44F /* TagModel.swift in Sources */,
0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */,
D2FB151D23A40F1500C20E10 /* MoleculeStackItem.swift in Sources */,
D28BA7452481652D00B75CB8 /* TabBarProtocol.swift in Sources */,
AA11A41F23F15D3100D7962F /* ListRightVariablePayments.swift in Sources */,
D28764AA2458980300CB882D /* ThreeLayerFillMiddleTemplate.swift in Sources */,
0116A4E5228B19640094F3ED /* RadioButtonSelectionHelper.swift in Sources */,
@ -2109,6 +2122,7 @@
BB1D17E0244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift in Sources */,
D29DF2CF21E7C104003B2FB9 /* MFLoadingViewController.m in Sources */,
D28A837B23C928DA00DFE4FC /* MoleculeListCellProtocol.swift in Sources */,
D28BA74D248589C800B75CB8 /* TabPageModelProtocol.swift in Sources */,
014AA72F23C5059B006F3E93 /* ThreeLayerPageTemplateModel.swift in Sources */,
0A21DB91235E0EDB00C160A2 /* DigitBox.swift in Sources */,
BBAA4F04243D8E3B005AAD5F /* RadioBoxModel.swift in Sources */,
@ -2287,6 +2301,7 @@
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,
AA2AD118244EE48C00BBFFE3 /* ListDeviceComplexLinkMediumModel.swift in Sources */,
DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */,
D28BA741248025A300B75CB8 /* TabBarModel.swift in Sources */,
D224798A2314445E003FCCF9 /* LabelToggle.swift in Sources */,
D2A92882241AAB67004E01C6 /* ScrollingViewController.swift in Sources */,
C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */,
@ -2387,6 +2402,7 @@
0AB764D324460FA400E7FE72 /* UIPickerView+Extension.swift in Sources */,
D29DF29E21E7AE3B003B2FB9 /* MFStyler.m in Sources */,
94C661D923CCF4B400D9FE5B /* LeftRightLabelModel.swift in Sources */,
D28BA7432480284E00B75CB8 /* TabBar.swift in Sources */,
AA26850C244840AE00CE34CC /* HeadersH2TinyButton.swift in Sources */,
011D95AB2405C553000E3791 /* FormItemProtocol.swift in Sources */,
D21EE53C23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift in Sources */,

View File

@ -206,6 +206,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
isEnabled = model.enabled
formatAccessibilityValue(index: currentIndex + 1, total: numberOfPages)
isHidden = model.hidesForSinglePage && numberOfPages <= 1
}
//--------------------------------------------------

View File

@ -33,7 +33,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
public var disabledIndicatorColor: Color = Color(uiColor: .mvmCoolGray3)
public var indicatorColor: Color = Color(uiColor: .mvmBlack)
public var indicatorColor_inverted: Color = Color(uiColor: .mvmWhite)
public var position: Float?
public var position: CGFloat?
/// Allows sendActions() to trigger even if index is already at min/max index.
public var alwaysSendAction = false
@ -79,7 +79,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro
self.inverted = inverted
}
if let position = try typeContainer.decodeIfPresent(Float.self, forKey: .position) {
if let position = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .position) {
self.position = position
}

View File

@ -14,15 +14,17 @@ import Foundation
public let leftImage = LoadImageView()
public let headlineBody = HeadlineBody()
public let rightLabel = Label.createLabelRegularBodySmall(true)
public let rightLabelStackItem: StackItem
public var stack: Stack<StackModel>
//-----------------------------------------------------
// MARK: - Initializers
//-----------------------------------------------------
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
stack = Stack<StackModel>.createStack(with: [(view: leftImage, model: StackItemModel(horizontalAlignment: .fill)),
(view: headlineBody, model: StackItemModel(horizontalAlignment: .leading)),
(view: rightLabel, model: StackItemModel(horizontalAlignment: .fill, verticalAlignment: .leading))], axis: .horizontal)
rightLabelStackItem = StackItem(andContain: rightLabel)
let stackItems = [StackItem(andContain: leftImage), StackItem(andContain: headlineBody), rightLabelStackItem]
let stackModel = StackModel(molecules: [StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill)], axis: .horizontal)
stack = Stack<StackModel>(with: stackModel, stackItems: stackItems)
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
@ -30,6 +32,16 @@ import Foundation
fatalError("init(coder:) has not been implemented")
}
open override func alignAccessoryToHero() -> CGPoint? {
// Ensures that the right label is centered vertically with headline.
let heroCenter = super.alignAccessoryToHero()
if let heroCenter = heroCenter {
let convertedPoint = stack.convert(heroCenter, from: self)
rightLabelStackItem.containerHelper.alignCenterVerticalConstraint?.constant = convertedPoint.y - stack.bounds.midY
}
return heroCenter
}
//-----------------------------------------------------
// MARK: - View Lifecycle
//-------------------------------------------------------

View File

@ -0,0 +1,101 @@
//
// TabBar.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 5/28/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers open class TabBar: UITabBar, MoleculeViewProtocol, TabBarProtocol, UITabBarDelegate {
public var model: TabBarModel
public var delegateObject: MVMCoreUIDelegateObject?
public let line = Line()
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let model = model as? TabBarModel else {
fatalError("model is not TabBarModel")
}
self.model = model
super.init(frame: .zero)
delegate = self
translatesAutoresizingMaskIntoConstraints = false
line.addLine(to: self, edge: .top, useMargin: false)
line.backgroundColor = .mvmCoolGray3
set(with: model, delegateObject, additionalData)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
guard let model = model as? TabBarModel else { return }
self.model = model
// Set appearance
if #available(iOS 13.0, *) {
let appearance = UITabBarAppearance()
appearance.backgroundColor = model.backgroundColor?.uiColor
setTabBarItemColors(appearance.stackedLayoutAppearance, model: model)
setTabBarItemColors(appearance.inlineLayoutAppearance, model: model)
setTabBarItemColors(appearance.compactInlineLayoutAppearance, model: model)
standardAppearance = appearance
} else {
// Fallback on earlier versions
backgroundColor = model.backgroundColor?.uiColor
tintColor = model.selectedColor.uiColor
unselectedItemTintColor = model.unSelectedColor.uiColor
barTintColor = model.backgroundColor?.uiColor
isTranslucent = false
}
// Add buttons
var tabs: [UITabBarItem] = []
for (index, tab) in model.tabs.enumerated() {
let tabBarItem = UITabBarItem(title: tab.title, image: UIImage(named: tab.image, in: MVMCoreCache.shared()?.bundleToUseForImages(), compatibleWith: nil), tag: index)
tabs.append(tabBarItem)
}
setItems(tabs, animated: false)
selectedItem = tabs[model.selectedTab]
}
/// Sets the item colors.
@available(iOS 13.0, *)
private func setTabBarItemColors(_ itemAppearance: UITabBarItemAppearance, model: TabBarModel) {
itemAppearance.normal.iconColor = model.unSelectedColor.uiColor
itemAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: model.unSelectedColor.uiColor]
itemAppearance.selected.iconColor = model.selectedColor.uiColor
itemAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: model.selectedColor.uiColor]
}
// MARK: - UITabBarDelegate
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
self.model.selectedTab = item.tag
Button.performButtonAction(with: model.tabs[item.tag].action, button: item, delegateObject: delegateObject, additionalData: nil)
}
// MARK: - TabBarProtocol
public func highlightTab(at index: Int) {
MVMCoreDispatchUtility.performBlock(onMainThread: {
guard let newSelectedItem = self.items?[index] else { return }
self.model.selectedTab = index
self.selectedItem = newSelectedItem
})
}
public func selectTab(at index: Int) {
MVMCoreDispatchUtility.performBlock(onMainThread: {
guard let newSelectedItem = self.items?[index] else { return }
self.selectedItem = newSelectedItem
self.tabBar(self, didSelect: newSelectedItem)
})
}
}
extension UITabBarItem: MFButtonProtocol {
}

View File

@ -0,0 +1,92 @@
//
// TabBarModel.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 5/28/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
public class TabBarModel: MoleculeModelProtocol {
public static var identifier: String = "tabBar"
public var backgroundColor: Color? = Color(uiColor: .white)
public var tabs: [TabBarItemModel]
public var selectedColor = Color(uiColor: .mvmBlack)
public var unSelectedColor = Color(uiColor: .mvmCoolGray3)
// Must be capped to 0...(tabs.count - 1)
public var selectedTab: Int = 0
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
case tabs
case selectedColor
case unSelectedColor
case selectedTab
}
public init(with tabs: [TabBarItemModel]) {
self.tabs = tabs
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
tabs = try typeContainer.decode([TabBarItemModel].self, forKey: .tabs)
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
backgroundColor = color
}
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .unSelectedColor) {
unSelectedColor = color
}
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedColor) {
selectedColor = color
}
if let index = try typeContainer.decodeIfPresent(Int.self, forKey: .selectedTab) {
selectedTab = index
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(tabs, forKey: .tabs)
try container.encode(backgroundColor, forKey: .backgroundColor)
try container.encode(selectedColor, forKey: .selectedColor)
try container.encode(unSelectedColor, forKey: .unSelectedColor)
try container.encode(selectedTab, forKey: .selectedTab)
}
}
public class TabBarItemModel: Codable {
var title: String
var image: String
var action: ActionModelProtocol
private enum CodingKeys: String, CodingKey {
case title
case image
case action
}
public init(with title: String, image: String, action: ActionModelProtocol) {
self.title = title
self.image = image
self.action = action
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
title = try typeContainer.decode(String.self, forKey: .title)
image = try typeContainer.decode(String.self, forKey: .image)
action = try typeContainer.decodeModel(codingKey: .action)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(image, forKey: .image)
try container.encodeModel(action, forKey: .action)
}
}

View File

@ -78,5 +78,4 @@ public class TabItemModel: Codable {
try container.encodeModel(label, forKey: .label)
try container.encodeModelIfPresent(action, forKey: .action)
}
}

View File

@ -13,6 +13,12 @@ import Foundation
open override class var identifier: String {
return "collectionItem"
}
public var action: ActionModelProtocol?
private enum CodingKeys: String, CodingKey {
case action
}
/// Defaults to set
public override func setDefaults() {
@ -35,10 +41,14 @@ import Foundation
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeModelIfPresent(action, forKey: .action)
try super.encode(to: encoder)
}
}

View File

@ -19,8 +19,14 @@ public protocol CarouselPageControlProtocol {
open class Carousel: View {
public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
public let collectionView: CollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
return CollectionView(frame: .zero, collectionViewLayout: layout)
}()
/// The current index of the collection view. Includes dummy cells when looping.
public var currentIndex = 0
@ -36,13 +42,13 @@ open class Carousel: View {
open var numberOfPages = 0
/// The models for the molecules.
var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]?
public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]?
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
public var itemAlignment = UICollectionView.ScrollPosition.left
/// From 0-1. The item width as a percent of the carousel width.
public var itemWidthPercent: Float = 1
public var itemWidthPercent: CGFloat = 1
/// The height of the carousel. Default is 300.
public var collectionViewHeight: NSLayoutConstraint?
@ -51,7 +57,7 @@ open class Carousel: View {
public var pagingView: (UIView & CarouselPageControlProtocol)?
/// If the carousel should loop after scrolling past the first and final cells.
var loop = false
public var loop = false
private var dragging = false
@ -81,6 +87,8 @@ open class Carousel: View {
showPeaking(false)
// Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled.
guard let model = model as? CarouselModel,
(model.paging == true || model.loop == true) else { return }
DispatchQueue.main.async {
self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: false)
self.collectionView.layoutIfNeeded()
@ -98,15 +106,23 @@ open class Carousel: View {
collectionView.delegate = self
addSubview(collectionView)
bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
collectionViewHeight?.isActive = false
collectionViewHeight?.isActive = true
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
self.size = size
// Set insets for the carousel.
var inset = UIEdgeInsets.zero
let carouselModel = model as? CarouselModel
if carouselModel?.useHorizontalMargins ?? false {
inset.left = carouselModel?.leftPadding ?? Padding.Component.horizontalPaddingForSize(size)
inset.right = carouselModel?.rightPadding ?? Padding.Component.horizontalPaddingForSize(size)
}
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset = inset
// Update cells and re-layout.
for cell in collectionView.visibleCells {
(cell as? MVMCoreViewProtocol)?.updateView(size)
@ -128,19 +144,19 @@ open class Carousel: View {
collectionView.layer.borderColor = backgroundColor?.cgColor
collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0
backgroundColor = .white
registerCells(with: carouselModel, delegateObject: delegateObject)
setupLayout(with: carouselModel)
prepareMolecules(with: carouselModel)
itemWidthPercent = (carouselModel.itemWidthPercent ?? 100) / 100
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing = carouselModel.spacing ?? 0
itemWidthPercent = carouselModel.itemWidthPercent / 100.0
if let alignment = carouselModel.itemAlignment {
itemAlignment = alignment
}
if let height = carouselModel.height {
collectionViewHeight?.constant = CGFloat(height)
collectionViewHeight?.isActive = true
collectionViewHeight?.constant = height
}
registerCells(with: carouselModel, delegateObject: delegateObject)
prepareMolecules(with: carouselModel)
setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
@ -153,16 +169,6 @@ open class Carousel: View {
// MARK: - JSON Setters
//--------------------------------------------------
/// Updates the layout being used
func setupLayout(with carouselModel: CarouselModel?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1)
layout.minimumInteritemSpacing = 0
collectionView.collectionViewLayout = layout
}
func prepareMolecules(with carouselModel: CarouselModel?) {
guard let newMolecules = carouselModel?.molecules else {
numberOfPages = 0
@ -187,11 +193,12 @@ open class Carousel: View {
/// Sets up the paging molecule
open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) {
var pagingView: (UIView & CarouselPageControlProtocol)? = nil
if let molecule = molecule {
if let molecule = molecule,
(!molecule.hidesForSinglePage || numberOfPages > 1) {
pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & CarouselPageControlProtocol)
}
addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20)))
addPaging(view: pagingView, position: molecule?.position ?? 20)
}
/// Registers the cells with the collection view
@ -294,7 +301,7 @@ open class Carousel: View {
extension Carousel: UICollectionViewDelegateFlowLayout {
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
let itemWidth = collectionView.bounds.width * itemWidthPercent
return CGSize(width: itemWidth, height: collectionView.bounds.height)
}
@ -324,8 +331,15 @@ extension Carousel: UICollectionViewDataSource {
}
}
extension Carousel: UICollectionViewDelegate {
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol)?.didSelectCell(at: indexPath, delegateObject: delegateObject, additionalData: nil)
}
}
extension Carousel: UIScrollViewDelegate {
/// Go to the cell at the specified index.
func goTo(_ index: Int, animated: Bool) {
showPeaking(false)
@ -339,51 +353,33 @@ extension Carousel: UIScrollViewDelegate {
}
}
func handleUserOnBufferCell() {
guard loop else { return }
let lastPageIndex = numberOfPages + 1
let goToIndex = { (index: Int) in
self.goTo(index, animated: false)
self.collectionView.layoutIfNeeded()
self.pagingView?.currentIndex = self.pageIndex
}
if currentIndex < 2 {
// If on a "buffer" last row (which is the first index), go to the real last row secretly. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking.
goToIndex(lastPageIndex)
} else if currentIndex > lastPageIndex {
// If on the "buffer" first row (which is the index after the real last row), go to the real first row secretly.
goToIndex(2)
}
}
func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) {
guard loop, dragging else { return }
// Checks if the user is not paging but attempting to drag endlessly and goes out of bounds. Caps the index.
if let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing {
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
let index = scrollView.contentOffset.x / (itemWidth + separatorWidth)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
if index < 1 {
currentIndex = 0
updateModelIndex()
} else if index > CGFloat(lastCellIndex - 1) {
currentIndex = lastCellIndex
updateModelIndex()
/// Adjusts the current contentOffset if we are going onto buffer cells while looping to help with the endless scrolling appearance.
func adjustOffsetForLooping(_ scrollView: UIScrollView) {
let translatedPoint = scrollView.panGestureRecognizer.translation(in: scrollView.superview).x
if translatedPoint > 0 {
// Moving left, see if we are moving passed the first left buffer card and adjust
if let threshold = collectionView.layoutAttributesForItem(at: IndexPath(item: 1, section: 0))?.frame.minX,
scrollView.contentOffset.x < threshold,
let newOffset = collectionView.layoutAttributesForItem(at: IndexPath(item: numberOfPages + 1, section: 0))?.frame.minX {
scrollView.contentOffset.x = newOffset
}
} else if translatedPoint < 0 {
// Moving right, see if we are moving passed the first right buffer card and adjust
if let threshold = collectionView.layoutAttributesForItem(at: IndexPath(item: numberOfPages + 2, section: 0))?.frame.maxX,
scrollView.contentOffset.x + scrollView.bounds.width > threshold,
let newEndOffset = collectionView.layoutAttributesForItem(at: IndexPath(item: 2, section: 0))?.frame.maxX {
scrollView.contentOffset.x = newEndOffset - scrollView.bounds.width
}
}
handleUserOnBufferCell()
}
open func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Check if the user is dragging the card even further past the next card.
//checkForDraggingOutOfBounds(scrollView)
// Adjust for looping
if let model = model as? CarouselModel,
model.loop == true {
adjustOffsetForLooping(scrollView)
}
// Let the pager know our progress if needed.
pagingView?.scrollViewDidScroll(collectionView)
@ -391,6 +387,7 @@ extension Carousel: UIScrollViewDelegate {
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// Disable peaking when dragging.
dragging = true
showPeaking(false)
}
@ -398,32 +395,63 @@ extension Carousel: UIScrollViewDelegate {
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
dragging = false
targetContentOffset.pointee = scrollView.contentOffset
// This is for setting up smooth custom paging. (Since UICollectionView only handles paging based on collection view size and not cell size).
guard let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing else { return }
// This is for setting up smooth custom paging. (Since UICollectionView only handles paging based on collection view size and not cell size). Math requires that we are using UICollectionViewFlowLayout.
guard (model as? CarouselModel)?.paging == true,
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
// We switch cards if we pass the velocity threshold or position threshold (currently 50%).
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
var cellToSwipeTo = Int(scrollView.contentOffset.x / (itemWidth + separatorWidth) + 0.5)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
let velocityThreshold: CGFloat = 1.1
let separatorWidth = layout.minimumLineSpacing
let itemWidth = collectionView.bounds.width * itemWidthPercent
let width = itemWidth + separatorWidth
if velocity.x > velocityThreshold {
cellToSwipeTo = currentIndex + 1
} else if velocity.x < -velocityThreshold {
cellToSwipeTo = currentIndex - 1
// Adjusts the offset for the contentInset. Adds imaginary half separator to the left of the first card, which is necessary for determining the percent of a given card we are currently at.
let adjustedOffset = scrollView.contentOffset.x - layout.sectionInset.left + (separatorWidth / 2)
// Calculates the offset per card depending on the alignment.
var offsetByCard: CGFloat
switch itemAlignment {
case .right:
offsetByCard = ((adjustedOffset + scrollView.bounds.width) / width) - 1
case .centeredHorizontally:
offsetByCard = ((adjustedOffset + (scrollView.bounds.width / 2)) / width) - 0.5
default:
offsetByCard = adjustedOffset / width
}
// Adjust card for velocity impact.
let velocityThreshold: CGFloat = 1.1
var cellToSwipeTo: Int
if velocity.x > velocityThreshold {
cellToSwipeTo = Int(ceil(offsetByCard))
} else if velocity.x < -velocityThreshold {
cellToSwipeTo = Int(floor(offsetByCard))
} else {
cellToSwipeTo = Int(round(offsetByCard))
}
// If we are swiping to a buffer cell, change to real cell before beginning animation so we don't go out of bounds.
if cellToSwipeTo < 2 {
let newOffset = scrollView.contentOffset.x + (width * CGFloat(numberOfPages))
scrollView.contentOffset.x = newOffset
targetContentOffset.pointee.x = newOffset
cellToSwipeTo = cellToSwipeTo + numberOfPages
} else if cellToSwipeTo > numberOfPages + 1 {
let newOffset = scrollView.contentOffset.x - (width * CGFloat(numberOfPages))
scrollView.contentOffset.x = newOffset
targetContentOffset.pointee.x = newOffset
cellToSwipeTo = cellToSwipeTo - numberOfPages
} else {
targetContentOffset.pointee = scrollView.contentOffset
}
// Cap the index.
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
goTo(min(max(cellToSwipeTo, 0), lastCellIndex), animated: true)
}
// To give the illusion of endless scrolling. Since we are always calling scrollToItem we can assume finished paging in here.
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// Cycle to other end if on buffer cell.
handleUserOnBufferCell()
pagingView?.currentIndex = pageIndex
showPeaking(true)
}

View File

@ -21,14 +21,18 @@ import UIKit
public var backgroundColor: Color?
public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]
public var index: Int = 0
public var spacing: Float?
public var spacing: CGFloat?
public var border: Bool?
public var loop: Bool?
public var height: Float?
public var itemWidthPercent: Float?
public var height: CGFloat?
@Percent public var itemWidthPercent = 100
public var itemAlignment: UICollectionView.ScrollPosition?
public var pagingMolecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?
public var paging: Bool = true
public var useHorizontalMargins: Bool?
public var leftPadding: CGFloat?
public var rightPadding: CGFloat?
public init(molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]) {
self.molecules = molecules
}
@ -49,6 +53,10 @@ import UIKit
case itemWidthPercent
case itemAlignment
case pagingMolecule
case paging
case useHorizontalMargins
case leftPadding
case rightPadding
}
//--------------------------------------------------
@ -60,13 +68,21 @@ import UIKit
molecules = try typeContainer.decodeModels(codingKey: .molecules)
index = try typeContainer.decodeIfPresent(Int.self, forKey: .index) ?? 0
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
spacing = try typeContainer.decodeIfPresent(Float.self, forKey: .spacing)
spacing = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .spacing)
border = try typeContainer.decodeIfPresent(Bool.self, forKey: .border)
loop = try typeContainer.decodeIfPresent(Bool.self, forKey: .loop)
height = try typeContainer.decodeIfPresent(Float.self, forKey: .height)
itemWidthPercent = try typeContainer.decodeIfPresent(Float.self, forKey: .itemWidthPercent)
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
if let itemWidthPercent = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .itemWidthPercent) {
self.itemWidthPercent = itemWidthPercent
}
itemAlignment = try typeContainer.decodeIfPresent(UICollectionView.ScrollPosition.self, forKey: .itemAlignment)
pagingMolecule = try typeContainer.decodeModelIfPresent(codingKey: .pagingMolecule)
if let paging = try typeContainer.decodeIfPresent(Bool.self, forKey: .paging) {
self.paging = paging
}
useHorizontalMargins = try typeContainer.decodeIfPresent(Bool.self, forKey: .useHorizontalMargins)
leftPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .leftPadding)
rightPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .rightPadding)
}
public func encode(to encoder: Encoder) throws {
@ -74,12 +90,16 @@ import UIKit
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeModels(molecules, forKey: .molecules)
try container.encode(spacing, forKey: .spacing)
try container.encode(border, forKey: .border)
try container.encode(loop, forKey: .loop)
try container.encode(height, forKey: .height)
try container.encodeIfPresent(spacing, forKey: .spacing)
try container.encodeIfPresent(border, forKey: .border)
try container.encodeIfPresent(loop, forKey: .loop)
try container.encodeIfPresent(height, forKey: .height)
try container.encode(itemWidthPercent, forKey: .itemWidthPercent)
try container.encode(itemAlignment, forKey: .itemAlignment)
try container.encodeIfPresent(itemAlignment, forKey: .itemAlignment)
try container.encodeModelIfPresent(pagingMolecule, forKey: .pagingMolecule)
try container.encode(paging, forKey: .paging)
try container.encodeIfPresent(useHorizontalMargins, forKey: .useHorizontalMargins)
try container.encodeIfPresent(leftPadding, forKey: .leftPadding)
try container.encodeIfPresent(rightPadding, forKey: .rightPadding)
}
}

View File

@ -10,5 +10,6 @@ import Foundation
public protocol CarouselPagingModelProtocol {
var position: Float? { get }
var position: CGFloat? { get }
var hidesForSinglePage: Bool { get }
}

View File

@ -0,0 +1,14 @@
//
// TabPageModelProtocol.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 6/1/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
public protocol TabPageModelProtocol {
var tabBarHidden: Bool { get set }
var tabBarIndex: Int? { get set }
}

View File

@ -0,0 +1,17 @@
//
// TabBarProtocol.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 5/29/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objc public protocol TabBarProtocol {
/// Should visually select the given tab index.
@objc func highlightTab(at index: Int)
/// Should select the tab index. As if the user selected it.
@objc func selectTab(at index: Int)
}

View File

@ -131,7 +131,7 @@ import Foundation
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol)?.didSelectCell(at: indexPath, delegateObject: delegateObjectIVar, additionalData: nil)
}

View File

@ -57,7 +57,9 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol {
let stack = MoleculeStackView(frame: .zero)
moleculeStackModel.useStackSpacingBeforeFirstItem = true
moleculeStackModel.useHorizontalMargins = true
if moleculeStackModel.useHorizontalMargins == nil {
moleculeStackModel.useHorizontalMargins = true
}
stack.set(with: moleculeStackModel, delegateObject() as? MVMCoreUIDelegateObject, nil)
return stack
}

View File

@ -9,7 +9,7 @@
import Foundation
@objcMembers public class TemplateModel: MVMControllerModelProtocol {
@objcMembers public class TemplateModel: MVMControllerModelProtocol, TabPageModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -29,6 +29,9 @@ import Foundation
public var navigationBar: (NavigationItemModelProtocol & MoleculeModelProtocol)?
public var formRules: [FormGroupRule]?
public var behaviors: [PageBehaviorProtocol]?
public var tabBarHidden: Bool = false
public var tabBarIndex: Int?
//--------------------------------------------------
// MARK: - Initializer
@ -50,6 +53,8 @@ import Foundation
case formRules
case behaviors
case navigationBar
case tabBarHidden
case tabBarIndex
}
//--------------------------------------------------
@ -64,6 +69,10 @@ import Foundation
formRules = try typeContainer.decodeIfPresent([FormGroupRule].self, forKey: .formRules)
behaviors = try typeContainer.decodeModelsIfPresent(codingKey: .behaviors)
navigationBar = try typeContainer.decodeModelIfPresent(codingKey: .navigationBar)
if let tabBarHidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .tabBarHidden) {
self.tabBarHidden = tabBarHidden
}
tabBarIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .tabBarIndex)
}
public func encode(to encoder: Encoder) throws {
@ -74,5 +83,7 @@ import Foundation
try container.encodeIfPresent(screenHeading, forKey: .screenHeading)
try container.encodeIfPresent(formRules, forKey: .formRules)
try container.encodeModelIfPresent(navigationBar, forKey: .navigationBar)
try container.encode(tabBarHidden, forKey: .tabBarHidden)
try container.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex)
}
}

View File

@ -31,11 +31,7 @@ public typealias BarButtonAction = (BarButtonItem) -> ()
open func set(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
buttonDelegate = delegateObject?.buttonDelegate
actionDelegate?.buttonAction = { sender in
if let data = try? actionModel.encode(using: JSONEncoder()),
let actionMap = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.init()) as? [AnyHashable: Any],
delegateObject?.buttonDelegate?.button?(sender, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
}
Button.performButtonAction(with: actionModel, button: sender, delegateObject: delegateObject, additionalData: additionalData)
}
}

View File

@ -78,11 +78,15 @@ public typealias ButtonAction = (Button) -> ()
addActionBlock(event: .touchUpInside) { [weak self] sender in
guard let self = self else { return }
if let data = try? actionModel.encode(using: JSONEncoder()),
let actionMap = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.init()) as? [AnyHashable: Any],
delegateObject?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
}
Self.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData)
}
}
open class func performButtonAction(with model: ActionModelProtocol, button: MFButtonProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
if let data = try? model.encode(using: JSONEncoder()),
let actionMap = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.init()) as? [AnyHashable: Any],
delegateObject?.buttonDelegate?.button?(button, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
}
}

View File

@ -10,7 +10,7 @@ import Foundation
/// A base collection view cell with basic mvm functionality.
open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCoreViewProtocol, CollectionTemplateItemProtocol {
open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCoreViewProtocol, CollectionTemplateItemProtocol, MFButtonProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -25,10 +25,6 @@ open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCo
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
// MARK: - Inits
public override init(frame: CGRect) {
super.init(frame: .zero)
@ -47,10 +43,6 @@ open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCo
}
}
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
// MARK: - MVMCoreViewProtocol
open func setupView() {
isAccessibilityElement = false
@ -68,16 +60,6 @@ open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCo
(molecule as? MVMCoreViewProtocol)?.updateView(size)
}
open func reset() {
molecule?.reset()
backgroundColor = .mvmWhite
width = nil
}
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let model = model as? CollectionItemModelProtocol else { return }
@ -94,6 +76,12 @@ open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCo
}
}
open func reset() {
molecule?.reset()
backgroundColor = .mvmWhite
width = nil
}
/// Convenience function. Adds a molecule to the view.
open func addMolecule(_ molecule: MoleculeViewProtocol) {
contentView.addSubview(molecule)
@ -109,6 +97,12 @@ open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCo
self.width = width
}
// MARK: - Override
public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) {
guard let action = model?.action else { return }
Button.performButtonAction(with: action, button: self, delegateObject: delegateObject, additionalData: additionalData)
}
// Column logic, set width.
override open func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)

View File

@ -9,5 +9,13 @@
import Foundation
public protocol CollectionItemModelProtocol {
var action: ActionModelProtocol? { get set }
}
// Not a strict requirement.
public extension CollectionItemModelProtocol {
var action: ActionModelProtocol? {
get { return nil }
set { }
}
}

View File

@ -8,7 +8,7 @@
import UIKit
@objcMembers open class TableViewCell: UITableViewCell, MoleculeViewProtocol, MoleculeListCellProtocol, MVMCoreViewProtocol {
@objcMembers open class TableViewCell: UITableViewCell, MoleculeViewProtocol, MoleculeListCellProtocol, MVMCoreViewProtocol, MFButtonProtocol {
open var molecule: MoleculeViewProtocol?
open var listItemModel: ListItemModelProtocol?
@ -267,10 +267,8 @@ import UIKit
}
public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
//TODO: Use object when handleAction is rewrote to handle action model
if let actionMap = self.listItemModel?.action?.toJSON() {
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
}
guard let action = listItemModel?.action else { return }
Button.performButtonAction(with: action, button: self, delegateObject: delegateObject, additionalData: additionalData)
}
public func willDisplay() {

View File

@ -233,4 +233,8 @@ import Foundation
}
fatalError()
}
open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol)?.didSelectCell(at: indexPath, delegateObject: delegateObjectIVar, additionalData: nil)
}
}

View File

@ -8,7 +8,7 @@
import UIKit
@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate {
@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate {
@objc public var pageType: String?
@objc public var loadObject: MVMCoreLoadObject?
public var pageModel: MVMControllerModelProtocol?
@ -259,6 +259,16 @@ import UIKit
MVMCoreUISplitViewController.main()?.setBottomProgressBarProgress(progress / Float(100))
}
}
// MARK: - TabBar
open func updateTabBar() {
guard MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() == self,
var tabModel = pageModel as? TabPageModelProtocol else { return }
if let index = tabModel.tabBarIndex {
MVMCoreUISplitViewController.main()?.tabBar?.highlightTab(at: index)
}
MVMCoreUISplitViewController.main()?.updateTabBarShowing(!tabModel.tabBarHidden)
}
// MARK: - View lifecycle
@ -323,6 +333,9 @@ import UIKit
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Update tab if needed.
updateTabBar()
if manager == nil {
MVMCoreUISession.sharedGlobal()?.currentPageType = pageType
MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self)
@ -371,6 +384,28 @@ import UIKit
MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self)
}
// MARK: - MVMCoreLoadDelegateProtocol
// TODO: Move this function out of here after architecture cleanup.
open func loadFinished(_ loadObject: MVMCoreLoadObject?, loadedViewController: (UIViewController & MVMCoreViewControllerProtocol)?, error: MVMCoreErrorObject?) {
MVMCoreUILoggingHandler.log(withDelegateLoadFinished: loadObject, loadedViewController: loadedViewController, error: error)
// Open the support panel
if error == nil,
loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true {
MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true)
}
// Selects the tab if needed. Page driven takes priority over action driven (see viewWillAppear)
if let tab: Int = loadObject?.requestParameters?.actionMap?["tabBarIndex"] as? Int,
error == nil,
loadObject?.pageJSON?["tabBarIndex"] == nil {
MVMCoreDispatchUtility.performBlock(onMainThread: {
MVMCoreUISplitViewController.main()?.tabBar?.highlightTab(at: tab)
})
}
}
// MARK: - MVMCoreActionDelegateProtocol
open func handleOpenPage(for requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
formValidator?.addFormParams(requestParameters: requestParameters)

View File

@ -11,9 +11,11 @@
@import MVMCore.MVMCoreActionDelegateProtocol;
#import <MVMCoreUI/MVMCoreUIPanelProtocol.h>
#import <MVMCoreUI/MVMCoreUIPanelButtonProtocol.h>
@class MVMCoreUITopAlertView;
@class MFViewController;
@class NavigationController;
@protocol TabBarProtocol;
typedef NS_ENUM(NSInteger, MFNumberOfDrawers) {
MFNoDrawer = 0,
@ -47,6 +49,9 @@ typedef NS_ENUM(NSInteger, MFNumberOfDrawers) {
@property (nonatomic, readonly) BOOL rightPanelIsAccessible;
@property (nullable, weak, nonatomic, readonly) UIViewController *navigationItemViewController;
/// Reference to the tabbar.
@property (nullable, weak, nonatomic) UIView <TabBarProtocol>*tabBar;
// Convenience getter
+ (nullable instancetype)mainSplitViewController;
@ -141,4 +146,15 @@ typedef NS_ENUM(NSInteger, MFNumberOfDrawers) {
- (IBAction)backButtonPressed:(nullable id)sender;
- (IBAction)rightPanelButtonPressed:(nullable id)sender;
#pragma mark - TabBar
/// Called when split view is loaded to create the initial tabbar. Default is nil.
- (nullable UIView <TabBarProtocol>*)createTabBar;
/// Adds any tabbar at the bottom of the split view.
- (void)addTabBar:(nonnull UIView <TabBarProtocol>*)tabBar;
/// Updates if the tab bar is showing or not.
- (void)updateTabBarShowing:(BOOL)showing;
@end

View File

@ -43,6 +43,8 @@ typedef NS_OPTIONS(NSInteger, MFExtendedDrawer) {
@property (weak, nonatomic) UIView *leftPanelSeparator;
@property (weak, nonatomic) UIView *rightPanelSeparator;
@property (weak, nonatomic) NSLayoutConstraint *bottomConstraint;
@property (weak, nonatomic, readwrite) NavigationController *navigationController;
// A view that covers the detail view when the master is out.
@ -716,7 +718,7 @@ CGFloat const PanelAnimationDuration = 0.2;
self.leftPanelWidth = leftPanelWidth;
[NSLayoutConstraint constraintWithItem:self.mainView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.leftView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.leftView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.leftView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.leftView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
if ([panel respondsToSelector:@selector(buttonForPanel)]) {
self.leftPanelButton = [panel buttonForPanel];
@ -751,7 +753,7 @@ CGFloat const PanelAnimationDuration = 0.2;
self.rightPanelWidth = rightPanelWidth;
[NSLayoutConstraint constraintWithItem:self.rightView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.rightView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.rightView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.rightView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
if ([panel respondsToSelector:@selector(buttonForPanel)]) {
self.rightPanelButton = [panel buttonForPanel];
@ -771,6 +773,36 @@ CGFloat const PanelAnimationDuration = 0.2;
[self.view layoutIfNeeded];
}
#pragma mark - TabBar
- (nullable UIView <TabBarProtocol>*)createTabBar {
return nil;
}
- (void)addTabBar:(nonnull UIView <TabBarProtocol>*)tabBar {
[self.view insertSubview:tabBar atIndex:0];
self.tabBar = tabBar;
[tabBar.topAnchor constraintEqualToAnchor:self.bottomProgressBar.bottomAnchor].active = YES;
[NSLayoutConstraint constraintWithItem:tabBar attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:tabBar attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.mainView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0].active = YES;
[self updateTabBarShowing:YES];
}
- (void)updateTabBarShowing:(BOOL)showing {
self.tabBar.hidden = !showing;
self.bottomConstraint.active = NO;
if (showing && self.tabBar) {
NSLayoutConstraint *bottom = [self.view.safeAreaLayoutGuide.bottomAnchor constraintEqualToAnchor:self.tabBar.bottomAnchor];
bottom.active = YES;
self.bottomConstraint = bottom;
} else {
NSLayoutConstraint *bottom = [self.view.bottomAnchor constraintEqualToAnchor:self.bottomProgressBar.bottomAnchor];
bottom.active = YES;
self.bottomConstraint = bottom;
}
[self.view layoutIfNeeded];
}
#pragma mark - Bottom Progress Bar
- (void)setBottomProgressBarProgress:(float)progress {
@ -841,9 +873,17 @@ CGFloat const PanelAnimationDuration = 0.2;
self.bottomProgressBarHeightConstraint = bottomProgressHeight;
if (topAlertView) {
[NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[topAlertView]-0-[mainView]-0-[progressView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(topAlertView, mainView, progressView)]];
[NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[topAlertView]-0-[mainView]-0-[progressView]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(topAlertView, mainView, progressView)]];
} else {
[NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[mainView]-0-[progressView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(mainView, progressView)]];
[NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[mainView]-0-[progressView]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(mainView, progressView)]];
}
// Add tabbar if we have it.
UIView <TabBarProtocol>*tabs = [self createTabBar];
if (tabs) {
[self addTabBar:tabs];
} else {
[self updateTabBarShowing:NO];
}
// Cover View
@ -855,8 +895,8 @@ CGFloat const PanelAnimationDuration = 0.2;
self.mainViewCoverView = coverView;
[NSLayoutConstraint constraintWithItem:coverView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:mainView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:coverView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:mainView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:coverView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:mainView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:coverView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:mainView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:coverView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:coverView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0].active = YES;
[self setupPanels];
}