iOS의 RxSwift 및 애니메이션
게시 됨: 2022-03-11UI 작업을 어느 정도 해왔고 그것에 열정이 있는 iOS 개발자라면 애니메이션과 관련하여 UIKit의 힘을 사랑해야 합니다. UIView 애니메이션은 케이크만큼 쉽습니다. 시간이 지남에 따라 페이드, 회전, 이동 또는 축소/확장 방법에 대해 많이 생각할 필요가 없습니다. 그러나 애니메이션을 함께 연결하고 애니메이션 간에 종속성을 설정하려는 경우 약간 복잡해집니다. 코드는 중첩된 클로저와 들여쓰기 수준이 많아 매우 장황하고 따르기 어려울 수 있습니다.
이 기사에서는 RxSwift와 같은 반응형 프레임워크의 기능을 적용하여 해당 코드를 훨씬 깔끔하고 읽기 쉽고 따라하기 쉽게 만드는 방법을 살펴보겠습니다. 클라이언트를 위한 프로젝트를 진행하던 중 아이디어가 떠올랐습니다. 그 특정 클라이언트는 매우 UI에 정통했습니다(내 열정과 완벽하게 일치했습니다)! 그들은 앱의 UI가 매우 매끄러운 전환과 애니메이션을 통해 매우 특정한 방식으로 작동하기를 원했습니다. 그들의 아이디어 중 하나는 앱에 대한 이야기를 들려줄 앱 소개를 만드는 것이었습니다. 그들은 쉽게 조정하고 조정할 수 있도록 미리 렌더링된 비디오를 재생하는 대신 일련의 애니메이션을 통해 스토리를 전달하기를 원했습니다. RxSwift는 그런 문제에 대한 완벽한 선택으로 판명되었습니다. 기사를 끝내고 나면 깨닫게 되기를 바랍니다.
반응형 프로그래밍에 대한 짧은 소개
반응형 프로그래밍은 필수 요소가 되었으며 대부분의 현대 프로그래밍 언어에서 채택되었습니다. 반응형 프로그래밍이 왜 그렇게 강력한 개념이고 특정 디자인 원칙과 패턴을 적용하여 좋은 소프트웨어 디자인을 장려하는 데 어떻게 도움이 되는지 자세히 설명하는 책과 블로그가 많이 있습니다. 또한 코드 혼란을 크게 줄이는 데 도움이 될 수 있는 툴킷을 제공합니다.
내가 정말 좋아하는 한 가지 측면, 즉 비동기 작업을 쉽게 연결하고 선언적이고 읽기 쉬운 방식으로 표현할 수 있다는 점에 대해 말씀드리고 싶습니다.
Swift와 관련하여 반응형 프로그래밍 언어로 전환하는 데 도움이 되는 두 가지 경쟁 프레임워크인 ReactiveSwift와 RxSwift가 있습니다. 나는 RxSwift가 더 좋기 때문이 아니라 더 익숙하기 때문에 내 예제에서 RxSwift를 사용할 것입니다. 나는 독자인 당신도 그것에 대해 잘 알고 있다고 가정할 것이므로 나는 그것의 핵심에 직접 접근할 수 있습니다.
애니메이션 연결: 예전 방식
보기를 180° 회전한 다음 페이드 아웃하려고 한다고 가정해 보겠습니다. completion 클로저를 사용하고 다음과 같이 할 수 있습니다.
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) 약간 부피가 있지만 그래도 괜찮습니다. 하지만 그 사이에 애니메이션을 하나 더 삽입하고 싶다면, 예를 들어 뷰가 회전한 후 사라지기 전에 오른쪽으로 이동하려면 어떻게 해야 할까요? 동일한 접근 방식을 적용하면 다음과 같이 됩니다.
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: 50, dy: 0) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.alpha = 0 }) }) }) 더 많은 단계를 추가할수록 더 비틀거리고 번거로워집니다. 그런 다음 특정 단계의 순서를 변경하기로 결정했다면 사소한 잘라내기 및 붙여넣기 시퀀스를 수행해야 하며 이는 오류가 발생하기 쉽습니다.
음, Apple은 분명히 그것에 대해 생각했습니다. 그들은 키프레임 기반 애니메이션 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: 50, dy: 0) }) UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: { self.animatableView.alpha = 0 }) })이는 다음과 같은 주요 이점과 함께 크게 개선되었습니다.
- 코드에 추가하는 단계 수에 관계없이 코드는 그대로 유지됩니다.
- 순서를 변경하는 것은 간단합니다(아래에 한 가지 주의 사항 포함).
이 접근 방식의 단점은 상대적 지속 시간의 관점에서 생각해야 하고 단계의 절대 타이밍이나 순서를 변경하는 것이 어려워진다는 것입니다(또는 최소한 간단하지는 않음). 보기를 1/2이 아닌 1초 이내에 흐리게 하기로 결정했다면 어떤 계산을 거쳐야 하고 각 애니메이션의 전체 지속 시간과 상대적 지속 시간/시작 시간에 어떤 종류의 변경을 해야 하는지 생각해 보십시오. 다른 모든 것을 동일하게 유지하면서 두 번째. 단계의 순서를 변경하려는 경우에도 마찬가지입니다. 상대 시작 시간을 다시 계산해야 합니다.
단점을 감안할 때 위의 접근 방식 중 어느 것도 충분하지 않습니다. 내가 찾고 있는 이상적인 솔루션은 다음 기준을 충족해야 합니다.
- 코드는 단계 수에 관계없이 평평하게 유지되어야 합니다.
- 다른 애니메이션에 대한 부작용 없이 애니메이션을 쉽게 추가/제거 또는 재정렬하고 지속 시간을 독립적으로 변경할 수 있어야 합니다.
애니메이션 연결: RxSwift 방식
RxSwift를 사용하면 이 두 가지 목표를 모두 쉽게 달성할 수 있습니다. RxSwift는 이와 같은 작업을 수행하는 데 사용할 수 있는 유일한 프레임워크가 아닙니다. 완료 블록을 사용하지 않고 구문적으로 함께 연결할 수 있는 메서드로 비동기 작업을 래핑할 수 있는 모든 약속 기반 프레임워크가 사용할 수 있습니다. 그러나 RxSwift는 연산자 배열을 통해 훨씬 더 많은 것을 제공할 수 있습니다. 이에 대해서는 나중에 다루겠습니다.
내가 어떻게 할 것인지에 대한 개요는 다음과 같습니다.
-
Observable<Void>유형의 관찰 가능 개체를 반환하는 함수로 각 애니메이션을 래핑하겠습니다. - 해당 Observable은 시퀀스를 완료하기 전에 하나의 요소만 방출합니다.
- 함수에 의해 래핑된 애니메이션이 완료되는 즉시 요소가 방출됩니다.
-
flatMap연산자를 사용하여 이러한 관찰 가능 항목을 함께 연결할 것입니다.
내 기능은 다음과 같습니다.
func rotate(_ view: UIView, duration: TimeInterval) -> Observable<Void> { 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<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.frame = view.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> { 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<Void> { 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)이제 뷰가 움직이기 시작하기 전에 특정 횟수만큼 회전하기를 원한다고 가정해 보겠습니다. 그리고 우리는 회전해야 하는 횟수를 쉽게 조정할 수 있기를 원합니다.
먼저 회전 애니메이션을 연속적으로 반복하고 각 회전 후에 요소를 방출하는 메서드를 만들 것입니다. Observable이 폐기되는 즉시 이러한 회전이 중지되기를 바랍니다. 다음과 같이 할 수 있습니다.
func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable<Void> { 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 연산자에 전달된 값만 변경하면 됩니다.
이제 내가 만든 애니메이션 기능을 UIView의 "Reactive" 확장( .rx 접미사를 통해 액세스할 수 있음)으로 래핑하여 구현을 한 단계 더 발전시키고 싶습니다. 이것은 RxSwift 규칙의 라인을 따라 더 만들 것입니다. 여기서 반응 함수는 일반적으로 .rx 접미사를 통해 액세스하여 관찰 가능한 항목을 반환한다는 것을 분명히 합니다.
extension Reactive where Base == UIView { func shift(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(duration: TimeInterval) -> Observable<Void> { 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<Void> { 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)여기에서 갈 곳
이 기사에서 설명하는 것처럼 RxSwift의 강력한 기능을 활용하고 기본 요소가 제자리에 있으면 애니메이션을 정말 재미있게 즐길 수 있습니다. 코드는 깨끗하고 읽기 쉬우며 더 이상 "코드"처럼 보이지 않습니다. 애니메이션이 어떻게 조합되고 생생하게 살아나는지 "설명"합니다! 여기에 설명된 것보다 더 정교한 작업을 수행하려는 경우 다른 유형의 애니메이션을 캡슐화하는 고유한 기본 요소를 더 추가하여 확실히 할 수 있습니다. 또한 오픈 소스 커뮤니티의 열정적인 사람들이 개발한 계속해서 증가하는 도구 및 프레임워크를 항상 활용할 수 있습니다.
RxSwift로 전환한 경우 RxSwiftCommunity 저장소를 정기적으로 방문하세요. 항상 새로운 것을 찾을 수 있습니다!
더 많은 UI 조언을 찾고 있다면 동료 Toptaler Roman Stetsenko 의 How to Implement Pixel-perfect iOS UI Design (픽셀 완벽한 iOS UI 디자인 구현 방법)을 읽어보세요.
소스 노트(기본 이해하기)
- Wikipedia - Swift(프로그래밍 언어)
- Lynda.com - RxSwift: iOS 개발자를 위한 디자인 패턴
- We Heart Swift - UIView 기초
- Angular - Observable
