Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,6 @@
moduleName = "UIKitCore"
usesParentBreakpointCondition = "Yes">
</Location>
<Location
uuid = "A2EF7869-1A53-4958-B6C1-1F0B223F1FBA - ca8a62ba89de0751"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "UIKit.UIApplicationMain(Swift.Int32, Swift.Optional&lt;Swift.UnsafeMutablePointer&lt;Swift.UnsafeMutablePointer&lt;Swift.Int8&gt;&gt;&gt;, Swift.Optional&lt;Swift.String&gt;, Swift.Optional&lt;Swift.String&gt;) -&gt; Swift.Int32"
moduleName = "libswiftUIKit.dylib"
usesParentBreakpointCondition = "Yes">
</Location>
<Location
uuid = "A2EF7869-1A53-4958-B6C1-1F0B223F1FBA - ed0225d50b1c0976"
shouldBeEnabled = "Yes"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ final class DemoRideauIntegrationViewController: FluidStackController {
_display_present()
}
)

Self.makeCell(
title: "System Sheet + UINavigationController",
onTap: { [unowned self] in
_display_systemSheet_navigationController()
}
)
}
)

Expand All @@ -73,16 +80,23 @@ final class DemoRideauIntegrationViewController: FluidStackController {
instance.fluidPop(transition: nil, completion: nil)
}

let fluidConfig = FluidViewController.Configuration(
transition: .modalStyle,
topBar: .navigation(.init(
navigationBarClass: UINavigationBar.self
))
)

let rideauController = FluidRideauViewController(
bodyViewController: body.fluidWrapped(configuration: .defaultModal),
bodyViewController: body.fluidWrapped(configuration: fluidConfig),
configuration: .init(
snapPoints: [.pointsFromTop(200)],
topMarginOption: .fromSafeArea(0)
),
initialSnapPoint: .pointsFromTop(200),
resizingOption: .noResize
)

fluidPush(rideauController, target: .current)

}
Expand All @@ -108,19 +122,50 @@ final class DemoRideauIntegrationViewController: FluidStackController {
}

private func _display_swiftui() {

let body = SwiftUIContentViewController()

let rideauController = FluidRideauViewController(
bodyViewController: body.fluidWrapped(configuration: .defaultModal),
configuration: .init(snapPoints: [.autoPointsFromBottom]),
initialSnapPoint: .autoPointsFromBottom,
resizingOption: .noResize
)

fluidPush(rideauController, target: .current)
}

private func _display_systemSheet_navigationController() {
let content = SimpleContentViewController()
content.navigationItem.title = "System Sheet"
content.navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: #selector(_dismissSheet)
)
content.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(_dismissSheet)
)

let nav = UINavigationController(rootViewController: content)
nav.modalPresentationStyle = .pageSheet

if #available(iOS 15.0, *) {
if let sheet = nav.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
}

present(nav, animated: true)
}

@objc private func _dismissSheet() {
dismiss(animated: true)
}

private static func makeCell(title: String, onTap: @escaping () -> Void) -> UIView {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
Expand All @@ -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() {
Expand Down Expand Up @@ -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?(
Expand Down
48 changes: 36 additions & 12 deletions Sources/FluidStack/ViewController/FluidViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
}

// MARK: - Functions

@objc
open func triggerFluidPop() {
fluidPop(transition: nil)
Expand Down Expand Up @@ -161,7 +161,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
}

// MARK: - UIViewController

open override func viewDidLoad() {
super.viewDidLoad()

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -445,19 +462,27 @@ extension FluidViewController {

let _activityHandler: @Sendable @MainActor (Activity<UINavigationBar>) -> 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<NavigationBar: UINavigationBar>(
displayMode: DisplayMode = .automatic,
usesBodyViewController: Bool = true,
navigationBarClass: NavigationBar.Type,
topPaddingProvider: @escaping @MainActor @Sendable (FluidViewController) -> CGFloat = { _ in 0 },
activityHandler: @escaping @MainActor (Activity<NavigationBar>) -> 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):
Expand All @@ -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 }
)

}
Expand Down
Loading