Skip to content

Commit 6a27ccc

Browse files
authored
Update Handling UINavigationBar (#190)
1 parent 1d85cd3 commit 6a27ccc

5 files changed

Lines changed: 190 additions & 27 deletions

File tree

.claude/settings.local.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(done)",
5+
"Bash(lldb:*)",
6+
"mcp__plugin_ios_mobile-mcp__mobile_list_available_devices",
7+
"mcp__plugin_ios_mobile-mcp__mobile_take_screenshot",
8+
"mcp__plugin_ios_mobile-mcp__mobile_list_apps",
9+
"mcp__plugin_ios_mobile-mcp__mobile_launch_app",
10+
"mcp__plugin_ios_mobile-mcp__mobile_list_elements_on_screen",
11+
"mcp__plugin_ios_mobile-mcp__mobile_click_on_screen_at_coordinates",
12+
"Bash(xcbeautify)"
13+
]
14+
},
15+
"enabledMcpjsonServers": [
16+
"XcodeBuildMCP",
17+
"mobile-mcp",
18+
"atlassian",
19+
"maestro"
20+
],
21+
"enableAllProjectMcpServers": true
22+
}

CLAUDE.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build Commands
6+
7+
```bash
8+
# Update git submodules (required before first build)
9+
make checkout
10+
11+
# Build for iOS
12+
make build
13+
14+
# Run tests
15+
xcodebuild -scheme "FluidInterfaceKit-Package" test \
16+
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0.1' | xcbeautify
17+
18+
# Run a single test (example)
19+
xcodebuild -scheme "FluidInterfaceKit-Package" test \
20+
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0.1' \
21+
-only-testing:FluidStackTests/FluidStackControllerTests | xcbeautify
22+
```
23+
24+
## Architecture
25+
26+
FluidInterfaceKit is a UIKit-based framework providing advanced view controller management with customizable transitions. The primary component **FluidStackController** replaces UINavigationController with flexible stacking behavior.
27+
28+
### Core Modules (SPM Libraries)
29+
30+
- **FluidStack** - Main container replacing UINavigationController. Key classes: `FluidStackController`, `FluidViewController`, `FluidGestureHandlingViewController`
31+
- **FluidGesture** - Makes views draggable with `makeDraggable(descriptor:)`
32+
- **FluidPortal** - Portal/layer display system for floating views
33+
- **FluidSnackbar** - Toast/snackbar notifications with gesture support
34+
- **FluidKeyboardSupport** - Keyboard frame tracking and integration
35+
- **FluidTooltipSupport** - Floating tooltips over specific points
36+
- **FluidPictureInPicture** - PiP floating view support
37+
- **FluidStackRideauSupport** - Integration with Rideau modal library
38+
39+
### Transition System
40+
41+
Adding transitions: `AnyAddingTransition` with presets (`.noAnimation`, `.navigationStyle`, `.fadeIn`, `.popup`, `.contextualExpanding`, `.modalIdiom`)
42+
43+
Removing transitions: `AnyRemovingTransition` with presets (`.noAnimation`, `.navigationStyle`, `.fadeOut`, `.vanishing`, `.contextual`, `.modalIdiom`)
44+
45+
Context objects: `AddingTransitionContext`, `RemovingTransitionContext` provide state for animations.
46+
47+
### Extension Pattern
48+
49+
All UIViewControllers gain fluid methods via extension protocol:
50+
- `fluidPush()` / `fluidPop()` - Safe navigation
51+
- `fluidPushUnsafely()` - Unsafe variants
52+
- `fluidStackController(with:)` - Finding strategies
53+
54+
## Code Style
55+
56+
- **Indentation:** 2 spaces
57+
- **MainActor:** Extensively used for thread safety
58+
- **MARK sections:** Properties, Initializers, Functions, ViewController lifecycle
59+
- **Naming:** `Fluid` prefix for all types, camelCase for methods
60+
61+
## Dependencies
62+
63+
- GeometryKit, ResultBuilderKit, Rideau, swiftui-Hosting, swift-rubber-banding (all from FluidGroup)

Development/FluidInterfaceKit.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,6 @@
3131
moduleName = "UIKitCore"
3232
usesParentBreakpointCondition = "Yes">
3333
</Location>
34-
<Location
35-
uuid = "A2EF7869-1A53-4958-B6C1-1F0B223F1FBA - ca8a62ba89de0751"
36-
shouldBeEnabled = "Yes"
37-
ignoreCount = "0"
38-
continueAfterRunningActions = "No"
39-
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"
40-
moduleName = "libswiftUIKit.dylib"
41-
usesParentBreakpointCondition = "Yes">
42-
</Location>
4334
<Location
4435
uuid = "A2EF7869-1A53-4958-B6C1-1F0B223F1FBA - ed0225d50b1c0976"
4536
shouldBeEnabled = "Yes"

Development/Sources/FluidInterfaceKit-Demo/DemoRideauIntegrationViewController.swift

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ final class DemoRideauIntegrationViewController: FluidStackController {
6262
_display_present()
6363
}
6464
)
65+
66+
Self.makeCell(
67+
title: "System Sheet + UINavigationController",
68+
onTap: { [unowned self] in
69+
_display_systemSheet_navigationController()
70+
}
71+
)
6572
}
6673
)
6774

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

83+
let fluidConfig = FluidViewController.Configuration(
84+
transition: .modalStyle,
85+
topBar: .navigation(.init(
86+
navigationBarClass: UINavigationBar.self
87+
))
88+
)
89+
7690
let rideauController = FluidRideauViewController(
77-
bodyViewController: body.fluidWrapped(configuration: .defaultModal),
91+
bodyViewController: body.fluidWrapped(configuration: fluidConfig),
7892
configuration: .init(
7993
snapPoints: [.pointsFromTop(200)],
8094
topMarginOption: .fromSafeArea(0)
8195
),
8296
initialSnapPoint: .pointsFromTop(200),
8397
resizingOption: .noResize
8498
)
85-
99+
86100
fluidPush(rideauController, target: .current)
87101

88102
}
@@ -108,19 +122,50 @@ final class DemoRideauIntegrationViewController: FluidStackController {
108122
}
109123

110124
private func _display_swiftui() {
111-
125+
112126
let body = SwiftUIContentViewController()
113-
127+
114128
let rideauController = FluidRideauViewController(
115129
bodyViewController: body.fluidWrapped(configuration: .defaultModal),
116130
configuration: .init(snapPoints: [.autoPointsFromBottom]),
117131
initialSnapPoint: .autoPointsFromBottom,
118132
resizingOption: .noResize
119133
)
120-
134+
121135
fluidPush(rideauController, target: .current)
122136
}
123137

138+
private func _display_systemSheet_navigationController() {
139+
let content = SimpleContentViewController()
140+
content.navigationItem.title = "System Sheet"
141+
content.navigationItem.rightBarButtonItem = UIBarButtonItem(
142+
barButtonSystemItem: .done,
143+
target: self,
144+
action: #selector(_dismissSheet)
145+
)
146+
content.navigationItem.leftBarButtonItem = UIBarButtonItem(
147+
barButtonSystemItem: .close,
148+
target: self,
149+
action: #selector(_dismissSheet)
150+
)
151+
152+
let nav = UINavigationController(rootViewController: content)
153+
nav.modalPresentationStyle = .pageSheet
154+
155+
if #available(iOS 15.0, *) {
156+
if let sheet = nav.sheetPresentationController {
157+
sheet.detents = [.medium(), .large()]
158+
sheet.prefersGrabberVisible = true
159+
}
160+
}
161+
162+
present(nav, animated: true)
163+
}
164+
165+
@objc private func _dismissSheet() {
166+
dismiss(animated: true)
167+
}
168+
124169
private static func makeCell(title: String, onTap: @escaping () -> Void) -> UIView {
125170
let button = UIButton(type: .system)
126171
button.setTitle(title, for: .normal)
@@ -143,6 +188,14 @@ import SwiftUI
143188
import SwiftUISupport
144189
import SwiftUIHosting
145190

191+
private final class SimpleContentViewController: UIViewController {
192+
193+
override func viewDidLoad() {
194+
super.viewDidLoad()
195+
view.backgroundColor = .neonRandom()
196+
}
197+
}
198+
146199
private final class SwiftUIContentViewController: UIViewController {
147200

148201
override func viewDidLoad() {
@@ -178,8 +231,18 @@ private final class ContentViewController: FluidStackController {
178231
) {
179232
self._dismiss = dismiss
180233
super.init()
181-
234+
182235
navigationItem.title = "Rideau"
236+
navigationItem.rightBarButtonItem = UIBarButtonItem(
237+
barButtonSystemItem: .done,
238+
target: nil,
239+
action: nil
240+
)
241+
navigationItem.leftBarButtonItem = UIBarButtonItem(
242+
barButtonSystemItem: .close,
243+
target: nil,
244+
action: nil
245+
)
183246
}
184247

185248
required init?(

Sources/FluidStack/ViewController/FluidViewController.swift

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
102102
}
103103

104104
// MARK: - Functions
105-
105+
106106
@objc
107107
open func triggerFluidPop() {
108108
fluidPop(transition: nil)
@@ -161,7 +161,7 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
161161
}
162162

163163
// MARK: - UIViewController
164-
164+
165165
open override func viewDidLoad() {
166166
super.viewDidLoad()
167167

@@ -179,20 +179,34 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
179179
subscriptions.append(
180180
navigationBar.observe(\.bounds, options: [.initial, .old, .new]) { [weak self] view, _ in
181181
guard let self else { return }
182-
self.additionalSafeAreaInsets.top = view.frame.height
182+
MainActor.assumeIsolated {
183+
self.additionalSafeAreaInsets.top = view.intrinsicContentSize.height + navigation._topPaddingProvider(self)
184+
view.invalidateIntrinsicContentSize()
185+
}
183186
}
184-
)
185187

186-
view.addSubview(navigationBar)
188+
)
187189

190+
subscriptions.append(
191+
navigationBar.observe(\.intrinsicContentSize, options: [.initial, .old, .new]) { [weak self] view, _ in
192+
guard let self else { return }
193+
MainActor.assumeIsolated {
194+
self.additionalSafeAreaInsets.top = view.intrinsicContentSize.height + navigation._topPaddingProvider(self)
195+
view.invalidateIntrinsicContentSize()
196+
}
197+
}
198+
)
199+
188200
navigationBar.translatesAutoresizingMaskIntoConstraints = false
201+
202+
view.addSubview(navigationBar)
189203

190204
NSLayoutConstraint.activate([
191205
navigationBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
192206
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor),
193207
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor),
194208
])
195-
209+
196210
let targetNavigationItem =
197211
navigation.usesBodyViewController
198212
? (content.bodyViewController?.navigationItem ?? navigationItem) : navigationItem
@@ -274,7 +288,10 @@ open class FluidViewController: FluidGestureHandlingViewController, UINavigation
274288

275289
if !state.isTopBarHidden && state.isTopBarAvailable {
276290
topBar.isHidden = false
277-
additionalSafeAreaInsets.top = topBar.frame.height
291+
if case .navigation(let navigation) = configuration.topBar {
292+
additionalSafeAreaInsets.top = topBar.intrinsicContentSize.height + navigation._topPaddingProvider(self)
293+
}
294+
topBar.invalidateIntrinsicContentSize()
278295
} else {
279296
topBar.isHidden = true
280297
additionalSafeAreaInsets.top = 0
@@ -445,19 +462,27 @@ extension FluidViewController {
445462

446463
let _activityHandler: @Sendable @MainActor (Activity<UINavigationBar>) -> Void
447464

465+
let _topPaddingProvider: @Sendable @MainActor (FluidViewController) -> CGFloat
466+
448467
/// Initializer
449468
///
450469
/// - Parameters:
451-
/// - updateNavigationBar: A closure to update the navigation bar with the owner.
470+
/// - displayMode: Controls when the navigation bar is visible.
471+
/// - usesBodyViewController: Whether to use the body view controller's navigation item.
472+
/// - navigationBarClass: The class of navigation bar to use.
473+
/// - topPaddingProvider: A closure that returns additional top padding above the navigation bar.
474+
/// - activityHandler: A closure called when navigation bar lifecycle events occur.
452475
public init<NavigationBar: UINavigationBar>(
453476
displayMode: DisplayMode = .automatic,
454477
usesBodyViewController: Bool = true,
455478
navigationBarClass: NavigationBar.Type,
479+
topPaddingProvider: @escaping @MainActor @Sendable (FluidViewController) -> CGFloat = { _ in 0 },
456480
activityHandler: @escaping @MainActor (Activity<NavigationBar>) -> Void = { _ in }
457481
) {
458482
self.displayMode = displayMode
459483
self.usesBodyViewController = usesBodyViewController
460484
self.navigationBarClass = navigationBarClass
485+
self._topPaddingProvider = topPaddingProvider
461486
self._activityHandler = { activity in
462487
switch activity {
463488
case .didLoad(let controller, let navigationBar):
@@ -467,14 +492,13 @@ extension FluidViewController {
467492
}
468493
}
469494
}
470-
495+
471496
public static let `default`: Self = .init(
472497
displayMode: .automatic,
473498
usesBodyViewController: true,
474499
navigationBarClass: UINavigationBar.self,
475-
activityHandler: { _ in
476-
477-
}
500+
topPaddingProvider: { _ in 0 },
501+
activityHandler: { _ in }
478502
)
479503

480504
}

0 commit comments

Comments
 (0)