2

Can someone explain the behavior of this ScrollView test code -- why I can't scroll to the left and top edges, and why I can scroll beyond the right and bottom edges? And how to fix this. (Note that if we remove VStack the behavior does not change.)

    var body: some View {

        ScrollView.init([.vertical,.horizontal]) {

            VStack {
                Text("AAA")
                    .padding(8)
                    .frame(width: 1024, height: 1024)
                    .background(Color.orange)
                    .border(Color.red)
            }

        }
        .border(Color.blue)

    }

In this image is as far to the left and up as I can scroll -- it doesn't scroll to the edges: enter image description here

But here I can scroll beyond the bottom and right edges: enter image description here enter image description here

6
  • by default content is not aligned in two-dimensional scrollview - how do you want to align it? Commented Mar 6, 2020 at 19:37
  • It is aligned, the text view appears in the center of the screen. Problem is that you can't scroll to the top and left edges of the content, and you can scroll beyond the right and bottom edges of the content. Commented Mar 6, 2020 at 20:03
  • You might find helpful this topic Commented Mar 6, 2020 at 20:11
  • That's a complicated workaround to a badly understood problem identical to mine. So what you're saying is that ScrollView currently has a bug, and this is a work around for it. Commented Mar 6, 2020 at 20:23
  • andrewz, please reduce the size of images (or buy me a bigger screen) Commented Mar 6, 2020 at 21:01

3 Answers 3

1

I have adapted @Asperi's answer to another question (SwiftUI How to align a view that's larger than screen width), and this works.

    @State private var offset : CGPoint = .zero

    var body: some View {

        ScrollView.init([.vertical,.horizontal]) {

            VStack {
                Text("AAA")
                    .padding(8)
                    .frame(width: 1024, height: 1024)
                    .background(Color.orange)
                    .border(Color.red)
            }
            .background(rectReader())
            .offset(x: self.offset.x, y: self.offset.y)

        }
        .border(Color.blue)

    }

    func rectReader() -> some View {
        return GeometryReader { (geometry) -> AnyView in
            let offset : CGPoint = CGPoint.init(
                x: -geometry.frame(in: .global).minX,
                y: -geometry.frame(in: .global).minY
            )
            if self.offset == .zero {
                DispatchQueue.main.async {
                    self.offset = offset
                }
            }
            return AnyView(Rectangle().fill(Color.clear))
        }
    }
Sign up to request clarification or add additional context in comments.

Comments

1

Or you can try to use one ScrollView for each direction, try this

import SwiftUI

struct ContentView: View {
    var body: some View {

        ScrollView(.horizontal, showsIndicators: true) {
            ScrollView(.vertical, showsIndicators: true) {
                Color.yellow.frame(width: 1024, height: 1024)
                    .overlay(Text("A").font(.largeTitle), alignment: .topLeading)
                    .overlay(Text("B").font(.largeTitle), alignment: .topTrailing)
                    .overlay(Text("C").font(.largeTitle), alignment: .bottomTrailing)
                    .overlay(Text("D").font(.largeTitle), alignment: .bottomLeading)
            }
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

UPDATE

to be able to see, what happens using .offset modifier, try imagine the result of this snippet

import SwiftUI

struct ContentView: View {
    var body: some View {

        HStack {
            Text("Hello").padding().background(Color.yellow).offset(x: 20, y: 40).border(Color.red)
            Text("World").padding().background(Color.pink).offset(x: -10, y: -10).border(Color.yellow)
            }.padding().border(Color.gray)

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

6 Comments

I like this solution but it does not exactly replicate the behavior -- here you're either scrolling horizontally or vertically, but not both at the same time. Still, this is a reasonable alternative without adding complicated code.
@andrewz the motivation is different, please see stackoverflow.com/questions/60407125/… the .offset modifier DOESN'T CHANGE the layout or position. The same is true for any View modifier which change the background or overlay etc.
The text rectangles are offset from the alignment calculated by HStack. I expected that the borders would be aligned with the text rectangles, but they're not. Hm, looks like the HStack alignment is calculated on the final view, ie border, and then calculation propagates back to the Text views, hitting offset on the way. So it's like a reverse traversal of the view modifier chain.
@andrewz the position and bounds are not affected! and finally, add ..clipped() on HStack! And exactly same is done with Asperi's solution. It could work, but not necessary in all situations.
Clearly bounds are not affected. The .offset() modifier changes the offset on its view -- the offset is an additional positional translation that can be applied on that view. I think what is interesting is that HStack actually aligns the product of the .border() modifier, and .offset() affects the view produced by the .background() modifier.
|
0

I came across this question and answers as I was today looking for a solution of this "content view miss-placement problem" for many hours as I'm trying to scroll a (zoomed) image in any direction.

Neither of the solutions proposed here are "nice" or applicable in my case. But after finding a solution for myself, I want to document here how I would solve your problem.

The main problem is that the center of your content view (=VStack with Text element) is aligned with the center of the ScrollView. This is always the case: whether the content size is smaller or bigger than the ScrollView size. (Remember: The ScrollView size corresponds to the screen size, minus safe area edge insets). To align your content views top leading edge with the scrollviews top leading edge, you will have to offset your content by the amount which it is currently overlapping on the left side.

The good thing: it can be calculated directly using the GeometryReader and the known content size.

    var body: some View {
        GeometryReader { proxy -> AnyView in

            let insets = proxy.safeAreaInsets
            let sw = proxy.size.width + insets.leading + insets.trailing
            let sh = proxy.size.height + insets.top + insets.bottom

            let contentSize : CGFloat = 1024
            let dx: CGFloat = (contentSize > sw ? (contentSize - sw)/2 : 0.0)
            let dy: CGFloat = (contentSize > sh ? (contentSize - sh)/2 : 0.0)

            return AnyView(
                ScrollView([.vertical,.horizontal]) {
                    VStack {
                        Text("AAA")
                            .padding(8)
                            .frame(width: 1024, height: 1024)
                            .background(Color.orange)
                            .border(Color.red)
                    }
                    .offset(x: dx, y: dy)
                }
                .border(Color.blue)
            )
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.