Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b0a2050
refactor Portal anchor handling
Aeastr Dec 8, 2025
5551541
remove debug print and add race condition note in Portal
Aeastr Dec 8, 2025
6ace45c
fix coordinator completion call in portal transition modifiers
Aeastr Dec 8, 2025
393ddf1
Support multiple portal models in overlay window
Aeastr Dec 17, 2025
e1133d3
Add UIPortalBridge and remove PortalView
Aeastr Dec 17, 2025
af41b6e
Add related projects section to README
Aeastr Dec 17, 2025
5b6d3ec
Add namespace support to portal transitions
Aeastr Dec 17, 2025
0eec255
Refactor portal modifiers to unify API and add groupID
Aeastr Dec 17, 2025
24ffc38
Require namespace parameter for portal transitions
Aeastr Dec 17, 2025
8d095de
Add namespace support to portal transitions and destinations
Aeastr Dec 17, 2025
857795c
Add portalNamespace to PortalPrivate example views
Aeastr Dec 17, 2025
7dca47b
Add namespace support to private portal transitions and views
Aeastr Dec 17, 2025
98c61cd
Rename portalPrivate to portalSourcePrivate
Aeastr Dec 17, 2025
77b0279
fix namespace bug + introduce namespace for more components
Aeastr Dec 18, 2025
a81652b
fix errors
Aeastr Dec 19, 2025
8a77966
Update AnimatedItemPortalLayer.swift
Aeastr Dec 19, 2025
f3d3e76
Refactor portal transition styling to use configuration closure
Aeastr Dec 22, 2025
13c741e
Refactor configuration closure to use size and position
Aeastr Dec 22, 2025
31b5cc3
Add multi-level portal configuration API
Aeastr Dec 22, 2025
a3743ac
Replace corners with configuration in CrossModel
Aeastr Dec 22, 2025
28a9cde
Add layer configuration examples to README
Aeastr Dec 22, 2025
f04bd32
Revise README and add usage tips to portal layers
Aeastr Jan 14, 2026
3ff2759
Revise README overview and usage sections
Aeastr Jan 14, 2026
c547ab9
Migrate documentation from wiki to docs folder
Aeastr Jan 14, 2026
9a38b38
Rename Resources directory to lowercase
Aeastr Jan 14, 2026
b0fe746
Remove CODE_OF_CONDUCT.md file
Aeastr Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "wiki"]
path = wiki
url = https://github.com/Aeastr/Portal.wiki.git
128 changes: 0 additions & 128 deletions CODE_OF_CONDUCT.md

This file was deleted.

4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/Aeastr/Chronicle.git", from: "3.0.1"),
.package(url: "https://github.com/Aeastr/Obfuscate.git", from: "1.0.0")
.package(url: "https://github.com/Aeastr/UIPortalBridge.git", from: "1.0.0")
],
targets: [
.target(
Expand All @@ -42,7 +42,7 @@ let package = Package(
name: "_PortalPrivate",
dependencies: [
"PortalTransitions",
.product(name: "Obfuscate", package: "Obfuscate")
.product(name: "UIPortalBridge", package: "UIPortalBridge")
],
path: "Sources/_PortalPrivate"
),
Expand Down
125 changes: 85 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
<div align="center">
<img width="200" height="200" src="/Resources/icon/icon.png" alt="Portal Logo">
<img width="128" height="128" src="/resources/icon/icon.png" alt="Portal Icon">
<h1><b>Portal</b></h1>
<p>
Element transitions across navigation contexts, scroll-based flowing headers, and view mirroring for SwiftUI.
</p>
</div>

<p align="center">
<a href="https://developer.apple.com/ios/"><img src="https://img.shields.io/badge/iOS-17%2B-purple.svg" alt="iOS 17+"></a>
<a href="https://swift.org/"><img src="https://img.shields.io/badge/Swift-6.2-orange.svg" alt="Swift 6.2"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"></a>
<a href="https://swift.org"><img src="https://img.shields.io/badge/Swift-6.2+-F05138?logo=swift&logoColor=white" alt="Swift 6.2+"></a>
<a href="https://developer.apple.com"><img src="https://img.shields.io/badge/iOS-17+-000000?logo=apple" alt="iOS 17+"></a>
<a href="https://github.com/Aeastr/Portal/actions/workflows/build.yml"><img src="https://github.com/Aeastr/Portal/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<a href="https://github.com/Aeastr/Portal/actions/workflows/tests.yml"><img src="https://github.com/Aeastr/Portal/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
</p>

<div align="center">
<img width="600" src="/Resources/examples/example1.gif" alt="Portal Demo">
<img width="600" src="/resources/examples/example1.gif" alt="Preview">
</div>


## 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.


## Requirements

- Swift 6.2+
- iOS 17+ (PortalTransitions)
- iOS 18+ (PortalHeaders)

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


## Installation

```swift
dependencies: [
.package(url: "https://github.com/Aeastr/Portal", from: "4.0.0")
.package(url: "https://github.com/Aeastr/Portal.git", from: "4.0.0")
]
```

Then import the module you need:

```swift
import PortalTransitions // Element transitions (iOS 17+)
import PortalHeaders // Flowing headers (iOS 18+)
import _PortalPrivate // View mirroring with private API
```

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

| Target | Description |
|--------|-------------|
| `PortalTransitions` | Element transitions (iOS 17+) |
| `PortalHeaders` | Flowing headers (iOS 18+) |
| `_PortalPrivate` | View mirroring with private API |

## Modules

### PortalTransitions
## Usage

Animate views between navigation contexts — sheets, navigation stacks, tabs — using a floating overlay layer.
### Element Transitions

```swift
// 1. Wrap your app in PortalContainer
Expand All @@ -67,11 +78,7 @@ Image("cover")

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

**iOS 17+** · Uses standard SwiftUI APIs

---

### PortalHeaders
### Flowing Headers

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

Expand All @@ -80,7 +87,6 @@ NavigationStack {
ScrollView {
PortalHeaderView()

// Your content
ForEach(items) { item in
ItemRow(item: item)
}
Expand All @@ -90,28 +96,55 @@ NavigationStack {
.portalHeader(title: "Favorites", subtitle: "Your starred items")
```

As the user scrolls, the title transitions from inline to the navigation bar with configurable snapping behavior.

**iOS 18+** · Uses advanced scroll tracking APIs

---

### _PortalPrivate
### 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. By importing `_PortalPrivate`, you accept full responsibility for any consequences.
> 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 [wiki](wiki/_PortalPrivate.md) for details.
Class names are obfuscated at compile-time. See the [docs](docs/PortalPrivate.md) for details.


## Documentation
## Customization

### Layer Configuration

The **[Portal Wiki](https://github.com/Aeastr/Portal/wiki)** has full guides and API reference for each module.
Customize the animating layer with optional configuration closures:

The wiki is included at `/wiki` when you clone, so it's available offline.
```swift
// 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](https://github.com/Aeastr/UIPortalBridge) for a standalone wrapper.


## Examples
Expand All @@ -125,11 +158,25 @@ Each module includes working examples in `Sources/*/Examples/`:
| [Grid Carousel](Sources/PortalTransitions/Examples/PortalExampleGridCarousel.swift) | [No Accessory](Sources/PortalHeaders/Examples/PortalHeaderExampleNoAccessory.swift) | [Comparison](Sources/_PortalPrivate/Transitions/Examples/PortalPrivateExampleComparison.swift) |


## Documentation

Full guides and API reference are available in the [docs](docs/) folder.


## Contributing

Contributions welcome. See the [Contributing Guide](CONTRIBUTING.md) for details.

Released under the [MIT License](LICENSE.md).

## License

MIT. See [LICENSE](LICENSE.md) for details.


## Related

- [UIPortalBridge](https://github.com/Aeastr/UIPortalBridge) - Standalone wrapper for `_UIPortalView`
- [Transmission](https://github.com/nathantannar4/Transmission) - UIKit-backed presentation and transitions for SwiftUI


## Contact
Expand All @@ -138,5 +185,3 @@ Released under the [MIT License](LICENSE.md).
- [Threads](https://www.threads.net/@aetheraurelia)
- [Bluesky](https://bsky.app/profile/aethers.world)
- [LinkedIn](https://www.linkedin.com/in/willjones24)

<p align="center">Built with 🍏🌀🚪 by Aether</p>
Loading
Loading