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 } ) }