디자이너가 요구한 조건을 구현하기 위해 많은 도전에 직면했다. 이러한 요구사항을 어떻게 구현할지에 대해 많은 고민을 했다. 이번 포스팅은 이에 관한 내용이다.
요구사항
1. 페이징 되도록 할 것. (걸리는 느낌? 촤라락 스크롤 되는 것이 아니라 페이징 되어야함.)
2. 추천 문화예술 스크롤 시 배경 업데이트
3. 업데이트 될 배경은 현재 추천 문화예술 포스터에 블러 처리를 한 것.
4. 동시에 스크롤 된 포스터에 집중할 수 있도록 해당 포스터를 확대하는 것.
5. 현재 추전 문화예술 포스터에 페이징 정보를 담고 있는 프로그레스바와 라벨을 표시할 것.
구현
✔️ 페이징 구현
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let layout = self.homeRecommendCollectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
let normalCellWidth = homeRecommendCollectionView.frame.width * 0.80
let cellWidthIncludingSpacing = normalCellWidth + layout.minimumLineSpacing
var offset = targetContentOffset.pointee
let index = (offset.x + scrollView.contentInset.left) / cellWidthIncludingSpacing
var roundedIndex = round(index)
if scrollView.contentOffset.x > targetContentOffset.pointee.x {
roundedIndex = floor(index)
} else if scrollView.contentOffset.x < targetContentOffset.pointee.x {
roundedIndex = ceil(index)
} else {
roundedIndex = round(index)
}
let adjustedOffset = roundedIndex * cellWidthIncludingSpacing
offset = CGPoint(x: adjustedOffset - scrollView.contentInset.left, y: -scrollView.contentInset.top)
targetContentOffset.pointee = offset
centerCellIndex = Int(roundedIndex)
homeRecommendCollectionView.collectionViewLayout.invalidateLayout()
}
페이징 효과는 컬렉션 뷰가 스크롤될 때 자연스럽게 하나씩 넘겨지는 느낌을 주는 것이 핵심이었다. 이를 위해 scrollViewWillEndDragging 메소드에서 스크롤의 속도와 목표 지점을 계산한 후, 페이징이 되도록 타겟 오프셋을 조정했다.
scrollViewDidScroll 메소드에서 withVelocity는 스크롤 속도, targetContentOffset은 스크롤이 멈출 예상 지점을 나타낸다. 이 값들을 활용해 페이징을 위한 위치 조정을 하며, 중앙에 맞춘 아이템으로 스크롤이 멈추도록 한다.
let normalCellWidth = homeRecommendCollectionView.frame.width * 0.80
let cellWidthIncludingSpacing = normalCellWidth + layout.minimumLineSpacing
normalCellWidth은 화면 너비의 80%로 계산된다. 즉, 각 셀의 크기는 컬렉션 뷰의 너비에서 80%로 설정 된다는 의미이다. cellWidthIncludingSpacing은 셀의 너비와 셀 사이의 간격을 더한 값이다. 이는 스크롤링 시 하나의 셀이 차지하는 전체 공간을 의미한다.
var offset = targetContentOffset.pointee
let index = (offset.x + scrollView.contentInset.left) / cellWidthIncludingSpacing
var roundedIndex = round(index)
offset은 스크롤이 멈출 예상 지점을 나타낸다. index는 오프셋을 기준으로 스크롤된 셀의 위치를 나타내는 값이다. (offset.x + scrollView.contentInset.left)로 스크롤 오프셋에 인셋을 더한 후, 이를 각 셀이 차지하는 공간으로 나눠서 현재 셀이 몇 번째인지 계산한다. roundedIndex는 index를 반올림한 값으로, 스크롤이 멈출 때 중앙에 맞춰질 아이템의 인덱스를 결정한다.
if scrollView.contentOffset.x > targetContentOffset.pointee.x {
roundedIndex = floor(index)
} else if scrollView.contentOffset.x < targetContentOffset.pointee.x {
roundedIndex = ceil(index)
} else {
roundedIndex = round(index)
}
스크롤의 방향을 고려하여 인덱스를 결정한다. 세 가지 경우의 수로 나누어 처리한다.
1. 스크롤이 왼쪽으로 향하는 경우 : scrollView.contentOffset.x > targetContentOffset.pointee.x를 내림 처리하여 이전 아이템으로 이동한다.
2. 스크롤이 오른쪽으로 향하는 경우 : scrollView.contentOffset.x < targetContentOffset.pointee.x를 올림 처리하여 다음 아이템으로 이동한다.
3. 속도가 거의 없는 경우 : round(index)를 반올림하여 가장 가까운 아이템에 멈춘다.
let adjustedOffset = roundedIndex * cellWidthIncludingSpacing
offset = CGPoint(x: adjustedOffset - scrollView.contentInset.left, y: -scrollView.contentInset.top)
targetContentOffset.pointee = offset
roundedIndex와 cellWidthIncludingSpacing을 곱하여 현재 스크롤된 위치에서 중앙에 맞출 셀의 정확한 위치를 계산한다. adjustedOffset은 스크롤이 멈출 최종 지점이다. scrollView.contentInset.left 값을 고려하여 최종 offset을 설정한 후, targetContentOffset.pointee에 이 값을 할당해 실제로 페이징 되도록 한다.
centerCellIndex = Int(roundedIndex)
homeRecommendCollectionView.collectionViewLayout.invalidateLayout()
centerCellIndex에 현재 중앙에 올 셀의 인덱스를 업데이트하여 셀의 크기를 조정하거나 다른 필요한 처리를 할 수 있게 한다.
invalidatedLayout()을 호출해 레이아웃을 강제로 갱신하여 중앙 셀이 더 크게 표시되도록 하거나 레이아웃을 재조정한다.
✔️ 동적 배경 업데이트
컬렉션 뷰를 스크롤할 때 배경이 동적으로 업데이트되는 기능을 구현해야 했다.
private func updateBackgroundImage() {
let visibleRect = CGRect(origin: homeRecommendCollectionView.contentOffset, size: homeRecommendCollectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
if let indexPath = homeRecommendCollectionView.indexPathForItem(at: visiblePoint),
let cell = homeRecommendCollectionView.cellForItem(at: indexPath) as? HomeRecommendCollectionViewCell {
UIView.transition(with: blurredBackgroundImageView, duration: 0.3, options: .transitionCrossDissolve) {
self.blurredBackgroundImageView.image = cell.thumbnailImage
}
}
}
우선, 컬렉션뷰에서 스크롤 시, 사용자가 보고 있는 중앙 아이템에 맞춰 배경 이미지를 업데이트하도록 구현했다. UIView.transition을 사용해 배경 이미지가 부드럽게 전환되도록 처리했다.
let visibleRect = CGRect(origin:
homeRecommendCollectionView.contentOffset, size: homeRecommendCollectionView.bounds.size)
현재 화면에 보이는 컬렉션 뷰의 영역을 계산한다. 'contentOffset'은 스크롤 위치를, 'bounds.size'는 컬렉션 뷰의 크기를 나타낸다.
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
보이는 영역의 중앙 지점을 계산한다. 중앙 지점을 계산하는 이유는, 업데이트 될 배경은 현재 추천 문화예술 포스터이고 이는 화면에 보이는 컬렉션 뷰의 중앙이기 때문이다.
if let indexPath = homeRecommendCollectionView.indexPathForItem(at: visiblePoint),
let cell = homeRecommendCollectionView.cellForItem(at: indexPath) as? HomeRecommendCollectionViewCell {
UIView.transition(with: blurredBackgroundImageView, duration: 0.3, options: .transitionCrossDissolve) {
self.blurredBackgroundImageView.image = cell.thumbnailImage
}
}
중앙 지점에 있는 셀의 indexPath를 찾고, 해당 셀을 가져온다. 찾은 셀의 'thumbnailImage'를 'blurredBackgroundImageView'의 이미지로 설정한다.
✔️ 중앙 셀 확대
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let isCenter = indexPath.item == centerCellIndex
let cellWidth = floor(collectionView.frame.width * (isCenter ? 0.80 * 1.05 : 0.80))
let cellHeight = floor(collectionView.frame.height * (isCenter ? 0.85 * 1.1 : 0.85))
return CGSize(width: cellWidth, height: cellHeight)
}
스크롤 시 중앙에 위치한 셀을 확대하는 기능을 구현했다. 이는 컬렉션뷰의 DelegateFlowLayout 프로토콜의 'sizeForItemAt' 메소드에서 처리 된다.
let isCenter = indexPath.item == centerCellIndex
현재 셀이 중앙에 위치한 셀인지 확인한다. 'centerCellIndex'는 별도로 관리되는 변수이다. (위의 페이징 이벤트 구현 참고)
let cellWidth = floor(collectionView.frame.width * (isCenter ? 0.80 * 1.05 : 0.80))
셀의 너비를 계산한다. 기본적으로 컬렉션 뷰 너비의 80%를 셀 너비로 사용한다. 중앙 셀인 경우 추가로 5% 더 크게 만든다.
let cellHeight = floor(collectionView.frame.height * (isCenter ? 0.85 * 1.1 : 0.85))
셀의 높이를 계산한다. 기본적으로 컬렉션 뷰 높이의 85%를 셀 높이로 사용한다. 중앙 셀인 경우 추가로 10% 더 크게 만든다.
✔️ 프로그레스바 이벤트
func configuration(_ item: ArtRandomList, currentPage: Int, totalPages: Int) {
thumbnailImageView.image = UIImage(named: item.poster)
updatePage(currentPage: currentPage, totalPages: totalPages)
}
func updatePage(currentPage: Int, totalPages: Int) {
progressBar.progress = Float(currentPage) / Float(totalPages)
let attributedString = NSMutableAttributedString(string: "\(currentPage)", attributes: [
.font: UIFont.num2,
.foregroundColor: UIColor.white
])
attributedString.append(NSAttributedString(string: " / \(totalPages)", attributes: [
.font: UIFont.num2,
.foregroundColor: UIColor.gray5
]))
progressLabel.attributedText = attributedString
}
셀에서 위의 메소드를 작성하여 이벤트를 처리할 수 있게 했다.
private func updateCurrentPage() {
let visibleRect = CGRect(origin: homeRecommendCollectionView.contentOffset, size: homeRecommendCollectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
if let indexPath = homeRecommendCollectionView.indexPathForItem(at: visiblePoint) {
currentPage = indexPath.item + 1
if let cell = homeRecommendCollectionView.cellForItem(at: indexPath) as? HomeRecommendCollectionViewCell {
cell.updatePage(currentPage: currentPage, totalPages: testData.count)
}
}
}
현재 페이지 번호를 업데이트하고 셀에서 작성한 메소드를 통해 해당 셀의 페이지 정보를 갱신하여 프로그레스 바 이벤트를 처리한다.
✔️ 스크롤 이벤트 처리
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateBackgroundImage()
updateCenterCell()
updateCurrentPage()
}
모든 이벤트는 스크롤 시 동작하기 때문에, scrollViewDidScroll(_:) 메소드에서 필요한 업데이트를 수행한다.
결과
마무리
이번 도전을 통해 컬렉션뷰를 활용한 복잡한 UI 구현에 대해 깊이 있게 생각하고 학습할 수 있었다.
1. 동적 레이아웃 구현 : UICollectionViewDelegateFlowLayout을 활용하여 스크롤 위치에 따라 셀 크기를 동적으로 조절하는 방법을 알게 됐다. 이를 통해 중앙 셀을 강조하는 효과적인 UI를 구현할 수 있었다.
2. 페이징 로직 최적화 : scrollViewWillEndDragging 메서드를 활용하여 부드러운 페이징 효과를 구현하는 방법을 알게 됐다. 이는 사용자 경험을 크게 향상시키는 중요한 요소라고 생각한다.
3. 동적 배경 업데이트 : UIView.transition을 사용하여 배경 이미지를 부드럽게 전환하는 방법을 알게 됐다.
이 프로젝트를 통해 단순히 기능 구현을 넘어서, 사용자 경험을 고려한 UI/UX 설계의 중요성을 깊이 이해하게 되었다. 특히, 디자이너의 요구사항을 정확히 이해하고 이를 기술적으로 구현하는 과정에서, 개발자와 디자이너 간의 원활한 소통과 협업의 중요성을 체감했다.
또한, 복잡한 UI 요구사항을 단계적으로 분석하고 구현해 나가는 과정에서 문제 해결 능력이 크게 향상되었다. 각 기능을 구현할 때마다 발생하는 도전과제들을 해결해 나가면서, 개발에 대한 더 깊은 이해와 자신감을 얻을 수 있었다.
'우리 같이 협업하자' 카테고리의 다른 글
[우협하] 키체인 도입 (0) | 2024.11.05 |
---|---|
[우협하] 16주차 회고 (1) | 2024.10.28 |
[우협하] 13~15주차 회고 (1) | 2024.10.21 |
[우협하] 9~12주차 회고 (4) | 2024.09.29 |
[우협하/문제해결] 네이버 다이나믹맵에서 자동으로 카메라 무브가 발생하는 원인을 해결해보자. (5) | 2024.09.02 |