【问题标题】:Changing zPosition does not change the view hierarchy更改 zPosition 不会更改视图层次结构
【发布时间】:2020-12-23 06:09:29
【问题描述】:

我正在构建一个卡片视图 - 选定的卡片在顶部,其余的在底部,相互堆叠。他们都有相同的superview。

所选卡片的 zPosition = 0,堆栈中的卡片具有递增的 zPositions:1、2、3 等。 Pre-Swap CardStack

当我从堆栈中挑选一张卡片时,我会为它与所选卡片(连同它们的 zPositions)的交换设置动画 - 类似于 Apple Wallet。 Post-Swap CardStack - correct zPositions

动画后,zPositions 被设置为正确的值,但视图层次结构无效。 View Hierarchy - Xcode visual debugger

是否可以使用 zPosition 实现这样的动画?

交换动画代码:

func didSelect(cardToBeSelected: CardView) {
    guard alreadySelectedCard !== cardToBeSelected else {
        return
    }
    
    guard let alreadySelectedCard = alreadySelectedCard else { return }
    
    let destinationOriginY = alreadySelectedCard.frame.origin.y
    let destinationZPosition = alreadySelectedCard.layer.zPosition

    alreadySelectedCard.layer.zPosition = cardToBeSelected.layer.zPosition
    
    let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
        self.alreadySelectedCard.frame.origin.y = cardToBeSelected.frame.origin.y
        cardToBeSelected.frame.origin.y = destinationOriginY
        
        self.view.layoutSubviews()
    }
    
    animator.addCompletion { (position) in
        switch position {
        case .end:
            cardToBeSelected.layer.zPosition = destinationZPosition
        default:
            break
        }
    }
    
    animator.startAnimation()
    
    self.alreadySelectedCard = cardToBeSelected
}

【问题讨论】:

  • 什么是视图调试器显示“无效”?
  • 我相信处理一堆卡片的典型方法是使用 zPosition,所以是的,它应该可以工作。我以前做过...我无法从您的代码中看出问题所在,但我建议简化您的 zPositions 并具有:(1)当前卡位于索引 0 且 zPosition = 2,(2)秒zPosition = 1 处的卡片,(3) zPosition = 0 处的其余卡片......然后您每次刷卡只需管理 3 张卡片(当前卡片、下一张卡片、下一张卡片)。牌组后面所有卡片的 zPosition 无关紧要,因为用户无论如何都看不到它们。也许这会解决它 idk
  • @matt 所有卡片都是兄弟,所以交换 zPosition 应该会导致视图层次结构的交换。
  • @purebreadd 不幸的是,卡片的行为没有改变。以下是所有代码,如果您想看的话:pastebin.com/G7gNPtcX

标签: ios swift animation uikit zposition


【解决方案1】:

我认为你会遇到几个问题......

  1. 您正在设置约束明确设置框架——几乎总是自找麻烦

  2. 更改layer.zPosition 不会更改对象在子视图集合中的顺序

  3. 在尝试更改卡片的位置/顺序时,使用相对于“顶部卡片”底部的垂直约束可能会变得复杂

我认为更好的方法:

  • 更新约束常量而不是框架
  • 使用insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)交换子视图“z-order”顺序
  • 将“已选择”卡片中的顶部约束常量值与“待选择”卡片交换

我看到你正在使用 SnapKit(我个人不喜欢它,但无论如何......)

从我的快速搜索中,似乎很难“即时”获得对 SnapKit 约束的引用以获取其 .constant 值。为了解决这个问题,您可以向 CardView 类添加一个属性,以保留对其“捕捉顶部约束”的引用。

这是来自您的 pastebin 链接的代码,按照我上面的描述进行了修改。请考虑它的示例 代码——但它可能会让您的工作顺利进行。大部分是相同的 - 我添加了 cmets,希望能澄清我添加/更改的代码:

class ViewController: UIViewController {
    private let contentInset: CGFloat = 20.0
    private var scrollView: UIScrollView!
    private var contentContainerView: UIView!
    private var mainCardView: CardView!
    
    private var alreadySelectedCard: CardView!
    private let colors: [UIColor] = [.black, .green, .blue, .red, .yellow, .orange, .brown, .cyan, .magenta, .purple]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initializeScrollView()
        initializeContentContainerView()

        generateCards(count: colors.count)
        
        alreadySelectedCard = cards[0]
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // first card is at the top of the view, so we'll set its offset
        //  inside the forEach loop to contentInset
        
        // start top of 2nd card at bottom of first card + cardOffset
        //  since first card is not "at the top" yet, calculate it
        var topOffset = contentInset + alreadySelectedCard.frame.height + cardOffset
        
        // update the top offset for the rest of the cards
        cards.forEach { card in
            guard let thisTopConstraint = card.topConstraint else {
                fatalError("Cards were not initialized correctly!!!")
            }
            if card == alreadySelectedCard {
                thisTopConstraint.update(offset: contentInset)
            } else {
                thisTopConstraint.update(offset: topOffset)
                topOffset += cardOffset
            }
        }
        // animate them into view
        let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
            self.contentContainerView.layoutSubviews()
        }
        animator.startAnimation()

    }
    
    private let cardOffset: CGFloat = 100.0
    private var cards = [CardView]()
    
    private func add(_ card: CardView) {
        cards.append(card)
        contentContainerView.addSubview(card)
        
        // position all cards below the bottom of the screen
        //  animate them into view in viewDidAppear
        
        let topOffset = UIScreen.main.bounds.height + 10
        
        card.snp.makeConstraints { (make) in
            let t = make.top.equalToSuperview().offset(topOffset).constraint
            card.topConstraint = t
            make.left.equalToSuperview().offset(contentInset)
            make.right.equalToSuperview().offset(-contentInset)
            make.height.equalTo(card.snp.width).multipliedBy(0.5)
            make.bottom.lessThanOrEqualToSuperview()
        }
        
    }
    
    private func generateCards(count: Int) {
        for index in 0..<count {
            let card = CardView(delegate: self)
            card.backgroundColor = colors[index % colors.count]
            card.layer.cornerRadius = 10
            add(card)
        }
    }
}

extension ViewController: CardViewDelegate {
    func didSelect(cardToBeSelected: CardView) {

        guard alreadySelectedCard !== cardToBeSelected else {
            return
        }

        guard
            // get the top "snap constraint" from alreadySelectedCard
            let alreadySnapConstraint = alreadySelectedCard.topConstraint,
            // get its constraint reference so we can get its .constant
            let alreadyConstraint = alreadySnapConstraint.layoutConstraints.first,
            // get the top "snap constraint" from cardToBeSelected
            let toBeSnapConstraint = cardToBeSelected.topConstraint,
            // get its constraint reference so we can get its .constant
            let toBeConstraint = toBeSnapConstraint.layoutConstraints.first
            else { return }

        // save the constant (the Top Offset) from cardToBeSelected
        let tmpOffset = toBeConstraint.constant

        // update the Top Offset for cardToBeSelected with the
        //  constant from alreadySelectedCard (it will be contentInset unless something has changed)
        toBeSnapConstraint.update(offset: alreadyConstraint.constant)
        
        // update the Top Offset for alreadySelectedCard
        alreadySnapConstraint.update(offset: tmpOffset)

        // swap the "z-order" of the views, instead of the view layers
        contentContainerView.insertSubview(alreadySelectedCard, belowSubview: cardToBeSelected)
        
        // animate the change
        let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
            self.contentContainerView.layoutSubviews()
        }
        animator.startAnimation()

        // update alreadySelectedCard
        self.alreadySelectedCard = cardToBeSelected

    }
}

extension ViewController {
    private func initializeScrollView() {
        scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.backgroundColor = .lightGray
        scrollView.contentInsetAdjustmentBehavior = .never
        
        scrollView.snp.makeConstraints { (make) in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }
    
    private func initializeContentContainerView() {
        contentContainerView = UIView()
        scrollView.addSubview(contentContainerView)
        
        contentContainerView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
            make.width.equalToSuperview()
        }
    }
}

protocol CardViewDelegate {
    func didSelect(cardToBeSelected: CardView)
}

class CardView: UIView {
    var tapGestureRecognizer: UITapGestureRecognizer!
    var delegate: CardViewDelegate?
    
    // snap constraint reference so we can modify it later
    weak var topConstraint: Constraint?
    
    convenience init(delegate: CardViewDelegate) {
        self.init(frame: .zero)
        self.delegate = delegate
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapCard))
        tapGestureRecognizer.delegate = self
        addGestureRecognizer(tapGestureRecognizer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc private func didTapCard() {
        delegate?.didSelect(cardToBeSelected: self)
    }
}

extension CardView: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-12-22
    • 1970-01-01
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 2023-04-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多