Portal

6.0.0

Portal is a SwiftUI package for element transitions across navigation contexts, scroll-based flowing headers, and advanced view mirroring capabilities.
Aeastr/Portal

What's New

6.0.0 — Namespace Scoping

2026-01-14T03:19:58Z

Breaking Changes

Namespace Required for Portal Transitions

All portal transition modifiers now require a namespace parameter. This scopes transitions so portals only match within the same namespace—essential for apps with multiple portal contexts.

Before (5.1.0):

.portalTransition(item: $selected) { item in
    ItemView(item: item)
}

After (6.0.0):

@Namespace private var portalNamespace

.portalTransition(item: $selected, in: portalNamespace) { item in
    ItemView(item: item)
}

PortalView Removed

PortalView in _PortalPrivate has been removed. Use UIPortalBridge directly instead.


New Features

Multi-Level Configuration API

Three levels of control for the animating layer:

// Level 1: Automatic — frame/offset handled for you
.portalTransition(item: $selected, in: namespace) { item in
    ItemView(item: item)
}

// Level 2: Styling only — add clips, shadows, etc.
.portalTransition(item: $selected, in: namespace) { item in
    ItemView(item: item)
} configuration: { content, isActive in
    content.clipShape(.rect(cornerRadius: isActive ? 0 : 12))
}

// Level 3: Full control — you handle frame/offset
.portalTransition(item: $selected, in: namespace) { item in
    ItemView(item: item)
} configuration: { content, isActive, size, position in
    content
        .frame(width: size.width, height: size.height)
        .clipShape(.rect(cornerRadius: isActive ? 0 : 12))
        .offset(x: position.x, y: position.y)
}

Multiple Portal Models

PortalContainer now supports multiple CrossModel instances in a single overlay window, enabling independent portal contexts within the same app.


Bug Fixes

  • Fixed coordinator completion call timing in group portal transition modifiers
  • Fixed namespace propagation bug in animated layers
  • Refactored anchor handling in Portal view to prevent race conditions
  • Removed debug print statements

Documentation

  • Migrated all documentation from wiki to docs/ folder
  • Updated README with new configuration API examples
  • Added Related Projects section linking to UIPortalBridge and Transmission

Infrastructure

  • Renamed Resources/ to resources/ for consistency
  • Removed CODE_OF_CONDUCT.md
Portal Icon

Portal

Element transitions across navigation contexts, scroll-based flowing headers, and view mirroring for SwiftUI.

Swift 6.2+ iOS 17+ Build Tests

Preview

Overview

Portal provides three modules for different use cases:

  • PortalTransitions — Animate views between navigation contexts (sheets, navigation stacks, tabs) using a floating overlay layer. Uses standard SwiftUI APIs.
  • PortalHeaders — Scroll-based header transitions that flow into the navigation bar, like Music or Photos. Uses iOS 18's advanced scroll tracking APIs.
  • _PortalPrivate — True view mirroring using Apple's private _UIPortalView API. The view instance is shared rather than recreated.

Installation

dependencies: [
    .package(url: "https://github.com/Aeastr/Portal.git", from: "4.0.0")
]
Target Description
PortalTransitions Element transitions (iOS 17+)
PortalHeaders Flowing headers (iOS 18+)
_PortalPrivate View mirroring with private API

Targeting iOS 15/16? Pin to v2.1.0 or the legacy/ios15 branch.

Usage

Element Transitions

// 1. Wrap your app in PortalContainer
PortalContainer {
    ContentView()
}

// 2. Mark the source view
Image("cover")
    .portal(id: "book", .source)

// 3. Mark the destination view
Image("cover")
    .portal(id: "book", .destination)

// 4. Apply the transition
.fullScreenCover(item: $selectedBook) { book in
    BookDetail(book: book)
}
.portalTransition(item: $selectedBook)

The view animates smoothly from source to destination when the cover presents, and back when it dismisses.

Flowing Headers

Scroll-based header transitions that flow into the navigation bar, like Music or Photos.

NavigationStack {
    ScrollView {
        PortalHeaderView()

        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
    .portalHeaderDestination()
}
.portalHeader(title: "Favorites", subtitle: "Your starred items")

Private API Mirroring

WARNING: Private API Usage

This module uses Apple's private _UIPortalView API. Apps using private APIs may be rejected by App Store Review. Use at your own discretion. Portal, Aether, and any maintainers assume no responsibility for App Store rejections, app crashes, or any other issues arising from the use of this module.

Same API as PortalTransitions, but uses Apple's private _UIPortalView for true view mirroring instead of layer snapshots. The view instance is shared rather than recreated.

Class names are obfuscated at compile-time. See the docs for details.

Customization

Layer Configuration

Customize the animating layer with optional configuration closures:

// No config — frame/offset handled automatically
.portalTransition(item: $selectedBook) { book in
    Image("cover")
}

// Styling only — add clips, shadows, etc. (frame/offset still automatic)
.portalTransition(item: $selectedBook) { book in
    Image("cover")
} configuration: { content, isActive in
    content.clipShape(.rect(cornerRadius: isActive ? 0 : 12))
}

// Full control — you handle frame/offset (for custom modifier ordering)
.portalTransition(item: $selectedBook) { book in
    Image("cover")
} configuration: { content, isActive, size, position in
    content
        .frame(width: size.width, height: size.height)
        .clipShape(.rect(cornerRadius: isActive ? 0 : 12))
        .offset(x: position.x, y: position.y)
}

How It Works

PortalTransitions creates a transparent PassThroughWindow that sits above your entire app UI. Source and destination views register their bounds via PreferenceKey. When a transition triggers, the view is rendered in this overlay window and animated between the two positions. The window uses a custom hitTest implementation that only captures touches on the animated layer itself—all other touches pass through to your app below, so interaction remains seamless during animations.

PortalHeaders tracks scroll position using iOS 18's ScrollGeometry and interpolates between inline and navigation bar states based on content offset thresholds.

_PortalPrivate wraps Apple's private _UIPortalView class, which creates a portal to another view's layer. Class names are obfuscated at compile-time to avoid detection. See UIPortalBridge for a standalone wrapper.

Examples

Each module includes working examples in Sources/*/Examples/:

PortalTransitions PortalHeaders _PortalPrivate
Card Grid With Accessory Card Grid
List Title Only List
Grid Carousel No Accessory Comparison

Documentation

Full guides and API reference are available in the docs folder.

Contributing

Contributions welcome. See the Contributing Guide for details.

License

MIT. See LICENSE for details.

Related

Contact

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

Last updated: Sun Feb 01 2026 18:23:12 GMT-1000 (Hawaii-Aleutian Standard Time)