202 lines
6.8 KiB
Swift
202 lines
6.8 KiB
Swift
//
|
||
// QuickLocationTabBar.swift
|
||
// QuickLocation
|
||
//
|
||
|
||
import UIKit
|
||
|
||
protocol QuickLocationTabBarDelegate: AnyObject {
|
||
func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int)
|
||
}
|
||
|
||
final class QuickLocationTabBar: UIView {
|
||
|
||
struct TabItem {
|
||
let title: String
|
||
let image: UIImage?
|
||
let selectedImage: UIImage?
|
||
|
||
init(title: String, image: UIImage?, selectedImage: UIImage? = nil) {
|
||
self.title = title
|
||
self.image = image
|
||
self.selectedImage = selectedImage ?? image
|
||
}
|
||
}
|
||
|
||
// MARK: - Design Constants (in iOS points, from Lanhu @2x 容器 2)
|
||
private let barContentHeight: CGFloat = 56
|
||
private let containerCornerRadius: CGFloat = 28
|
||
private let horizontalInset: CGFloat = 18
|
||
private let iconSize: CGFloat = 32
|
||
private let iconLabelSpacing: CGFloat = 4
|
||
private let labelHeight: CGFloat = 11
|
||
|
||
private let selectedTextColor = UIColor(hexStr: "#1A1A1A")
|
||
private let unselectedTextColor = UIColor(hexStr: "#AAAAAA")
|
||
|
||
private let selectedFont = UIFont(name: "PingFangSC-Semibold", size: 11)
|
||
?? UIFont.systemFont(ofSize: 11, weight: .semibold)
|
||
private let unselectedFont = UIFont.systemFont(ofSize: 11, weight: .regular)
|
||
|
||
// MARK: - Properties
|
||
weak var delegate: QuickLocationTabBarDelegate?
|
||
/// 返回 false 阻止选中(用于游客拦截等)
|
||
var shouldSelectTab: ((Int) -> Bool)?
|
||
|
||
private let items: [TabItem]
|
||
private var tabViews: [UIView] = []
|
||
private let containerView = UIView()
|
||
|
||
private(set) var selectedIndex: Int = 0
|
||
|
||
// MARK: - Init
|
||
init(items: [TabItem]) {
|
||
self.items = items
|
||
super.init(frame: .zero)
|
||
setupUI()
|
||
updateSelection(animated: false)
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
// MARK: - Layout
|
||
override var intrinsicContentSize: CGSize {
|
||
CGSize(width: UIView.noIntrinsicMetric, height: barContentHeight + kSafeBottomMargin)
|
||
}
|
||
|
||
// MARK: - Setup
|
||
private func setupUI() {
|
||
backgroundColor = .clear
|
||
|
||
containerView.backgroundColor = .white
|
||
containerView.layer.cornerRadius = containerCornerRadius
|
||
containerView.layer.shadowColor = UIColor(red: 0.059, green: 0.157, blue: 0.275, alpha: 0.1).cgColor
|
||
containerView.layer.shadowOffset = .zero
|
||
containerView.layer.shadowRadius = 8
|
||
containerView.layer.shadowOpacity = 1
|
||
addSubview(containerView)
|
||
|
||
for (index, item) in items.enumerated() {
|
||
let tabView = makeTabView(item: item, index: index)
|
||
containerView.addSubview(tabView)
|
||
tabViews.append(tabView)
|
||
}
|
||
|
||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
NSLayoutConstraint.activate([
|
||
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
||
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
||
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -kSafeBottomMargin),
|
||
containerView.heightAnchor.constraint(equalToConstant: barContentHeight)
|
||
])
|
||
}
|
||
|
||
private func makeTabView(item: TabItem, index: Int) -> UIView {
|
||
let view = UIView()
|
||
view.tag = index
|
||
view.backgroundColor = .clear
|
||
|
||
let imageView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
|
||
imageView.contentMode = .scaleAspectFit
|
||
imageView.tag = 100
|
||
view.addSubview(imageView)
|
||
|
||
let label = UILabel()
|
||
label.text = item.title
|
||
label.font = unselectedFont
|
||
label.textColor = unselectedTextColor
|
||
label.textAlignment = .center
|
||
label.tag = 200
|
||
view.addSubview(label)
|
||
|
||
let tap = UITapGestureRecognizer(target: self, action: #selector(tabTapped(_:)))
|
||
view.addGestureRecognizer(tap)
|
||
|
||
return view
|
||
}
|
||
|
||
// MARK: - Actions
|
||
@objc private func tabTapped(_ gesture: UITapGestureRecognizer) {
|
||
guard let view = gesture.view else { return }
|
||
let index = view.tag
|
||
guard index != selectedIndex else { return }
|
||
if shouldSelectTab?(index) == false { return }
|
||
selectedIndex = index
|
||
updateSelection(animated: true)
|
||
delegate?.tabBar(self, didSelectTabAt: index)
|
||
}
|
||
|
||
// MARK: - Selection
|
||
func setSelectedIndex(_ index: Int, animated: Bool = true) {
|
||
guard index != selectedIndex, index >= 0, index < items.count else { return }
|
||
selectedIndex = index
|
||
updateSelection(animated: animated)
|
||
}
|
||
|
||
private func updateSelection(animated: Bool) {
|
||
for (i, view) in tabViews.enumerated() {
|
||
let isSelected = i == selectedIndex
|
||
if let imageView = view.viewWithTag(100) as? UIImageView {
|
||
let img = isSelected ? items[i].selectedImage : items[i].image
|
||
imageView.image = img?.withRenderingMode(.alwaysOriginal)
|
||
}
|
||
if let label = view.viewWithTag(200) as? UILabel {
|
||
label.font = isSelected ? selectedFont : unselectedFont
|
||
label.textColor = isSelected ? selectedTextColor : unselectedTextColor
|
||
}
|
||
}
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
let containerWidth = containerView.bounds.width
|
||
guard containerWidth > 0, !tabViews.isEmpty else { return }
|
||
|
||
let tabWidth = containerWidth / CGFloat(tabViews.count)
|
||
let tabHeight = containerView.bounds.height
|
||
|
||
let totalHeight = iconSize + iconLabelSpacing + labelHeight
|
||
let startY = (tabHeight - totalHeight) / 2
|
||
|
||
for (i, view) in tabViews.enumerated() {
|
||
view.frame = CGRect(
|
||
x: CGFloat(i) * tabWidth,
|
||
y: 0,
|
||
width: tabWidth,
|
||
height: tabHeight
|
||
)
|
||
|
||
if let imageView = view.viewWithTag(100) as? UIImageView {
|
||
imageView.frame = CGRect(
|
||
x: (tabWidth - iconSize) / 2,
|
||
y: startY,
|
||
width: iconSize,
|
||
height: iconSize
|
||
)
|
||
}
|
||
|
||
if let label = view.viewWithTag(200) as? UILabel {
|
||
label.frame = CGRect(
|
||
x: 0,
|
||
y: startY + iconSize + iconLabelSpacing,
|
||
width: tabWidth,
|
||
height: labelHeight
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Hit Testing
|
||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||
if containerView.frame.contains(point) {
|
||
return true
|
||
}
|
||
let extendedBounds = bounds.insetBy(dx: 0, dy: -kSafeBottomMargin)
|
||
return extendedBounds.contains(point)
|
||
}
|
||
}
|