iOS Swipe Transition: Native API for Camera/Safari 'Liquid Glass' Effect
Implement the native iOS swipe transition effect seen in the Camera app's mode switcher or Safari's tab groups. Explore UIKit and SwiftUI approaches for the 'Liquid Glass' interaction.
How can I implement the native iOS swipe transition effect, often described as “Liquid Glass”, similar to the mode switcher in the Camera app or the tab group selector in Safari?
I am looking for the specific native API or implementation approach to replicate the horizontal swipe interaction found in these iOS system apps:
- iOS Camera App: The ‘Mode Switcher’ (e.g., swiping between Photo, Video, Cinematic, etc.).
- Safari: The bottom bar interaction when switching between Tab Groups or Private Browsing.
The objective is to reproduce the exact visual style and swipe interaction, including snapping, fading opacity, and background blending.
Is there a specific UIKit component or SwiftUI modifier designed to achieve this ‘Camera Mode’ transition natively? If no single API exists, what is the standard native method to implement this combination of UI effects?
You can reproduce the iOS swipe transition “Liquid Glass” effect (the Camera app mode switcher and the Safari tab switcher) by combining a centered horizontal scroll (UICollectionView or UIScrollView / SwiftUI ScrollView), an interactive blur (UIVisualEffectView animated via UIViewPropertyAnimator), and per-item transform/alpha driven from the scroll progress. There is no single public “Liquid Glass” API; the native approach is to tie gesture progress to animations (animator.fractionComplete) and use snapping logic (custom flow layout or compositional layout) plus material backgrounds to achieve snapping, fading opacity, and background blending. The recipe below gives concrete UIKit and SwiftUI strategies, sample code, and performance/accessibility notes.
Contents
- Implementation overview
- Core effects: snapping, fading opacity, and background blending
- UIKit Implementation for iOS swipe transition (Camera Mode)
- SwiftUI Approaches for iOS swipe transition (Safari Tab Switcher)
- Example: UIKit code (UICollectionView + UIVisualEffectView)
- Example: SwiftUI code (ScrollView + Material / Blur wrapper)
- Performance, accessibility and tuning tips
- Sources
- Conclusion
Implementation overview
The system Camera and Safari controls are built from primitives rather than a single public control. To replicate them natively you combine three pieces:
- A horizontally scrolling container that reports continuous offsets:
UIScrollView/UICollectionView(UIKit) orScrollView(SwiftUI). Use a centered layout (paging-centered) or custom snapping to get the mode centered when release. See the UICollectionView documentation and UIScrollView documentation. - A blur/material backdrop for the “glass” look:
UIVisualEffectView/UIBlurEffect(UIKit) orMaterial(.ultraThinMaterial etc.) in SwiftUI. These give the frosted, blending background. See UIVisualEffectView documentation and the SwiftUI Material documentation. - Animator-driven, interactive transitions: use
UIViewPropertyAnimatorto animate blur intensity andfractionCompleteto tie the blur to gesture progress, and update per-item transforms/alpha fromscrollViewDidScroll. Refer to UIViewPropertyAnimator documentation.
You connect the scroll progress to visual effects (scale/opacity and blur) to produce the Liquid Glass feel. For full view-controller transitions between modes you can combine this with UIPercentDrivenInteractiveTransition.
Core effects: snapping, fading opacity, and background blending
- Snapping: implement nearest-item snapping with a custom
UICollectionViewFlowLayoutoverridetargetContentOffset(forProposedContentOffset:withScrollingVelocity:)or use a compositional layout with.groupPagingCentered. For simple lists,scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)can also adjust the target offset. - Fading opacity & scale: compute each cell’s distance from the center and map that to a scale and alpha curve (linear or ease curve). Update these transforms in
scrollViewDidScrollfor smooth per-frame feedback. - Background blending (blur intensity): animate a
UIVisualEffectViewusingUIViewPropertyAnimator. Create an animator whose animation block sets the visual effect (e.g.,UIBlurEffect(style: .systemUltraThinMaterial)) and then setanimator.fractionComplete = progressas the user swipes. This lets you smoothly interpolate blur intensity to match the gesture.
How do you compute progress? For fixed-width items use contentOffset / itemWidth to get a page fraction; for variable layouts compute distance between adjacent centers and normalize.
UIKit Implementation for iOS swipe transition (Camera Mode)
Plan:
- Use a horizontally-scrolling
UICollectionViewwith centered cells (customFlowLayoutor compositional layout). - In
scrollViewDidScroll, compute each visible cell’s distance to the center, setcell.transformandcell.alpha. - Prepare a
UIVisualEffectViewwith an associatedUIViewPropertyAnimatorthat animates to the target blur; updateanimator.fractionCompletefrom the scroll progress. - Implement snapping via layout override or
scrollViewWillEndDragging.
Key APIs: UICollectionView, UIScrollViewDelegate, UIVisualEffectView + UIBlurEffect, UIViewPropertyAnimator, optionally UIPercentDrivenInteractiveTransition for view-controller transitions.
Advantages: full control, high fidelity to system, easy to optimize. Drawback: more code than a single built-in control.
SwiftUI Approaches for iOS swipe transition (Safari Tab Switcher)
Two approaches:
-
Pure SwiftUI (simpler, works on iOS 15+):
- Use
ScrollView(.horizontal)+LazyHStack. - Use
GeometryReaderinside each item to compute the item’s center vs. container center and apply.scaleEffectand.opacitybased on that distance. - For the background use
.background(.ultraThinMaterial)or.background(Material.ultraThin). This gives the frosted glass look without bridging to UIKit. - For snapping, use a
DragGestureandScrollViewReaderto scroll to the nearest item on.onEnded— works well but is less precise for continuous interactive blur.
- Use
-
SwiftUI + UIKit bridge (highest fidelity):
- Wrap a
UICollectionVieworUIScrollViewinUIViewRepresentable. Expose content offset to SwiftUI viaBindingorCoordinator. - Wrap a
UIVisualEffectViewwith aUIViewRepresentablethat exposes an animator and aprogressbinding; set itsanimator.fractionCompletefrom the scroll offset to get interactive blur control. - This hybrid approach gives the best match to the Camera/Safari feel.
- Wrap a
SwiftUI helpers: matchedGeometryEffect can help for fluid transitions between selected states, but the continuous mode switcher effect is primarily driven by horizontal scrolling and material background (see matchedGeometryEffect documentation).
Example: UIKit code (UICollectionView + UIVisualEffectView)
// 1) Centering flow layout (snap to center)
class CenteredFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let cv = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let proposedCenterX = proposedContentOffset.x + cv.bounds.width / 2
let rect = CGRect(origin: proposedContentOffset, size: cv.bounds.size)
guard let attrs = layoutAttributesForElements(in: rect) else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
var candidate: UICollectionViewLayoutAttributes?
for a in attrs {
if candidate == nil || abs(a.center.x - proposedCenterX) < abs(candidate!.center.x - proposedCenterX) {
candidate = a
}
}
guard let c = candidate else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let x = c.center.x - cv.bounds.width / 2
return CGPoint(x: x, y: proposedContentOffset.y)
}
}
// 2) In your view controller
class ModeSwitcherViewController: UIViewController, UICollectionViewDelegate {
var collectionView: UICollectionView!
let blurView = UIVisualEffectView(effect: nil)
var blurAnimator: UIViewPropertyAnimator?
override func viewDidLoad() {
super.viewDidLoad()
let layout = CenteredFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 140, height: 44)
layout.minimumLineSpacing = 12
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.decelerationRate = .fast
collectionView.showsHorizontalScrollIndicator = false
collectionView.delegate = self
view.addSubview(collectionView)
// Place blur behind items (or as a background tray)
blurView.frame = view.bounds
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.insertSubview(blurView, belowSubview: collectionView)
// Create animator for interactive blur
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) {
self.blurView.effect = blurEffect
}
blurAnimator?.pausesOnCompletion = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Update per-cell transform + opacity
let centerX = scrollView.contentOffset.x + scrollView.bounds.width / 2
for cell in collectionView.visibleCells {
guard let ip = collectionView.indexPath(for: cell),
let attr = collectionView.layoutAttributesForItem(at: ip) else { continue }
let distance = abs(attr.center.x - centerX)
let maxDistance = collectionView.bounds.width / 2 + attr.size.width
let normalized = min(1, distance / maxDistance)
let scale = 1.0 - 0.12 * normalized
let alpha = 1.0 - 0.5 * normalized
cell.transform = CGAffineTransform(scaleX: scale, y: scale)
cell.alpha = alpha
}
// Animate blur based on fractional progress between pages (assumes fixed item width + spacing)
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let pageWidth = layout.itemSize.width + layout.minimumLineSpacing
let raw = scrollView.contentOffset.x / pageWidth
let frac = abs(raw - round(raw)) // 0.0 at center, up to ~0.5 between centers
blurAnimator?.fractionComplete = CGFloat(min(1.0, frac * 2.0)) // tune multiplier
}
}
}
Notes:
- The
blurAnimatorapproach lets you animate the blur effect interactively; the same technique can animate other properties (color tint, vibrancy). - Tweak item size, spacing and normalization constants to match the exact feel.
Example: SwiftUI code (ScrollView + Material / Blur wrapper)
Simple pure-SwiftUI pattern:
struct ModeSwitcher: View {
let items: [String]
var body: some View {
GeometryReader { geo in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(items.indices, id: \.self) { i in
GeometryReader { itemGeo in
let mid = itemGeo.frame(in: .global).midX
let dist = abs(mid - geo.size.width / 2)
let ratio = min(1, dist / (geo.size.width / 2))
Text(items[i])
.padding(.horizontal, 16).padding(.vertical, 10)
.background(.ultraThinMaterial)
.cornerRadius(10)
.scaleEffect(1 - 0.08 * ratio)
.opacity(1 - 0.45 * ratio)
}
.frame(width: 140, height: 50)
}
}
.padding(.horizontal, (geo.size.width - 140) / 2)
}
}
}
}
Higher-fidelity SwiftUI (bridge to UIKit blur animator) uses a UIViewRepresentable BlurView that exposes a progress binding and uses UIViewPropertyAnimator internally (see code sample in the explanation). That lets you update the blur with precise fractionComplete while still building your layout in SwiftUI.
Performance, accessibility and tuning tips
- Performance:
- Update transforms/alpha on the cell’s layer (avoid complex subview layout) and keep per-frame work minimal.
- Prefer
UIVisualEffectViewfor blur; it’s GPU-accelerated but still heavier than plain backgrounds—test on older devices. - Reuse cells and prefetch content in
UICollectionView.
- Accessibility:
- Honor Reduce Motion: check
UIAccessibility.isReduceMotionEnabledand fall back to simpler fades/cross-fades rather than heavy parallax/scale. - Ensure elements are reachable by VoiceOver and provide accessibility labels for modes.
- Honor Reduce Motion: check
- Tuning:
- Snap curve: use custom
targetContentOffsetlogic orUICollectionViewCompositionalLayout.groupPagingCenteredfor quick results. - Blur mapping: tune how
fractionCompletemaps to content offset (linear often works but a short curve can feel closer to system). - Test with different font sizes and dynamic type; ensure the material background keeps contrast.
- Snap curve: use custom
Sources
- UIVisualEffectView documentation
- UIBlurEffect documentation
- UIViewPropertyAnimator documentation
- UICollectionView documentation
- UIScrollView documentation
- UIPercentDrivenInteractiveTransition documentation
- SwiftUI Material documentation
- matchedGeometryEffect documentation
- Human Interface Guidelines — Motion
Conclusion
There is no single public “Liquid Glass” native API for the Camera mode switcher or Safari tab switcher; the native iOS swipe transition is implemented by combining a centered horizontal scroll (UICollectionView/UIScrollView or SwiftUI ScrollView), a material/blur backdrop (UIVisualEffectView or SwiftUI Material) and animator-driven transforms (UIViewPropertyAnimator / fractionComplete). Implement snapping, update per-item scale/alpha in scroll callbacks, and drive blur/material intensity from the same progress to reproduce the exact iOS swipe transition and Liquid Glass effect.