UICollectionView에서 사용할 수 있는 카드 형식의 커스텀 트랜지션을 지원하는 라이브러리입니다.Compositional Layout 기반의 컬렉션 뷰에서 사용 가능WispConfiguration을 통해 커스터마이징 가능| 직관적인 Dismiss 인터페이스 | 배경을 탭하여 Dismiss 가능 |
|:–:|:–:|
| |
|
—
Wisp는 Swift Package Manager를 통해 설치할 수 있습니다:
https://github.com/WispKit/Wisp.gitTarget에 추가WispCompositionalLayout은 UICollectionViewCompositionalLayout과 거의 동일한 방식으로 동작합니다.
이미 알고 있는 UIKit의 API를 그대로 사용할 수 있으며, .wisp.make를 사용하여 생성합니다.
즉, UICollectionViewCompositionalLayout에서 사용할 수 있는 모든 팩토리 메서드
(init(section:), init(sectionProvider:), list(using:) 등)는
Wisp에서도 동일하게 제공됩니다:
@MainActor
func make(section: NSCollectionLayoutSection) -> WispCompositionalLayout
@MainActor
func make(
section: NSCollectionLayoutSection,
configuration: UICollectionViewCompositionalLayoutConfiguration
) -> WispCompositionalLayout
@MainActor
func make(
sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider
) -> WispCompositionalLayout
@MainActor
func make(
sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider,
configuration: UICollectionViewCompositionalLayoutConfiguration
) -> WispCompositionalLayout
@MainActor
func list(using configuration: UICollectionLayoutListConfiguration) -> WispCompositionalLayout
UIKit의 이니셜라이저를 직접 호출하는 대신 .wisp.make(…) 문법을 사용하면 됩니다:
// 멀티 섹션 레이아웃
let layout = UICollectionViewCompositionalLayout.wisp.make { sectionIndex, layoutEnvironment in
// 여기서 SectionProvider를 반환하세요.
}
// 단일 섹션 레이아웃
let simpleLayout = UICollectionViewCompositionalLayout.wisp.make {
// 여기서 NSCollectionLayoutSection을 반환하세요.
}
// 리스트 레이아웃
let listLayout = UICollectionViewCompositionalLayout.wisp.list(using: .plain)
이 방식을 사용하면 기존 UICollectionViewCompositionalLayout 코드를 거의 수정하지 않고 그대로 재사용할 수 있습니다.
생성 구문(.wisp.make { … })만 변경하여 사용할 수 있습니다.
WispableCollectionView는 기본적으로 UICollectionView와 동일하지만,
UICollectionViewLayout 대신 WispCompositionalLayout을 받습니다.
생성된 레이아웃을 그대로 전달하면 됩니다:
let myCollectionView = WispableCollectionView(
frame: .zero,
collectionViewLayout: layout
)
또는 한 줄로 간단히 작성할 수도 있습니다:
let myCollectionView = WispableCollectionView(
frame: .zero,
collectionViewLayout: .wisp.make {
// NSCollectionLayoutSection을 반환하세요.
}
)
리스트 형태의 레이아웃을 사용하는 경우:
let myListView = WispableCollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewCompositionalLayout.wisp.list(using: .plain)
)
// 위 코드는 다음과 같이 한 줄로 간단하게 작성할 수도 있습니다.
let myListView = WispableCollectionView(frame: .zero, collectionViewLayout: .wisp.list(using: .plain))
별도의 복잡한 커스텀 트랜지션 설정 없이 바로 사용할 수 있습니다.
class MyViewController: UIViewController, UICollectionViewDelegate {
// ...
let myCollectionView = WispableCollectionView(
frame: .zero,
collectionViewLayout: .wisp.make { ... }
)
// ...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let secondVC = MyViewController()
wisp.present(secondVC, collectionView: myCollectionView, at: indexPath)
// ⚠️ 주의: collectionView는 반드시 현재 ViewController의 하위 뷰로 포함되어 있어야 합니다.
}
// ...
}
기본적으로 wisp로 present된 뷰컨트롤러는 드래그 제스처(pan gesture) 또는 배경을 탭하는 것으로 dismiss할 수 있습니다. 하지만, 개발자가 원하는 시점에 명시적으로 dismiss를 하고 싶다면, 다음과 같이 public API를 호출할 수 있습니다:
func dismiss(
to indexPath: IndexPath? = nil,
animated: Bool = true
)
// present된 뷰컨트롤러 내부에서 스스로 dismiss하는 경우
self.wisp.dismiss(animated: true)
indexPath가 nil인 경우, 처음 present될 때 사용한 원래 indexPath로 wisp가 dismiss를 시도합니다. dismiss 시점에 다른 indexPath로 되돌아가야 한다면, to 매개변수에 원하는 indexPath를 넣어주면 됩니다.
예시:
// 처음 present된 셀과는 다른 셀로 dismiss하기
self.wisp.dismiss(to: IndexPath(item: 5, section: 0), animated: true)
Wisp는 카드가 원래 셀로 되돌아가는 복원(restoring) 애니메이션의 시작과 끝 시점을 감지할 수 있도록 delegate를 제공합니다.
이 복원 애니메이션은 실제 뷰 컨트롤러의 생명주기(lifecycle)와는 별개로 동작합니다.
왜냐하면 카드가 복원을 시작하는 시점에는 이미 해당 뷰 컨트롤러가 dismiss되었기 때문입니다.
delegate는 presenting view controller 쪽에서 설정할 수 있습니다:
import Wisp
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
wisp.delegate = self
}
}
extension MyViewController: WispPresenterDelegate {
func wispWillRestore() {
print("복원이 시작됩니다.")
}
func wispDidRestore() {
print("복원이 완료되었습니다.")
}
}
Wisp로 present된 뷰 컨트롤러가 드래그, 탭 또는 코드로 dismiss될 때, 복원 애니메이션은 실제 뷰 컨트롤러가 아니라 캡처된 스냅샷 뷰에 의해 처리됩니다. 따라서 UIKit의 생명주기 메서드(viewWillAppear, viewDidDisappear 등)에서는 이 복원 시점을 감지할 수 없습니다.
대신 Wisp의 delegate 메서드를 통해 다음과 같은 시점을 알 수 있습니다:
이 delegate를 이용하면 collection view의 상태를 동기화하거나, 복원 시점에 맞춰 커스텀 UI 변경을 수행할 수 있습니다.
이 저장소에는 Wisp 라이브러리의 다양한 기능을 보여주는 예제 앱 WispExample이 포함되어 있습니다. 앱을 직접 실행하여 다양한 트랜지션 스타일과 설정을 실제로 확인할 수 있습니다.
Wisp.xcworkspace 파일을 엽니다.WispExample 스킴(Scheme)을 선택합니다.만약 애니메이션 속도, 펼쳐질 카드의 크기나 corner radius 등을 변경하고 싶다면
WispConfiguration을 통해 커스텀 설정을 할 수 있습니다.
버전 1.3.0부터 WispConfiguration이 DSL 기반 구성 방식으로 리팩토링되었습니다.
이로 인해 코드 가독성, 유지보수성, 확장성이 개선되었습니다.
WispConfiguration에 대한 자세한 내용은 WispConfiguration DSL 가이드를 참고하세요.
let configuration = WispConfiguration { config in
// Animation configuration
config.setAnimation { animation in
animation.speed = .fast
}
// Gesture configuration
config.setGesture { gesture in
gesture.allowedDirections = [.right, .down]
gesture.dismissByTap = false
}
// Layout configuration
config.setLayout { layout in
layout.presentedAreaInset = inset
layout.initialCornerRadius = 15
layout.finalCornerRadius = 30
}
}
각 속성은 기본값을 가지고 있으므로, 원하는 항목만 설정하시면 됩니다.
예를 들어, presentedAreaInset 값을 사용하면 다음과 같이 카드가 펼쳐질 영역을 커스텀할 수 있습니다.
| fullscreen | formSheet style | card | small pop up |
|---|---|---|---|
MIT License. See LICENSE for more information.