구현 과제
햄버거 키오스크에서 상단의 카테고리바를 선택했을 때 애니메이션 효과를 적용하기
어제 카테고리바를 구현한 뒤에 특정 셀을 선택하면 색이 바뀌도록 구현을 했다.
그러나 그 과정이 너무 딱딱하다고 느꼈고, 애니메이션을 넣어 부드럽게 바꾸거나 조금 더 다이나믹하게 바꾸고 싶다고 생각했다.
처음에는 색이 변하는 모습을 부드럽게 변하는 것처럼 alpha(opacity) 값을 0 -> 1 로 천천히 변하는 애니메이션을 구현하고자 했다. 그러나 어떤 코드를 작성해도 원하는 대로 구현되지 않았고, 원하는 대로 구현하기 위해서는 메소드의 성능비용을 많이 높아지는 문제가 발생했다.
때문에 아이디어를 바꿔 셀을 선택했을 때 통통 튀는 느낌을 줄 수 있도록 레이블의 크기가 줄었다가 커지는 애니메이션을 만들기로 결정하였다.
셀 아이템은 UILabel로 설정되어있는데 어떻게 사이즈를 바꾸면 좋을지 고민하다가, SwiftUI에서 scaleEffect로 레이블의 크기를 바꿀 수 있었던 것이 떠올랐다.
UIKit에도 비슷한 기능이 있지 않을까 싶어 찾아보니 역시 뷰의 속성을 다이나믹하게 바꿔줄 수 있는 메소드가 있었다.
UIView는 transform이라는 속성을 가지고 있는데, 이 속성은 뷰의 크기, 위치, 회전 등의 변형을 적용할 때 사용된다.
보통 CGAffineTransform과 함께 사용되며, 이 변형을 통해 뷰에 다양한 애니메이션 효과를 줄 수 있다고 한다.
이번에는 그 중에서도 2D 그래픽 변형을 줄 수 있는 CGAffineTransform을 사용하여 크기를 변경하기로 했다.
label.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
이렇게 코드를 작성하면 label의 폰트, 레이블 크기 등 레이블이 가진 모든 속성의 크기의 배율을 90%로 줄여줄 수 있다.
나는 특정 셀이 선택되었을 때 이 액션이 작동해야 했고, 줄어든 사이즈가 다시 원래상태로 복구되어야 했기에 메소드 안에 코드를 작성하고 추가 작업을 해주었다.
private func scaleEffect() {
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut]) {
label.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
// 0.05초 후 셀의 크기 복구
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
// .identity = 셀의 변형을 취소, 원래 상태로 복귀
label.transform = .identity
}
}
}
그리고 이 메소드를 컬렉션뷰의 델리게이트 메소드에서 호출하여 무사히 적용되는 모습을 확인할 수 있었다.

구현 과제
햄버거 키오스크에서 상단의 카테고리바를 선택했을 때 자동으로 스크롤 되어 해당 셀 위치로 스크롤 이동시키기
프로젝트를 진행하며 구현한 코드를 팀원들에게 보여주고 피드백을 받던 중 팀원 한 분이 의견을 하나 제시해주었다.
"카테고리를 선택했을 때 스크롤이 자동으로 이동해야 하지 않을까요?"
음... 생각하지 못했던 문제다. 확실히, 지금까지 썼던 키오스크들을 보더라도 카테고리를 하나 선택하면 자동으로 스크롤이 되어 해당 카테고리 바가 잘 보이도록 했던 것 같다.
어떻게 구현하면 좋을까 고민하다가 예전에 스크롤뷰의 offset을 설정했던 것이 기억났다.(링크)
컬렉션뷰도 스크롤뷰를 상속하고 있으니, 비슷하게 구현하면 되지 않을까??
바로 실행에 옮겨보도록 하자.
컬렉션뷰는 스크롤뷰를 상속하고 있고, 때문에 스크롤뷰처럼 contentView를 가지고 있다.
스크롤이 되는 원리도 스크롤뷰와 똑같기 때문에 contentViewOffset의 값을 조절하면 되지 않을까? 생각이 들었다.
그렇다면 우선 가설을 검증하기 위해 contentViewOffset 값을 출력해보기로 했다.
// 컬렉션뷰의 컨텐츠뷰 오프셋 값 출력
print(collectionView.contentOffset)
결과는...

예상대로 스크롤을 할 때마다 offset 값이 변하는 것을 확인할 수 있었다.
그럼 이제 offset에 어떤 값을 주면 좋을지 생각해봐야 하는데... 번뜩 아이디어가 떠올랐다.
어차피 셀을 선택했을 때 자동 스크롤이 된다면, 셀의 위치와 관계가 있으니 셀의 offset 값을 사용하면 되지 않을까??
그래서 셀의 offset 값도 출력해보기로 했다.
다만, 셀은 contentView를 가지고 있지 않기 때문에 셀 본인의 좌표값을 구해야 했다.
그리고 셀의 위치값은 슈퍼뷰에서 얼마나 떨어졌는지를 구해야하기 때문에 frame을 사용해서 값을 구해야 했다.(frame과 bounds의 차이)
또, 카테고리바는 횡스크롤을 지원하기 때문에 굳이 y에 대한 값은 필요가 없어서 x의 값만 구하기로 했다.
print(cell.frame.origin.x)
출력 결과는...

예상한 대로 슈퍼뷰에서 셀의 x좌표가 얼마만큼 떨어졌는지 출력되는 모습을 볼 수 있었다.
(컬렉션뷰는 셀을 재사용하기 때문에 화면에 보이지 않는 셀은 값이 출력되지 않다가 화면에 보이는 순간 값이 출력되는 모습을 볼 수 있다.)
값을 보면 각 셀의 x값이 110씩 증가하는 것을 볼 수 있는데, 아마 컬렉션뷰의 레이아웃을 설정할 때 아이템의 사이즈를 width: 100으로 설정하고 아이템간의 간격을 spacing: 10으로 설정했기 때문인 것 같다.
어쨌든 필요한 값들을 모두 구했으니 이제 자동 스크롤을 구현해보자!!
솔직히 위에서 필요한 값을 다 구했으니 간단하지 않나? 라고 생각했다.
그래서 생각나는 코드를 바로 작성했는데...
// isSelected: Bool = 셀을 선택했는지 확인하는 변수
if isSelected {
collectionView.contentOffset.x = cell.frame.origin.x
}
특정 셀을 선택하면 선택된 셀의 x 좌표를 가져와서 즉시 컨텐츠뷰에 적용시키는 코드이다.
그리고 바로 빌드를 해보았는데...

역시 생각대로 잘 될리가 없지...
생각해보면 아까 셀의 x 값이 최대 550까지 출력되었는데, 컨텐츠뷰의 offset을 550으로 하면 다른 셀들이 화면 밖으로 벗어나는게 당연하다...
게다가 나는 자동으로 스크롤되는 것을 원했는데 무척이나 딱딱하게 이동해서 실망했다...
그래도 일단은 스크롤뷰가 화면 밖으로 벗어나는 문제부터 해결해보자...!!
문제의 원인은 파악했으니 해결 방법도 쉽게 떠올릴 수 있었다!
바로 셀의 x 좌표 값이 컨텐츠뷰의 width값보다 큰 경우 컨텐츠뷰의 Offset은 항상 끝을 가르키도록 하는 것이다!!
이건 스크롤뷰를 연습할 때도 했던 일이라 어렵지 않았다.
if isSelected {
if cell.frame.origin.x < collectionView.contentSize.width - collectionView.bounds.width {
collectionView.contentOffset.x = cell.frame.origin.x
} else {
collectionView.contentOffset.x = collectionView.contentSize.width - collectionView.bounds.width
}
}
여기서 collectionView.bounds.width를 왜 빼야하는지 궁금할 수 있다.
그 이유는 간단하다. collectionView.contentSize.width는 컨텐츠뷰의 사이즈이기 때문에 컨텐츠뷰 내의 아이템(셀)이 가지는 위치값보다 항상 클 수밖에 없다. 때문에 어떤 값을 더 빼주지 않으면 이전의 빌드와 달라지는 점이 없다.
그럼 왜 하필 collectionView.bounds.width를 빼줘야 할까? 사진으로 이해해보자.

사진에서 스크롤뷰의 위치는 변하지 않는다. 다만 컨텐츠뷰의 offset 값이 변하며 보여지는 컨텐츠가 변하는 것인데,
만약 collectionView.contentOffset.x이 collectionView.contentSize.width만큼 이동한다면 아래 사진처럼 변할 것이다.

위의 사진은 스크롤뷰의 위치가 변한게 아니라 컨텐츠뷰의 오프셋값이 collectionView.contentSize.width만큼 이동한 것이다.
그럼 collectionView.bounds.width 빼주면 어떻게 될까??

위 사진처럼 컬렉션뷰 내에 컨텐츠뷰가 보여지는 모습을 볼 수 있다.
즉, 컨텐츠뷰가 컬렉션뷰를 벗어나지 못하도록 하기 위해 collectionView.bounds.width만큼 값을 빼주는 것이다.
이렇게 설정을 완료하면 이런 결과물이 만들어진다.

그럼 이제 보다 자연스럽도록 스크롤 애니메이션을 만들어주자
처음에는 UIView의 메소드인 animate를 사용하려고 했다.
그런데 찾아보니 컬렉션뷰는 setContentOffset라는 메소드가 있는데, 여기서 애니메이션을 줄 수 있다고 한다.

말그래도 컨텐츠뷰의 CGPoint 값을 새로 세팅해주고, 애니메이션을 줄지 말지 정하는 메소드이다.
그래서 위에서 만든 if문을 살짝 수정해서 사용해주면 된다.
// 카테고리를 선택하면 자동으로 스크롤 되도록 설정
if isSelected {
let targetOffsetX: CGFloat
if cell.frame.origin.x < collectionView.contentSize.width - collectionView.bounds.width {
targetOffsetX = cell.frame.origin.x
} else {
targetOffsetX = collectionView.contentSize.width - collectionView.bounds.width
}
}
이렇게 하면 특정 셀을 선택했을 때targetOffsetX에 if문에 따라 값을 저장하게 된다.
그리고 이 저장된 값을 setContentOffset을 이용해서 적용하면 끝이다.
// 애니메이션으로 스크롤 이동
collectionView.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: true)
이 때 y에 대해서는 지정할 필요가 없기 때문에 0으로 둔다.
그리고 빌드를 해보면...

드디어 자연스러운 자동 스크롤을 구현했다!!!
어렵다고 생각했지만 생각보다 구현이 쉬워서 다행이었다.
오늘은 팀 프로젝트에서 카테고리에 애니메이션을 넣는 작업을 진행하였다.
이전에 딱딱하게 넘어가는 모션이 너무 마음에 안들었기 때문에 수정해주고 싶었는데,
드디어 염원을 이룬 것 같아 기쁘다...
사실 아직도 아쉬운 부분들이 있긴 하지만, 지금 내 수준으로 가능할지 알 수 없어서
일단은 시간이 남으면 추가구현을 해보는 것으로 시도해보려고 한다.
멋있다..