본문 바로가기

학습 노트/iOS (2021)

124 ~ 127. UIVIew Animation, Spring Animation & Keyframe Animation, Property Animator and Motion Effect

UIView Animation

UIView 클래스는 Animation 구현에 필요한 API를 Type Method 방식으로 제공한다.
해당 메서드를 사용해서 비교적 간단하게 고품질의 Animation을 구현할 수 있다.

다룰 Animation들은 Block 기반의 API로, Animation Block에서 원하는 최종 값을 설정하면
UIKit이 현재 값에서 최종 값으로 전환되는 Animation을 실행한다.

다음의 항목들이 UIKit이 Animation을 지원하는 항목들이다.

  • frame
  • bounds
  • center
  • transform
  • alpha
  • backgroundColor

//
//  SimpleUIViewAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/11.
//

import UIKit

class SimpleUIViewAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    @IBAction func reset(_ sender: Any?) {
        redView.backgroundColor = UIColor.red
        redView.alpha = 1.0
        redView.frame = CGRect(x: 50, y: 100, width: 50, height: 50)
    }
    
    
    @IBAction func animate(_ sender: Any) {
        
    }
    
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationItem.title = "Simple UIView Animation"
        
        reset(nil)
    }
}

사용할 Scene과 Code는 위와 같다.
Button은 Outlet과 reset 메서드에 Action으로 연결하고,
Right Bar Button은 anime 메서드와 Action으로 연결했다.

@IBAction func animate(_ sender: Any) {
	var frame = redView.frame
	frame.origin = view.center
}

현재 Frame을 가져와 위치값을 center로 설정한다.

이후 animate 메소드를메서드를 호출하는데 가장 단순하게 생긴 메서드를 선택한다.

@IBAction func animate(_ sender: Any) {
	var frame = redView.frame
	frame.origin = view.center
	
	UIView.animate(withDuration: 0.3) {
		self.redView.frame = frame
	}
}

Duration은 Animation이 재생될 시간을 초단위로 설정한다.
보통 0.25 ~ 0.3초로 설정한다.
animation은 Animation Block을 전달하는데
보통 inline Closure나 Trailing Closure를 전달한다.
위와 같이 Animation을 지원하는 속성의 값을 Animation Block에서 최종 값으로 바꾸기만 하면 나머지는 알아서 처리한다.

Right Bar Button을 터치하면 redView가 중앙으로 이동한다.
만약 Animation이 너무 빨라서 확인하기 힘들다면 시뮬레이터에서 'cmd + t'를 눌러 슬로모션을 적용할 수 있다.

하지만 redView를 터치해 위치를 초기화할 때는 Animation이 재생되지 않는다.

@IBAction func animate(_ sender: Any) {
	var frame = redView.frame
	frame.origin = view.center
	frame.size = CGSize(width: 100, height: 100)
	
	UIView.animate(withDuration: 0.3) {
		self.redView.frame = frame
		self.redView.alpha = 0.5
		self.redView.backgroundColor = .blue
	}
}

animation block에 몇가지 속성을 추가한다.
또한 size도 100으로 변경한다.
animation을 설정할 때 한가지 속성에만 적용할 수 있는 것은 아니다.
지금처럼 여러가지 속성에 대해 animation을 적용할 수 있고, 동일한 block에 존재한다면 동시에 실행된다.

색과 크기, 투명도, 위치가 동시에 바뀌게 된다.

animate 메소드메서드 중 completion을 파라미터로 가지는 메서드가 있다.
해당 메소드는 animation block을 실행하고, completion에 전달된 블록을 실행한다.

UIView.animate(withDuration: 0.3) {
    self.redView.frame = frame
    self.redView.alpha = 0.5
    self.redView.backgroundColor = .blue
} completion: { finished in
    self.reset(nil)
}

 animation이 종료되면 true를 반환하고,
이를 감지하면 completion을 실행한다.

이렇게 하면 animation 이후 후속 동작을 연계할 수 있다.

보다시피 completion block에서는 animation이 재생되지 않는다.
따라서 초기화도 animation을 적용하고 싶다면 reset 메서드가 animation block에서 실행되어야 한다.

@IBAction func animate(_ sender: Any) {
    var frame = redView.frame
    frame.origin = view.center
    frame.size = CGSize(width: 100, height: 100)

    UIView.animate(withDuration: 0.3) {
        self.redView.frame = frame
        self.redView.alpha = 0.5
        self.redView.backgroundColor = .blue
    } completion: { finished in
        UIView.animate(withDuration: 0.3) {
            self.reset(nil)
        }
    }
}

completion 안에 animate 메서드를 한 번 더 호출하면

이제 위치를 초기화할 때도 animation이 적용된다.
단, 이렇게 구현하는 경우 코드가 복잡해지는 것은 피할 수 없다.
이를 해결하기 위해선 Keyframe Animation이 필요하다.

Animation에서 중요한 것은 Animation Curve이다.
지금까지 사용한 Animation은 EaseInOut Curve를 사용한다.
따라서 Animation이 시작과 끝에서 느려지고, 중간이 가장 빠르다.
다양한 Animation Curve 중 대신 사용하고 싶은 Curve가 있다면 Animation Option을 지정해야 한다.

//
//  AnimationOptionsViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/11.
//

import UIKit

class AnimationOptionsViewController: UIViewController {

    @IBOutlet weak var redView: UIButton!
    
    @IBAction func reset(_ sender: Any?) {
        redView.backgroundColor = UIColor.red
        redView.alpha = 1.0
        redView.frame = CGRect(x: 50, y: 100, width: 50, height: 50)
    }
    
    @IBAction func stopAction(_ sender: Any) {
        
    }
    
    
    @IBAction func animate(_ sender: Any) {
        let animations: () -> () = {
            var frame = self.redView.frame
            frame.origin = self.view.center
            frame.size = CGSize(width: 100, height: 100)
            self.redView.frame = frame
            self.redView.alpha = 0.5
            self.redView.backgroundColor = .blue
        }
    }
    
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationItem.title = "Animation Options"
        
        reset(nil)
    }
}

사용할 Scene과 Code는 위와 같다.

이번엔 animate 메서드 중 options를 파라미터로 가지는 메서드를 사용한다.

@IBAction func animate(_ sender: Any) {
    let animations: () -> () = {
        var frame = self.redView.frame
        frame.origin = self.view.center
        frame.size = CGSize(width: 100, height: 100)
        self.redView.frame = frame
        self.redView.alpha = 0.5
        self.redView.backgroundColor = .blue
    }

    UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: animations, completion: nil)
}

withDuration은 Animation의 재생시간,
delay는 Animation을 재생 지연시간,
options는

optionset 구조체로 선언되어있고, 다양한 옵션을 제공한다.
Animation Curve를 사용할 때는 curve라는 접두어를 가진 옵션을 사용한다.

나머지는 동일하다.

큰 차이가 나지는 않지만 Linear는 Animation의 시작부터 끝까지 동일한 속도로 진행된다.

Animation option은 Option Set으로 정의되어있어, 여러 개의 옵션을 전달할 수 있다.

@IBAction func animate(_ sender: Any) {
    let animations: () -> () = {
        var frame = self.redView.frame
        frame.origin = self.view.center
        frame.size = CGSize(width: 100, height: 100)
        self.redView.frame = frame
        self.redView.alpha = 0.5
        self.redView.backgroundColor = .blue
    }

    UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse], animations: animations, completion: nil)
}

지금처럼 반환 값을 배열로 바꾸고 repeat과 autoreverse를 추가하면,
반복적으로 되돌아간다.

UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse, .allowUserInteraction], animations: animations, completion: nil)

Animation이 재생되는 동안에는 Touch Event가 처리되지 않는다.
만약 이를 처리하고 싶다면 allowUserInteraction을 추가한다.

UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse, .allowUserInteraction, .beginFromCurrentState], animations: animations, completion: nil)

Animation이 실행 중인 상태에서 다른 Animation을 실행하면
먼저 실행된 Animation이 종료된 다음 새로운 Animation이 실행된다.
beginFromCurrentState를 추가하면 실행 중인 Animation을 중지하고 새로운 Animation을 시작한다.

이밖에도 다양한 옵션이 존재한다.

//
//  AnimationOptionsViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/11.
//

import UIKit

class AnimationOptionsViewController: UIViewController {

    @IBOutlet weak var redView: UIButton!
    
    @IBAction func reset(_ sender: Any?) {
        redView.backgroundColor = UIColor.red
        redView.alpha = 1.0
        redView.frame = CGRect(x: 50, y: 100, width: 50, height: 50)
    }
    
    @IBAction func stopAction(_ sender: Any) {
        redView.layer.removeAllAnimations()
        reset(nil)
    }
    
    
    @IBAction func animate(_ sender: Any) {
        let animations: () -> () = {
            var frame = self.redView.frame
            frame.origin = self.view.center
            frame.size = CGSize(width: 100, height: 100)
            self.redView.frame = frame
            self.redView.alpha = 0.5
            self.redView.backgroundColor = .blue
        }
        
        UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse], animations: animations, completion: nil)
    }
    
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationItem.title = "Animation Options"
        
        reset(nil)
    }
}

마지막에 추가한 두 개의 옵션을 제거하고,
stop 버튼의 메서드에 removeAllAnimations 메서드를 호출하도록 코드를 수정했다.
이렇게 되면 실행 중인 모든 Animation이 제거된다.
UIView가 제공하는 Animation API들은 iOS 4부터 제공된 오래된 API다.
그래서 실행 중인 Animation을 개별적으로 중지하는 Animation은 제공되지 않는다.
iOS 10부터는 UIView Animation 대신 Property Animator를 사용한다.

Animation이 지속적으로 반복되고, Stop 버튼을 누르면 멈춘다.

 

Spring Animation

//
//  UIViewSpringAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/12.
//

import UIKit

class UIViewSpringAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    @IBOutlet weak var dampingSlider: UISlider!
    @IBOutlet weak var velocitySlider: UISlider!
    
    @IBAction func reset(_ sender: Any?) {
        redView.frame = CGRect(x: 50, y: 100, width: 50, height: 50)
    }
    
    @IBAction func animate(_ sender: Any) {
        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Spring Animation"
        
        reset(nil)
    }

}

사용할 Scene과 Code는 위와 같다.

Button은 redView와 Outlet으로, reset 메서드와 Action으로 연결되어있다.
reset 메서드는 위치를 초기화하도록 구현되어있다.
화면 아래에 있는 Slider는 각각 dampingSlider와 velocitySlider와 Outlet으로 연결되어있다.
Rught Bar Button은 animate 메서드와 Action으로 연결되어있다.
메서드의 안에는 Animation의 최종 Frame이 선언되어있다.

redView를 targetFrame으로 이동시키는 Spring Animation을 구현해 본다.

사용할 animate 메서드는 파라미터에 Spring을 포함하고 있는 메서드이다.

UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: CGFloat(dampingSlider.value), initialSpringVelocity: CGFloat(velocitySlider.value), options: []) {
    self.redView.frame = targetFrame
} completion: { _ in
    return
}

Duration은 Animation의 재생 시간을 설정한다.
delay는 Animation의 지연 시간을 설정한다.
usingSpringDamping은 0.0 ~ 1.0의 값으로 반발 강도를 설정한다.
initialSpringVelocity는 0 ~ 5의 값으로 속도를 설정한다.
options는 Animation Option을 전달한다.
animation에서는 animation의 최종 값을 입력한다. reView의 최종 위치를 targetFrame으로 설정한다.
completion은 후속 동작을 구현한다.

적용된 Spring Animation은 Damping이 0.5, Velocity가 2.5인 Spring Animation을 재생한다.

Damping을 더 작게 조절하자 탄성 있는 Spring Animation이 됐다.

Velocity를 높게 설정하면 Animation의 속도가 느려지지만,
Duration을 넘기게 되면 최종 위치에서 강제 종료되기 때문에 어색해질 수 있다.

Velocity를 낮게 설정하면 Animation의 속도가 빨라지고,
Duration 내에서 Animation을 충분히 표현할 수 있다.

따라서 Spring Animation을 구현할 때는 Damping과 Velocity를 고려해 충분한 실행시간을 확보해야 한다.

 

Keyframe Animation

//
//  KeyframeAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/12.
//

import UIKit

class KeyframeAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    func phase1() {
        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
        redView.frame = targetFrame
    }
    
    func phase2() {
        redView.backgroundColor = UIColor.blue
    }
    
    func phase3() {
        let targetFrame = CGRect(x: 50, y: 100, width: 50, height: 50)
        redView.frame = targetFrame
        redView.backgroundColor = UIColor.red
    }
    
    @IBAction func animate(_ sender: Any) {
        UIView.animate(withDuration: 1) {
            self.phase1()
        } completion: { finished in
            UIView.animate(withDuration: 1) {
                self.phase2()
            } completion: { finished in
                UIView.animate(withDuration: 1) {
                    self.phase3()
                }
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Keyframe Animation"
    }
}

사용할 Scene과 Code는 위와 같다.

실행하면 Frame Animation과 Background Color Animation이 순차적으로 실행된다.

@IBAction func animate(_ sender: Any) {
    UIView.animate(withDuration: 1) {
        self.phase1()
    } completion: { finished in
        UIView.animate(withDuration: 1) {
            self.phase2()
        } completion: { finished in
            UIView.animate(withDuration: 1) {
                self.phase3()
            }
        }
    }
}

phase별로 구분된 Animation을 Completion을 사용해 순차적으로 호출하고 있다.
Animation의 수가 늘어나면 중 첨이 증가해 효율적이지 못하다.
동일한 동작을 Keyframe Animation으로 구현하면 조금 더 효율적이다.

이번에 사용할 메서드는 animateKeyframes 메서드다.

UIView.animateKeyframes(withDuration: 3, delay: 0, options: []) {
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.3) {
        self.phase1()
    }
    UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.3) {
        self.phase2()
    }
    UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.4) {
        self.phase3()
    }
} completion: { _ in
    return
}

사용되는 파라미터는 비슷하다.

Duration은 Animation의 재생시간.
delay는 Animation의 지연시간.
options는 Animation option.
animations는 실행할 animation.
completion은 후속 동작.

을 각각 나타낸다.
Keyframe이 조금 다른 점은 animations에서 addKeyframe 메서드로 kayframe을 생성하고,
addKeyframe은 각각 withRelativeStartTime, relativeDuration, animations의 세 가지 파라미터를 갖는다는 점이다.
이름이 의미하듯 '연관된' 값이기에 Keyframe Animation이 가지는 전체 Duration의 비례 값이며,
0.0 ~ 1.0 사이의 값을 전달해 실행한다.

결과를 보면 완전히 동일한 동작을 수행하는 것을 볼 수 있다.
하지만 코드는 중첩되지 않고 깔끔해졌다.

Auto Layout Animation

//
//  AutoLayoutAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/12.
//

import UIKit

class AutoLayoutAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    @IBAction func animate(_ sender: Any) {
        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
        UIView.animate(withDuration: 0.3) {
            self.redView.frame = targetFrame
        }
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Auto Layout Animation"
    }

}

사용할 Scene과 Code는 위와 같다.
여태 적용했던 Animation은 Auto Layout, 즉 제약조건이 없는 상태에서 적용했었다.
하지만 이번엔 화면의 정 중앙에 50x50의 크기로 button이 위치하도록 제약조건이 존재한다.

구현한 것과는 다른 결과가 나타난다.

이전과 마찬가지로 animation block에서 frame을 설정하고 있고, 실제로 animation은 정상적으로 실행됐지만,
제약조건은 업데이트되지 않았다.
즉, 제약조건을 수정하고 animation이 실행되어야 정상적인 결과를 얻을 수 있다.

//
//  AutoLayoutAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/12.
//

import UIKit

class AutoLayoutAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    @IBOutlet weak var widthContraint: NSLayoutConstraint!
    @IBOutlet weak var heightConstraint: NSLayoutConstraint!
    
    @IBAction func animate(_ sender: Any) {
//        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
//        UIView.animate(withDuration: 2) {
//            self.redView.frame = targetFrame
//        }
        
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Auto Layout Animation"
    }

}

우선 제약조건을 수정하기 위해 Outlet으로 연결한다.

@IBAction func animate(_ sender: Any) {
//        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
//        UIView.animate(withDuration: 2) {
//            self.redView.frame = targetFrame
//        }
    UIView.animate(withDuration: 2) {
        self.widthContraint.constant = 200
        self.heightConstraint.constant = 200

    }
}

이후 animate 메서드에서 제약조건을 변경한다.

실행하면 Animation 없이 곧바로 크기가 변경된다.
즉, 제약에 Animation을 적용할 때는 다른 방식으로 구현해야 한다.

@IBAction func animate(_ sender: Any) {
//        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
//        UIView.animate(withDuration: 2) {
//            self.redView.frame = targetFrame
//        }

    self.widthContraint.constant = 200
    self.heightConstraint.constant = 200

    UIView.animate(withDuration: 2) {
        self.view.layoutIfNeeded()
    }
}

제약조건 수정을 메서드 밖으로 옮기고,
animation block 안에서는 layoutIfNeeded 메서드를 호출한다.

의도한 대로 Animation이 정상적으로 재생된다.

간혹 제약조건이 의도한 대로 업데이트되지 않는 경우가 있는데,
이런 경우 제약을 업데이트 한 다음

redView.setNeedsUpdateConstraints()

대상 view에서 setNeedsUpdateConstraints 메서드를 호출하면 된다.

 

Property Animator

Property Animator는 iOS 10부터 새롭게 도입되어 UIView Animation을 대체하는 새로운 API다.
UIVIew Animation을 사용해도 문제는 없지만 최소 배포 버전이 iOS 10 이후라면 Property Animator가 권장된다.

Property Animator는 여러 장점을 가지고 있다.

  • Animation 상태를 제어할 수 있다.
  • Animation을 자동으로 시작하거나 메서드를 사용해서 직접 실행할 수 있다.
  • Animation을 일시정지하거나 완전히 중지할 수 있다.
  • UIView Animation에서 지원하던 Animation Curve를 모두 지원한다.
  • Bezier Timing Curve를 사용해 직접  Animation Curve를 만들 수 있다.
  • 이미 시작된 Animation에 새로운 Animation을 동적으로 추가할 수 있다.
  • Interactive Animation 구현이 가능하다.

Property Animation은 세 가지 상태를 가진다.

  • Inactive
    Animation을 생성하면 진입하는 상태이다.
  • Active
    Animation을 시작하거나 시작된 Animation을 일시 정지하면 진입하는 상태이다.
    해당 상태에서 Animation이 정상적으로 정지되면 다시 Inactive 상태로 돌아간다.
  • Stopped
    Active 상태에서 Animation을 직접 중지하면 진입하는 상태이다.
    Animation이 완전히 종료되지 않은 상태이기 때문에, 메서드를 호출해서 Inactive로 돌아가야 한다.

Property Animator가 제공하는 메서드 중에는 Stopped에서 예외가 발생하는 메서드가 존재한다.
예를 들면 Stopped 상태에선 새로운 Animation을 추가할 수 없다.
따라서 Animation 상태에 따라 적절한 순간에 메서드를 호출해야 한다.

//
//  PropertyAnimatorViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/12.
//

import UIKit

class PropertyAnimatorViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    var animator: UIViewPropertyAnimator?
    
    func moveAndResize() {
        let targetFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
        redView.frame = targetFrame
    }
    
    @IBAction func reset(_ sender: Any) {
        redView.backgroundColor = UIColor.red
        redView.frame = CGRect(x: 50, y: 100, width: 50, height: 50)
    }
    
    @IBAction func pause(_ sender: Any) {
    }
    
    @IBAction func animate(_ sender: Any) {
    }
    
    @IBAction func resume(_ sender: Any) {
    }
    
    @IBAction func stop(_ sender: Any) {
    }
    
    @IBAction func add(_ sender: Any) {
    }
    
    
    

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Property Animator"
    }
}

사용할 Scene과 Code는 위와 같다.

Button은 redView와 Outlet으로, reset 메서드와 Action으로 연결되어있다.
Toolbar Item들도 순서대로 각각의 메서드에 Action으로 연결되어있다.
animation block에서 실행항 코드는 moveAndResize 메서드로 구현되어있다.

Property Animator는 UIViewPropertyAnimator 클래스로 구현되어있다.
Animation을 구현하는 가장 단순한 방법은 클래스가 제공하는 Type 메서드를 사용하는 것이다.

@IBAction func animate(_ sender: Any) {
    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 7, delay: 0, options: []) {
        self.moveAndResize()
    } completion: { position in
        switch position {
        case .start:
            print("Start")
        case .end:
            print("End")
        case .current:
            print("Current")
        }
    }
}

전달해야 하는 파라미터는 UIViewAnimation과 유사하지만 차이가 존재한다.
options에 전달하는 값은 UIViewAnimation과 동일하지만,
Transition에 관련된 option과 animation 방향에 관련돼 option은 무시된다.
animation 블록은 동일하다.
completion에 클로저를 전달하는 것은 동일하지만 closer의 형식이 다르다.
UIViewAnimation에서는 Animation의 완료 여부를 나타내는 Boolean 값이 전달되지만,
PropertyAnimator에서는 Animation 종료 위치를 나타내는 열거형이 전달된다.
해당 파라미터의 값에 따라 분기하도록 코드를 구성했다.

재생 버튼을 누르면 Animation이 실행되고, 콘솔엔 End 로그가 표시된다.
방금 사용한 메서드는 호출과 동시에 Animation이 시작한다.
그리고 생성된 Animator를 반환한다.

@IBAction func pause(_ sender: Any) {
    animator?.pauseAnimation()
    print(animator?.fractionComplete)
}

@IBAction func animate(_ sender: Any) {
    animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 7, delay: 0, options: []) {
        self.moveAndResize()
    } completion: { position in
        switch position {
        case .start:
            print("Start")
        case .end:
            print("End")
        case .current:
            print("Current")
        }
    }
}

반환된 Animator를 animator 변수에 저장하고, 이를 pauseAnimation 메서드로 일시 정지한다.
이미 실행된 Animation 비율은 fractionComplete 속성을 통해 확인할 수 있다.

@IBAction func resume(_ sender: Any) {
    animator?.startAnimation()
}

아직 시작되지 않았거나 이미 일시 정지된 Animation을 시작할 때는 startAnimation 메서드를 호출한다.

@IBAction func stop(_ sender: Any) {
    animator?.stopAnimation(false)
    animator?.finishAnimation(at: .current)
}

Animation을 정지할 때는 stopAnimation 메서드를 사용한다.
해당 메서드는 Boolean 값을 파라미터로 받고, true인 경우 정지하고 별도의 작업 없이 Inactive 상태로 전환한다.
false인 경우 stopped 상태로 전환한다. 또한, 이 경우 finishAnimation 메서드를 이어서 호출한다.
finishAnimation 메서드가 호출되면 completeHandler가 호출되고, Inactive 상태로 전달된다.

@IBAction func add(_ sender: Any) {
    animator?.addAnimations({
        self.redView.backgroundColor = UIColor.blue
    }, delayFactor: 0)
}

addAnimations 메서드는 파라미터로 전달한 Animation Block을 Property Animator가 관리하는 실행 Stack에 추가한다.
Inactive 상태에서 추가하면 원본 Animation과 동일한 시간 동안 실행된다.
Active 상태에서 추가하면 남은 시간동안 실행된다.
Stopped 상태에서 추가하면 충돌이 발생하므로 주의한다.
DelayFactor는 지연 시간을 나타낸다. 남은 Animation 시간에 대한 비율이며 지금처럼 0을 전달하면 지연 없이 실행된다.

재생 후 일시정지를 선택하면 Animation이 멈추고 실행 비율이 콘솔에 출력된다.
15% 정도의 Animation이 실행된 상태이다.

Resume을 선택하면 다시 재생되고,
Stop을 선택하면 Animation은 중지된다.
finishAnimation 메서드에서 Complete Handler로 current가 전달되어 콘솔에 표시된다.
마지막 상태는 Inactive 상태이다.
지금 상태에서 Resume을 선택해도 Animation은 다시 시작되지 않는다.
stopAnimation을 호출하면 실행 Stack에 존재하는 모든 Animation이 삭제된다.
즉, Animation을 다시 시작하려면 Stack에 Aimation을 다시 추가해야 한다.

Add를 선택하고 이어서 Resume을 선택하면 현재 위치에서 배경색을 바꾸는 Animation이 실행된다.

Animation이 실행 중인 동안에 Add를 선택하면 Animation의 종료 시점에 함께 종료되도록 배경색이 바뀌는 Animation이 추가된다.

생성자를 사용해 Animator를 사용하는 경우는 Spring Animation을 구현하거나 Animation Curve를 직접 구현하는 경우이다.

해당 생성자는 두 개의 CGPoint 파라미터를 통해 Bezier Timing Curve를 지정할 때 사용한다.

해당 생성자는 UIViewAnimation Curve를 지정할 때 사용한다.

해당 생성자는 Timing Curve를 상세히 지정할 때 사용한다.

해당 생성자는 Spring Animation을 구현할 때 사용한다.

@IBAction func animate(_ sender: Any) {
//        animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 7, delay: 0, options: []) {
//            self.moveAndResize()
//        } completion: { position in
//            switch position {
//            case .start:
//                print("Start")
//            case .end:
//                print("End")
//            case .current:
//                print("Current")
//            }
//        }
    animator = UIViewPropertyAnimator(duration: 7, curve: .linear, animations: {
        self.moveAndResize()
    })
    animator?.addCompletion({ (position) in
            print("Done \(position)")
        })
    }
}

UIViewAnimation Vurve를 사용하는 생성자를 사용해 같은 Animation을 구현한다.
생성자는 Completion Handler를 파라미터로 사용하지 않는다.
따라서 addCompletion 메서드를 호출해 직접 추가해야 한다.

지금 상태로는 재생 버튼을 선택해도 Animation이 시작되지 않는다.
Type메서드로 구현했을 경우 Animation이 자동으로 시작되지만 생성자를 사용할 때는 직접 Start Animation을 호출해야 한다.
즉, Resume 버튼을 선택해야 Animation이 시작된다.

Interactive Animation

지금까지 다룬 Animation은 NonInteractive Animation이다.
Property Animator는 Interaction Animation도 지원한다.
Interactive Animation을 구현하면 사용자가 터치를 통해 Animation을 계속 진행하거나 뒤로 돌아갈 수 있다.

//
//  InteractiveAnimationViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/13.
//

import UIKit

class InteractiveAnimationViewController: UIViewController {
    
    @IBOutlet weak var redView: UIButton!
    
    var animator: UIViewPropertyAnimator?
    
    func moveAndResize() {
        let targerFrame = CGRect(x: view.center.x - 100, y: view.center.y - 100, width: 200, height: 200)
        redView.frame = targerFrame
        redView.backgroundColor = UIColor.blue
    }
    
    @IBAction func animate(_ sender: Any) {
        animator?.startAnimation()
    }
    
    @IBAction func sliderChanged(_ sender: UISlider) {
        
    }
    
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.title = "Interactive Animation"
        
        animator = UIViewPropertyAnimator(duration: 7, curve: .linear, animations: {
            self.moveAndResize()
        })
    }
}

사용할 Scene과 코드는 위와 같다.
Slider가 추가된 것을 제외하면 이전과 동일하다.
해당 Slider는 sliderChanged 메서드와 Action으로 연결되어있다.
Slider를 통해 Animation을 제어한다.

@IBAction func sliderChanged(_ sender: UISlider) {
    animator?.fractionComplete = CGFloat(sender.value)
}

Fraction Complete 속성은 실행된 Animation의 비율을 확인할 때 사용한다.
여기에 0.0 ~ 1.0 사이의 값을 설정하면 해당 시점의 Animation 상태로 설정된다.
위와 같이 비율만 전달하면 나머지는 Property Animator에서 알아서 결정한다.
Slider가 아닌 Pan 제스처를 사용하는 것도 가능하다.

Slider를 통해 Animation의 진행 정도를 조절할 수 있고,
재생 버튼을 선택하면 해당 지점부터 Animation이 이어서 재생된다.

 

Motion Effect

iOS의 배경화면에서 기기를 기울이면 배경이 움직이는 걸 확인할 수 있다.
이것을 Motion Effect라고 부른다.
UIKit은 이를 구현하기 위한 API를 제공한다.

  • UIInterpolatingMotionEffect
    수평과 수직 Motion Effect를 구현할 때 사용한다.
    Motion Effect 구현에는 대부분 해당 클래스를 사용한다.
  • UIMotionEffectGroup
    Motion Effect 컬렉션을 구성할 때 사용한다.
    실제 View에는 단일 Motion Effect 처럼 처리할 수 있다.
  • UIMotionEffect
    위의 클래스들이 상속하는 추상 클래스이다.
    Motion Effect 구현에 필요한 기본 인프라를 제공한다.

//
//  MotionEffectViewController.swift
//  Animation Practice
//
//  Created by Martin.Q on 2021/11/16.
//

import UIKit

class MotionEffectViewController: UIViewController {

    @IBOutlet weak var crossHair: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

    }
}

사용할 Scene과 코드는 위와 같다.

Scene에는 두개의 Image View가 존재하고,
과녁 위에 크로스헤어가 위치하고 있다.
둘은 모두 화면 중앙에 특정 크기로 출력되도록 제약이 추가되어있다.

우선 UIInterpolatingMotionEffect 클래스로 x축 Motion Effect를 생성한다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    let x = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
    
}

UIInterpolationgMotionEffect의 생성자는 두 개의 파라미터를 갖는다.
첫 번째 파라미터에는 Motion Effect를 적용할 대상에 대한 Keypath를 전달해야 한다.
대상 속성은 반드시 Animation을 지원해야 한다.
두 번째 파라미터에는 Motion Effect를 적용할 축을 전달한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	let x = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
	
	x.minimumRelativeValue = -100
	x.maximumRelativeValue = 100
}

다음으로 Motion Effect의 크기를 설정해야 한다.
기기를 기울이거나 회전시키면 기준점으로부터 offset을 계산하고, Motion Effect를 사용한다.

minimumRelativeValue 속성은 offset이 -1일 때 사용할 최솟값을 설정한다.
maximumRelativeValue 속성은 offset이 1일 때 사용할 최댓값을 설정한다.
이 둘은 보통 절댓값이 같은 두 값으로 설정한다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    let x = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
    
    x.minimumRelativeValue = -100
    x.maximumRelativeValue = 100
    
    crossHair.addMotionEffect(x)
}

Motion Effect를 View에 추가한다.

기기를 기울이면 방향에 따라 Crosshair가 움직인다.
현재는 x축에만 적용한 상태라 좌우로만 움직인다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    let x = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
    let y = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
    
    x.minimumRelativeValue = -100
    x.maximumRelativeValue = 100
    y.minimumRelativeValue = -100
    y.maximumRelativeValue = 100
    
    crossHair.addMotionEffect(x)
    crossHair.addMotionEffect(y)
}

지금처럼 y축에 대해 Motion Effect를 적용하고 addMotionEffect 메서드를 한 번 더 호출해서 추가해도 되지만

override func viewDidLoad() {
    super.viewDidLoad()
    
    let x = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
    let y = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
    
    x.minimumRelativeValue = -100
    x.maximumRelativeValue = 100
    y.minimumRelativeValue = -100
    y.maximumRelativeValue = 100
    
    let group = UIMotionEffectGroup()
    group.motionEffects = [x, y]
    crossHair.addMotionEffect(group)
}

이렇게 Motion Effect Group으로 묶어 설정해도 문제없다.

y축에도 적용되어 상하좌우 모든 방향에 대응한다.

Motion Effect는 iOS의 접근성의 '동작 줄이기' 설정에 영향을 받는다.
해당 설정에 관계없이 Motion Effect를 사용하고 싶다면 Gyroscope 값을 받아 직접 구현해야 한다.

 


Log

2021.11.16.
127강 Motion Effect 내용 추가