diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 000000000..0480df653
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,22 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(done)",
+ "Bash(lldb:*)",
+ "mcp__plugin_ios_mobile-mcp__mobile_list_available_devices",
+ "mcp__plugin_ios_mobile-mcp__mobile_take_screenshot",
+ "mcp__plugin_ios_mobile-mcp__mobile_list_apps",
+ "mcp__plugin_ios_mobile-mcp__mobile_launch_app",
+ "mcp__plugin_ios_mobile-mcp__mobile_list_elements_on_screen",
+ "mcp__plugin_ios_mobile-mcp__mobile_click_on_screen_at_coordinates",
+ "Bash(xcbeautify)"
+ ]
+ },
+ "enabledMcpjsonServers": [
+ "XcodeBuildMCP",
+ "mobile-mcp",
+ "atlassian",
+ "maestro"
+ ],
+ "enableAllProjectMcpServers": true
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..0509d2ecf
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,63 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build Commands
+
+```bash
+# Update git submodules (required before first build)
+make checkout
+
+# Build for iOS
+make build
+
+# Run tests
+xcodebuild -scheme "FluidInterfaceKit-Package" test \
+ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0.1' | xcbeautify
+
+# Run a single test (example)
+xcodebuild -scheme "FluidInterfaceKit-Package" test \
+ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0.1' \
+ -only-testing:FluidStackTests/FluidStackControllerTests | xcbeautify
+```
+
+## Architecture
+
+FluidInterfaceKit is a UIKit-based framework providing advanced view controller management with customizable transitions. The primary component **FluidStackController** replaces UINavigationController with flexible stacking behavior.
+
+### Core Modules (SPM Libraries)
+
+- **FluidStack** - Main container replacing UINavigationController. Key classes: `FluidStackController`, `FluidViewController`, `FluidGestureHandlingViewController`
+- **FluidGesture** - Makes views draggable with `makeDraggable(descriptor:)`
+- **FluidPortal** - Portal/layer display system for floating views
+- **FluidSnackbar** - Toast/snackbar notifications with gesture support
+- **FluidKeyboardSupport** - Keyboard frame tracking and integration
+- **FluidTooltipSupport** - Floating tooltips over specific points
+- **FluidPictureInPicture** - PiP floating view support
+- **FluidStackRideauSupport** - Integration with Rideau modal library
+
+### Transition System
+
+Adding transitions: `AnyAddingTransition` with presets (`.noAnimation`, `.navigationStyle`, `.fadeIn`, `.popup`, `.contextualExpanding`, `.modalIdiom`)
+
+Removing transitions: `AnyRemovingTransition` with presets (`.noAnimation`, `.navigationStyle`, `.fadeOut`, `.vanishing`, `.contextual`, `.modalIdiom`)
+
+Context objects: `AddingTransitionContext`, `RemovingTransitionContext` provide state for animations.
+
+### Extension Pattern
+
+All UIViewControllers gain fluid methods via extension protocol:
+- `fluidPush()` / `fluidPop()` - Safe navigation
+- `fluidPushUnsafely()` - Unsafe variants
+- `fluidStackController(with:)` - Finding strategies
+
+## Code Style
+
+- **Indentation:** 2 spaces
+- **MainActor:** Extensively used for thread safety
+- **MARK sections:** Properties, Initializers, Functions, ViewController lifecycle
+- **Naming:** `Fluid` prefix for all types, camelCase for methods
+
+## Dependencies
+
+- GeometryKit, ResultBuilderKit, Rideau, swiftui-Hosting, swift-rubber-banding (all from FluidGroup)
diff --git a/Development/FluidInterfaceKit.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist b/Development/FluidInterfaceKit.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist
index d96fd727a..ea1fdbd33 100644
--- a/Development/FluidInterfaceKit.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist
+++ b/Development/FluidInterfaceKit.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -31,15 +31,6 @@
moduleName = "UIKitCore"
usesParentBreakpointCondition = "Yes">
-
-
Void) -> UIView {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
@@ -143,6 +188,14 @@ import SwiftUI
import SwiftUISupport
import SwiftUIHosting
+private final class SimpleContentViewController: UIViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .neonRandom()
+ }
+}
+
private final class SwiftUIContentViewController: UIViewController {
override func viewDidLoad() {
@@ -178,8 +231,18 @@ private final class ContentViewController: FluidStackController {
) {
self._dismiss = dismiss
super.init()
-
+
navigationItem.title = "Rideau"
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ barButtonSystemItem: .done,
+ target: nil,
+ action: nil
+ )
+ navigationItem.leftBarButtonItem = UIBarButtonItem(
+ barButtonSystemItem: .close,
+ target: nil,
+ action: nil
+ )
}
required init?(
diff --git a/Sources/FluidStack/ViewController/FluidViewController.swift b/Sources/FluidStack/ViewController/FluidViewController.swift
index 443418e8a..da958e94d 100644
--- a/Sources/FluidStack/ViewController/FluidViewController.swift
+++ b/Sources/FluidStack/ViewController/FluidViewController.swift
@@ -102,7 +102,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
}
// MARK: - Functions
-
+
@objc
open func triggerFluidPop() {
fluidPop(transition: nil)
@@ -161,7 +161,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
}
// MARK: - UIViewController
-
+
open override func viewDidLoad() {
super.viewDidLoad()
@@ -179,20 +179,34 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
subscriptions.append(
navigationBar.observe(\.bounds, options: [.initial, .old, .new]) { [weak self] view, _ in
guard let self else { return }
- self.additionalSafeAreaInsets.top = view.frame.height
+ MainActor.assumeIsolated {
+ self.additionalSafeAreaInsets.top = view.intrinsicContentSize.height + navigation._topPaddingProvider(self)
+ view.invalidateIntrinsicContentSize()
+ }
}
- )
- view.addSubview(navigationBar)
+ )
+ subscriptions.append(
+ navigationBar.observe(\.intrinsicContentSize, options: [.initial, .old, .new]) { [weak self] view, _ in
+ guard let self else { return }
+ MainActor.assumeIsolated {
+ self.additionalSafeAreaInsets.top = view.intrinsicContentSize.height + navigation._topPaddingProvider(self)
+ view.invalidateIntrinsicContentSize()
+ }
+ }
+ )
+
navigationBar.translatesAutoresizingMaskIntoConstraints = false
+
+ view.addSubview(navigationBar)
NSLayoutConstraint.activate([
navigationBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor),
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor),
])
-
+
let targetNavigationItem =
navigation.usesBodyViewController
? (content.bodyViewController?.navigationItem ?? navigationItem) : navigationItem
@@ -274,7 +288,10 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
if !state.isTopBarHidden && state.isTopBarAvailable {
topBar.isHidden = false
- additionalSafeAreaInsets.top = topBar.frame.height
+ if case .navigation(let navigation) = configuration.topBar {
+ additionalSafeAreaInsets.top = topBar.intrinsicContentSize.height + navigation._topPaddingProvider(self)
+ }
+ topBar.invalidateIntrinsicContentSize()
} else {
topBar.isHidden = true
additionalSafeAreaInsets.top = 0
@@ -445,19 +462,27 @@ extension FluidViewController {
let _activityHandler: @Sendable @MainActor (Activity) -> Void
+ let _topPaddingProvider: @Sendable @MainActor (FluidViewController) -> CGFloat
+
/// Initializer
///
/// - Parameters:
- /// - updateNavigationBar: A closure to update the navigation bar with the owner.
+ /// - displayMode: Controls when the navigation bar is visible.
+ /// - usesBodyViewController: Whether to use the body view controller's navigation item.
+ /// - navigationBarClass: The class of navigation bar to use.
+ /// - topPaddingProvider: A closure that returns additional top padding above the navigation bar.
+ /// - activityHandler: A closure called when navigation bar lifecycle events occur.
public init(
displayMode: DisplayMode = .automatic,
usesBodyViewController: Bool = true,
navigationBarClass: NavigationBar.Type,
+ topPaddingProvider: @escaping @MainActor @Sendable (FluidViewController) -> CGFloat = { _ in 0 },
activityHandler: @escaping @MainActor (Activity) -> Void = { _ in }
) {
self.displayMode = displayMode
self.usesBodyViewController = usesBodyViewController
self.navigationBarClass = navigationBarClass
+ self._topPaddingProvider = topPaddingProvider
self._activityHandler = { activity in
switch activity {
case .didLoad(let controller, let navigationBar):
@@ -467,14 +492,13 @@ extension FluidViewController {
}
}
}
-
+
public static let `default`: Self = .init(
displayMode: .automatic,
usesBodyViewController: true,
navigationBarClass: UINavigationBar.self,
- activityHandler: { _ in
-
- }
+ topPaddingProvider: { _ in 0 },
+ activityHandler: { _ in }
)
}