iOS комплексная анимация координации, как Android Animator(набор)

Я сделал довольно сложную анимацию в своем приложении для 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 ответа

Решение

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

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

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

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

Это решение дает полную свободу анимации и дает возможность координировать их. Я много смотрел на 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 обратный вызов для изменения значений, используемых в представлении, как в этом случае перевод х и альфа.

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

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

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

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

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

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

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
Другие вопросы по тегам