在 scrollableStack 中处理选定的 UIView

Handle selected UIView in scrollableStack

我创建了一些标签。选项卡按预期在堆栈内水平滚动。

我正在尝试向选定的选项卡(请参见下图)添加底线(高度为 UIView = 2.0),并在选择新选项卡时为底线的过渡设置动画。

这是我的代码:

      UIView.animate(withDuration: 0.5, animations: {
        let center = viewSelected.center        
        let xPointToMove = center.x

        self.animatedView.transform = CGAffineTransform(translationX: xPointToMove, y: 0.0)
      })

问题是,如果用户点击第二个选项卡,animatedView 只会向右移动几个像素。我意识到动画发生后“viewSelected != animatedView”的center.x点。

感谢您对此的任何意见!

问题是您的“选项卡”视图是 UIStackView 的子视图...因此它们的坐标系(框架、中心等)是相对于它们在堆栈视图中的位置的。

您需要转换坐标。

这样试试:

    // get a reference to the stack view holding viewSelected
    // and a reference to the stack view's superView
    guard let stackV = viewSelected.superview as? UIStackView,
          let stackSV = stackV.superview
    else {
        return
    }
    // convert the frame of viewSelected from the stack view to its superView
    let r = stackV.convert(viewSelected.frame, to: stackSV)
    // animatedView center.x will be the converted frame's .midX
    let xPointToMove = r.midX
    UIView.animate(withDuration: 0.5, animations: {
        // animate the view itself, instead of Transforming its layer
        self.animatedView.center.x = xPointToMove
    })

Edit - 您可能会发现使用自动布局约束比设置动画视图的框架更容易。此外,使用约束将有助于保持动画视图的正确定位和大小 if/when 整体视图更改大小。

下面是一个完整的示例:

class MyCustomTabView: UIView {
    
    var selected: Bool = false {
        didSet {
            label.textColor = selected ? tintSelectedColor : tintNormalColor
            imgView.tintColor = selected ? tintSelectedColor : tintNormalColor
            backgroundColor = selected ? bkgSelectedColor : bkgNormalColor
        }
    }
    
    let stack = UIStackView()
    let imgView = UIImageView()
    let label = UILabel()

    let bkgNormalColor: UIColor = #colorLiteral(red: 0.9625813365, green: 0.9627193809, blue: 0.9625510573, alpha: 1)
    let bkgSelectedColor: UIColor = #colorLiteral(red: 0.9008318782, green: 0.8487855792, blue: 0.9591421485, alpha: 1)
    let tintNormalColor: UIColor = #colorLiteral(red: 0.4399604499, green: 0.4400276542, blue: 0.4399457574, alpha: 1)
    let tintSelectedColor: UIColor = #colorLiteral(red: 0.3831990361, green: 0.0002554753446, blue: 0.9317755103, alpha: 1)

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.spacing = 8
        addSubview(stack)
        stack.addArrangedSubview(imgView)
        stack.addArrangedSubview(label)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
            stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
            stack.centerYAnchor.constraint(equalTo: centerYAnchor),
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
        ])
        label.font = .boldSystemFont(ofSize: 14.0)
        label.textColor = tintNormalColor
        imgView.tintColor = tintNormalColor
        backgroundColor = bkgNormalColor
    }
    
}


class MyCustomTabBarView: UIView {
    let stack = UIStackView()
    let animatedView = UIView()
    var avWidthConstraint: NSLayoutConstraint!
    var avCenterXConstraint: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        animatedView.translatesAutoresizingMaskIntoConstraints = false
        stack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stack)
        addSubview(animatedView)
        
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: trailingAnchor),
            stack.heightAnchor.constraint(equalTo: heightAnchor),
            stack.centerYAnchor.constraint(equalTo: centerYAnchor),
            
            animatedView.bottomAnchor.constraint(equalTo: stack.bottomAnchor),
            animatedView.heightAnchor.constraint(equalToConstant: 2.0),
        ])

        animatedView.frame = CGRect(origin: .zero, size: CGSize(width: 100, height: 2))
        
        let names: [String] = [
            "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX",
        ]
        
        for (sName, i) in zip(names, Array(1...names.count)) {
            let v = MyCustomTabView()
            v.label.text = "TAB " + sName
            v.imgView.image = UIImage(systemName: "\(i).circle")
            let t = UITapGestureRecognizer(target: self, action: #selector(tabTapped(_:)))
            v.addGestureRecognizer(t)
            stack.addArrangedSubview(v)
        }

        // position animatedView at tab 0
        if let v = stack.arrangedSubviews.first as? MyCustomTabView {
            avWidthConstraint = animatedView.widthAnchor.constraint(equalTo: v.widthAnchor)
            avCenterXConstraint = animatedView.centerXAnchor.constraint(equalTo: v.centerXAnchor)
            avWidthConstraint.isActive = true
            avCenterXConstraint.isActive = true
        }

        animatedView.backgroundColor = #colorLiteral(red: 0.3831990361, green: 0.0002554753446, blue: 0.9317755103, alpha: 1)

    }
    
    @objc func tabTapped(_ g: UITapGestureRecognizer) -> Void {
        guard let viewSelected = g.view as? MyCustomTabView,
              let n = stack.arrangedSubviews.firstIndex(of: viewSelected)
        else {
            return
        }
        selectTab(n)
    }
    
    // so we can select a tab programmatically from the controller
    func selectTab(_ idx: Int) -> Void {
        // make sure we're not trying to select a tab that doesn't exists
        guard idx > -1, idx < stack.arrangedSubviews.count,
              let viewSelected = stack.arrangedSubviews[idx] as? MyCustomTabView
        else {
            return
        }

        stack.arrangedSubviews.forEach { v in
            if let vv = v as? MyCustomTabView {
                vv.selected = vv == viewSelected
            }
        }
        if avWidthConstraint != nil {
            avWidthConstraint.isActive = false
            avCenterXConstraint.isActive = false
        }
        avWidthConstraint = animatedView.widthAnchor.constraint(equalTo: viewSelected.widthAnchor)
        avCenterXConstraint = animatedView.centerXAnchor.constraint(equalTo: viewSelected.centerXAnchor)
        avWidthConstraint.isActive = true
        avCenterXConstraint.isActive = true
        UIView.animate(withDuration: 0.5, animations: {
            self.layoutIfNeeded()
        })
    }

}

class qCheckTVVC: UIViewController {
    
    let scrollView = UIScrollView()
    let myTabsView = MyCustomTabBarView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let g = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        let frameG = scrollView.frameLayoutGuide
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        myTabsView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(scrollView)
        scrollView.addSubview(myTabsView)
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            scrollView.heightAnchor.constraint(equalToConstant: 40.0),
            
            myTabsView.topAnchor.constraint(equalTo: contentG.topAnchor),
            myTabsView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
            myTabsView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
            myTabsView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
            myTabsView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
        ])
        
        scrollView.showsHorizontalScrollIndicator = false
    }
    
}