UIButton.Configuration을 기반으로 만든 모듈이다.
iOS 15 이상 적용 가능하다는 의미
Meet the UIKit button system - WWDC21 - Videos - Apple Developer
Every app uses Buttons. With iOS 15, you can adopt updated styles to create gorgeous buttons that fit effortlessly into your interface...
developer.apple.com
UIButton.Configuration 참고용 WWDC 영상
기존에 버튼 애니메이션을 구성하는 코드
final class CustomButton:UIButton{ // 크기 변화만 발생하는 코드
init(){
...
let handler: UIButton.ConfigurationUpdateHandler = {[weak self] button in // 1
guard let self else {return}
switch button.state {
case .selected,.highlighted,[.selected,.highlighted]:
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5,initialSpringVelocity: 0.5,options: .curveEaseIn) {
button.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
}completion: { _ in
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5,initialSpringVelocity: 0.5,options: .curveEaseIn){
button.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
default: break
}
}
self.configurationUpdateHandler = handler
...
}
}
⚠️ 각각의 UIButton.ConfigurationUpdateHandler에 넣고 싶은 애니메이션 코드를 넣어주어야 한다.
애니메이션 로직과 ConfigurationUpdateHandler를 분리해서 애니메이션 로직을 추가하면 버튼에서 알아서 동작하도록 만들자
모듈을 적용한 코드
fileprivate final class OnboardingBtn:UIButton{
init(text:String,textColor:UIColor,bgColor:UIColor){
...
var animSnapshot = self.animationSnapshot
// 여기에서 버튼에 동작 시키고 싶은 애니메이션을 추가한다.
let animSnapshot = self.animationSnapshot
.bgEffect(effectColor: .blue).scaleEffect(ratio: 0.95)
do{
// 애니메이션을 적용한다.
try self.apply(animationSnapshot: updatedSnapshot)
}catch{
print(error)
}
...
}
}
내부 구현
- UIButton의 Extension 메서드
var animationSnapshot:ButtonAnimations
func apply(animationSnapshot: ButtonAnimations) throws
⇒ 애니메이션 스냅샷이라는 구조체를 가져오고 이를 UIButton 인스턴스에 적용하는데 사용됨
- 모듈 ⭐ ButtonAnimations 구조체
⇒ 버튼 애니메이션 로직을 추가하고 UIButton.ConfigurationUpdateHandler를 만드는데 사용됨 - UIButton apply시 오류 케이스 열거형
ButtonAnimations 구조체
- 저장 프로퍼티
- 애니메이션 로직을 담는 배열:
[()→()]
- 버튼 State에 알맞는 애니메이션 로직을 담는 딕셔너리:
[ActionType:[()->()]]
- 적용할 버튼의 인스턴스:
UIButton
- 애니메이션 로직을 담는 배열:
- 계산 프로퍼티
- UIButton.ConfigurationUpdateHandler 반환
- ⇒ 각각의 버튼 상태에 맞게 애니메이션 로직 클로져 출력
- 메서드
- 저장 프로퍼티에 갖고 있는 버튼과 일치하는지 확인하는 메서드: checkSame(btn: UIButton)->Bool
- 애니메이션 메서드 → 필요한 애니메이션이 있을 때 마다 추가하자
- 내부 열거형
- 커스텀하게 조절할 버튼 State를 갖는 Enum: enum ActionType:CaseIterable
전체 코드
import Foundation
import UIKit
extension UIButton{
var animationSnapshot:ButtonAnimations{ ButtonAnimations(btn: self) }
func apply(animationSnapshot: ButtonAnimations) throws {
if animationSnapshot.checkSame(btn: self){
self.configurationUpdateHandler = animationSnapshot.myHandler
}else{
throw SnapshotError.buttonNotSame
}
}
}
struct ButtonAnimations{
private var anims:[()->()] = []
private var actionByState:[ActionType:[()->()]]
enum ActionType:CaseIterable{
case selected
case highlighed
case defaults
}
private weak var btn : UIButton!
init(btn: UIButton) {
self.btn = btn
self.actionByState = ActionType.allCases.reduce(into: [:], { partialResult, type in
partialResult[type] = []
})
}
private init(btn: UIButton,anims:[()->()],actionByState:[ActionType:[()->()]]){
self.btn = btn
self.anims = anims
self.actionByState = actionByState
}
@discardableResult
func scaleEffect(ratio:CGFloat = 0.9)->Self{
var anim = self.anims
anim.append{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.4,initialSpringVelocity: 1,options: .preferredFramesPerSecond60){
btn.transform = CGAffineTransform(scaleX: ratio, y: ratio)
}completion: { _ in
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5,initialSpringVelocity: 0.5,options: .curveEaseIn){
btn.transform = CGAffineTransform(scaleX: 1, y: 1)
}
}
}
return ButtonAnimations(btn: self.btn, anims: anim,actionByState: self.actionByState)
}
@discardableResult
func bgEffect(effectColor:UIColor)->Self{
var actionByStates = self.actionByState
let prevColor = self.btn.configuration?.background.backgroundColor
let effectAction = {
self.btn.configuration?.baseBackgroundColor = effectColor
self.btn.configuration?.background.backgroundColor = effectColor
}
actionByStates[.selected]?.append(effectAction)
actionByStates[.highlighed]?.append(effectAction)
actionByStates[.defaults]?.append {
self.btn.configuration?.baseBackgroundColor = prevColor
self.btn.configuration?.background.backgroundColor = prevColor
}
return ButtonAnimations(btn: self.btn, anims: anims,actionByState: actionByStates)
}
func checkSame(btn: UIButton)->Bool{
return self.btn == btn
}
}
extension ButtonAnimations{
var myHandler: UIButton.ConfigurationUpdateHandler{
return {button in // 1
switch button.state {
case .selected,.highlighted,[.selected,.highlighted]:
anims.forEach { anim in anim() }
actionByState[.selected]?.forEach({$0() })
// 이렇게 케이스를 전부 나누는게 좋겠지만,
// 일단 위에처럼 selected, highlighted 상태를 모두 동일한 로직을 처리하게 쓰고 있다.
// case .selected:
// actionByState[.selected]?.forEach({$0() })
// case .highlighted:
// actionByState[.highlighed]?.forEach{$0()}
default:
actionByState[.defaults]?.forEach({ $0() })
}
}
}
}
enum SnapshotError: Error{
case buttonNotSame
}
⛔ 같은 애니메이션 메서드를 중복해서 체이닝하는 경우 오류 처리를 해야한다.
✅ UIViewPropertyAnimator라는 새로운 요소로 UI의 Animation을 적용하면 더 좋게 개선할 수 있을것 같기도 하다.
(아직 자세히 모름...)
'이모저모 > UIKit' 카테고리의 다른 글
커스텀 이미지 피커 뷰 만들기 #1 (0) | 2023.12.02 |
---|---|
싱글톤 앨범 이미지 서비스 만들기 with Rx or Combine (0) | 2023.11.28 |
CustomDiffableDataSource와 MVVM으로 Cell 데이터 관리하기 #0 (0) | 2023.11.18 |
다양한 셀을 그리는 DiffableDataSource 데이터 구성하기 #2 (0) | 2023.11.10 |
다양한 셀을 그리는 DiffableDataSource 데이터 구성하기 #1 (0) | 2023.11.10 |
댓글