使用 UIKit 在 iOS 中更改设备方向时更新自定义 TabBar 中的滑块位置

问题描述 投票:0回答:1

我正在尝试创建一个带有滑块的自定义 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
        }
    }
}

有没有办法在设备旋转时更新父视图,或者有更好的方法来实现我想要完成的目标?

ios swift uikit uistackview
1个回答
0
投票

您可以避免任何“计算”的需要......

而不是使用

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
                }
            }
        }
    }
    
}
© www.soinside.com 2019 - 2024. All rights reserved.