_UIPortalView: From Live Mirroring to Liquid Glass-Style Effects


Greetings, traveler!

I recently spent some time studying one of the more interesting private UIKit components: _UIPortalView. Sometimes you want the same visual content to appear in more than one place on the screen, while keeping the original view alive, interactive, and continuously updated.

This kind of requirement appears in many UI effects:

  • A picture-in-picture preview can mirror a larger content area.
  • A transition can temporarily show the same element in a floating overlay.
  • A reflection can reuse the original view while applying a transform.
  • A glass or lens effect can show a distorted part of the content behind it.

The usual public UIKit options are familiar. You can create a second view hierarchy and keep it synchronized with the source. You can render snapshots with snapshotView(afterScreenUpdates:), drawHierarchy(in:afterScreenUpdates:), or layer rendering. You can build a custom representation that shares the same model and recreates the visual state elsewhere.

Those approaches are valid, and they are the right answer for production code in many cases. They also come with tradeoffs. A duplicated view tree increases state synchronization complexity. A snapshot captures pixels at a moment in time, so live content requires repeated rendering. A custom representation can drift from the original UI as the design evolves.

_UIPortalView is interesting because it approaches the problem from a different layer of the system.

What _UIPortalView Appears To Be

_UIPortalView is a private UIKit view backed by CAPortalLayer. At a high level, it acts as a portal into another view’s rendered layer content. Instead of asking your app to create a second view hierarchy, it lets Core Animation and the system compositor reuse the visual output of an existing source view.

The rough mental model is a live window into another layer subtree.

A normal UIView owns a backing CALayer. UIKit builds the view hierarchy, Core Animation collects layer changes into transactions, and the render server composites the final result. CAPortalLayer appears to participate closer to that composition stage. It can refer to an existing source layer through internal render identity, then ask the compositor to draw that source content at the portal’s position.

UIKit’s _UIPortalView gives this lower-level mechanism a view-level wrapper. Instead of manually dealing with layer render identifiers, you work with a sourceView. The private class also exposes behavior flags such as matchesPosition, matchesTransform, matchesAlpha, hidesSourceView, allowsHitTesting, allowsBackdropGroups, and forwardsClientHitTestingToSourceView.

The API shape tells a lot about the intended behavior. The portal can mirror the source visually, follow its transform, inherit its alpha, hide the original while keeping the portal visible, and optionally route interaction back to the source. This makes it much closer to live compositing than to a static screenshot.

A minimal runtime experiment looks like this:

import UIKit

final class PortalExperimentViewController: UIViewController {
    private let sourceView = UILabel()
    private var portalView: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemBackground

        sourceView.text = "Live source"
        sourceView.textAlignment = .center
        sourceView.textColor = .white
        sourceView.backgroundColor = .systemBlue
        sourceView.layer.cornerRadius = 16
        sourceView.layer.masksToBounds = true
        sourceView.frame = CGRect(x: 32, y: 120, width: 180, height: 64)
        view.addSubview(sourceView)

        guard let portalClass = NSClassFromString("_UIPortalView") as? UIView.Type else {
            return
        }

        let portal = portalClass.init(frame: CGRect(x: 32, y: 240, width: 180, height: 64))
        portal.setValue(sourceView, forKey: "sourceView")
        portal.setValue(false, forKey: "matchesPosition")
        portal.setValue(true, forKey: "matchesAlpha")
        portal.setValue(true, forKey: "matchesTransform")

        view.addSubview(portal)
        portalView = portal
    }
}

This kind of code is useful for research, debugger exploration, and understanding UIKit internals. It also crosses into private API territory immediately, which matters a lot for real applications.

The Difference Between Portal Mirroring And Snapshots

The key distinction is lifetime. A snapshot captures the rendered output of a view at one point in time. The returned view can be moved, animated, clipped, transformed, and used during transitions, yet the content has already been captured. When the source updates, the snapshot requires a new capture if you want the visual result to stay current.

A portal keeps a connection to the source. If the source view changes its text, color, transform, layout, animation state, or subview content in a way that reaches the layer tree, the portal can reflect that content through the same rendered source. The portal view does not own a second copy of the source view hierarchy. The source remains the source of truth.

That distinction changes the kinds of effects you can build. A transition can show a live element while the original continues animating. A floating preview can reflect loading progress without a second progress view. A glass lens can show the real content behind an element while applying masks, transforms, and clipping around it. A warped edge can split one source into multiple small portal slices, then rotate each slice in 3D to create a bending illusion.

matchesPosition And The Shared Coordinate Space

The matchesPosition flag is one of the most important details. When matchesPosition is enabled, the portal behaves like it is looking into the source through a shared coordinate space. The portal shows the part of the source that corresponds to the same position in the window. A useful analogy is a set of small holes in a wall, where every hole reveals the matching part of a larger surface behind it.

This mode is useful for effects based on a global background or a continuous surface. Imagine several bubbles placed over a single large gradient. Each bubble can show the part of the gradient that belongs to its screen position, giving the impression that all bubbles are cut from the same material.

When matchesPosition is disabled, the portal behaves more like a viewport into the source starting near the source origin. The visible region can then be controlled through clipping, bounds, frame offsets, and transforms. This mode is especially useful when you want to replicate the source several times and make each replica show a different segment.

That second mode appears in warp-style effects. You can take one source view, create multiple portal views pointing to it, clip each portal to a thin horizontal slice, offset the mirrored content inside each slice, and then apply different 3D transforms to each slice. The result feels like a single live view bending in space, while every piece still comes from the same source content.

Examples

The Connection To _UILiquidLensView And Liquid Glass

The connection with iOS 26 Liquid Glass is hard to ignore. During runtime inspection, private components such as _UILiquidLensView appear around Liquid Glass-style UI, and _UIPortalView participates in the same internal rendering family.

A similar portal-based approach also appeared in the winning Telegram iOS Contest 2025 implementation for custom Liquid Glass effects. Instead of rendering everything through Metal, the solution used PortalView with transformed live UI surfaces.

That does not mean custom apps should call _UILiquidLensView or _UIPortalView directly. The public path for developers is through APIs such as UIVisualEffectView, UIGlassEffect, SwiftUI materials, system bars, controls, and the platform-provided Liquid Glass adoption points. The private classes are still useful to study because they reveal how much of modern iOS visual design happens at the composition layer, where views, layers, backdrops, and render surfaces meet.

Performance Characteristics

The performance profile of a portal differs from repeated snapshots. A snapshot path asks the app to render pixels into an intermediate representation. If that happens often, the CPU and main thread can become involved in work that feels unrelated to the actual state change. A portal shifts the core operation toward the compositor. The source already has a layer representation, and the portal references that rendered output.

This can reduce CPU work for effects that need live duplication. It can also preserve visual freshness without forcing manual snapshot invalidation. A progress view, animated image, or changing label can appear through the portal as the source changes.

There is another subtle detail around content sources such as video. Some layers update their visible pixels through mechanisms that may not always look like ordinary layer tree mutations. For example, an AVPlayerLayer can update video frames through surface swaps. If the portal observes layer tree changes more reliably than internal surface content changes, the mirrored output can appear stale until some layer property changes. A forced animation on a source sublayer can keep the subtree dirty, although that workaround trades correctness for continuous rendering work.

The Risk Of Private API

_UIPortalView, CAPortalLayer portal behavior, and _UILiquidLensView are private implementation details. They are available through runtime lookup because Objective-C and UIKit expose a dynamic runtime, not because Apple provides a stable developer contract for them.

Using private API in an App Store build carries real risk. Apple’s review rules require apps to use public APIs, and private selectors, class names, or behavior can lead to rejection. Even when a build passes review, a future iOS update can rename the class, remove a selector, change default behavior, alter the layer semantics, or break the effect completely. Obfuscating class names can reduce simple string detection, yet it does not remove the policy or maintenance risk.

For that reason, I treat _UIPortalView as research material. It is excellent for learning how UIKit, Core Animation, and the render server can compose live content. It is also useful for prototypes, demos, and internal experiments. Production code should prefer public APIs and recreate the effect with supported primitives whenever possible.

Conclusion

_UIPortalView is one of those private APIs that explains a lot about how modern iOS effects are built. It sits at the boundary between UIKit ergonomics and Core Animation composition. It gives a view-level shape to a lower-level idea: one source, many live projections.

A portal can power live mirroring, hero transitions, reflections, shared-background effects, lens-like UI, and segmented warping. The same idea also helps explain why Liquid Glass feels different from older blur-based materials. The system is increasingly compositing live context, not just drawing isolated views.

I would not ship _UIPortalView in an App Store product, but I find it valuable to study. Private APIs often reveal the direction of the framework. In this case, the direction is clear: more effects are moving closer to the compositor, where live content, materials, transforms, and interaction can be combined into a single visual system.

Example project: https://github.com/Livsy90/EdgeDistortion