A SwiftUI sample project demonstrating how to pass button functionality from a list view to the NavigationBar using GeometryReader and related techniques.
This project showcases advanced SwiftUI layout techniques, specifically focusing on GeometryReader and the modern onGeometryChange() modifier to dynamically control UI elements based on scroll position and view geometry. The main implementation is found in BlogListView, which demonstrates how to create a collapsing navigation bar effect based on scroll position.
- Dynamic Navigation Bar: Navigation bar elements that respond to scroll position
- GeometryReader Integration: Demonstrates practical use of GeometryReader for layout calculations
- Scroll Position Detection: Detects when content crosses specific thresholds
- Smooth Animations: Animated transitions based on geometry changes
The project demonstrates two approaches to monitoring view geometry:
GeometryReader { geo in
let minY = geo.frame(in: .global).minY
Color.clear
.onChange(of: minY) { _, newValue in
withAnimation(.easeInOut(duration: 0.15)) {
isCollapsed = newValue < 100
}
}
}
.frame(height: 1)This approach uses GeometryReader to monitor the global Y position of a view anchor. When the view scrolls past a threshold (100 points), it triggers a state change that can affect the navigation bar appearance.
Key Points:
GeometryReaderprovides access to the view's geometry through aGeometryProxy.frame(in: .global)returns the view's position in global coordinate spaceonChange(of:)monitors changes to the calculated position- A transparent
Color.clearacts as an invisible anchor point
Color.clear
.frame(height: 1)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .global).minY
} action: { newMinY in
withAnimation(.easeInOut(duration: 0.15)) {
isCollapsed = newMinY < 100
}
}The onGeometryChange() modifier provides a cleaner, more performant alternative:
Advantages:
- No need for nested
GeometryReaderstructure - Doesn't affect layout hierarchy
- Better performance with lazy containers
- More declarative syntax
- Only triggers when the monitored value actually changes
The BlogListView serves as the main demonstration component, implementing:
- Scroll Detection: Monitors scroll position using geometry tracking
- State Management: Uses
@Stateto control UI element visibility - Conditional Rendering: Shows/hides navigation elements based on scroll position
- Smooth Transitions: Applies animations to state changes for polished UX
The project demonstrates understanding of SwiftUI's coordinate spaces:
.global: Screen-level coordinate system.local: Parent view's coordinate system.named("custom"): Custom coordinate spaces for specific use cases
- iOS 14.0+ (for GeometryReader + onChange)
- iOS 18.0+ (for onGeometryChange - optional)
- Xcode 13.0+
- Swift 5.5+
To implement similar functionality in your project:
- Add a geometry anchor at the top of your scrollable content
- Monitor the anchor's position using either approach
- Update state based on position thresholds
- Apply animations for smooth transitions
struct MyListView: View {
@State private var isNavigationCollapsed = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
// Anchor point for scroll detection
Color.clear
.frame(height: 1)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .global).minY
} action: { minY in
withAnimation {
isNavigationCollapsed = minY < 100
}
}
// Your content here
ForEach(items) { item in
ItemRow(item: item)
}
}
}
.navigationTitle("My List")
.toolbar {
if !isNavigationCollapsed {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Action") {
// Action
}
}
}
}
}
}
}- Creates a container that takes up all available space
- Can cause unexpected layout behavior in List and LazyVStack
- Triggers view updates on every geometry change
- Doesn't affect layout hierarchy
- Only triggers on actual value changes (Equatable-based)
- Optimized for use with lazy containers
- Reduces unnecessary view updates
- Use Minimal Height: Keep geometry tracking views small (
height: 1) - Threshold-Based Logic: Use clear thresholds for state changes
- Animation Consistency: Apply consistent animation durations
- iOS Version Checking: Provide fallbacks for older iOS versions
- Avoid Overuse: Only track geometry when necessary
GeometoryReaderSample/
├── BlogListView.swift # Main demonstration view
├── ContentView.swift # Entry point
└── Supporting Files/
└── Assets.xcassets
- Understanding SwiftUI's layout system
- Coordinate space transformations
- View geometry calculations
- Using @State for view updates
- Binding and data flow
- Animation integration
- onGeometryChange() for iOS 18+
- PreferenceKey for complex scenarios
- Custom layout protocols
If you're updating existing code to use the modern approach:
Before (iOS 14-17):
GeometryReader { geo in
let value = geo.frame(in: .global).minY
Color.clear.onChange(of: value) { _, newValue in
// Handle change
}
}After (iOS 18+):
Color.clear
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .global).minY
} action: { newValue in
// Handle change
}Contributions are welcome! Please feel free to submit a Pull Request.
This project is available under the MIT license. See the LICENSE file for more info.
Kaname Noto (@notoroid)
This sample demonstrates practical implementation patterns for:
- Dynamic navigation bar behavior
- Scroll-based UI transformations
- Modern SwiftUI geometry tracking techniques
- Performance optimization in list views
Note: Replace the screenshot placeholders with actual screenshots from your running application to complete the documentation.

