본문 바로가기
이모저모/UIKit

커스텀 이미지 피커 뷰 만들기 #2

by ARpple 2023. 12. 2.

커스텀 이미지 피커 뷰 화면 애니메이션

주요 구현 View 3가지

1. 사용자 이미지 목록 View

  • 이미지 썸네일 클릭시 클릭 순서에 맞게 데이터를 보관할 필요가 있다.
  • 살짝 이미지를 어둡게 가리고 순서에 맞는 숫자를 보여줄 필요가 있다.

2. 선택한 이미지 목록 View

  • 선택한 이미지를 순서에 맞게 보여줄 필요가 있다.
  • 클릭하면 이미지를 삭제시키고 보관한 데이터를 변경해 줄 필요가 있다.

3. 사용자 앨범 목록 View

 

  • 네이게이션 타이틀을 클릭하면 아래로 내려왔다가 다시 올라가는 화면 이동을 보여줘야한다.

이미지 클릭 시, 선택한 이미지 목록 View 올라오게 하기

  • 선택한 이미지가 존재하면 선택한 이미지 목록 View 위로 올리는 애니메이션, 없으면 아래로 내리는 애니메이션
  • 사용자 이미지 목록 View의 Constraint 변경

구현 예시

예시 코드 프로퍼티, 메서드 정의

  • collectionView ⇒ 사용자 이미지 목록 View
  • pinCreatingBottomView ⇒ 선택한 이미지 목록 View
  • albumSelector ⇒ 사용자 앨범 목록 View
  • func configureConstraints(){} ⇒ Constraint 설정하는 메서드, viewDidLoad에서 가장 먼저 실행

Constraint 관련 코드

final class CreatingPinVC:UIViewController{
        var pinConstraint:Constraint?
    var bottomConstraint: Constraint?
        ...
        func configureConstraints() {
            ...
            pinCreatingBottomView.snp.makeConstraints { make in
            make.horizontalEdges.equalTo(view.safeAreaLayoutGuide)
            make.height.equalTo(88)
            make.bottom.equalTo(view.safeAreaLayoutGuide)
        }
            collectionView.snp.makeConstraints { make in
            make.top.horizontalEdges.equalTo(view.safeAreaLayoutGuide)
            self.pinConstraint = make.bottom.equalTo(pinCreatingBottomView.snp.top).constraint
            self.bottomConstraint = make.bottom.equalTo(view.safeAreaLayoutGuide).constraint
        }
            ...
            pinConstraint?.deactivate()
        bottomConstraint?.activate()
        }
        ...
}

미리 사용자 이미지 목록 View의 Constraint를 두개를 만들고 저장변수에 담아둔다.

애니메이션 로직 코드

final class CreatingPinVC: UIViewController{
...
    func imageSelectorDrawer(isEmpty: Bool){
        let height = self.pinCreatingBottomView.bounds.height + 32
        pinCreatingBottomView.transform = .init(translationX: 0, y: isEmpty ? height : 0)
    }
...
}
  • 이미지 존재하지 않으면 뷰를 아래로 내린다.

애니메이션 적용 코드

final class CreatingPinVC: UIViewController{
	...
    override func viewDidLoad() {
        super.viewDidLoad()
        configureConstraints()
        ...
        vm.selectedAlbums.map{$0.isEmpty}.debounce(.milliseconds(200), scheduler: MainScheduler.instance).bind(with: self) { owner, isEmpty in
            Task{@MainActor in
                if isEmpty{
                    owner.pinConstraint?.deactivate()
                    owner.bottomConstraint?.activate()
                    owner.collectionView.layoutIfNeeded()
                    try await Task.sleep(for: .seconds(0.2))
                }
                UIView.animate(withDuration: 0.3) {
                    owner.imageSelectorDrawer(isEmpty: isEmpty)
                }completion: { _ in
                    if !isEmpty{
                        owner.pinConstraint?.activate()
                        owner.bottomConstraint?.deactivate()
                        owner.collectionView.layoutIfNeeded()

                    }
                }
            }
        }.disposed(by: disposeBag)
        ...
    }
}
  1. 선택한 사진이 존재하는지 유무를 Bool 처리
  2. debounce 처리로 유저 액션 중첩 발생 처리
  3. 선택한 사진이 존재하는지에 따른 로직은 2가지가 존재
    • 선택한 사진이 없는 경우:
    • 사용자 이미지 목록 View에 BottomConstraint를 뷰 컨트롤러 하단으로 변경하는 작업 후 애니메이션 작동
    • 선택한 사진이 존재하는 경우:
    • 애니메이션 작동 후 사용자 이미지 목록 View에 BottomConstraint를 선택한 이미지 목록 View 상단으로 변경하는 작업
⚠️ 애니메이션 작동에 따라 Constraint 변경 작업 시기를 다르게 배치해서 메인 쓰레드 충돌을 없앤다.

+ 이미지가 비어있을 때, 선택한 이미지 목록 View DiffableDataSource 처리

final class BottomDataSource:UICollectionViewDiffableDataSource<String,AlbumItem.ID>{
...
    @MainActor func resetSnapshot(albumIDs:[AlbumItem.ID]){
            var snapshot = NSDiffableDataSourceSnapshot<String,AlbumItem.ID>()
            snapshot.appendSections(["hello"])
            snapshot.appendItems(albumIDs)
            Task{@MainActor in
                if albumIDs.isEmpty{
                    try await Task.sleep(for: .seconds(0.66))
                }
                await apply(snapshot,animatingDifferences: true)
            }
        }
...
}
💡 위에서 처리한 선택한 이미지 목록 View가 아래로 내려가는 Animation 작동 후, 셀 아이템이 사라져야 한다.
그렇기 때문에 일정 시간이 지나고 비어있는 Snapshot을 apply한다.

사용자 앨범 목록 View

애니메이션 로직 코드

final class CreatingPinVC: UIViewController{
...
    func albumSelectorDrawer(isOpen: Bool){
        let bounds = albumSelector.bounds
        let height = bounds.height
        let ny = bounds.origin.y
        albumSelector.bounds = .init(origin: bounds.origin, size: .init(width: bounds.width, height: height - ny))
        albumSelector.transform = .init(translationX: 0, y: isOpen ? 0: -(height - ny))
  }
}

애니메이션 적용 코드

final class CreatingPinVC: UIViewController{
	...
        override func viewDidLoad() {
        super.viewDidLoad()
         ...
        vm.openAlbumSelector.debounce(.milliseconds(200), scheduler: MainScheduler.instance).bind(with: self) { owner, val in
           UIView.animate(withDuration: 0.33) {
              	owner.albumSelectorDrawer(isOpen: val)
           		}
        	}.disposed(by: disposeBag)
            ...
        }
	...
}

초기 애니메이션 진행 막기

발생하는 이유..?

초기값을 isOpen = false, isEmpty = true로 설정해도 present 직후 animation 작동이 완료되지 않음…

👉 viewDidAppear에서 일정 시간이 지나서 선택한 이미지 목록 View, 사용자 앨범 목록 View의 Hidden 처리를 false로 바꾼다.

final class CreatingPinVC: UIViewController{
...
        override func viewDidLoad() {
        ...
        pinCreatingBottomView.isHidden = true
        albumSelector.isHidden = true
        ...
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Task{@MainActor in
            try await Task.sleep(for: .seconds(0.2))
            pinCreatingBottomView.isHidden = false
            albumSelector.isHidden = false
        }
    }
}

댓글