为啥UIView动画会改变View frame?

Posted

技术标签:

【中文标题】为啥UIView动画会改变View frame?【英文标题】:Why does UIView animation change View frame?为什么UIView动画会改变View frame? 【发布时间】:2017-11-15 05:53:15 【问题描述】:

更新:我添加了一个显式调用,以在过渡的完成处理程序中设置“to”视图的框架。 这解决了“问题”,但没有回答为什么框架在隐藏时会发生变化的问题。这是新的动画调用。注意 - 我通过创建一个处理 spanSuperView() 和动画的函数来稍微清理代码,因此这与原始代码不完全匹配。要点已更新。我希望它对某人有所帮助,也许其他人知道为什么。

这是多次运行的动画:

func animateView(_ new: UIView) 
    UIView.transition(from: self.currentView, to: new, duration: 1, options: UIViewAnimationOptions.transitionFlipFromRight, completion:  finished in
            new.spanSuperView()
            new.isHidden = false
            self.currentView.isHidden = true
            self.currentView = new
            self.container.layoutIfNeeded()
    )

动画现在在按钮切换方法中调用而不是重复:

@objc func viewChange(_ sender: UIButton) 
        print(sender.tag)
        switch sender.tag 
        case 1 :
            print("should look at view1")
            print(currentView)
            animateView(viewone)
        case 2 :
            print(currentView)
            animateView(viewtwo)
        case 3 :
            print(currentView)
            animateView(viewthree)
        case 4 :
            print(currentView)
            animateView(viewfour)
        default:
            animateView(viewone)
        
    

这里是 spanSuperView():

extension UIView 
    func spanSuperView() 
        guard superview != nil else  return 
        self.topAnchor.constraint(equalTo: superview!.topAnchor).isActive = true
        self.leadingAnchor.constraint(equalTo: superview!.leadingAnchor).isActive = true
        self.widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 1.0).isActive = true
        self.heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 1.0).isActive = true
        self.translatesAutoresizingMaskIntoConstraints = false
    

另一个更新: 如果没有 UIView.transition(只显示 | 隐藏),则根本不会出现问题:

func animateView(_ new: UIView) 
        if new == currentView  return 
        new.spanSuperView()
        new.isHidden = false
        self.currentView.isHidden = true
        self.currentView = new

原始问题: 我正在尝试创建一个带有动画子视图的视图,用户可以通过单击按钮在它们之间切换。 这在第一次显示视图时工作正常,但子视图的框架在第一次后会发生变化,然后视图会在没有子视图的情况下进行动画处理。

这是操场的代码。我还创建了一个GIST

import UIKit
import PlaygroundSupport


class ButtonRow: UIView 
    var button1 = UIButton()
    var button2 = UIButton()
    var button3 = UIButton()
    var button4 = UIButton()
    var stackView = UIStackView()

    func updateButtonLabels(_ texts:[String]? = ["1", "2", "3", "4"]) 
        guard let texts = texts else  return 
        let buttons = [button1, button2, button3, button4]
        var i = 0
        for btn in buttons 
            btn.setTitle(texts[i], for: .normal)
            i += 1
        
    

    override init(frame: CGRect)
        super.init(frame: frame)
        makeStack()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(stackView)
        stackView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.0).isActive = true
        stackView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1.0).isActive = true
        stackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
    

    func makeStack() 
        let buttons = [button1, button2, button3, button4]
        for btn in buttons 
            btn.backgroundColor = .gray
            btn.setTitleColor(.white, for: .normal)
            btn.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
            btn.translatesAutoresizingMaskIntoConstraints = false
        
        stackView = UIStackView(arrangedSubviews: buttons)
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
        stackView.spacing = 10
    

    override func layoutSubviews() 
        super.layoutSubviews()
        stackView.frame = bounds
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    


class MyViewController : UIViewController 

    // MARK: - Properties
    var viewone = UIView()
    var viewtwo = UIView()
    var viewthree = UIView()
    var viewfour = UIView()
    var container = UIView()
    var stackView = UIStackView()
    var buttonRow = ButtonRow()
    var currentView = UIView()

    override func loadView() 
        let view = UIView()
        view.backgroundColor = .white

        self.view = view
        setupStack()
    

    override func viewWillLayoutSubviews() 
        print("will layout subviews")
        //autolayout the stack view
        let hSpacing: CGFloat = UIScreen.main.bounds.width/8
        let wSpacing: CGFloat = UIScreen.main.bounds.width/10
        if #available(ios 11.0, *) 
            let guide = self.view.safeAreaLayoutGuide
            stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -wSpacing).isActive = true
            stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: wSpacing).isActive = true
            stackView.topAnchor.constraint(equalTo: guide.topAnchor, constant: hSpacing).isActive = true
            stackView.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: -hSpacing).isActive = true

        
        else 
            let margins = view.layoutMarginsGuide
            stackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: -wSpacing).isActive = true
            stackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: wSpacing).isActive = true
            stackView.topAnchor.constraint(equalTo: margins.topAnchor, constant: hSpacing).isActive = true
            stackView.bottomAnchor.constraint(equalTo: margins.bottomAnchor, constant: -hSpacing).isActive = true
        
    

    func setupContainer() 
        container.backgroundColor = .yellow
        viewone.backgroundColor = .blue
        viewtwo.backgroundColor = .red
        viewthree.backgroundColor = .green
        viewfour.backgroundColor = .black
        for vl in [viewone, viewtwo, viewthree, viewfour] as [UIView]  
            container.addSubview(vl)
            vl.isHidden = true
            vl.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
            vl.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
            vl.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1.0).isActive = true
            vl.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1.0).isActive = true
            vl.translatesAutoresizingMaskIntoConstraints = false
        

        currentView = viewone
        viewone.isHidden = false
    

    func setupButtons() 
        buttonRow.updateButtonLabels()
        var i = 1
        for btn in [buttonRow.button1, buttonRow.button2, buttonRow.button3, buttonRow.button4] 
            btn.tag = i
            btn.addTarget(self, action: #selector(viewChange), for: .touchUpInside)
            i += 1
        

    

    func setupStack() 
        setupButtons()
        setupContainer()
        stackView = UIStackView(arrangedSubviews: [container, buttonRow])
        stackView.axis = .vertical
        stackView.distribution = .fill
        stackView.alignment = .fill
        stackView.spacing = 10
        stackView.translatesAutoresizingMaskIntoConstraints = false
        // add some component constraints
        let bHeight = buttonRow.heightAnchor.constraint(equalToConstant: 50)
        stackView.addConstraints([bHeight])

        view.addSubview(stackView)
    

    // MARK: - Methods
    @objc func viewChange(_ sender: UIButton) 
        print(sender.tag)
        switch sender.tag 
        case 1 :
            print("should look at view1")
            print(currentView)
            UIView.transition(from: self.currentView, to: self.viewone, duration: 1, options: UIViewAnimationOptions.transitionFlipFromRight, completion: nil)
            currentView = viewone
            viewone.isHidden = false
            viewtwo.isHidden = true
            viewthree.isHidden = true
            viewfour.isHidden = true
        case 2 :
            print(currentView)
            UIView.transition(from: self.currentView, to: self.viewtwo, duration: 1, options: UIViewAnimationOptions.transitionFlipFromRight, completion: nil)
            currentView = viewtwo
            viewone.isHidden = true
            viewtwo.isHidden = false
            viewthree.isHidden = true
            viewfour.isHidden = true
        case 3 :
            print(currentView)
            UIView.transition(from: self.currentView, to: self.viewthree, duration: 1, options: UIViewAnimationOptions.transitionFlipFromRight, completion: nil)
            currentView = viewthree
            viewone.isHidden = true
            viewtwo.isHidden = true
            viewthree.isHidden = false
            viewfour.isHidden = true
        case 4 :
            print(currentView)
            UIView.transition(from: self.currentView, to: self.viewfour, duration: 1, options: UIViewAnimationOptions.curveEaseInOut, completion: nil)
            currentView = viewfour
            viewone.isHidden = true
            viewtwo.isHidden = true
            viewthree.isHidden = true
            viewfour.isHidden = false
        default:
            currentView = viewone
            viewone.isHidden = false
            viewtwo.isHidden = true
            viewthree.isHidden = true
            viewfour.isHidden = true
        
    

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

打印语句显示仍然有一个 currentview 被识别,但它的帧从“frame = (0 0; 221 416)”变为“frame = (-77 -96; 0 0)":

will layout subviews
will layout subviews
1
should look at view1
<UIView: 0x7f7f42100630; frame = (0 0; 221 416); layer = <CALayer: 0x600000221440>>
2
<UIView: 0x7f7f42100630; frame = (0 0; 221 416); layer = <CALayer: 0x600000221440>>
3
<UIView: 0x7f7f421031f0; frame = (0 0; 221 416); layer = <CALayer: 0x600000221340>>
4
<UIView: 0x7f7f421033d0; frame = (0 0; 221 416); layer = <CALayer: 0x600000220140>>
1
should look at view1
<UIView: 0x7f7f421035b0; frame = (0 0; 221 416); layer = <CALayer: 0x600000220de0>>
2
<UIView: 0x7f7f42100630; frame = (-77 -96; 0 0); layer = <CALayer: 0x600000221440>>
3
<UIView: 0x7f7f421031f0; frame = (-77 -96; 0 0); layer = <CALayer: 0x600000221340>>
4
<UIView: 0x7f7f421033d0; frame = (-77 -96; 0 0); layer = <CALayer: 0x600000220140>>

注意:我创建了一个视频,但我似乎无法将其上传到 SO 或我的 Gist。

我不明白为什么框架会改变。什么给了?

【问题讨论】:

【参考方案1】:

我认为每次你在 NSLayoutAnchor 上调用 constraint 时,你都会创建一个新的约束,所以我不会对在 2 次迭代后 UIKit 无法再解决约束感到惊讶。

您应该尝试为UIViewAlertForUnsatisfiableConstraints 添加一个符号断点,看看这是否是您的问题。

【讨论】:

我无法在操场上添加符号断点(全部变灰)。此外,它不应该有约束问题,因为子视图.translatesAutoresizingMaskIntoConstraints = false。但是,您的评论给了我一个想法,我在动画的完成块中明确设置了框架,现在它可以工作了。如果没有更详细的调试,我仍然不知道为什么。

以上是关于为啥UIView动画会改变View frame?的主要内容,如果未能解决你的问题,请参考以下文章

20160122UIView动画

UICollectionView 滚动时 UIView 动画停止工作

UIView 滑动动画故障

UIView 动画交互

在父 UIView 中垂直居中 UILabel

Scenekit:动画时模型改变大小