我正在尝试创建一个带有滑块的自定义 TabBar,该滑块的宽度与其中的按钮相同。滑块应根据在 TabBar 中按下的按钮的索引更改颜色和位置。我当前的实现如下所示,但问题是当设备旋转或应用程序调整大小时,滑块卡在错误的位置。我尝试了几种方法来解决这个问题,但我开始认为我的实现背后的整个逻辑可能存在缺陷。
import UIKit
class SlidingTabBar: UIView {
public var tapCallback: ((Int) -> ())?
private let horizontalStack = UIStackView()
private let sliderView = UIView()
private var sliderLeftConstraint: NSLayoutConstraint?
let items : [CustomTabBarItem] = CustomTabBarItem.allCases
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 60)
}
private func commonInit() {
// Set up the slider view
sliderView.backgroundColor = items[0].color.withAlphaComponent(0.2)
sliderView.layer.cornerRadius = 10
sliderView.translatesAutoresizingMaskIntoConstraints = false
addSubview(sliderView)
horizontalStack.distribution = .fillEqually
horizontalStack.alignment = .center
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(horizontalStack)
let pad: CGFloat = 8.0
sliderLeftConstraint = sliderView.leftAnchor.constraint(equalTo: horizontalStack.leftAnchor)
NSLayoutConstraint.activate([
horizontalStack.topAnchor.constraint(equalTo: topAnchor, constant: pad),
horizontalStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: pad),
horizontalStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -pad),
horizontalStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -pad),
sliderLeftConstraint!,
sliderView.widthAnchor.constraint(equalTo: horizontalStack.widthAnchor, multiplier: 1.0 / CGFloat(items.count)),
sliderView.heightAnchor.constraint(equalTo: horizontalStack.heightAnchor),
sliderView.centerYAnchor.constraint(equalTo: horizontalStack.centerYAnchor)
])
for (index, item) in items.enumerated() {
let itemView = createTabBarItemView(for: item)
itemView.tag = index
horizontalStack.addArrangedSubview(itemView)
itemView.topAnchor.constraint(equalTo: horizontalStack.topAnchor).isActive = true
itemView.bottomAnchor.constraint(equalTo: horizontalStack.bottomAnchor).isActive = true
}
self.layer.cornerRadius = 10.0
self.backgroundColor = .white
}
private func createTabBarItemView(for item: CustomTabBarItem) -> UIView {
let itemView = UIView()
let imageView = UIImageView(image: item.image)
imageView.contentMode = .scaleAspectFit
imageView.tintColor = item == items[0] ? item.color : .gray
let label = UILabel()
label.text = item.title
label.textColor = item == items[0] ? item.color : .gray
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
let itemStackView = UIStackView(arrangedSubviews: [imageView, label])
itemStackView.axis = .vertical
itemStackView.alignment = .center
itemStackView.spacing = 2
itemStackView.translatesAutoresizingMaskIntoConstraints = false
itemView.addSubview(itemStackView)
NSLayoutConstraint.activate([
itemStackView.centerXAnchor.constraint(equalTo: itemView.centerXAnchor),
itemStackView.centerYAnchor.constraint(equalTo: itemView.centerYAnchor),
])
itemView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(itemTapped(_:))))
return itemView
}
@objc private func itemTapped(_ recognizer: UITapGestureRecognizer) {
guard let itemStackView = recognizer.view,
let indexItem = horizontalStack.arrangedSubviews.firstIndex(of: itemStackView) else { return }
tapCallback?(indexItem)
UIView.animate(withDuration: 0.3) {
self.sliderView.backgroundColor = self.items[indexItem].color.withAlphaComponent(0.2)
self.sliderLeftConstraint?.constant = CGFloat(indexItem) * self.sliderView.bounds.width
self.layoutIfNeeded()
self.horizontalStack.arrangedSubviews.enumerated().forEach { (index, view) in
if let imageView = (view.subviews.first as? UIStackView)?.arrangedSubviews.first as? UIImageView,
let label = (view.subviews.first as? UIStackView)?.arrangedSubviews.last as? UILabel {
imageView.tintColor = index == indexItem ? self.items[index].color : .gray
label.textColor = index == indexItem ? self.items[index].color : .gray
}
}
}
}
}
我还创建了一个
enum
,CustomTabBarItem:
enum CustomTabBarItem: CaseIterable {
case home, search, messages, profile
var image: UIImage? {
switch self {
case .home: return UIImage(systemName: "house")
case .search: return UIImage(systemName: "magnifyingglass")
case .messages: return UIImage(systemName: "message")
case .profile: return UIImage(systemName: "person")
}
}
var title: String {
switch self {
case .home: return "Home"
case .search: return "Search"
case .messages: return "Messages"
case .profile: return "Profile"
}
}
var color: UIColor {
switch self {
case .home: return .orange
case .search: return .blue
case .messages: return .green
case .profile: return .red
}
}
}
有没有办法在设备旋转时更新父视图,或者有更好的方法来实现我想要完成的目标?
您可以避免任何“计算”的需要......
而不是使用
sliderLeftConstraint
,让我们将其更改为:
private var sliderCenterConstraint: NSLayoutConstraint?
而不是控制
sliderView
宽度:
sliderView.widthAnchor.constraint(equalTo: horizontalStack.widthAnchor, multiplier: 1.0 / CGFloat(items.count)),
将其宽度限制为第一个“项目”(您的堆栈视图设置为
.fillEqually
因此我们可以只依赖第一个排列的子视图)。
所以,在你
for (index, item) in items.enumerated() {
循环中:
if index == 0 {
sliderView.widthAnchor.constraint(equalTo: itemView.widthAnchor).isActive = true
// and constrain sliderView centerX to itemView centerX
sliderCenterConstraint = sliderView.centerXAnchor.constraint(equalTo: itemView.centerXAnchor)
sliderCenterConstraint?.isActive = true
}
然后在
itemTapped(...)
处理程序中,像这样定位sliderView
:
self.sliderCenterConstraint?.isActive = false
self.sliderCenterConstraint = self.sliderView.centerXAnchor.constraint(equalTo: self.horizontalStack.arrangedSubviews[indexItem].centerXAnchor)
self.sliderCenterConstraint?.isActive = true
现在,当视图改变宽度时(例如在设备旋转时),
sliderView
约束仍然有效,它会自动调整大小和定位。
这是对您的
SlidingTabBar
课程的修改 - 请参阅评论以了解一些其他更改
class SlidingTabBar: UIView {
public var tapCallback: ((Int) -> ())?
private let horizontalStack = UIStackView()
private let sliderView = UIView()
private var sliderCenterConstraint: NSLayoutConstraint?
let items : [CustomTabBarItem] = CustomTabBarItem.allCases
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 60)
}
private func commonInit() {
// Set up the slider view
sliderView.backgroundColor = items[0].color.withAlphaComponent(0.2)
sliderView.layer.cornerRadius = 10
sliderView.translatesAutoresizingMaskIntoConstraints = false
addSubview(sliderView)
horizontalStack.distribution = .fillEqually
// leave this at the default (.fill) and you don't need to constrain top/bottom of itemViews
//horizontalStack.alignment = .center
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(horizontalStack)
let pad: CGFloat = 8.0
NSLayoutConstraint.activate([
horizontalStack.topAnchor.constraint(equalTo: topAnchor, constant: pad),
horizontalStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: pad),
horizontalStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -pad),
horizontalStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -pad),
sliderView.heightAnchor.constraint(equalTo: horizontalStack.heightAnchor),
sliderView.centerYAnchor.constraint(equalTo: horizontalStack.centerYAnchor)
])
for (index, item) in items.enumerated() {
let itemView = createTabBarItemView(for: item)
itemView.tag = index
horizontalStack.addArrangedSubview(itemView)
if index == 0 {
sliderView.widthAnchor.constraint(equalTo: itemView.widthAnchor).isActive = true
sliderCenterConstraint = sliderView.centerXAnchor.constraint(equalTo: itemView.centerXAnchor)
sliderCenterConstraint?.isActive = true
}
// don't need these
//itemView.topAnchor.constraint(equalTo: horizontalStack.topAnchor).isActive = true
//itemView.bottomAnchor.constraint(equalTo: horizontalStack.bottomAnchor).isActive = true
}
self.layer.cornerRadius = 10.0
self.backgroundColor = .white
}
private func createTabBarItemView(for item: CustomTabBarItem) -> UIView {
let itemView = UIView()
let imageView = UIImageView(image: item.image)
imageView.contentMode = .scaleAspectFit
imageView.tintColor = item == items[0] ? item.color : .gray
let label = UILabel()
label.text = item.title
label.textColor = item == items[0] ? item.color : .gray
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
let itemStackView = UIStackView(arrangedSubviews: [imageView, label])
itemStackView.axis = .vertical
itemStackView.alignment = .center
itemStackView.spacing = 2
itemStackView.translatesAutoresizingMaskIntoConstraints = false
itemView.addSubview(itemStackView)
NSLayoutConstraint.activate([
itemStackView.centerXAnchor.constraint(equalTo: itemView.centerXAnchor),
itemStackView.centerYAnchor.constraint(equalTo: itemView.centerYAnchor),
])
itemView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(itemTapped(_:))))
return itemView
}
@objc private func itemTapped(_ recognizer: UITapGestureRecognizer) {
guard let itemStackView = recognizer.view,
let indexItem = horizontalStack.arrangedSubviews.firstIndex(of: itemStackView) else { return }
tapCallback?(indexItem)
UIView.animate(withDuration: 0.3) {
self.sliderView.backgroundColor = self.items[indexItem].color.withAlphaComponent(0.2)
// re-make the sliderCenterConstraint, setting it to the "selected" view
self.sliderCenterConstraint?.isActive = false
self.sliderCenterConstraint = self.sliderView.centerXAnchor.constraint(equalTo: self.horizontalStack.arrangedSubviews[indexItem].centerXAnchor)
self.sliderCenterConstraint?.isActive = true
self.layoutIfNeeded()
self.horizontalStack.arrangedSubviews.enumerated().forEach { (index, view) in
if let imageView = (view.subviews.first as? UIStackView)?.arrangedSubviews.first as? UIImageView,
let label = (view.subviews.first as? UIStackView)?.arrangedSubviews.last as? UILabel {
imageView.tintColor = index == indexItem ? self.items[index].color : .gray
label.textColor = index == indexItem ? self.items[index].color : .gray
}
}
}
}
}