代码之家  ›  专栏  ›  技术社区  ›  Kevin van Mierlo

iOS复杂的动画协调,如Android Animator(set)

  •  1
  • Kevin van Mierlo  · 技术社区  · 7 年前

    我在我的android应用程序中使用animator类制作了一个相当复杂的动画。我想把这个动画移植到ios。最好是有点像安卓动画师。我环顾四周,似乎什么都不是我想要的。我得到的最接近的是兴奋。但不幸的是,如果将所有的子代理放在一个组中,它们都会被忽略。

    让我从我在android上制作的动画开始。我正在设置三个视图组(包含一个imageview和一个textview)的动画。每个按钮我有一个动画,它将视图转换到左边,同时将alpha动画设置为0。在该动画之后,还有另一个动画将同一视图从右侧转换到原始位置,并将alpha设置为1。除了“平移”和“Alpha”动画之外,还有一个视图也具有缩放动画。所有视图都使用不同的计时功能(放松)。动画输入和动画输出是不同的,一个视图对于缩放有不同的计时功能,而alpha和translate动画使用相同的计时功能。第一个动画结束后,我正在设置值以准备第二个动画。缩放动画的持续时间也比平移和Alpha动画短。我把单个动画(translate和alpha)放在一个animatorset(基本上是一个动画组)中。这个animatorset放在另一个animatorset中,以便在每个animatorset之后运行动画(第一个animate和than in)。这个animatorset放在另一个animatorset中,这个animatorset同时运行所有3个按钮的动画。

    对不起,解释太长了。但这样你就知道我是怎么把这个移植到ios的。这个对uiview.animate()来说太复杂了。如果将caanimation放入caanimationgroup,则caanimation将重写委托。据我所知,ViewPropertyImator不允许自定义计时函数,并且无法协调多个动画。

    有人知道我能用什么做这个吗?我也可以自定义实现,它给我一个回调每个动画勾号,以便我可以相应地更新视图。


    编辑

    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 回复  |  直到 7 年前
        1
  •  2
  •   Kevin van Mierlo    7 年前

    所以我一直在研究自己的解决方案 CADisplayLink . 文档就是这样描述的 Cadisplaylink公司 :

    CadisplayLink是一个计时器对象,它允许应用程序 将其图形与显示的刷新率同步。

    它基本上提供了执行绘图代码时的回调(以便可以顺利运行动画)。

    我不打算在这个答案中解释所有的事情,因为这将是一个很大的代码,而且大部分应该是清楚的。如果有什么不清楚的地方或者你有问题,你可以在这个答案下面发表评论。

    此解决方案使动画完全自由,并提供协调动画的能力。我看了很多 Animator 类,并希望使用类似的语法,以便我们可以轻松地将动画从Android移植到iOS或其他方式。我已经测试了几天了,也去掉了一些怪癖。但是说够了,让我们看看代码!

    这就是 动画师 类,这是动画类的基本结构:

    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布尔值。这个布尔值是当 动画师 被一群人控制。如果是,则组将提供动画标记,而此类不应启动 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 在这里。这个 插补器 稍后将显示类。这个类提供了动画的简化。这里就是 内插进度 进来。 progress 只是从0.0到1.0的线性进程,但是 内插进度 可能对宽松政策有不同的价值。例如,当 进步 值为0.2, 内插进度 可能已经有0.4基于你将使用的宽松。同时确保使用 内插进度 计算正确的值。一个例子和 值动画师 在下面。

    下面是 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)
        }
    }
    

    这是一个如何对 值动画师 如果你需要像双倍或整数这样的其他函数,你可以做更多这样的函数。你只需要提供一个开始和结束值 动画师 基于 内插进度 什么电流 animatedValue 是。你可以用这个 动画值 更新您的视图。最后我会举一个例子。

    因为我说过 插补器 已经几次了,我们会继续 插补器 现在:

    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
    
            }
        }
    }
    

    这些只是一些特定测量的计算示例。实际上,我在这里移植了所有为安卓设计的版本: 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 布尔函数。你可以在这个类中放置多个你制作的动画来协调它们。因为这个类还扩展了 动画师 你也可以放另一个 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()
        }
    
    }
    

    我认为这其中的大部分都可以说明问题。我创建了一个视图来转换x并淡出。对于您实现的每个动画 onAnimationUpdate 回调以更改视图中使用的值,如本例中的转换x和alpha。

    注: 与android相反,这里的持续时间和延迟以秒为单位,而不是以毫秒为单位。

    我们现在正在使用这个代码,它工作得很好!我已经在我们的android应用程序中写了一些动画。我可以很容易地将动画移植到iOS,只需少量重写,动画的工作原理完全相同!我可以复制我问题中写的代码,将kotlin代码改为swift,应用 动画更新 ,将持续时间和延迟更改为秒,动画的效果就像一个符咒。

    我想把它作为一个开放源码库发布,但是我还没有这样做。我发布后会更新这个答案。

    如果您对代码或它如何工作有任何疑问,请随时提出。

        2
  •  1
  •   Nordeast mohammad_Z74    7 年前

    这是一个动画的开始,我想你正在寻找。如果你不喜欢幻灯片的计时,你可以把 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