diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index c5b5d7d7..deb3d015 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -333,6 +333,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 */; }; @@ -766,6 +770,10 @@ D28A839023CD4FD400DFE4FC /* CornerLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerLabelsModel.swift; sourceTree = ""; }; D28A839223CE828900DFE4FC /* HeadlineBodyCaretLinkImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyCaretLinkImageModel.swift; sourceTree = ""; }; D28BA72F247EC2EB00B75CB8 /* NavigationButtomModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtomModelProtocol.swift; sourceTree = ""; }; + D28BA740248025A300B75CB8 /* TabBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarModel.swift; sourceTree = ""; }; + D28BA7422480284E00B75CB8 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; + D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarProtocol.swift; sourceTree = ""; }; + D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageModelProtocol.swift; sourceTree = ""; }; D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = ""; }; D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = ""; }; D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = ""; }; @@ -902,6 +910,7 @@ D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */, D2092354244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift */, D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */, + D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */, ); path = ModelProtocols; sourceTree = ""; @@ -1229,6 +1238,8 @@ 017BEB372360C6AC0024EF95 /* RadioButtonLabel.swift */, D28764FA245A33A500CB882D /* TwoLinkViewModel.swift */, D28764F8245A327200CB882D /* TwoLinkView.swift */, + D28BA740248025A300B75CB8 /* TabBarModel.swift */, + D28BA7422480284E00B75CB8 /* TabBar.swift */, ); path = HorizontalCombinationViews; sourceTree = ""; @@ -1880,6 +1891,7 @@ 012A88C7238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift */, 017BEB47236230DB0024EF95 /* MoleculeViewProtocol.swift */, 012A88AC238C418100FE3DA1 /* TemplateProtocol.swift */, + D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */, 011B58EE23A2AA850085F53C /* ModelProtocols */, ); path = Protocols; @@ -2029,6 +2041,7 @@ D264FAAA2440F97600D98315 /* CollectionView.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 */, @@ -2096,6 +2109,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 */, @@ -2273,6 +2287,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 */, @@ -2371,6 +2386,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 */, diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift index d05fea10..e508c8f4 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift @@ -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 //----------------------------------------------------- // MARK: - Initializers //----------------------------------------------------- public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - stack = Stack.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(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 //------------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift new file mode 100644 index 00000000..94e53790 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift @@ -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 { +} diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift new file mode 100644 index 00000000..0afb5ff5 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -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) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index d663a703..e318b47c 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -78,5 +78,4 @@ public class TabItemModel: Codable { try container.encodeModel(label, forKey: .label) try container.encodeModelIfPresent(action, forKey: .action) } - } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TabPageModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TabPageModelProtocol.swift new file mode 100644 index 00000000..dbee61bd --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TabPageModelProtocol.swift @@ -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 } +} diff --git a/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift b/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift new file mode 100644 index 00000000..1bf79795 --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/TabBarProtocol.swift @@ -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) +} diff --git a/MVMCoreUI/Atomic/Templates/TemplateModel.swift b/MVMCoreUI/Atomic/Templates/TemplateModel.swift index b478167d..01753414 100644 --- a/MVMCoreUI/Atomic/Templates/TemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/TemplateModel.swift @@ -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) } } diff --git a/MVMCoreUI/BaseClasses/BarButtonItem.swift b/MVMCoreUI/BaseClasses/BarButtonItem.swift index cbb6ba2d..93eb63a3 100644 --- a/MVMCoreUI/BaseClasses/BarButtonItem.swift +++ b/MVMCoreUI/BaseClasses/BarButtonItem.swift @@ -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) } } diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index 5d42c5fb..38c48ddc 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -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) } } diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 6ec63c24..717a5761 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -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) diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h index e70a8b8b..4d6b2cff 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h @@ -11,9 +11,11 @@ @import MVMCore.MVMCoreActionDelegateProtocol; #import #import + @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 *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 *)createTabBar; + +/// Adds any tabbar at the bottom of the split view. +- (void)addTabBar:(nonnull UIView *)tabBar; + +/// Updates if the tab bar is showing or not. +- (void)updateTabBarShowing:(BOOL)showing; + @end diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 414895dc..1e04d987 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -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 *)createTabBar { + return nil; +} + +- (void)addTabBar:(nonnull UIView *)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 *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]; }