Swift: UIView.animate работает неожиданно

Я пытаюсь сделать «кнопку записи», которая представляет собой UIView с распознавателем жестов для моего приложения. Прямо сейчас я реализую функцию, согласно которой, когда я нажимаю на вид, я хочу уменьшить внутренний круг (красный), однако это заканчивается неожиданным преобразованием. Для этого я использую функцию UIView.animate, и ниже приведен соответствующий код:

import UIKit

@IBDesignable
class RecordView: UIView {

    @IBInspectable
    var borderColor: UIColor = UIColor.clear {
        didSet {
            self.layer.borderColor = borderColor.cgColor
        }
    }

    @IBInspectable
    var borderWidth: CGFloat = 20 {
        didSet {
            layer.borderWidth = borderWidth
        }
    }

    @IBInspectable
    var cornerRadius: CGFloat = 100 {
        didSet {
            layer.cornerRadius = cornerRadius
        }
    }

    private var fillView = UIView()

    private func setupFillView() {
        let radius = (self.cornerRadius - self.borderWidth) * 0.95
        fillView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
        fillView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
        fillView.layer.cornerRadius = radius
        fillView.backgroundColor = UIColor.red
        self.addSubview(fillView)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        setupFillView()
    }

    func didClick() {
        UIView.animate(withDuration: 1.0, animations: {
            self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
        }) { (true) in
            print()
        }
    }

}

В моем ViewController у меня есть:

@IBAction func recoreViewTapped(_ sender: UITapGestureRecognizer) {
        recordView.didClick()
    }

Однако это приводит к такому эффекту: https://media.giphy.com/media/3ohjUQrWt7vfIxzBrG/giphy.gif. Я новичок и действительно не знаю, что не так с моим кодом?


person Yongliang He    schedule 03.12.2017    source источник
comment
У вас неопределенное поведение. Вы применяете преобразование к представлению.. но в подпредставлениях макета вы изменяете его фрейм с помощью функции setupFillView. Вы НЕ МОЖЕТЕ этого сделать. Преобразования уже изменяют фрейм представления. По этой причине при использовании необработанной базовой анимации мы всегда изменяем границы и положение слоев, а не фреймов. Затем вы добавляете представления в подпредставления макета.. это вызовет рекурсию, потому что представление снова будет макетироваться..   -  person Brandon    schedule 03.12.2017
comment
@Брэндон Спасибо за ваш ответ. Я все еще чувствую себя сбитым с толку. Я нахожу небольшие ресурсы по этой теме и просто собираю код из нескольких туториалов. Не могли бы вы рассказать мне, как это исправить?   -  person Yongliang He    schedule 03.12.2017


Ответы (1)


Проблема в том, что вы применяете преобразование, которое снова запускает layoutSubviews, поэтому сброс frame из fillView применяется к преобразованному представлению.

Есть как минимум два варианта:

  1. Вы можете просто преобразовать весь вид RecordButton:

    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        private var fillView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(fillView)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let radius = min(bounds.width, bounds.height) / 2  - borderWidth
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = radius
        }
    
        func didClick() {
            UIView.animate(withDuration: 1.0, animations: {
                self.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
            }, completion: { _ in
                print("completion", #function)
            })
        }
    
    }
    
  2. Или вы можете поместить fillView в контейнер, и тогда преобразование fillView не вызовет layoutSubviews RecordView:

    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        private var fillView = UIView()
        private var containerView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(containerView)
            containerView.addSubview(fillView)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            containerView.frame = bounds
    
            let radius = min(bounds.width, bounds.height) / 2  - borderWidth
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = radius
        }
    
        func didClick() {
            UIView.animate(withDuration: 1.0, animations: {
                self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
            }, completion: { _ in
                print("completion", #function)
            })
        }
    
    }
    

Кстати, еще несколько мелочей:

  • Как мы с вами обсуждали в другом месте, первоначальная конфигурация (например, добавление подвида) должна вызываться из init. Но любой frame/bounds случайный материал должен вызываться из layoutSubviews.

  • Совершенно не связанный, но параметр, переданный completion из UIView.animate(withDuration:animations:completion:), является логическим значением, указывающим, является ли анимация finished или нет. Если вы не собираетесь использовать это логическое значение, вам следует использовать _, а не (true).

  • В более долгосрочной перспективе я бы предложил переместить элементы, связанные с «прикосновениями»/жестами, в RecordButton. Я легко могу представить, как вы становитесь все более увлеченным: например. одна анимация, когда вы «прикасаетесь» (уменьшаете кнопку, чтобы отобразить вибрацию «нажатия»), а другая — для «поднятия» (например, уменьшаете кнопку и преобразуете круглую кнопку «запись» в квадратную кнопку «стоп»). Затем вы можете заставить кнопку информировать контроллер представления, когда происходит что-то важное (например, запись или остановка, распознанная), но это снижает сложность самого контроллера представления.

    protocol RecordViewDelegate: class {
        func didTapRecord()
        func didTapStop()
    }
    
    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        weak var delegate: RecordViewDelegate?
    
        var isRecordButton = true
    
        private var fillView = UIView()
        private var containerView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(containerView)
            containerView.addSubview(fillView)
        }
    
        private var radius: CGFloat { return min(bounds.width, bounds.height) / 2  - borderWidth }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            containerView.frame = bounds
    
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = isRecordButton ? radius : 0
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            UIView.animate(withDuration: 0.25) {
                self.fillView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                self.fillView.backgroundColor = UIColor(red: 0.75, green: 0, blue: 0, alpha: 1)
            }
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            if let touch = touches.first, containerView.bounds.contains(touch.location(in: containerView)) {
                if isRecordButton {
                    delegate?.didTapRecord()
                } else {
                    delegate?.didTapStop()
                }
                isRecordButton = !isRecordButton
            }
    
            UIView.animate(withDuration: 0.25) {
                self.fillView.transform = .identity
                self.fillView.backgroundColor = UIColor.red
                self.fillView.layer.cornerRadius = self.isRecordButton ? self.radius : 0
            }
    
        }
    }
    

    Это дает:

    введите здесь описание изображения

person Rob    schedule 03.12.2017
comment
когда я реализую это и даже копирую код напрямую, я не вижу, чтобы layer.cornerRadius анимировался. Затем в другом потоке кто-то говорит, что основная анимация должна использоваться для того, чтобы анимировать угловой радиус. Как вы могли реализовать это с помощью UIKit? Или что-то еще пропущено в моем коде? - person Yongliang He; 05.12.2017
comment
В iOS 11 его можно анимировать, но в более ранних версиях вам нужно использовать CoreAnimation или другие методы. - person Rob; 05.12.2017