- 📱 Smooth and elegant transition from a cell to fullscreen.
- đź”™ Drag to dismiss: return back to the original cell by dragging or tapping background.
- 🎯 Smooth and intuitive interactions, inspired by transitions seen in apps like Instagram and Netflix
- đź§± Fully compatible with
UICollectionViewbuilt using Compositional Layout. - ⚙️ Simple and customizable via
WispConfiguration.
| Intuitive Drag Interaction | Tap to Dismiss |
|---|---|
![]() |
![]() |
This library supports installation via Swift Package Manager:
- Open your Xcode project.
- Go to File > Add Package Dependencies...
- Enter the package URL:
https://github.com/WispKit/Wisp.git - Select the version and add it to your target.
WispCompositionalLayout is designed to work almost identically to UICollectionViewCompositionalLayout.
You can use the same APIs you already know — the only difference is that you call them through .wisp.make.
That means every factory method available on UICollectionViewCompositionalLayout
(e.g. init(section:), init(sectionProvider:), or list(using:))
has its Wisp equivalent:
@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) -> WispCompositionalLayoutSo instead of calling UIKit’s initializer directly, you can simply use the .wisp.make(...) syntax:
// Multi-section layout
let layout = UICollectionViewCompositionalLayout.wisp.make { sectionIndex, layoutEnvironment in
// return your SectionProvider here
}
// Single-section layout
let simpleLayout = UICollectionViewCompositionalLayout.wisp.make {
// return your NSCollectionLayoutSection here
}
// List layout
let listLayout = UICollectionViewCompositionalLayout.wisp.make.list(using: .plain)WispableCollectionView is just like UICollectionView, but it takes a WispCompositionalLayout instead of UICollectionViewLayout.
Once you have your layout, pass it to WispableCollectionView:
let myCollectionView = WispableCollectionView(
frame: .zero,
collectionViewLayout: layout
)Or inline it:
let myCollectionView = WispableCollectionView(
frame: .zero,
collectionViewLayout: .wisp.make {
// return your NSCollectionLayoutSection here
}
)You can also build with list layout easily:
let myListView = WispableCollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewCompositionalLayout.wisp.list(using: .plain)
)or you can simplify like this:
let myListView = WispableCollectionView(frame: .zero, collectionViewLayout: .wisp.list(using: .plain))No extra delegates or boilerplate needed.
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)
// ⚠️ Note: The collection view must be a subview of the presenting view controller.
}
// ...
}By default, a wisp-presented view controller can be dismissed with a drag gesture (pan gesture) or by tapping the background. However, if you want to dismiss explicitly at a specific moment in your code, you can call the public API:
func dismiss(
to indexPath: IndexPath? = nil,
animated: Bool = true
)// Inside the presented view controller
self.wisp.dismiss(animated: true)If indexPath is nil, the view will try to use the original indexPath used at the time of presentation. If you want the view to dismiss to a different indexPath, just provide it in the to parameter.
Example:
// Dismiss to a different cell than the one originally presented from
self.wisp.dismiss(to: IndexPath(item: 5, section: 0), animated: true)Wisp provides a delegate so you can detect when a card restoration (returning animation) begins and ends.
This is useful because the restoring animation is not part of the actual view controller’s lifecycle —
the view controller is already dismissed when the card starts restoring.
You can set the delegate from the presenting view controller:
import Wisp
import UIKit
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
wisp.delegate = self
}
}
extension MyViewController: WispPresenterDelegate {
func wispWillRestore() {
print("Restoring will begin.")
}
func wispDidRestore() {
print("Restoring completed.")
}
}When a Wisp-presented view controller is dismissed (via drag, tap, or programmatically), the restoring animation is handled internally by a captured snapshot view, not by the dismissed view controller itself. Therefore, UIKit’s lifecycle methods such as viewWillAppear or viewDidDisappear won’t notify you of this transition. Instead, you can rely on these two delegate methods:
wispWillRestore(): called when the card restoration beginswispDidRestore(): called when the restoration animation finishes
You can use this delegate to coordinate updates with your collection view or perform custom UI changes.
- Familiar API, just like UICollectionView
- Simple creation of custom or list layouts
- Smooth presentation with zero hassle
This repository includes a fully functional example app, WispExample, to demonstrate the features of the Wisp library. You can run it to see live examples of different transition styles and configurations.
- Clone the repository to your local machine.
- Open
Wisp.xcworkspacein Xcode. - Select the
WispExamplescheme. - Build and run on your preferred simulator or a physical device.
WispConfiguration allows you to tweak the animation and layout behavior.
From version 1.3.0, WispConfiguration has been refactored to use a DSL-based configuration style for better readability, maintainability, and future extensibility.
For details, see the WispConfiguration DSL Guide.
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
}
}All properties are optional and have default values.
For example, Use presentedAreaInset to customize the width and height of each card presented.
| fullscreen | formSheet style | card | small pop up |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
- iOS: 15.0+
- swift compiler: 5.9+
- UIKit
- Compositional Layout
MIT License. See LICENSE for more information.





