234 lines
8.2 KiB
Swift
234 lines
8.2 KiB
Swift
//
|
|
// TabsContainer.swift
|
|
// VDS
|
|
//
|
|
// Created by Matt Bruce on 5/25/23.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
@objc(VDSTabsContainer)
|
|
open class TabsContainer: View {
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Initializers
|
|
//--------------------------------------------------
|
|
required public init() {
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Enums
|
|
//--------------------------------------------------
|
|
public enum TabsWidth {
|
|
case percentage(CGFloat)
|
|
case value(CGFloat)
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Properties
|
|
//--------------------------------------------------
|
|
private var contentViewWidthConstraint: NSLayoutConstraint?
|
|
|
|
private var stackView = UIStackView().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.axis = .vertical
|
|
$0.alignment = .fill
|
|
$0.distribution = .fill
|
|
}
|
|
|
|
private var contentView = UIStackView().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.alignment = .fill
|
|
$0.distribution = .fillProportionally
|
|
$0.axis = .vertical
|
|
$0.spacing = 10
|
|
}
|
|
|
|
private var tabMenuLayoutGuide = UILayoutGuide()
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Properties
|
|
//--------------------------------------------------
|
|
open var tabMenu = Tabs()
|
|
|
|
///An optional callback that is called when the selectedIndex changes. Passes parameters (event, tabIndex).
|
|
open var onTabChange: ((Int) -> Void)?
|
|
|
|
///Determines the layout of the Tabs, defaults to horizontal
|
|
open var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } }
|
|
|
|
///When true, Tabs will have border line. If false is passed then the border line won't be visible.
|
|
open var borderLine: Bool = true { didSet { setNeedsUpdate() } }
|
|
|
|
///It will fill the Tabs to the width of the compoent and all Tabs will be in equal width when orientation is horizontal. This is recommended when there are no more than 2-3 tabs.
|
|
open var fillContainer: Bool = false { didSet { setNeedsUpdate() } }
|
|
|
|
///When true, Tabs will be sticky to top of page, when orientation is vertical.
|
|
open var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } }
|
|
|
|
///Sets the Position of the Selected/Hover Border Accent for All Tabs, only for Horizontal Orientation
|
|
open var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
|
|
|
|
///Minimum Width for All Tabs, when orientation is horizontal.
|
|
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
|
|
|
|
///If set to 'scroll', Tabs can be overflow and scrollable. With 'none', tabs will not overflow and labels will be wrapped to multiple lines if the label text is long.
|
|
open var overflow: Tabs.Overflow = .scroll { didSet { setNeedsUpdate() } }
|
|
|
|
///The initial Selected Tab's index and is set once a Tab is clicked
|
|
open var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } }
|
|
|
|
///Determines the size of the Tabs TextStyle
|
|
open var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } }
|
|
|
|
///Space between the Tabs and Contentl.
|
|
open var space: CGFloat = 5.0 { didSet { setNeedsUpdate() } }
|
|
|
|
///When true, Tabs will be sticky to top of page, when orientation is vertical.
|
|
open var sticky: Bool = false { didSet { setNeedsUpdate() } }
|
|
|
|
///rules for width
|
|
private var _width: TabsWidth = .percentage(0.25)
|
|
|
|
///Width of all Tabs when orientation is vertical, defaults to 25%.
|
|
open var width: TabsWidth {
|
|
get {
|
|
return _width
|
|
}
|
|
set {
|
|
switch newValue {
|
|
case .percentage(let percentage):
|
|
if percentage >= 0 && percentage <= 1 {
|
|
_width = newValue
|
|
setNeedsUpdate()
|
|
} else {
|
|
print("Invalid percentage value. It should be between 0 and 1.")
|
|
}
|
|
case .value(let value):
|
|
if value >= minWidth {
|
|
_width = newValue
|
|
setNeedsUpdate()
|
|
} else {
|
|
print("Invalid value. It should be greater than or equal to \(minWidth).")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///Model of the Tabs you are wanting to show.
|
|
open var tabModels: [TabModel] = [] {
|
|
didSet {
|
|
tabMenu.tabModels = tabModels.compactMap{ $0.model }
|
|
contentView.arrangedSubviews.forEach{ $0.removeFromSuperview() }
|
|
tabModels.forEach {
|
|
let view = $0.view
|
|
view.isHidden = true
|
|
contentView.addArrangedSubview(view)
|
|
}
|
|
setNeedsUpdate()
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
|
open override func setup() {
|
|
super.setup()
|
|
|
|
tabMenu.addLayoutGuide(tabMenuLayoutGuide)
|
|
addSubview(stackView)
|
|
stackView.pinToSuperView()
|
|
stackView.addArrangedSubview(tabMenu)
|
|
stackView.addArrangedSubview(contentView)
|
|
|
|
tabMenuLayoutGuide.pinToSuperView()
|
|
}
|
|
|
|
/// Used to make changes to the View based off a change events or from local properties.
|
|
open override func updateView() {
|
|
super.updateView()
|
|
|
|
stackView.alignment = orientation == .horizontal ? .fill : .top
|
|
stackView.axis = orientation == .horizontal ? .vertical : .horizontal
|
|
stackView.spacing = space
|
|
|
|
tabMenu.onTabChange = { [weak self] index in
|
|
guard let self else { return }
|
|
self.tabClicked(index: index)
|
|
}
|
|
|
|
//update tabs width constraints
|
|
contentViewWidthConstraint?.isActive = false
|
|
if orientation == .vertical {
|
|
switch width {
|
|
case .percentage(let amount):
|
|
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: tabMenuLayoutGuide.widthAnchor, multiplier: amount)
|
|
case .value(let amount):
|
|
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalToConstant: amount)
|
|
}
|
|
} else {
|
|
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: widthAnchor)
|
|
}
|
|
contentViewWidthConstraint?.isActive = true
|
|
|
|
tabMenu.surface = surface
|
|
tabMenu.isEnabled = isEnabled
|
|
tabMenu.orientation = orientation
|
|
tabMenu.borderLine = borderLine
|
|
tabMenu.fillContainer = fillContainer
|
|
tabMenu.indicatorFillTab = indicatorFillTab
|
|
tabMenu.indicatorPosition = indicatorPosition
|
|
tabMenu.minWidth = minWidth
|
|
tabMenu.overflow = overflow
|
|
tabMenu.selectedIndex = selectedIndex
|
|
tabMenu.size = size
|
|
tabMenu.sticky = sticky
|
|
setSelected(index: selectedIndex)
|
|
|
|
tabModels.forEach {
|
|
var view = $0.view
|
|
view.surface = surface
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Methods
|
|
//--------------------------------------------------
|
|
private func tabClicked(index: Int) {
|
|
onTabChange?(index)
|
|
setSelected(index: index)
|
|
}
|
|
|
|
private func setSelected(index: Int) {
|
|
for (modelIndex, model) in tabModels.enumerated() {
|
|
let view = model.view
|
|
let shouldShow = index == modelIndex
|
|
view.isHidden = !shouldShow
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TabsContainer {
|
|
public struct TabModel {
|
|
public typealias AnySurfaceableView = UIView & Surfaceable
|
|
public var model: Tabs.TabModel
|
|
public var view: AnySurfaceableView
|
|
|
|
public init(model: Tabs.TabModel, view: AnySurfaceableView) {
|
|
self.model = model
|
|
self.view = view
|
|
}
|
|
}
|
|
}
|