【问题标题】:The isUserInteraction setting is not taking effect when UIView.animate() call startsUIView.animate() 调用开始时 isUserInteraction 设置不生效
【发布时间】:2021-08-12 06:10:14
【问题描述】:

我最近注意到在同一个视图上两次按钮点击之间存在竞争条件。为了快速重现这个问题,我们可以使用以下代码--

class LandingView: UIView {
    
    private var blackButton: UIButton!
    private var redButton: UIButton!
    private var testView1: UIView!
    private var testView2: UIView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        blackButton = UIButton()
        self.addSubview(blackButton)

        // sorry I am using Snapkit here for a quick POC. but if you
        // just draw any button, it should reproduce the problem as well.
        blackButton.snp.makeConstraints{ (maker) in
            maker.height.equalTo(30)
            maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
            maker.left.equalToSuperview().offset(30)
            maker.right.equalTo(self.snp.centerX).offset(-30)
        }
        blackButton.backgroundColor = .black
        blackButton.addTarget(self, action: #selector(self.showView1), for: .touchUpInside)
        
        redButton = UIButton()
        self.addSubview(redButton)
        redButton.snp.makeConstraints{ (maker) in
            maker.height.equalTo(30)
            maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
            maker.left.equalTo(self.snp.centerX).offset(30)
            maker.right.equalToSuperview().offset(-30)
        }
        redButton.backgroundColor = .red
        redButton.addTarget(self, action: #selector(self.showView2), for: .touchUpInside)
    }

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

    @objc func showView1() {
        self.isUserInteractionEnabled = false
        self.redButton.isEnabled = false
        self.blackButton.isEnabled = false
        NSLog("DEBUG -- before constructing testview1")

        testView1 = UIView()
        self.addSubview(testView1)
        testView1.snp.makeConstraints{ (maker) in
            maker.size.equalToSuperview()
            maker.center.equalToSuperview()
        }
        testView1.backgroundColor = .black
        
        let backButton = UIButton()
        testView1.addSubview(backButton)
        backButton.snp.makeConstraints{ (maker) in
            maker.center.equalToSuperview()
            maker.width.equalTo(100)
            maker.height.equalTo(100)
        }
        backButton.backgroundColor = .white
        backButton.addTarget(self, action: #selector(quitView1), for: .touchUpInside)
        
        testView1.transform = CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.height)
        NSLog("DEBUG -- before animating testview1")

        UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseInOut], animations: {
            self.testView1.transform = CGAffineTransform.identity
        }, completion: { _ in
            NSLog("DEBUG -- finished animating testview1")
            self.isUserInteractionEnabled = true
            self.redButton.isEnabled = true
            self.blackButton.isEnabled = true
        })
    }
    
    @objc func quitView1() {
        testView1.removeFromSuperview()
        testView1 = nil
    }
    
    @objc func showView2() {
        self.isUserInteractionEnabled = false
        self.redButton.isEnabled = false
        self.blackButton.isEnabled = false
        NSLog("DEBUG -- before constructing testview2")

        testView2 = UIView()
        self.addSubview(testView2)
        testView2.snp.makeConstraints{ (maker) in
            maker.size.equalToSuperview()
            maker.center.equalToSuperview()
        }
        testView2.backgroundColor = .red
        
        let backButton = UIButton()
        testView2.addSubview(backButton)
        backButton.snp.makeConstraints{ (maker) in
            maker.center.equalToSuperview()
            maker.width.equalTo(100)
            maker.height.equalTo(100)
        }
        backButton.backgroundColor = .white
        backButton.addTarget(self, action: #selector(quitView2), for: .touchUpInside)
        
        testView2.transform = CGAffineTransform(translationX: UIScreen.main.bounds.width, y: 0)
        NSLog("DEBUG -- before animating testview2")
        UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut], animations: {
            self.testView2.transform = CGAffineTransform.identity
        }, completion: { _ in
            NSLog("DEBUG -- finished animating testview2")
            self.isUserInteractionEnabled = true
            self.redButton.isEnabled = true
            self.blackButton.isEnabled = true
        })
    }
    
    @objc func quitView2() {
        testView2.removeFromSuperview()
        testView2 = nil
    }
}

这个实验很简单——你可以在视图控制器中运行这个视图后同时点击这两个按钮,你会看到两个动画同时发生以显示黑色/红色视图。

所以我的期望是,既然我打电话给self.isUserInteractionEnabled = false,我不期望另一个会发生,因为这对我来说是一场意想不到的比赛。但不知何故,self.isUserInteractionEnabled = false 调用在 UIView.animate() 调用的镜头窗口内无效——当然,如果我们没有在完成处理程序中设置self.isUserInteractionEnabled = true,那么整个 UI 就会冻结,这意味着@987654325 @call 大部分时间都在工作。

日志也在一定程度上证明了这一理论。如果我们几乎同时点击按钮,我们会看到如下的日志序列,然后我们会看到view1覆盖view1覆盖testView,而testview1的出现意味着isUserInteractionEnabled = false连同set blackButton/的调用redButton isEnabled = false 不能正常工作,因为另一个按钮仍然对点击有反应。

DEBUG -- before constructing testview2
DEBUG -- before animating testview2
DEBUG -- before constructing testview1
DEBUG -- before animating testview1
DEBUG -- finished animating testview1
DEBUG -- finished animating testview2

有人知道比赛为什么会发生吗?

【问题讨论】:

    标签: ios swift swift5 race-condition


    【解决方案1】:

    问题不是“竞争”条件...您已将按钮操作设置为 .touchUpInside

    因此,如果您触摸并按住黑色按钮,然后点击(触摸并释放)红色按钮,则黑色按钮已经“激活”并等待修饰。在它已经处于活动状态之后尝试禁用它是行不通的。

    一种方法是创建两个 Bool 变量:

    private var blackButtonTapped: Bool = false
    private var redButtonTapped: Bool = false
    

    那么你在每个showView func 中做的第一件事就是:

    @objc func showView1() {
        blackButtonTapped = true
        if redButtonTapped {
            return
        }
        self.redButton.isEnabled = false
        self.blackButton.isEnabled = false
        // ... continue with showView1 code
    }
    
    @objc func showView2() {
        redButtonTapped = true
        if blackButtonTapped {
            return
        }
        self.redButton.isEnabled = false
        self.blackButton.isEnabled = false
        // ... continue with showView2 code
    }
    

    然后当你quit你的其他视图时,重新启用按钮:

    private func reEnableButtons() -> Void {
        blackButton.isEnabled = true
        redButton.isEnabled = true
        blackButtonTapped = false
        redButtonTapped = false
    }
    

    这是您的 LandingView 课程的完整修改版本:

    class LandingView: UIView {
        
        private var blackButton: UIButton!
        private var redButton: UIButton!
        private var testView1: UIView!
        private var testView2: UIView!
        
        private var blackButtonTapped: Bool = false
        private var redButtonTapped: Bool = false
    
        // use a common init function to allow adding this view
        //  in Storyboard / Interface Builder
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
    
        private func commonInit() -> Void {
            
            blackButton = UIButton()
            self.addSubview(blackButton)
            
            // sorry I am using Snapkit here for a quick POC. but if you
            // just draw any button, it should reproduce the problem as well.
            blackButton.snp.makeConstraints{ (maker) in
                maker.height.equalTo(30)
                maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
                maker.left.equalToSuperview().offset(30)
                maker.right.equalTo(self.snp.centerX).offset(-30)
            }
            blackButton.backgroundColor = .black
            blackButton.addTarget(self, action: #selector(self.showView1), for: .touchUpInside)
            
            redButton = UIButton()
            self.addSubview(redButton)
            redButton.snp.makeConstraints{ (maker) in
                maker.height.equalTo(30)
                maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
                maker.left.equalTo(self.snp.centerX).offset(30)
                maker.right.equalToSuperview().offset(-30)
            }
            redButton.backgroundColor = .red
            redButton.addTarget(self, action: #selector(self.showView2), for: .touchUpInside)
    
        }
    
        @objc func showView1() {
            blackButtonTapped = true
            if redButtonTapped {
                return
            }
            self.redButton.isEnabled = false
            self.blackButton.isEnabled = false
            NSLog("DEBUG -- before constructing testview1")
            
            testView1 = UIView()
            self.addSubview(testView1)
            testView1.snp.makeConstraints{ (maker) in
                maker.size.equalToSuperview()
                maker.center.equalToSuperview()
            }
            testView1.backgroundColor = .black
            
            let backButton = UIButton()
            testView1.addSubview(backButton)
            backButton.snp.makeConstraints{ (maker) in
                maker.center.equalToSuperview()
                maker.width.equalTo(100)
                maker.height.equalTo(100)
            }
            backButton.backgroundColor = .white
            backButton.addTarget(self, action: #selector(quitView1), for: .touchUpInside)
            
            testView1.transform = CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.height)
            NSLog("DEBUG -- before animating testview1")
            
            UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseInOut], animations: {
                self.testView1.transform = CGAffineTransform.identity
            }, completion: { _ in
            })
        }
        
        @objc func quitView1() {
            testView1.removeFromSuperview()
            testView1 = nil
            reEnableButtons()
        }
        
        @objc func showView2() {
            redButtonTapped = true
            if blackButtonTapped {
                return
            }
            self.redButton.isEnabled = false
            self.blackButton.isEnabled = false
            NSLog("DEBUG -- before constructing testview2")
            
            testView2 = UIView()
            self.addSubview(testView2)
            testView2.snp.makeConstraints{ (maker) in
                maker.size.equalToSuperview()
                maker.center.equalToSuperview()
            }
            testView2.backgroundColor = .red
            
            let backButton = UIButton()
            testView2.addSubview(backButton)
            backButton.snp.makeConstraints{ (maker) in
                maker.center.equalToSuperview()
                maker.width.equalTo(100)
                maker.height.equalTo(100)
            }
            backButton.backgroundColor = .white
            backButton.addTarget(self, action: #selector(quitView2), for: .touchUpInside)
            
            testView2.transform = CGAffineTransform(translationX: UIScreen.main.bounds.width, y: 0)
            NSLog("DEBUG -- before animating testview2")
            UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut], animations: {
                self.testView2.transform = CGAffineTransform.identity
            }, completion: { _ in
            })
        }
        
        @objc func quitView2() {
            testView2.removeFromSuperview()
            testView2 = nil
            reEnableButtons()
        }
        
        private func reEnableButtons() -> Void {
            blackButton.isEnabled = true
            redButton.isEnabled = true
            blackButtonTapped = false
            redButtonTapped = false
        }
    }
    

    编辑

    这是另一个修改。这会禁用.touchDown 事件上的“其他按钮”(而不是使用 Bool 标志来控制代码执行流程):

    class SecondLandingView: UIView {
        
        private var blackButton: UIButton!
        private var redButton: UIButton!
        private var testView1: UIView!
        private var testView2: UIView!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        func commonInit() -> Void {
            
            blackButton = UIButton()
            self.addSubview(blackButton)
            
            // sorry I am using Snapkit here for a quick POC. but if you
            // just draw any button, it should reproduce the problem as well.
            blackButton.snp.makeConstraints{ (maker) in
                maker.height.equalTo(30)
                maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
                maker.left.equalToSuperview().offset(30)
                maker.right.equalTo(self.snp.centerX).offset(-30)
            }
            blackButton.backgroundColor = .black
            blackButton.addTarget(self, action: #selector(self.blackTouchUpInside), for: .touchUpInside)
            blackButton.addTarget(self, action: #selector(self.touchUpOutside(_:)), for: .touchUpOutside)
            blackButton.addTarget(self, action: #selector(self.touchUpOutside(_:)), for: .touchCancel)
    
            blackButton.addTarget(self, action: #selector(self.touchDown(_:)), for: .touchDown)
    
            redButton = UIButton()
            self.addSubview(redButton)
            redButton.snp.makeConstraints{ (maker) in
                maker.height.equalTo(30)
                maker.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(20)
                maker.left.equalTo(self.snp.centerX).offset(30)
                maker.right.equalToSuperview().offset(-30)
            }
            redButton.backgroundColor = .red
            redButton.addTarget(self, action: #selector(self.redTouchUpInside), for: .touchUpInside)
            redButton.addTarget(self, action: #selector(self.touchUpOutside(_:)), for: .touchUpOutside)
            redButton.addTarget(self, action: #selector(self.touchUpOutside(_:)), for: .touchCancel)
    
            redButton.addTarget(self, action: #selector(self.touchDown(_:)), for: .touchDown)
    
        }
    
        @objc func touchDown(_ sender: Any?) {
            guard let btn = sender as? UIButton else {
                return
            }
            if btn == blackButton {
                print("black touch down")
                redButton.isEnabled = false
            } else {
                print("red touch down")
                blackButton.isEnabled = false
            }
        }
        
        @objc func touchUpOutside(_ sender: Any?) {
            guard let btn = sender as? UIButton else {
                return
            }
            if btn == blackButton {
                print("black touch up outside")
            } else {
                print("red touch up outside")
            }
            reEnableButtons()
        }
        
        @objc func blackTouchUpInside() {
    
            // redButton is already disabled in touchDown()
            self.redButton.isEnabled = false
    
            // red button tapped, so disable it
            self.blackButton.isEnabled = false
    
            NSLog("DEBUG -- before constructing testview1")
            
            testView1 = UIView()
            self.addSubview(testView1)
            testView1.snp.makeConstraints{ (maker) in
                maker.size.equalToSuperview()
                maker.center.equalToSuperview()
            }
            testView1.backgroundColor = .black
            
            let backButton = UIButton()
            testView1.addSubview(backButton)
            backButton.snp.makeConstraints{ (maker) in
                maker.center.equalToSuperview()
                maker.width.equalTo(100)
                maker.height.equalTo(100)
            }
            backButton.backgroundColor = .white
            backButton.addTarget(self, action: #selector(quitView1), for: .touchUpInside)
            
            testView1.transform = CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.height)
            NSLog("DEBUG -- before animating testview1")
            
            UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseInOut], animations: {
                self.testView1.transform = CGAffineTransform.identity
            }, completion: { _ in
            })
        }
        
        @objc func quitView1() {
            testView1.removeFromSuperview()
            testView1 = nil
            reEnableButtons()
        }
        
        @objc func redTouchUpInside() {
            
            // red button tapped, so disable it
            self.redButton.isEnabled = false
            
            // blackButton is already disabled in touchDown()
            //self.blackButton.isEnabled = false
            
            NSLog("DEBUG -- before constructing testview2")
            
            testView2 = UIView()
            self.addSubview(testView2)
            testView2.snp.makeConstraints{ (maker) in
                maker.size.equalToSuperview()
                maker.center.equalToSuperview()
            }
            testView2.backgroundColor = .red
            
            let backButton = UIButton()
            testView2.addSubview(backButton)
            backButton.snp.makeConstraints{ (maker) in
                maker.center.equalToSuperview()
                maker.width.equalTo(100)
                maker.height.equalTo(100)
            }
            backButton.backgroundColor = .white
            backButton.addTarget(self, action: #selector(quitView2), for: .touchUpInside)
            
            testView2.transform = CGAffineTransform(translationX: UIScreen.main.bounds.width, y: 0)
            NSLog("DEBUG -- before animating testview2")
            UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut], animations: {
                self.testView2.transform = CGAffineTransform.identity
            }, completion: { _ in
            })
        }
        
        @objc func quitView2() {
            testView2.removeFromSuperview()
            testView2 = nil
            reEnableButtons()
        }
        
        private func reEnableButtons() -> Void {
            blackButton.isEnabled = true
            redButton.isEnabled = true
        }
    }
    

    【讨论】:

    • 您好,DonMag,感谢您的回答!我可能不接受答案,原因如下—— 1. 您在代码中创建了一个互斥锁,表示如果单击任何红色/黑色按钮,那么我们将直接返回。不过,这听起来不是一个理想的解决方案。 2. 你说 self.redButton.isEnabled = false 例如不起作用,但如果你在这个操作之后添加一些睡眠,这确实有效。在我的测试中,我发现这个锁只有在我们进入 UIView.animate() 调用时才会被释放,参见我在最新描述中提供的日志序列。
    • @user2870133 - 与设置 Bool .isEnabled 标志没有太大区别。但是,如果您真的不想这样做,请参阅我的答案的编辑
    猜你喜欢
    • 2018-08-15
    • 1970-01-01
    • 1970-01-01
    • 2015-08-24
    • 2020-02-25
    • 1970-01-01
    • 1970-01-01
    • 2013-05-15
    • 1970-01-01
    相关资源
    最近更新 更多