Mobile Dev

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.

1 answer 1 view

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

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) or ScrollView (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) or Material (.ultraThinMaterial etc.) in SwiftUI. These give the frosted, blending background. See UIVisualEffectView documentation and the SwiftUI Material documentation.
  • Animator-driven, interactive transitions: use UIViewPropertyAnimator to animate blur intensity and fractionComplete to tie the blur to gesture progress, and update per-item transforms/alpha from scrollViewDidScroll. 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 UICollectionViewFlowLayout override targetContentOffset(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 scrollViewDidScroll for smooth per-frame feedback.
  • Background blending (blur intensity): animate a UIVisualEffectView using UIViewPropertyAnimator. Create an animator whose animation block sets the visual effect (e.g., UIBlurEffect(style: .systemUltraThinMaterial)) and then set animator.fractionComplete = progress as 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:

  1. Use a horizontally-scrolling UICollectionView with centered cells (custom FlowLayout or compositional layout).
  2. In scrollViewDidScroll, compute each visible cell’s distance to the center, set cell.transform and cell.alpha.
  3. Prepare a UIVisualEffectView with an associated UIViewPropertyAnimator that animates to the target blur; update animator.fractionComplete from the scroll progress.
  4. 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:

  1. Pure SwiftUI (simpler, works on iOS 15+):

    • Use ScrollView(.horizontal) + LazyHStack.
    • Use GeometryReader inside each item to compute the item’s center vs. container center and apply .scaleEffect and .opacity based 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 DragGesture and ScrollViewReader to scroll to the nearest item on .onEnded — works well but is less precise for continuous interactive blur.
  2. SwiftUI + UIKit bridge (highest fidelity):

    • Wrap a UICollectionView or UIScrollView in UIViewRepresentable. Expose content offset to SwiftUI via Binding or Coordinator.
    • Wrap a UIVisualEffectView with a UIViewRepresentable that exposes an animator and a progress binding; set its animator.fractionComplete from the scroll offset to get interactive blur control.
    • This hybrid approach gives the best match to the Camera/Safari feel.

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)

swift
// 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 blurAnimator approach 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:

swift
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 UIVisualEffectView for 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.isReduceMotionEnabled and fall back to simpler fades/cross-fades rather than heavy parallax/scale.
    • Ensure elements are reachable by VoiceOver and provide accessibility labels for modes.
  • Tuning:
    • Snap curve: use custom targetContentOffset logic or UICollectionViewCompositionalLayout .groupPagingCentered for quick results.
    • Blur mapping: tune how fractionComplete maps 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.

Sources

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.

Authors
Verified by moderation
Moderation
iOS Swipe Transition: Native API for Camera/Safari 'Liquid Glass' Effect