iOS комплексная анимация координации, как Android Animator (набор) - PullRequest
0 голосов
/ 06 июня 2018

Я сделал довольно сложную анимацию в своем приложении для Android с использованием классов Animator.Я хочу портировать эту анимацию на iOS.Предпочтительно это чем-то похоже на Android Animator.Я оглянулся вокруг, и, кажется, ничего не то, что я хочу.Самый близкий, который я получил, был с CAAnimation.Но, к сожалению, все дочерние делегаты игнорируются, если их помещают в группу.

Позвольте мне начать с анимации, которую я сделал на Android.Я анимирую три группы представления (которая содержит ImageView и TextView).Для каждой кнопки у меня есть анимация, которая переводит вид влево и одновременно анимирует альфа в 0. После этой анимации есть другая анимация, которая переводит тот же самый вид справа в исходное положение, а также анимирует альфа обратно в 1.Есть один вид, который также имеет масштабную анимацию, кроме анимации перевода и альфа.Все виды используют разные функции синхронизации (ослабление).Анимация и анимация различны, и один вид имеет другую функцию синхронизации для масштаба, в то время как анимация альфа и трансляция использует то же самое.После окончания первой анимации я устанавливаю значения для подготовки второй анимации.Продолжительность анимации масштаба также короче, чем анимация перевода и альфа.Я помещаю отдельные анимации (перевод и альфа) в AnimatorSet (в основном это группа анимаций).Этот AnimatorSet помещается в другой AnimatorSet для запуска анимации после каждого другого (сначала анимация, а затем -).И этот AnimatorSet помещен в другой AnimatorSet, который запускает анимацию всех 3 кнопок одновременно.

Извините за длинное объяснение.Но так вы понимаете, как я пытаюсь перенести это на iOS.Этот слишком сложен для UIView.animate ().CAAnimation переопределяет делегатов, если помещается в CAAnimationGroup.ViewPropertyAnimator не позволяет использовать собственные функции синхронизации, насколько мне известно, и не может координировать несколько анимаций.

Кто-нибудь знает, что я могу использовать для этого?Я также в порядке с пользовательской реализацией, которая дает мне обратный вызов при каждом тике анимации, чтобы я мог соответствующим образом обновить представление.


Редактировать

Анимация Androidкод:

fun setState(newState: State) {
    if(state == newState) {
        return
    }

    processing = false

    val prevState = state
    state = newState

    val reversed = newState.ordinal < prevState.ordinal

    val animators = ArrayList<Animator>()
    animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
        displayMiddleButtonState()
    }))

    if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
        animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
            displayLeftButtonState()
        }))
    }

    if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
        animators.add(getButtonAnimator(
            if(newState == State.TAKE_PICTURE) rightButton else null,
            if(newState == State.CROP_PICTURE) rightButton else null,
            rightButton.imageView.width.toFloat(),
            reversed,
            halfAnimationDone = {
                displayRightButtonState(inAnimation = true)
            }))
    }

    val animatorSet = AnimatorSet()
    animatorSet.playTogether(animators)
    animatorSet.start()
}

fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
    val animators = ArrayList<Animator>()

    if(animateInView != null) {
        val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
        if(animateOutView == null) {
            animateInAnimator.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animation: Animator?) {
                    halfAnimationDone()
                }
            })
        }
        animators.add(animateInAnimator)
    }

    if(animateOutView != null) {
        val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
        animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                halfAnimationDone()
            }
        })
        animators.add(animateOutAnimator)
    }

    val animatorSet = AnimatorSet()
    animatorSet.playTogether(animators)

    return animatorSet
}

private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
    val translateDuration = 140L
    val fadeDuration = translateDuration

    val translateValues =
        if(animateIn) {
            if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
            else floatArrayOf(maxTranslationXValue, 0f)
        } else {
            if(reversed) floatArrayOf(0f, maxTranslationXValue)
            else floatArrayOf(0f, -maxTranslationXValue)
        }
    val alphaValues =
        if(animateIn) {
            floatArrayOf(0f, 1f)
        } else {
            floatArrayOf(1f, 0f)
        }

    val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
    val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)

    translateAnimator.duration = translateDuration
    fadeAnimator.duration = fadeDuration

    if(animateIn) {
        translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
        fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
    } else {
        translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
        fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
    }

    val animateSet = AnimatorSet()
    if(animateIn) {
        animateSet.startDelay = translateDuration
    }
    animateSet.playTogether(translateAnimator, fadeAnimator)

    return animateSet
}

fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
    val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
    val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)

    animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator?) {
            halfAnimationDone()
        }
    })

    val animatorSet = AnimatorSet()
    animatorSet.playTogether(animateInAnimator, animateOutAnimator)

    return animatorSet
}

private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
    val translateDuration = 140L
    val scaleDuration = 100L
    val fadeDuration = translateDuration
    val maxTranslationXValue = middleButtonImageView.width.toFloat()

    val translateValues =
        if(animateIn) {
            if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
            else floatArrayOf(maxTranslationXValue, 0f)
        } else {
            if(reversed) floatArrayOf(0f, maxTranslationXValue)
            else floatArrayOf(0f, -maxTranslationXValue)
        }
    val scaleValues =
        if(animateIn) floatArrayOf(0.8f, 1f)
        else floatArrayOf(1f, 0.8f)
    val alphaValues =
        if(animateIn) {
            floatArrayOf(0f, 1f)
        } else {
            floatArrayOf(1f, 0f)
        }

    val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
    val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
    val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
    val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)

    translateAnimator.duration = translateDuration
    scaleXAnimator.duration = scaleDuration
    scaleYAnimator.duration = scaleDuration
    fadeAnimator.duration = fadeDuration

    if(animateIn) {
        translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
        scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
        scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
        fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
    } else {
        translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
        scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
        scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
        fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
    }

    if(animateIn) {
        val scaleStartDelay = translateDuration - scaleDuration
        val scaleStartValue = scaleValues[0]

        middleButtonImageView.scaleX = scaleStartValue
        middleButtonImageView.scaleY = scaleStartValue

        scaleXAnimator.startDelay = scaleStartDelay
        scaleYAnimator.startDelay = scaleStartDelay
    }

    val animateSet = AnimatorSet()
    if(animateIn) {
        animateSet.startDelay = translateDuration
    }
    animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)

    return animateSet
}

Редактировать 2

Вот видео о том, как выглядит анимация на Android:

https://youtu.be/IKAB9A9qHic

Ответы [ 2 ]

0 голосов
/ 13 июня 2018

Итак, я работаю над своим собственным решением, используя CADisplayLink.Вот как описывается в документации CADisplayLink:

CADisplayLink - это объект таймера, который позволяет вашему приложению синхронизировать чертеж с частотой обновления дисплея.

Itв основном обеспечивает обратный вызов, когда нужно выполнить код рисования (чтобы вы могли плавно запускать анимацию).

Я не собираюсь все объяснять во время этого ответа, потому что это будет много кода, и большая часть его должнабыть ясным.Если что-то неясно или у вас есть вопрос, вы можете прокомментировать этот ответ ниже.

Это решение дает полную свободу анимации и дает возможность координировать их.Я много смотрел на класс Animator на Android и хотел подобный синтаксис, чтобы мы могли легко переносить анимацию с Android на iOS или наоборот.Я проверил это в течение нескольких дней и также удалил некоторые причуды.Но достаточно разговоров, давайте посмотрим некоторый код!

Это класс Animator, который является базовой структурой для классов анимации:

class Animator {
    internal var displayLink: CADisplayLink? = nil
    internal var startTime: Double = 0.0
    var hasStarted: Bool = false
    var hasStartedAnimating: Bool = false
    var hasFinished: Bool = false
    var isManaged: Bool = false
    var isCancelled: Bool = false

    var onAnimationStart: () -> Void = {}
    var onAnimationEnd: () -> Void = {}
    var onAnimationUpdate: () -> Void = {}
    var onAnimationCancelled: () -> Void = {}

    public func start() {
        hasStarted = true

        startTime = CACurrentMediaTime()
        if(!isManaged) {
            startDisplayLink()
        }
    }

    internal func startDisplayLink() {
        stopDisplayLink() // make sure to stop a previous running display link

        displayLink = CADisplayLink(target: self, selector: #selector(animationTick))
        displayLink?.add(to: .main, forMode: .commonModes)
    }

    internal func stopDisplayLink() {
        displayLink?.invalidate()
        displayLink = nil
    }

    @objc internal func animationTick() {

    }

    public func cancel() {
        isCancelled = true
        onAnimationCancelled()
        if(!isManaged) {
            animationTick()
        }
    }
}

Он содержит все жизненно важные функции, такие как запускCADisplayLink, предоставляя возможность остановки CADisplayLink (когда анимация завершена), логические значения, которые указывают состояние и некоторые обратные вызовы.Вы также заметите логическое значение isManaged.Это логическое значение, когда Animator контролируется группой.Если это так, группа будет предоставлять анимационные тики, и этот класс не должен начинать CADisplayLink.

Следующим будет ValueAnimator:

class ValueAnimator : Animator {
    public internal(set) var progress: Double = 0.0
    public internal(set) var interpolatedProgress: Double = 0.0

    var duration: Double = 0.3
    var delay: Double = 0
    var interpolator: Interpolator = EasingInterpolator(ease: .LINEAR)

    override func animationTick() {
        // In case this gets called after we finished
        if(hasFinished) {
            return
        }

        let elapsed: Double = (isCancelled) ? self.duration : CACurrentMediaTime() - startTime - delay

        if(elapsed < 0) {
            return
        }

        if(!hasStartedAnimating) {
            hasStartedAnimating = true
            onAnimationStart()
        }

        if(duration <= 0) {
            progress = 1.0
        } else {
            progress = min(elapsed / duration, 1.0)
        }
        interpolatedProgress = interpolator.interpolate(elapsedTimeRate: progress)

        updateAnimationValues()
        onAnimationUpdate()

        if(elapsed >= duration) {
            endAnimation()
        }
    }

    private func endAnimation() {
        hasFinished = true
        if(!isManaged) {
            stopDisplayLink()
        }
        onAnimationEnd()
    }

    internal func updateAnimationValues() {

    }
}

Этот класс являетсябазовый класс для всех значений аниматоров.Но он также может быть использован для анимации, если вы хотите сделать вычисления самостоятельно.Вы, наверное, заметили Interpolator и interpolatedProgress здесь.Класс Interpolator будет показан чуть позже.Этот класс обеспечивает ослабление анимации.Вот где приходит interpolatedProgress. progress - это просто линейный прогресс от 0,0 до 1,0, но interpolatedProgress может иметь другое значение для замедления.Например, когда progress имеет значение 0,2, interpolatedProgress может уже иметь 0,4 в зависимости от того, какое смягчение вы будете использовать.Также обязательно используйте interpolatedProgress для вычисления правильного значения.Ниже приведен пример первого подкласса ValueAnimator.

Ниже приведен CGFloatValueAnimator, который, как следует из названия, анимирует значения CGFloat:

class CGFloatValueAnimator : ValueAnimator {
    private let startValue: CGFloat
    private let endValue: CGFloat
    public private(set) var animatedValue: CGFloat

    init(startValue: CGFloat, endValue: CGFloat) {
        self.startValue = startValue
        self.endValue = endValue
        self.animatedValue = startValue
    }

    override func updateAnimationValues() {
        animatedValue = startValue + CGFloat(Double(endValue - startValue) * interpolatedProgress)
    }
}

Это примеро том, как создать подкласс ValueAnimator, и вы можете сделать многое другое, например, если вам нужны другие, например, двойные или целые числа.Вы просто указываете начальное и конечное значение, и Animator вычисляет на основе interpolatedProgress, что является текущим animatedValue.Вы можете использовать это animatedValue, чтобы обновить ваш взгляд.В конце я приведу пример.

Поскольку я уже упоминал Interpolator уже пару раз, мы перейдем к Interpolator сейчас:

protocol Interpolator {
    func interpolate(elapsedTimeRate: Double) -> Double
}

Это простопротокол, который вы можете реализовать самостоятельно.Я покажу вам часть класса EasingInterpolator, который я использую сам.Я могу предоставить больше, если кому-то это нужно.

class EasingInterpolator : Interpolator {
    private let ease: Ease

    init(ease: Ease) {
        self.ease = ease
    }

    func interpolate(elapsedTimeRate: Double) -> Double {
        switch (ease) {
            case Ease.LINEAR:
                return elapsedTimeRate
            case Ease.SINE_IN:
                return (1.0 - cos(elapsedTimeRate * Double.pi / 2.0))
            case Ease.SINE_OUT:
                return sin(elapsedTimeRate * Double.pi / 2.0)
            case Ease.SINE_IN_OUT:
                return (-0.5 * (cos(Double.pi * elapsedTimeRate) - 1.0))
            case Ease.CIRC_IN:
                return  -(sqrt(1.0 - elapsedTimeRate * elapsedTimeRate) - 1.0)
            case Ease.CIRC_OUT:
                let newElapsedTimeRate = elapsedTimeRate - 1
                return sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate)
            case Ease.CIRC_IN_OUT:
                var newElapsedTimeRate = elapsedTimeRate * 2.0
                if (newElapsedTimeRate < 1.0) {
                    return (-0.5 * (sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate) - 1.0))
                }
                newElapsedTimeRate -= 2.0
                return (0.5 * (sqrt(1 - newElapsedTimeRate * newElapsedTimeRate) + 1.0))

            default:
                return elapsedTimeRate

        }
    }
}

Это всего лишь несколько примеров расчетов для конкретных послаблений.Я фактически перенес все настройки, сделанные для Android, расположенные здесь: https://github.com/MasayukiSuda/EasingInterpolator.

Перед тем, как показать пример, у меня есть еще один класс для показа.Какой класс позволяет группировать аниматоры:

class AnimatorSet : Animator {
    private var animators: [Animator] = []

    var delay: Double = 0
    var playSequential: Bool = false

    override func start() {
        super.start()
    }

    override func animationTick() {
        // In case this gets called after we finished
        if(hasFinished) {
            return
        }

        let elapsed = CACurrentMediaTime() - startTime - delay
        if(elapsed < 0 && !isCancelled) {
            return
        }

        if(!hasStartedAnimating) {
            hasStartedAnimating = true
            onAnimationStart()
        }

        var finishedNumber = 0
        for animator in animators {
            if(!animator.hasStarted) {
                animator.start()
            }
            animator.animationTick()
            if(animator.hasFinished) {
                finishedNumber += 1
            } else {
                if(playSequential) {
                    break
                }
            }
        }

        if(finishedNumber >= animators.count) {
            endAnimation()
        }
    }

    private func endAnimation() {
        hasFinished = true
        if(!isManaged) {
            stopDisplayLink()
        }
        onAnimationEnd()
    }

    public func addAnimator(_ animator: Animator) {
        animator.isManaged = true
        animators.append(animator)
    }

    public func addAnimators(_ animators: [Animator]) {
        for animator in animators {
            animator.isManaged = true
            self.animators.append(animator)
        }
    }

    override func cancel() {
        for animator in animators {
            animator.cancel()
        }

        super.cancel()
    }
}

Как вы можете видеть, здесь я установил логическое значение isManaged.Вы можете поместить несколько аниматоров, которые вы делаете внутри этого класса, чтобы координировать их.И поскольку этот класс также расширяет Animator, вы также можете добавить еще AnimatorSet или несколько.По умолчанию он запускает все анимации одновременно, но если для playSequential установлено значение true, он будет запускать все анимации по порядку.

Время для демонстрации:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let animView = UIView()
        animView.backgroundColor = UIColor.yellow
        self.view.addSubview(animView)

        animView.snp.makeConstraints { maker in
            maker.width.height.equalTo(100)
            maker.center.equalTo(self.view)
        }

        let translateAnimator = CGFloatValueAnimator(startValue: 0, endValue: 100)
        translateAnimator.delay = 1.0
        translateAnimator.duration = 1.0
        translateAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
        translateAnimator.onAnimationStart = {
            animView.backgroundColor = UIColor.blue
        }
        translateAnimator.onAnimationEnd = {
            animView.backgroundColor = UIColor.green
        }
        translateAnimator.onAnimationUpdate = {
            animView.transform.tx = translateAnimator.animatedValue
        }

        let alphaAnimator = CGFloatValueAnimator(startValue: animView.alpha, endValue: 0)
        alphaAnimator.delay = 1.0
        alphaAnimator.duration = 1.0
        alphaAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
        alphaAnimator.onAnimationUpdate = {
            animView.alpha = alphaAnimator.animatedValue
        }

        let animatorSet = AnimatorSet()
//        animatorSet.playSequential = true // Uncomment this to play animations in order
        animatorSet.addAnimator(translateAnimator)
        animatorSet.addAnimator(alphaAnimator)

        animatorSet.start()
    }

}

Я думаюбольшая часть этого будет говорить сама за себя.Я создал вид, который переводит х и исчезает.Для каждой анимации вы реализуете обратный вызов onAnimationUpdate для изменения значений, используемых в представлении, как в этом случае для перевода x и alpha.

Примечание: В отличие от Android, продолжительность изадержка здесь в секундах вместо миллисекунд.

Мы работаем с этим кодом прямо сейчас, и он отлично работает!Я уже написал некоторые анимационные материалы в нашем приложении для Android.Я мог бы легко перенести анимацию на iOS с минимальным переписыванием, и анимация работает точно так же!Я мог скопировать код, написанный в моем вопросе, изменив код Котлина на Swift, применив onAnimationUpdate, изменил продолжительность и задержки на секунды, и анимация работала как чудо.

Я хочу выпустить это какбиблиотека с открытым исходным кодом, но я еще не сделал этого.Я обновлю этот ответ, когда выпущу его.

Если у вас есть какие-либо вопросы о коде или его работе, не стесняйтесь спрашивать.

0 голосов
/ 07 июня 2018

Вот начало анимации, которую, я думаю, вы ищете.Если вам не нравится время показа слайдов, вы можете отключить UIView.animate с .curveEaseInOut для CAKeyframeAnimation, где вы можете более детально контролировать каждый кадр.Вы бы хотели CAKeyFrameAnimation для каждого анимируемого представления.

enter image description here

Это игровая площадка, и вы можете скопировать и вставить ее на пустую игровую площадку, чтобы увидеть ее в действии.

import UIKit
import Foundation
import PlaygroundSupport

class ViewController: UIViewController {

    let bottomBar = UIView()
    let orangeButton = UIButton(frame: CGRect(x: 0, y: 10, width: 75, height: 75))
    let yellow = UIView(frame: CGRect(x: 20, y: 20, width: 35, height: 35))
    let magenta = UIView(frame: CGRect(x: 80, y: 30, width: 15, height: 15))
    let cyan = UIView(frame: CGRect(x: 50, y: 20, width: 35, height: 35))
    let brown = UIView(frame: CGRect(x: 150, y: 30, width:
    15, height: 15))
    let leftBox = UIView(frame: CGRect(x: 15, y: 10, width: 125, height: 75))

    func setup() {

        let reset = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        reset.backgroundColor = .white
        reset.addTarget(self, action: #selector(resetAnimation), for: .touchUpInside)
        self.view.addSubview(reset)

        bottomBar.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
        bottomBar.backgroundColor = .purple
        self.view.addSubview(bottomBar)

        orangeButton.backgroundColor = .orange
        orangeButton.center.x = bottomBar.frame.size.width / 2
        orangeButton.addTarget(self, action: #selector(orangeTapped(sender:)), for: .touchUpInside)
        orangeButton.clipsToBounds = true
        bottomBar.addSubview(orangeButton)

        yellow.backgroundColor = .yellow
        orangeButton.addSubview(yellow)

        magenta.backgroundColor = .magenta
        magenta.alpha = 0
        orangeButton.addSubview(magenta)

        // Left box is an invisible bounding box to get the effect that the view appeared from nowhere
        // Clips to bounds so you cannot see the view when it has not been animated
        // Try setting to false
        leftBox.clipsToBounds = true
        bottomBar.addSubview(leftBox)

        cyan.backgroundColor = .cyan
        leftBox.addSubview(cyan)

        brown.backgroundColor = .brown
        brown.alpha = 0
        leftBox.addSubview(brown)
    }

    @objc func orangeTapped(sender: UIButton) {

        // Perform animation
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {

            self.yellow.frame = CGRect(x: -20, y: 30, width: 15, height: 15)
            self.yellow.alpha = 0

            self.magenta.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
            self.magenta.alpha = 1

            self.cyan.frame = CGRect(x: -150, y: 30, width: 15, height: 15)
            self.cyan.alpha = 0

            self.brown.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
            self.brown.alpha = 1

        }, completion: nil)
    }

    @objc func resetAnimation() {
        // Reset the animation back to the start
        yellow.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
        yellow.alpha = 1
        magenta.frame = CGRect(x: 80, y: 30, width: 15, height: 15)
        magenta.alpha = 0
        cyan.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
        cyan.alpha = 1
        brown.frame = CGRect(x: 150, y: 30, width: 15, height: 15)
        brown.alpha = 0
    }

}
let viewController = ViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
viewController.view.backgroundColor = .blue
viewController.setup()
PlaygroundPage.current.liveView = viewController
...