iOS 使用 RxSwift 实现链式动画

英语原文地址:RxSwift and Animations in iOS - VADIM DAGMAN


如果你是一个对UI界面充满激情的 iOS 开发者,当谈及动画的时候,你会感受到 UIKit 的强大。让一个UIView 执行动画像做蛋糕一样简单。你不需要去想太多的关于随着时间变透明,旋转,移动或者晃动/伸展。但是, 如果你想将动画连接在一起,建立它们之间的依赖关系,代码可能会因许多嵌套的闭包和缩进变的相当冗长,难以理解。

这篇文章,我将探讨如果运用响应式框架例如 RxSwift, 使代码看起来更加的干净,更好的阅读和理解。这个想法出现源于一个客户的一个项目,一个对UI非常挑剔的客户(完全匹配我的激情),他想让他 app 的 UI 以一种非常特别的方式展示,拥有很多非常圆滑的过渡和动画。其中他们的一个想法就是像讲故事一样介绍他们 APP,他们想让整个故事通过一连串的动画去叙述,同时也可以很容易的进行调整和优化,而不是通过播放一个预先定义好的视频。RxSwift 就是一个完美的选择对于这个问题,因此我希望你读完这篇文章能感受得到。


响应式编程简单介绍

响应式编程成为主要的和运用于大多数现代编程语言。为什么响应式编程是一个强大的概念,有大量的书和博客进行了详细的解释, 以及它的设计原则和设计模式是如何有助于鼓励好的软件去执行。同时它也是一个工具,可以帮助你大大减少代码杂乱。

我非常喜欢其中的一个方面—就是你可以通过一个非常简单和易读的方式去连接很多异步操作并传递他们。

在 swift 中, 有两个相互竞争的框架帮你实现响应式编程:ReactiveSwift 和 RxSwift,我将使用 RxSwift 并不是因为它更好,而是因为我对它更加的熟悉,我将假设你以及读者也熟悉它, 这样我就可以直接介绍和使用它。


链式动画:传统的方式

如果你想180度旋转一个 view,然后褪色消失,你可能使用 UIView animation completion 来实现

UIView.animate(withDuration: 0.5, animations: {
    self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
    UIView.animate(withDuration: 0.5, animations: {
        self.animatableView.alpha = 0
    })
})

iOS 使用 RxSwift 实现链式动画_第1张图片

虽然有一点笨重,但也仍然可行。但是如果你想添加更多的动画在里面,比如说在旋转中消失前移动视图?运用同样的方法,将会这样写

UIView.animate(withDuration: 0.5, animations: {
    self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
    UIView.animate(withDuration: 0.5, animations: {
        self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 100, dy: 0)
    }, completion: { _ in
        UIView.animate(withDuration: 0.5, animations: {
            self.animatableView.alpha = 0
        })
    })
})

iOS 使用 RxSwift 实现链式动画_第2张图片

添加的步骤越多,代码越交错,麻烦也越多。如果你想改变某些步骤的顺序,你必须执行一些简单的剪切和粘贴序列,这是容易出错的。

庆幸的是,苹果提供一个更好的方法,使用keyframe-based animations API, 使用这个方法,代码可以重写为:

UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: {
    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: {
        self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
    })
    UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: {
        self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 100, dy: 0)
    })
    UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: {
        self.animatableView.alpha = 0
    })
})

这是一个巨大的进步,主要优点是:

  • 1、不管有多少步骤添加, 代码不变
  • 2、改变动画的顺序也是非常简单的

这种方法的缺点是,改变顺序,你必须考虑相对时间,变得很困难(或至少不是非常简单的)。想想计算你会经历什么,你需要做什么样的修改, 总体持续时间和相对时间/开始时间为每个动画, 如果你决定让视图1秒内消失,而不是半秒,同时保持相同的一切。同样如果你想改变步骤的顺序必须验算相对的开始时间。

考虑到缺点,我认为上述方法并不够好。

我要找理想的解决方案应该满足以下目标:

  • 1、代码已经持平不管多少数量的步骤
  • 2、我应该能够轻松地添加/删除或重新排序独立动画和改变他们的持续时间,对其他动画没有任何副作用

链式动画:使用 RxSwift

我发现使用RxSwift,我可以很容易地实现这两个目标。
RxSwift不是唯一的框架,你可以使用类似的,任何类似的框架,只要让你用异步操作方法但没有利用闭包连在在一起完成就可以了。
但 RxSwift 提供了非常多的操作符, 我们将在后面接触一点。

这里是我想要实现的想法:

  • 1、我将每个动画包裹成一个函数,返回一个观察者对象 Observable
  • 2、可观察对象完成每个任务只发出一个元素
  • 3、这个元素将在动画包裹函数完成后发出
  • 4、将这些可观察对象通过 flatMap 操作符进行连接

下面就是我的函数:


func rotate(_ view: UIView, duration: TimeInterval) -> Observable {
    return Observable.create({ (observer) -> Disposable in
        UIView.animate(withDuration: duration, animations: {
            view.transform = CGAffineTransform(rotationAngle: .pi/2)
        }, completion: { _ in
            observer.onNext(())
            observer.onCompleted()
        })
        return Disposables.create()
    })
}

func shift(_ view: UIView, duration: TimeInterval) -> Observable {
    return Observable.create({ (observer) -> Disposable in
        UIView.animate(withDuration: duration, animations: {
            view.frame = view.frame.offsetBy(dx: 100, dy: 0)
        }, completion: { _ in
            observer.onNext(())
            observer.onCompleted()
        })
        return Disposables.create()
    })
}

func fade(_ view: UIView, duration: TimeInterval) -> Observable {
    return Observable.create({ (observer) -> Disposable in
        UIView.animate(withDuration: duration, animations: {
            view.alpha = 0
        }, completion: { _ in
            observer.onNext(())
            observer.onCompleted()
        })
        return Disposables.create()
    })
}

然后将他们放在一起

rotate(animatableView, duration: 0.5).flatMap { [unowned self] in
    self.shift(self.animatableView, duration: 0.5)
    }
    .flatMap({ [unowned self] in
        self.fade(self.animatableView, duration: 0.5)
    })
    .subscribe()
    .disposed(by: disposeBag)

这显然比在前面的使用了更加多代码,这样一个简单的动画序列,看起来有点大材小用了,但美丽的是,它可以扩展到处理一些非常复杂的动画序列,并且非常容易阅读。

一旦你掌握它, 通过处理各种各样的RxSwift操作符,您可以创建和电影一样复杂的动画。上面那些很难实现的,你可以轻易完成。

这里我们运用 .concat 操作符,将动画连接在一起,使我们的代码更加简洁

Observable.concat([rotate(animatableView, duration: 0.5),
                   shift(animatableView, duration: 0.5),
                   fade(animatableView, duration: 0.5)
    ])
    .subscribe()
    .disposed(by: disposeBag)

你可以添加延迟在每个动画之间:

func delay(_ duration: TimeInterval) -> Observable {
    return Observable.of(()).delay(duration, scheduler: MainScheduler.instance)
}
 
 
 Observable.concat([
    rotate(animatableView, duration: 0.5),
    delay(0.5),
    shift(animatableView, duration: 0.5),
    delay(1),
    fade(animatableView, duration: 0.5)
    ])
    .subscribe()
    .disposed(by: disposeBag)

现在,让我们假设我们希望视图旋转一定数量的次数后它开始移动。我们想轻松地调整应该多少次旋转。

func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable {
    var disposed = false
    return Observable.create({ (observer) -> Disposable in
        func animate() {
            UIView.animate(withDuration: duration, animations: {
                view.transform = view.transform.rotated(by: .pi/2)
            }, completion: { _ in
                observer.onNext(())
                if !disposed {
                    animate()
                }
            })
        }
        animate()
        return Disposables.create{
            disposed = true
        }
    })
}

然后非常完美的将动画连接在一起,例如:

Observable.concat([
    rotateEndlessly(animatableView, duration: 0.5).take(5),
    shift(animatableView, duration: 0.5),
    fade(animatableView, duration: 0.5)
    ])
    .subscribe()
    .disposed(by: disposeBag)

你看到是多么容易控制视图选择次数,只需要改变 take 操作符中的值

现在,我们进行更高级的封装来实现动画功能。将 动画添加到 Reactive 扩展中(通过.rx后缀来访问),这将使它成为 RxSwift的实例, 响应式编程函数通常是通过.rx 后缀来完成,返回一个可观察对象,是代码更加的清晰明了。

extension Reactive where Base == UIView {
    
    func shift(duration: TimeInterval) -> Observable {
        return Observable.create({ (observer) -> Disposable in
            UIView.animate(withDuration: duration, animations: {
                self.base.frame = self.base.frame.offsetBy(dx: 100, dy: 0)
            }, completion: { _ in
                observer.onNext(())
                observer.onCompleted()
            })
            return Disposables.create()
        })
    }
    
    func fade(duration: TimeInterval) -> Observable {
        return Observable.create({ (observer) -> Disposable in
            UIView.animate(withDuration: duration, animations: {
                self.base.alpha = 0
            }, completion: { _ in
                observer.onNext(())
                observer.onCompleted()
            })
            return Disposables.create()
        })
    }
    
    
    func rotateEndlessly(duration: TimeInterval) -> Observable {
        var disposed = false
        return Observable.create({ (observer) -> Disposable in
            func animate() {
                UIView.animate(withDuration: duration, animations: {
                    self.base.transform = self.base.transform.rotated(by: .pi/2)
                }, completion: { _ in
                    observer.onNext(())
                    if !disposed {
                        animate()
                    }
                })
            }
            animate()
            return Disposables.create{
                disposed = true
            }
        })
    }
}

通过它,我们可以这样将他们连接在一起:

Observable.concat([
    animatableView.rx.rotateEndlessly(duration: 0.5).take(5),
    animatableView.rx.shift(duration: 0.5),
    animatableView.rx.fade(duration: 0.5)
    ])
    .subscribe()
    .disposed(by: disposeBag)

over!!!

你可能感兴趣的:(iOS,Swift,RxSwift笔记)