안녕하세요. JourneyOfFaith 앱을 개발하며 4번째 포스트를 작성하게 되었습니다.
이번에는 BorderAnchor를 통해 연필로 밑줄을 긋는 효과인, 제가 원했던 시작점에서 도달점까지 보여지는 애니메이션 효과를 주는 방법에 대해 소개하려구 합니다!
화면 소개
여기 빨간색 점선 영역 안에 초록색이 맨 처음 보여질 때 빨간색 영역 왼쪽 엣지 부분에서부터 보여져서 오른쪽으로 커지는 애니메이션을 주려고 합니다!
이렇게 width가 0이었다가 "3일 동안" 글자 크기만큼의 width까지 underline이 자연스레 연필로 긋듯 그어지는 효과를 연출해보려구 합니다.
본격적인 애니메이션
NSUnderline에서 지원하는 여러 style보다 thickness가 좀 커져야 했었어서 저는 Layer를 추가했습니다!!
let underlineBorder = CALayer()
let textHeight = underLineSubtitle.frame.height
let underlineHeight = textHeight * 0.25
underlineBorder.backgroundColor = UIColor.palette(.underHighlightGreen).cgColor
underlineBorder.frame = CGRect(
x: 0, y: underLineSubtitle.frame.height - underlineHeight,
width: underLineSubtitle.frame.width, height: underlineHeight)
underLineSubtitle.layer.addSublayer(underlineBorder)
이렇게 보더를 만들고. layer 애니메이션 지원이 되는 CABasicAnimation을 이용합니다.
let toOriginWidthAnim = CABasicAnimation(keyPath: "bounds.size.width")
toOriginWidthAnim.fromValue = 0
toOriginWidthAnim.toValue = underLineSubtitle.frame.width
toOriginWidthAnim.duration = 0.777
toOriginWidthAnim.timingFunction = CAMediaTimingFunction(name: .easeOut)
underlineBorder.add(toOriginWidthAnim, forKey: "underlineExpand")
이렇게 위에 코드처럼 호출되면, 어떻게 될까요?
생각해보면 width는 0부터(from value ) 원래 크기(toValue)까지 시간의 흐름(duration)에 따라서 easeOut하게 "왼쪽에서 오른쪽"으로 보여지길 기대할 수 있을 것입니다. (width는 0이 었다가, 1.2.3 ... 지정된 원래 크기까지! bounds.origin 0. 0위치에서부터..)
그러나 실제로 width가 0에서 원래 지정된 width까지 길어지는 애니메이션은 해당 컴포넌트의 (UI컴포넌트.center) 정 가운데에서부터 시작됩니다.
이는 실제로 width Constraint를 따로 0 -> 원하는 크기로 지정하던, transform의 scale을 (x: 0, y: 1) 로 x축을 작게 했다가 .identity로 원 상태로 커지게 하던 이 3가지 전부 동일하게 위와 같이 정 가운데서부터 scale이 지정된 크기까지 커지게 되는 것입니다.
이러한 이유는 UI 컴포넌트마다 갖고 있는 anchorPoint! 때문입니다.
이 개념을 모르거나 익숙치 않다면
모든 화면에서 특정한 컴포넌트의 width나 height가 커지기 위한 애니메이션을 사용할 때 원하는 위치부터 커졌으면 좋겠다는 생각만하고 실제로 애니메이션은 해당 컴포넌트의 정 가운데서부터부터 scale up이 되어 지정한 원래 크기로 돌아가는 애니메이션만 보여지게 됩니다.
다시 돌아와서. 그럼 컴포넌트의 정 중앙부터 양 옆으로 커지는게 아닌 맨 왼쪽에서부터 오른쪽으로 커지게하려면 anchorPoint를 활용하면 됩니다.
/// 이전 코드에 새로운 코드 추가!
underlineBorder.anchorPoint = CGPoint(x: 0, y: 0.5)
underlineBorder.position = CGPoint(x: 0, y: underLineSubtitle.frame.height - underlineHeight / 2)
/// 이전 코드
let toOriginWidthAnim = CABasicAnimation(keyPath: "bounds.size.width")
...
anchorPoint는 기본이 0.5 0.5! 컴포넌트의 정 중앙인데, 저는 왼쪽에서부터 커지게 하고 싶은거니까 AnchorPoint = .init(x:0, y: 0.5)로 하면 왼쪽을 기준으로 커지게됩니다.
이렇게 한다면 뷰의 원점!이 변경됩니다. 그러기에 원래 중앙에서 왼쪽 끝으로 변하기에, 다시 position을 원 위치로(x = 0) 밀리도록 설정해야 원하는 형광펜으로 밑줄 긋는 듯한 효과를 애니메이션할 수 있게 됩니다.
이대로 끝나긴 아쉽습니다. (제 기준에서는요) 위에 동영상에선, 밑줄 아래에 위치한 저 분홍색 바가 보여지는 애니메이션을 보면 회색 바가 보여지고 나서, 정 중앙에서부터 분홍색 바 게이지가 위 아래로 동시에 차오르는 것을 볼 수 있습니다.
앗 참고로
화면 소개 2
애니메이션을 주는 이 컴포넌트의 구조는 크게 3가지입니다.
제가 원했던 애니메이션은
1. backgroundBar가 보여진다
2. progressColorView가 backgroundBar 아래 시점에서 지정된 위치로 보여진다
3. 2번 애니메이션이 진행중일 때 progressBar가 backgroundBar의 bototm부터 시작해서 위로 채워진다.
입니다.
이 애니메이션 코드를 잠깐 보자면..
/// 애니메이션 코드
progressColorView.transform = .init(translationX: 0, y: -11)
progressBar.transform = .init(scaleX: 1, y: 0)
UIView.animateKeyframes(withDuration: 1.2, delay: 0, animations: {
/// 1.
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.28, animations: {
self.backgroundBar.alpha = 1
})
/// 2.
UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.5, animations: {
self.progressColorView.transform = .identity
self.progressColorView.alpha = 1
})
/// 3.
UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.4, animations: {
self.progressBar.transform = .identity
self.progressBar.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.4) {
self.progressBar.setShadow(with: shadowInfo, cornerRadius: 2)
self.progressColorView.setShadow(with: shadowInfo, cornerRadius: 3)
}
})
이렇게 keyframe의 duration에 따라서 상대적으로 애니메이션이 진행됩니다.
progressBar의 trnasform.scale을 초기에 y = 0 으로 지정했더라도, 이는 결과적으로 위에서 본 gif처럼 backgroundBar의 정 가운데서부터 위 아래로 동시에 scale up되집니다.
이유도 동일하게 기본 anchorPoint 설정 때문입니다.
따라서 제가 위에 언급했던 애니메이션처럼 아래서 위로 보여지게 하려면 anchor를 수정하고, 이로인해 변경된 position을 원 위치로 바꾸면 됩니다.
progressBar.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
progressBar.layer.position.y += progressBar.bounds.height / 2
progressColorView.transform = .init(translationX: 0, y: -11)
/// 이어서 키프레임 애니메이션
...
그러면 기존 애니메이션 적용시 자동으로 활성화 되는 위치가 아닌 제가 원하는 시점에서부터 애니메이션이 진행하게 됩니다!
추가적인 에러 방지하기
위에서 경우처럼, anchorPoint를 바꾼 이후에, 재사용 뷰(컬랙션or테이블 뷰) 해당 그래프 뷰가 사라지고, 데이터는 새로 갱신됬고, 다시 그래프 뷰가 화면에 보여질 때
이렇게 재사용 큐에서 나오는 시점(cellForRowAt)에 progressBar 컴포넌트에 대한 새로운 높이를 반영해 사용자에게 보여준다면 어덯게 될까요?
반드시 색이 있는 프로그래스 바는 백그라운드 바(회색)의 바텀 위치에 위치해서 위로 증가되어야 합니다.
사용자에 의해 오른쪽 초록색 진행도 그래프는 업데이트 됬고, config함수에 의해 새롭게 갱신되어 뷰에 반영됬는데, 백그라운드 바의 바텀위치에 초록 색 프로그래스 바의 바텀이 위치하지 않고 살 짝 붕 뜬 채로, 높이가 업데이트 된 결과를 알 수 있습니다.
이는 이전에 아래서 위로 애니메이션할 때 지정해둔 anchorPoint 때문입니다. 시작 위치가 변경되었고, 그래서 새롭게 layoutIfNeeded를 하면, 해당 포지션에서 다시 높이가 그려지기 때문입니다.
그렇다면 다시 anchorPoint를 원 위치(0.5, 0.5)로 바꿔주고 포지션도 원 위치로 바꿔주면 해결됩니다. ( TMI. 이 그래프 컴포넌트는 이 VC에서 섹션에 유일한 하나의 셀이어서 재사용 큐에서 꺼내올 때도 초기에 할당한 컴포넌트에서 cellForRowAt, prepareForReuse만 호출되어 화면에 보여집니다. 따로 셀의 prepareForReuse 시점에 그래프에 대한 초기화는 진행하지 않았기에 config시점에 한번 변경될 수 있도록 반영했습니다 )