TL;DR: In iOS 16.4+ (and iOS 26), the optimal solution to fix “non-scrolling content” behavior is the .scrollBounceBehavior(.basedOnSize) modifier. For more complex scenarios requiring layout switching, ViewThatFits is the recommended approach.
By default, a SwiftUI ScrollView always allows the user to drag and bounce the content, even if the content height is smaller than the screen height. In many UI designs, this “always-on” bounce can feel unpolished. This article explores three solutions ranging from native APIs to layout tricks.
Solution 1: The Native Gold Standard (iOS 16.4+)
This is currently the standard practice. Using the scrollBounceBehavior modifier, the system automatically detects the relationship between the content size and the container size.
- Use Case: Most standard lists and scroll views.
- Pros: One line of code, system-level support, optimal performance.
struct BasedOnSizeView: View {
var body: some View {
ScrollView {
VStack {
ForEach(0..<5) { idx in
Text("Item \(idx)")
.frame(maxWidth: .infinity)
.padding()
}
}
}
// Key code: Allow scroll bounce only when content exceeds container size
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
}
}
Solution 2: Layout Switching (ViewThatFits)
If you want to completely change the view structure when content is sparse (e.g., centering the content using a VStack when it fits, but using a ScrollView when it overflows), ViewThatFits is the best choice.
- Use Case: Scenarios requiring completely different layouts for scrolling vs. non-scrolling states.
- Mechanism: It attempts to render the views provided in the closure in order, selecting the first one that “fits” within the available space.
struct AdaptiveScrollView: View {
let contentString: String
var body: some View {
// 1. Try presenting content directly first (no scrolling)
// 2. Fallback to ScrollView if height overflows
ViewThatFits(in: .vertical) {
textContent
ScrollView {
textContent
}
}
.frame(height: 200)
.border(.gray)
}
var textContent: some View {
Text(contentString)
.padding()
.fixedSize(horizontal: false, vertical: true) // Ensure text expands vertically
}
}
Solution 3: Low-Level Control (Introspect)
In rare scenarios where you need granular control via underlying UIScrollView properties (such as completely disabling gesture interaction via isScrollEnabled = false rather than just disabling the bounce), you can use the Introspect library.
Note: In the era of Swift 6 and iOS 26, the first two native solutions cover 99% of requirements. This should be considered a last resort.
import SwiftUIIntrospect
ScrollView {
// Content...
}
.introspect(.scrollView, on: .iOS(.v16, .v17, .v18, .v26)) { scrollView in
// Manual logic to check content size vs bounds
if scrollView.contentSize.height <= scrollView.bounds.height {
scrollView.isScrollEnabled = false
} else {
scrollView.isScrollEnabled = true
}
}
Summary
| Approach | API Requirement | Complexity | Recommendation |
|---|---|---|---|
| scrollBounceBehavior | iOS 16.4+ | ⭐ | ⭐⭐⭐⭐⭐ |
| ViewThatFits | iOS 16.0+ | ⭐⭐ | ⭐⭐⭐⭐ |
| Introspect | 3rd Party Lib | ⭐⭐⭐ | ⭐⭐ |