안녕하세요. 인스타그램 앱을 유심히 보는데 조금만 스크롤 해도 다음 포스트가 자동으로 보여지는 기능이 신기해서 Carousel effect에 대해서 공부를 해봤습니다. 이번에는 Carousel effect를 제 인스타그램 피드에 적용하며 배운 개념, 느낀점을 정리하려고 합니다.
1. Carousel effect's concept
인스타그램의 포스트 내려갈 때 인데요. 짧은 스크롤을 하면, 다음 포스트를 보여주는데 그냥보여주는게 아니라 상대방의 프로필 부터 포스트 전체를 보여줍니다. Carousel은 컨텐츠를 순환시키는데 user가 많은 드래그 필요없이 더 많은 양의 정보를 보여주는 그런 디자인입니다. Paging도 비슷한 개념입니다. 스크롤 방향에 맞게 위 동영상처럼 horizontal로 포커싱을 맞출 수 있고 vertical로 포커싱 맞출 수도 있습니다. 이 뿐 아니라 타이머를 통해서 끊임없는, 몇 안되는 화면을 특정 timer의 흐름에 따라 자동으로 다음 화면 포커싱을 통해 새로운 정보들을 보여줄 수도 있습니다.
저도 위 그림처럼 제 피드를 맞춰보려고 합니다. +_+
2. UICollectionView, UICollectionViewController's concept
제 feed는 UICollectionViewController 기반입니다. UICollectionView는 저번 포스트에서 공부했었는데요. 컬랙션 뷰는 테이블 뷰와 다르게 cell을 grid방식으로 배치할 수 있는 장점이 있는 반면 tableView와 달리 각각의 cell's location이나 크기 등은 layout object를 통해서 배치해야 합니다. 지금 적용할 carousel effect는 각각의 cell에 대해 포커싱을 맞추기 위해서 data source, delegate object를 건들이지 않고 layout만 참조하면 된다는 장점이 있습니다.
UICollectionViewController에서 collectionView는 UIScrollView를 상속받았습니다. 이 덕분에 UIScrollView의 10개 정도 되는 각각의 메서드, 프로퍼티를 원할 경우 오버라이딩 해서 사용할 수 있습니다. UICollectionViewController는 UICollectionViewDelegate를 채택했는데, UICollectionViewDelegate는 UIScrollViewDelegate를 채택합니다. 그래서 만약 UICollectionVIewController 기반 class를 사용할 경우 UIScrollViewDelegate를 추가적으로 채택하지 않아도 오버라이드를 통해 사용할 수 있게 됩니다:]
(이미지 캐싱은 지금 적용 중 이어서,, 양해 부탁드립니다.)
지금 제 피드에선 스크롤을 할 때 드래그 속도에 따라 훅 아래로 내려갑니다. (감속률 적용x), 특정 cell 포커싱 없이 스크롤이 완료되는 시점에 화면이 멈추는데, 포스트와 포스트 사이에 멈출 때가 많습니다. 인스타그램 동영상 보는 파트는 한번의 짧은 스크롤 후에 다음 동영상이 재생되듯, 이를 포스트에 적용시켜 짧은 드래그 한번 할 때, owner의 프로필이 담긴 포스트로 cell의 포커싱을 적용하려고 합니다.
class FeedController: UICollectionViewController, FeedViewModelConvenience {
...
}
제 피드 컨트롤러는 UICollectionViewController 타입이고, 이는 UIScrollView를 적용한 collectionView, UICollectionViewDelegate가 채택한 UIScrollViewDelegate가 있기에 스크롤 뷰 델리게이트 따로 적용하지 않아도 해당 델리게이트 메서드 오버라이딩 + 스크롤이 가능합니다.
스크롤 뷰의 드래깅이 끝나기 전에!!! 위 함수를 호출합니다. 이 함수를 통해 드래그 후의 특정 cell이 보여지는 위치를 계산한다면 !! 해당 지점으로 screen에서 cell의 포커싱이 되어 보여집니다. 드래그가 끝나기 직전에 호출되기 때문에, next item는 screen의 어느 영역에 위치해야 할지 point를 계산해야 합니다. 즉, target content offset를 제공해서 다음 cell이 보여져야 할 위치가 정확히 어딘지 collectionView가 인지해야 합니다. 그러고 나서, 스크롤 에니메이션은 멈추게 되며 원하는 cell이 계산된 특정 위치에서 보여집니다.
이 함수를 오버라이딩해서 부가적인 계산을 한다면 Carousel effect를 적용할 수 있습니다.
override func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
//1.
guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout,
let navHeight = navigationController?.navigationBar.frame.height else {
return
}
//2.
let profileArea = 8 + 40 + 8
let cellHeightPlusLineSpacing = CGFloat(layout.itemSize.height) + layout.minimumLineSpacing
//3.
var offset = targetContentOffset.pointee
//4.
let idx = (offset.y + scrollView.contentInset.top)/cellHeightPlusLineSpacing
//5.
targetContentOffset.pointee = CGPoint(
x: 0,
y: Int(round(idx) * cellHeightPlusLineSpacing - navHeight) - profileArea)
}
1. collectionView에서 layout object를 가져온다. + 제 경우엔 아래로 스크롤 해도 네비게이션을 사라지지 않도록 해서 네비게이션 높이 정보도 가져옵니다.
2. 스크롤 되는 방향에 대해, 특정 cell의 size와 line간 minimum spacing의 길이를 포함해 측정합니다. 이를 기반으로 prev cell, next cell위치를 결정지을 수 있습니다. 만약 이때
collectionView(_:layout:sizeForItemAt:) 이 메서드로 각각의 cell size를 동적으로 지정했다면 음.. 고민을 좀 해봐야겠습니다. 일단 저는 cell의 사이즈가 동일합니다. (나중에 동영상도 피드에 추가할 예정이라 dynamic한 cell height을 설정할 건데,, 그전에 고민좀 더 해봐야겠습니다.)
3. targetContentOffset 매개변수를 통해 어디에 스크롤 중인 애니메이션(스크롤이 진행중이기에)이 끝나는지점인지를 조정해야 합니다. 그리고 offset.y를 통해 y좌표가 얼마나 이동했는지를 알 수 있습니다.
4. scrollView 안에서 얼마나 y좌표로 scroll됬는지는 offset.y로 알 수 있으니 이젠 스크롤 뷰에 있을 contentInset.top 크기(전 없습니다.) 에서 한 cell의 size + line간 minimum spacing//2에서 구한 cellHeightPlusLineSpacing을 빼면 특정 cell의 index가 보여지게 됩니다. (음.. 근데 만약 맨 처음에 헤더가 있다면 이 또 고민해봐야겠네요..) 이를 통해서 어느 cell이 전체 스크롤 범위 중에서 현재 스크린에 scrolling되고 있는지 보여지는 특정 cell의 index를 구할 수 있습니다!!
5. 절반을 넘었다면? 반올림 round() func를 통해 다음 index * 해당 cell의 높이를 곱하는데!!! 저는 여기서 기본적으로 navigationBar를 없애지 않았음으로 이 길이만큼 빼주고, 또 프로필이 가려지기 때문에 프로필 영역 크기만큼 빼줬습니다. minimum line spacing, minimum interitem spacing을 설정했다면 각각의 경우에 대해서 계산을 해서 최적의 next cell's content offset 해야합니다.
화면 스크롤 속도를 빠르게 하려면,
collectionView.decelerationRate = .fast
이 설정을 해주셔야 합니다. 저는 하지 않았습니다.
오호,, 다른 화면 구성할 땐 에니메이션도 넣고, dynamic한 cell의 height일 때 어떻게 적용해야 할지 고민좀 해봐야겠습니다..
전체 코드는 이 링크 맨 마지막에 있습니다.
다음 글 : horizontal + animation + carousel effect 포스트 링크
References:
https://www.youtube.com/watch?v=_d-xZv0JrRE