更改 zPosition 不会更改视图层次结构
Changing zPosition does not change the view hierarchy
我正在构建卡片视图 - 所选卡片在顶部,其余卡片在底部,彼此堆叠。他们都有相同的超级视图。
所选卡片的 zPosition = 0,堆栈中卡片的 zPosition 递增: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
}
我想你会 运行 遇到几个问题...
你正在设置约束和明确设置框架——几乎总是自找麻烦
更改layer.zPosition
不会更改对象在子视图集合中的顺序
尝试更改卡片的位置/顺序时,使用相对于“顶部卡片”底部的垂直约束会变得复杂
我认为更好的方法:
- 更新约束常量而不是帧
- 使用
insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)
交换子视图“z-order”顺序
- 将“已选”卡片与“待选”卡片的顶部约束常量值交换
我看到你在使用 SnapKit(个人而言,我不喜欢它,但无论如何......)
根据我的快速搜索,似乎很难获得对 SnapKit 约束“on-the-fly”的引用以获取其 .constant
值。要解决这个问题,您可以将 属性 添加到 CardView
class 以保留对其“snap top constraint”的引用。
这是您的 pastebin link 中的代码,按照我上面的描述进行了修改。请考虑它 example 代码——但它可能会让你上路。其中大部分是相同的——我添加了评论,希望能澄清我添加/更改的代码:
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
}
}
我正在构建卡片视图 - 所选卡片在顶部,其余卡片在底部,彼此堆叠。他们都有相同的超级视图。
所选卡片的 zPosition = 0,堆栈中卡片的 zPosition 递增: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
}
我想你会 运行 遇到几个问题...
你正在设置约束和明确设置框架——几乎总是自找麻烦
更改
layer.zPosition
不会更改对象在子视图集合中的顺序尝试更改卡片的位置/顺序时,使用相对于“顶部卡片”底部的垂直约束会变得复杂
我认为更好的方法:
- 更新约束常量而不是帧
- 使用
insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)
交换子视图“z-order”顺序
- 将“已选”卡片与“待选”卡片的顶部约束常量值交换
我看到你在使用 SnapKit(个人而言,我不喜欢它,但无论如何......)
根据我的快速搜索,似乎很难获得对 SnapKit 约束“on-the-fly”的引用以获取其 .constant
值。要解决这个问题,您可以将 属性 添加到 CardView
class 以保留对其“snap top constraint”的引用。
这是您的 pastebin link 中的代码,按照我上面的描述进行了修改。请考虑它 example 代码——但它可能会让你上路。其中大部分是相同的——我添加了评论,希望能澄清我添加/更改的代码:
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
}
}