199 lines
6.3 KiB
Swift
199 lines
6.3 KiB
Swift
import UIKit
|
|
|
|
// MARK: - MainTabBarController
|
|
|
|
final class MainTabBarController: UITabBarController {
|
|
|
|
private let customTabBar = CustomTabBar()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
tabBar.isHidden = true
|
|
view.backgroundColor = Theme.Color.background
|
|
setupCustomTabBar()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
layoutCustomTabBar()
|
|
}
|
|
|
|
private func setupCustomTabBar() {
|
|
customTabBar.translatesAutoresizingMaskIntoConstraints = false
|
|
customTabBar.onTabSelected = { [weak self] index in
|
|
self?.selectedIndex = index
|
|
}
|
|
view.addSubview(customTabBar)
|
|
|
|
NSLayoutConstraint.activate([
|
|
customTabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
|
|
customTabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
|
|
customTabBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
|
|
customTabBar.heightAnchor.constraint(equalToConstant: 64)
|
|
])
|
|
}
|
|
|
|
private func layoutCustomTabBar() {
|
|
view.bringSubviewToFront(customTabBar)
|
|
}
|
|
|
|
// Call this after setting viewControllers to sync selection
|
|
override var selectedIndex: Int {
|
|
didSet { customTabBar.setSelected(selectedIndex) }
|
|
}
|
|
|
|
override func setTabBarHidden(_ hidden: Bool, animated: Bool = true) {
|
|
let duration = animated ? 0.25 : 0
|
|
UIView.animate(withDuration: duration) {
|
|
self.customTabBar.alpha = hidden ? 0 : 1
|
|
}
|
|
customTabBar.isUserInteractionEnabled = !hidden
|
|
}
|
|
}
|
|
|
|
// MARK: - CustomTabBar
|
|
|
|
private final class CustomTabBar: UIView {
|
|
|
|
var onTabSelected: ((Int) -> Void)?
|
|
|
|
private struct TabItem {
|
|
let icon: String // SF Symbol name
|
|
let label: String
|
|
}
|
|
|
|
private var items: [TabItem] {[
|
|
TabItem(icon: "wave.3.right.circle.fill", label: L10n.tabEmoney),
|
|
TabItem(icon: "gearshape.fill", label: L10n.tabSettings)
|
|
]}
|
|
|
|
private var itemViews: [TabItemView] = []
|
|
private var selectedIndex: Int = 0
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setup()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setup()
|
|
}
|
|
|
|
private func setup() {
|
|
backgroundColor = .white
|
|
layer.cornerRadius = 32
|
|
layer.shadowColor = UIColor.black.cgColor
|
|
layer.shadowOpacity = 0.10
|
|
layer.shadowOffset = CGSize(width: 0, height: 4)
|
|
layer.shadowRadius = 16
|
|
|
|
let stack = UIStackView()
|
|
stack.axis = .horizontal
|
|
stack.distribution = .fillEqually
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(stack)
|
|
|
|
NSLayoutConstraint.activate([
|
|
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
|
stack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8)
|
|
])
|
|
|
|
for (index, item) in items.enumerated() {
|
|
let tabView = TabItemView(icon: item.icon, label: item.label)
|
|
tabView.isActive = (index == selectedIndex)
|
|
tabView.tag = index
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tabTapped(_:)))
|
|
tabView.addGestureRecognizer(tap)
|
|
stack.addArrangedSubview(tabView)
|
|
itemViews.append(tabView)
|
|
}
|
|
}
|
|
|
|
@objc private func tabTapped(_ gesture: UITapGestureRecognizer) {
|
|
guard let index = gesture.view?.tag else { return }
|
|
setSelected(index)
|
|
onTabSelected?(index)
|
|
}
|
|
|
|
func setSelected(_ index: Int) {
|
|
guard index != selectedIndex else { return }
|
|
selectedIndex = index
|
|
UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
|
|
self.itemViews.enumerated().forEach { i, view in
|
|
view.isActive = (i == index)
|
|
view.layoutIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - TabItemView
|
|
|
|
private final class TabItemView: UIView {
|
|
|
|
var isActive: Bool = false {
|
|
didSet { applyState() }
|
|
}
|
|
|
|
private let iconView = UIImageView()
|
|
private let labelView = UILabel()
|
|
private let pill = UIView()
|
|
|
|
init(icon: String, label: String) {
|
|
super.init(frame: .zero)
|
|
iconView.image = UIImage(systemName: icon)
|
|
iconView.contentMode = .scaleAspectFit
|
|
labelView.text = label
|
|
labelView.font = Theme.Font.caption(weight: .semibold)
|
|
labelView.textAlignment = .center
|
|
setup()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
private func setup() {
|
|
pill.layer.cornerRadius = 20
|
|
pill.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(pill)
|
|
|
|
let stack = UIStackView(arrangedSubviews: [iconView, labelView])
|
|
stack.axis = .vertical
|
|
stack.spacing = 2
|
|
stack.alignment = .center
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(stack)
|
|
|
|
NSLayoutConstraint.activate([
|
|
pill.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
pill.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
pill.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.85),
|
|
pill.heightAnchor.constraint(equalToConstant: 48),
|
|
|
|
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
|
iconView.widthAnchor.constraint(equalToConstant: 20),
|
|
iconView.heightAnchor.constraint(equalToConstant: 20)
|
|
])
|
|
|
|
applyState()
|
|
}
|
|
|
|
private func applyState() {
|
|
if isActive {
|
|
pill.backgroundColor = Theme.Color.primary
|
|
iconView.tintColor = .white
|
|
labelView.textColor = .white
|
|
pill.transform = .identity
|
|
} else {
|
|
pill.backgroundColor = .clear
|
|
iconView.tintColor = Theme.Color.textSecondary
|
|
labelView.textColor = Theme.Color.textSecondary
|
|
pill.transform = .identity
|
|
}
|
|
}
|
|
}
|