Initial commit
This commit is contained in:
198
Emoney Info/MainTabView.swift
Normal file
198
Emoney Info/MainTabView.swift
Normal file
@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user