29

Recently I wrote a small SwiftUI library that can rotate an array of Views and select view.

Gif-1

But found an issue that I can't figure out how to disable scroll in ScrollView while in normal mode.

Gif-2

I tried to put .disabled(true) at the end of the ScrollView unfortunately, it not only disable scroll but also all the views in ScrollView.

Here's source code of the project.

What modifier should I add to solve this?

--Edited--

I have tried to change the scroll axis but once it becomes [], scrollview will reset its content offset, wondering if there's a way to block scrolling without changing the axis.

--Solved--

At last, I just add a DragGesture() to block scroll event and works fine.

10 Answers 10

58

UPDATE

If your deployment target is iOS 16 (macOS 13) or later, you can use the scrollDisabled modifier to enable or disable scrolling.

ORIGINAL

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 : []
    }
}
Sign up to request clarification or add additional context in comments.

8 Comments

Sorry for not mention this, I have tried this before but once axis set to [], scrollview will reset its content offset and cause some problems to my view. Still, thank you a lot.
hey any progress /solution with that?
@raxabizze you just need to set the frame's height when using a horizontal axis, or the frame's width when using the vertical axis. I had kind of the same issue and setting the frame's height worked out :)
this doesn't work... it still scrolls even when set to [ ]
For the next person that stumbles across this post. This works fine in iOS14. In iOS13 you end up with funny offsets.
|
20

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

1 Comment

Works well, but still struggled with the layout a little, needed fixedSize flag in there and a .offset on my content in the PreventableScrollView [latter part]
18

I think you can use

.simultaneousGesture(DragGesture(minimumDistance: 0), including: .all)

It disables scroll in ScrollView but other gestures still work like tapGesture inside this scrollview, and ScrollViewReader also works

6 Comments

It's like a gem if you're considering using ScrollView to fix any unexpected UI problem. Recently, I ran into a kingfisher image view named KFImage didn't follow its parent's transition in a normal way. To solve this issue, I unintentionally tried to embrace the KFImage with a ScrollView then the problem had gone away. Like my case, if you need to turn off the scroll function of ScrollView (sounds weird though..) for any reason, it's a good approach I think.
Nice approach... But textfields are disabled with this method
Great suggestion! I thought I would not be able to do this on iOS 15 so this is great. Thanks a lot :)
great walkaround for a ScrollView with only labels inside!
This disables the scroll gesture on the ScrollView, but it also disables the scroll gesture on any child views... I know there is a .subviews variant (instead of the .all) for the including param, but it seems to not work in the case of nested Scrollviews... any ideas? CONTEXT: I have a horizontal ScrollView as the parent which I use with a ScrollViewReader to enable programmatic scrolling through full-page items and I want to disable the ability to manually scroll through these. However, some of the full-page items are vertical ScrollViews and I need them to still be scrollable
|
14

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!

1 Comment

This is awesome
11

Xcode 14 / iOS 16

Now we have explicit environment value for this. So a possible solution would be

struct DemoView: View {
    @Binding var shouldScroll: Bool

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            // ... other code
        }
        .environment(\.isScrollEnabled, shouldScroll)   // << here !!
    }
}

Comments

7

I've tried all above and even more. None of solutions works for me. I found next one:

ScrollView(.horizontal) {
    YourContentView
              .introspectScrollView(customize: { scrollView in
                   scrollView.addGestureRecognizer(UIPanGestureRecognizer()) // disable scrollView scroll      
               })
}

Comments

6

with iOS 16 and above it is quite simple:

ScrollView {
...
}
.scrollDisabled(true)

but for iOS versions less it needs a view modifier:

struct DisableScrollingModifier: ViewModifier {
    var disabled: Bool
    
    func body(content: Content) -> some View {
    
        if disabled {
            content
                .simultaneousGesture(DragGesture(minimumDistance: 0))
        } else {
            content
        }
        
    }
}

extension View {
    func scrollingDisabled(_ disabled: Bool) -> some View {
        modifier(DisableScrollingModifier(disabled: disabled))
    }
}

Usage Example:

ScrollView {
    ...
}
.scrollingDisabled(true)

1 Comment

It is disabling the scroll and also it is disabling tap gesture. I want tap gesture to be enabled
1

I went through all the answers and nothing really solved the issue I have been having, so I came up with this solution for those that need to support iOS < 16.

ScrollView([]) {
    YourContentView()
}

Scroll view accepts the first argument as an axis set. If you provide an empty set, that is - just an empty collection, the scroll will be disabled.

This approach comes in handy if you have a view model, in which you can encapsulate this behaviour and change it in runtime.

Comments

1

May be the answer is too late but that it might help somebody, so sharing this. Also I am developing a project with target iOS 14 and above, so have to deal with some old code. Therefore old technique might come handy for someone.

ScrollView([], showsIndicators: false) {
    EmptyView()
}

Just need to put empty set in place of axes of scrollView, thats it.

Comments

-4

That's OK. can try

ScrollView {
}
.allowsTightening(true)
.disabled(true)

1 Comment

Please provide additional details in your answer. As it's currently written, it's hard to understand your solution.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.