How to only disable scroll in ScrollView but not content view?
I tried using the accepted answer provided by Rob Mayoff but the content offset being reset is a frustrating drawback to the suggested approach.
Using the Introspect library (which I would recommend for anyone looking for UIKit level controls for SwiftUI APIs - whilst we wait for SwiftUI 2 at WWDC 2020) - you can access the underlying isScollEnabled
boolean flag of the underlying UIScrollView that powers SwiftUI's ScrollView
and set is accordingly. Changing the isScollEnabled
has no impact on contentOffset and works exactly like it should.
It can be implemented as simply as this:
struct TestView: View {
@Binding var shouldScroll: Bool
var body: some View {
ScrollView {
Text("Your content here").introspectScrollView { scrollView in
scrollView.isScrollEnabled = shouldScroll
}
}
}
}
It really is that simple with the Introspect library!
Only pass .horizontal
as the scroll axis if you want the view to scroll. Otherwise, pass the empty set.
struct TestView: View {
@Binding var shouldScroll: Bool
var body: some View {
ScrollView(axes, showsIndicators: false) {
Text("Your content here")
}
}
private var axes: Axis.Set {
return shouldScroll ? .horizontal : []
}
}
I keep coming back to this question and have not been satisfied with any of the answers here. What I found has worked is to create a custom wrapper that evaluates whether a ScrollView should be used or not. It was surprisingly easier than I thought.
Creating a preventable scroll view
Essentially we want a custom view where scrolling can be toggled. Using a binding, anyone that uses our view will have full control over the scrolling function.
Forgive my naming but PreventableScrollView
was all I could come up with :)
struct PreventableScrollView<Content>: View where Content: View {
@Binding var canScroll: Bool
var content: () -> Content
var body: some View {
if canScroll {
ScrollView(.vertical, showsIndicators: false, content: content)
} else {
content()
}
}
}
Essentially all we need to do is decide whether to embed the content in a ScrollView or just return it directly. Pretty straightforward.
I created a demo to showcase how this might work with dynamic content and the ability to toggle anytime. As I add items it also toggles scrolling.
Here is the code if you are interested.
struct DemoView: View {
@State private var canScroll: Bool = false
let allColors: [UIColor] = [.purple, .systemPink, .systemGreen, .systemBlue, .black, .cyan, .magenta, .orange, .systemYellow, .systemIndigo, .systemRed]
@State var colors: [UIColor] = [.white]
var body: some View {
VStack {
Spacer()
VStack {
PreventableScrollView(canScroll: $canScroll) {
ForEach(colors.indices, id: \.self) { index in
Rectangle().fill().foregroundColor(Color(colors[index]))
.frame(height: 44)
.cornerRadius(8)
}
}.fixedSize(horizontal: false, vertical: true)
HStack {
Spacer()
Toggle(isOn: $canScroll, label: {
Text("Toggle Scroll")
}).toggleStyle(SwitchToggleStyle(tint: .orange))
}.padding(.top)
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.background(Rectangle().fill().foregroundColor(Color(UIColor.secondarySystemFill)).cornerRadius(8))
HStack {
Button(action: {
addField()
}, label: {
Image(systemName: "plus")
}).padding()
}
}
.padding()
}
func addField() {
canScroll.toggle()
colors.append(allColors.randomElement()!)
}
}