티스토리 뷰

 

 

여러 앱이나 웹에 위와 같은 배너가 사용되고 있고, 오픈 소스 라이브러리도 많이 있다.

 

개인적으로 UI가 이쁘다고 생각해 프로젝트에 적용도 해볼 겸 UICollectionView를 이용해 직접 구현해봤다.

이전 글인 iOS) UICollectionView Programmatically에서 만든 CollectionView를 사용했다.

 

구현해야 할 내용을 간단하게 정리해 보자면 다섯 가지가 있다.

  • 셀 Size, Inset 조절
  • 스크롤 시 셀이 가운데 위치
  • 페이징
  • 일정 시간마다 자동으로 스크롤
  • 중앙에 위치하는 아이템의 사이즈를 조절

 

먼저 Cell의 Size와 을 조절해 Cell이 카드처럼 보이도록 만들어보자.

 

1. Cell Size, Inset 조절

Cell Size와 Inset은 UICollectionViewDelegateFlowLayout에서 설정한다.

///let cellWidthMultiplier: CGFloat = 0.7
///let cellHeightMultiplier: CGFloat = 0.8
///let minimumLineSpacing: CGFloat = 20

/// UICollectionViewDelegateFlowLayout
/// Cell Size
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
    let width: CGFloat = collectionView.frame.size.width * cellWidthMultiplier
    let height: CGFloat = collectionView.frame.size.height * cellHeightMultiplier
    
    let size: CGSize = CGSize(width: width, height: height)
   
    return size
}

/// MinimumLineSpacing
/// Cell간 최소 간격
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return minimumLineSpacing
}

/// Inset
/// Section의 Contents 여백
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    
    // 첫번째 Cell이 중앙에 오도록 설정하자.
    let width: CGFloat = collectionView.frame.size.width * cellWidthMultiplier
    
    let insetX = (view.frame.size.width - width) / 2.0
    
    let inset = UIEdgeInsets(top: 0, left: insetX, bottom: 0, right: insetX)
    
    return inset
}

sizeForItemAt에서 사이즈 조절

minimumLineSpacingForSectionAt에서 cell 간 간격을 설정해 주었다.

 

그리고 완성본과 같이 첫 번째와 마지막 Cell을 화면의 중간에 위치하도록 만들기 위해 왼쪽과 오른쪽에 Inset을 추가했다.

(가로 전체 길이 - Cell Width)를 하면 가운데 Cell을 제외하고 남는 공간의 길이를 알 수 있는데 그 길이를 절반 나눠서 왼쪽과 오른쪽에 Inset을 추가했다. 

 

여기까지 하면 아이템이 화면 가운데부터 시작하는 CollectionView가 만들어져 있을 것이다.

 

 

다음으로 스크롤을 하면 아이템이 가운데 위치하게 만들어보자.

 

2.  스크롤시 셀이 가운데 위치

정확하게 말하자면 스크롤이 끝날 때 아이템이 CollectionView의 가운데 위치하도록 만들어야 한다.

UIScrollViewDelegate의 ScrollViewWillEndDragging 메서드를 이용하자. 이 메서드는 드래그가 끝날 때 호출된다.

드래그가 끝날때 targetContentOffset의 위치를 조절해서 구현해보자.

 

먼저 코드이다.

/// ScrollViewWillEndDragging

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    
    // Cell의 너비
    let width: CGFloat = collectionView.frame.size.width * cellWidthMultiplier
    
    // Cell간 간격을 포함한 Cell의 너비
    let cellWidthIncludingSpacing = width + minimumLineSpacing
    
    // 스크롤 정지시 예상되는 위치
    var offset = targetContentOffset.pointee
    
    // 스크롤 정지시 예상되는 x축 위치와 스크롤뷰의 좌측 Inset을 더한 값을
    // Cell간 간격을 포함한 Cell의 너비로 나눈다.
    // 그 값이 index
    let index = (offset.x + scrollView.contentInset.left) / cellWidthIncludingSpacing
    
    // Index 값을 반올림한다.
    var roundedIndex = round(index)
    
    // 스크롤 방향을 체크해 올림, 내림을 정한다.
    if scrollView.contentOffset.x > targetContentOffset.pointee.x {
        roundedIndex = floor(index)
    } else {
        roundedIndex = ceil(index)
    }
     
    // 새로운 OffSet을 설정한다.
    // roundedIndex값에 cellWidthIncludingSpacing값을 곱하고 scrollView의 좌측 Inset만큼 빼준다.
    // (Rounded Index 번째의 Cell의 x축을 알기 위함)
    offset = CGPoint(x: roundedIndex * cellWidthIncludingSpacing - scrollView.contentInset.left, y: scrollView.contentInset.top)
    
    // Offset 적용
    targetContentOffset.pointee = offset
}

먼저 스크롤 정지 시 예상되는 위치와 스크롤 뷰의 왼쪽 Inset을 더한 값이 아이템이 있을 위치이다. 그 값을 (아이템의 크기+셀간 간격)으로 나누면 대략 몇 번째 아이템이 와야 하는지 알 수 있다. 이 값을 index라 한다.

 

정확히 몇 번째 Item인지 확인하기 위해 index값을 반올림해준다.→ roundedIndex 

 

이 roundedIndex 값으로도 offset을 조절해줄 수 있지만 

좀 더 정확하게 스크롤 방향까지 체크해주자.

 

scrollView.contentOffset.x는 현재 scrollView의 offset이고 targetContentOffset.pointee.x는 스크롤이 멈췄을 때 예상되는 offset이다.

 

만약 왼쪽으로 스크롤 중이라면 targetContentOffset.pointee.x가 scrollView.contentOffset.x보다 작을 것이다. 그럼 roundedIndex를 floor(내림)해준다.

반대로 오른쪽으로 스크롤 중이라면 ceil(올림)해서 스크롤 방향에 따라 보다 정확하게 몇 번째 아이템이 중앙에 위치해야 하는지 결정해 주었다.

 

스크롤이 끝날 때 몇 번째 아이템(= roundedIndex)이 위치할지 정했다면 그에 맞게 스크롤이 멈췄을 때 offset (=targetContentOffset)을 설정해주자.

offset = CGPoint(x: roundedIndex * cellWidthIncludingSpacing - scrollView.contentInset.left, y: scrollView.contentInset.top)
targetContentOffset.pointee = offset

직접 그려보면서 하거나 offset을 print 해서 확인해 보면 더 이해가 쉽다.

 

 

3. 페이징

위에서 roundedIndex를 업데이트해주는 방법을 아래 코드로 바꿔주자.

//var currentIndex: CGFloat = 0 // 현재 CollectionView의 페이지 인덱스


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)
}

// 한 페이지씩 스크롤 제한
if currentIndex > roundedIndex {
    currentIndex -= 1
    roundedIndex = currentIndex
} else if currentIndex < roundedIndex {
    currentIndex += 1
    roundedIndex = currentIndex
}

currentIndex와 roundedIndex를 비교해줘서 한 페이지씩 이동하도록 제한했다.

currentIndex는 현재 페이지이고 roundedIndex는 이동할 페이지로 현재 페이지가 이동할 페이지보다 크면 -1로 한 페이지만 이동하도록 해주었다.

 

 

4. 일정 시간마다 자동으로 스크롤

Timer를 이용해 일정 시간마다 moveToNextPage()를 호출하자.

사용자가 스크롤할 수도 있기에 위에서도 사용했던 currentIndex 변수를 이용해 현재 페이지를 기준으로 스크롤되도록 해야 한다.

 

코드를 보자

///let minimumLineSpacing: CGFloat = 20
///var currentIndex: CGFloat = 0 // 현재 CollectionView의 페이지 인덱스

func moveToNextPage() {
    // Cell의 너비
    let width: CGFloat = collectionView.frame.size.width * cellWidthMultiplier
    let height: CGFloat = collectionView.frame.size.height * cellHeightMultiplier

    // Cell간 간격을 포함한 Cell의 너비
    let cellWidthIncludingSpacing = width + minimumLineSpacing * 2
    
    // Inset
    let insetX = (view.frame.size.width - width) / 2.0
    
    // ContentOffset: 현재 스크롤 위치
    let contentOffset = collectionView.contentOffset
    
    // if - 마지막 페이지가 아닌 경우
    if Int(currentIndex) != viewModels.count - 1 {
        collectionView.scrollRectToVisible(CGRectMake(contentOffset.x + cellWidthIncludingSpacing + insetX, contentOffset.y, cellWidthIncludingSpacing, height ), animated: true)
        
        currentIndex += 1
    }
    
    // else - 마지막 페이지인 경우
    else {
        collectionView.scrollRectToVisible(CGRectMake( 0, contentOffset.y, cellWidthIncludingSpacing, height), animated: true)
        
        currentIndex = 0
    }
}

func pagingTimer() {
    let _: Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { Timer in
        self.moveToNextPage()
    }
}

collectionView를 스크롤시키기 위해 scrollRectToVisible 메서드를 사용했다. 

 

마지막 페이지가 아닌 경우에는 현재 스크롤 뷰의 offset에 cell 간 간격을 포함한 cell의 너비와 Inset을 더해 다음 페이지의 위치를 구한 후 CGRectMake로 다음 페이지에 해당하는 CGRect를 생성한다. 

 

그리고 scrollRectToVisible을 이용해 생성한 CGRect로 이동시킨다.

 

마지막 페이지인 경우에는 x좌표를 0으로 이동시켜 첫 번째 페이지로 돌아가도록 해주자.

 

추가로 꼭 currentIndex를 업데이트 해주자.

5. 중앙에 위치하는 아이템의 사이즈를 조절

cell에 Animation을 추가해서 구현하자.

 

적용할 Animation은 다음과 같다.

extension ViewController {
    func applyAnimation(cell: UICollectionViewCell) {
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            options: .curveEaseOut,
            animations: {
                cell.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        },
            completion: nil
        )
    }
    
    func removeAnimation(cell: UICollectionViewCell) {
        UIView.animate(
                    withDuration: 0.4,
                    delay: 0,
                    options: .curveEaseOut,
                    animations: {
                        cell.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
                },
                    completion: nil)
    }
}

applyAnimation()으로 중앙의 cell의 스케일을 키워주고 다음 cell로 넘어갈 때 원래 크기로 돌려주자.

 

scrollViewDidScroll() 메서드를 이용해 스크롤이 될 때마다 적용되도록 만들어주자.

// var previousIndex = 0

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // Cell의 너비
    let width: CGFloat = collectionView.frame.size.width * cellWidthMultiplier
    
    // Cell간 간격을 포함한 Cell의 너비
    let cellWidthIncludingSpacing = width + minimumLineSpacing
    
    let offsetX = collectionView.contentOffset.x
    let index = (offsetX + collectionView.contentInset.left) / cellWidthIncludingSpacing
    let roundedIndex = round(index)
    let indexPath = IndexPath(item: Int(roundedIndex), section: 0)
    
    if let cell = collectionView.cellForItem(at: indexPath) {
        applyAnimation(cell: cell)
    }
    
    if Int(roundedIndex) != previousIndex {
        let preIndexPath = IndexPath(item: previousIndex, section: 0)
        if let preCell = collectionView.cellForItem(at: preIndexPath) {
            removeAnimation(cell: preCell)
        }
        previousIndex = indexPath.item
    }
}

이번에도 roundedIndex를 이용한다.

roundedIndex로 indexPath를 생성해 현재 Cell을 구해주자.

그리고 Cell에 애니메이션 적용!

 

그리고 페이지가 넘어갈 때 다시 원래 상태로 돌려줘야 한다.

previousIndex 변수를 이용해 넘어간 cell을 구해준 뒤 removeAnimation()을 적용해주자.

 

다만 여기까지 하고 프로젝트를 실행해보면 첫 번째 Cell에 applyAnimation이 적용되지 않는다. 

왜냐하면 scrollViewDidScroll()이 호출되지 않았기 때문이다.

 

scroll 되기 전에 첫 번째 cell에 애니메이션 효과를 적용해주기 위해 이것저것 찾아보다가 UICollectionViewDelegate 메서드에서 willDisplaycell이라는 메서드를 발견했다.

 

willDisplayCell을 이용해 cell의 스케일을 미리 키워주었다.

// 첫번째 Item 크기 조절
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

    if indexPath.row == 0 {
        applyAnimation(cell: cell)
    }
    print(indexPath.row)
}

추가로 코드를 조금 수정하면 scrollViewDidScroll() 말고 willDisplayCell()에서 애니메이션을 호출해도 된다.

 

 

끝..!

전체 코드는 Github에 있습니다.


References 

 

collectionView paging 해보기!

collectionView에 간단히 paging을 설정할 수 있는 프로퍼티가 있다. collectionView.isPagingEnabled = true 위 코드와 같이 간단히 가능하다. 하지만 이 코드로는 상하좌우에 여백이 생기면 스크롤 시 어그러지

jintaewoo.tistory.com

 

[Swift] - Simple Carousel Effect CollectionView With Animation

최근에 애니메이션에 관심이 많아져서 계속 애니메이션 관련글만 올리고있네요 ㅎㅎ 오늘 구현해볼 애니메이션이에요 대표적으로 음악앱을보면 앨범을 띄울때 이런 방식으로 띄우는걸 볼수있

nsios.tistory.com

https://www.youtube.com/watch?v=vB-HKnhOgl8 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함