Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting and setting the position of content in a SwiftUI ScrollView

I have a horizontal scroll view where I'd like to set a position programmatically. Here is the body of the view:

let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)

...

    var body: some View {
        GeometryReader { viewGeometry in
            ScrollView () {
                VStack(spacing: 0) {
                    Spacer(minLength: spacing)

                    Circle()
                        .fill(Color.black.opacity(0.5))
                        .scaledToFit()
                        .frame(width: circleDiameter, height: circleDiameter)

                    ScrollView(.horizontal) {
                        Text("The quick brown fox jumps over the lazy dog. Finalmente.")
                            .font(Font.title)
                            .frame(width: scrollWidth, height: scrollHeight)
                            .foregroundColor(Color.white)
                            .background(Color.white.opacity(0.25))
                    }
                    .frame(width: viewGeometry.size.width, height: scrollHeight)
                    .padding([.top, .bottom], spacing)

                    Circle()
                        .fill(Color.white.opacity(0.5))
                        .scaledToFit()
                        .frame(width: circleDiameter, height: circleDiameter)

                    Spacer(minLength: spacing)
                }
                .frame(width: viewGeometry.size.width)
            }
            .background(Color.orange)
        }
        .frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
        .cornerRadius(radius)
        .background(Color.black)
    }

How do I change this code so that I can get the current position of "The quick brown fox" and restore it at a later time? I'm just trying to do something like we've always done with contentOffset in UIKit.

I can see how a GeometryReader might be useful to get the content's current frame, but there's no equivalent writer. Setting a .position() or .offset() for the scroll view or text hasn't gotten me anywhere either.

Any help would be most appreciated!

like image 242
chockenberry Avatar asked Sep 13 '19 20:09

chockenberry


3 Answers

I've been playing around with a solution and posted a Gist to what I have working in terms of programmatically setting content offsets https://gist.github.com/jfuellert/67e91df63394d7c9b713419ed8e2beb7

like image 105
jfuellert Avatar answered Nov 20 '22 01:11

jfuellert


With the regular SwiftUI ScrollView, as far as I can tell, you can get the position with GeometryReader with proxy.frame(in: .global).minY (see your modified example below), but you cannot set the "contentOffset".

Actually if you look at the Debug View Hierarchy you will notice that our content view is embedded in an internal SwiftUI other content view to the scrollview. So you will offset vs this internal view and not the scroll one.

After searching for quite a while, I could not find any way to do it with the SwiftUI ScrollView (I guess will have to wait for Apple on this one). The best I could do (with hacks) is a scrolltobottom.

UPDATE: I previously made a mistake, as it was on the vertical scroll. Now corrected.

class SGScrollViewModel: ObservableObject{
    var scrollOffset:CGFloat = 0{
        didSet{
            print("scrollOffset: \(scrollOffset)")
        }
    }

}


struct ContentView: View {
    public var scrollModel:SGScrollViewModel = SGScrollViewModel()

    let radius = CGFloat(25)
     let scrollWidth = CGFloat(700)
     let scrollHeight = CGFloat(100)
     let spacing = CGFloat(20)
     let circleDiameter = CGFloat(50)

         var body: some View {
            var topMarker:CGFloat = 0
            let scrollTopMarkerView =  GeometryReader { proxy -> Color in
                topMarker = proxy.frame(in: .global).minX
                return Color.clear
            }
            let scrollOffsetMarkerView =  GeometryReader { proxy -> Color in
                self.scrollModel.scrollOffset = proxy.frame(in: .global).minX - topMarker
                return Color.clear
            }

            return GeometryReader { viewGeometry in                    
                 ScrollView () {
                     VStack(spacing: 0) {

                        Spacer(minLength: self.spacing)

                         Circle()
                             .fill(Color.black.opacity(0.5))
                             .scaledToFit()
                            .frame(width: self.circleDiameter, height: self.circleDiameter)
                         scrollTopMarkerView.frame(height:0)
                         ScrollView(.horizontal) {
                             Text("The quick brown fox jumps over the lazy dog. Finally.")
                                 .font(Font.title)
                                .frame(width: self.scrollWidth, height: self.scrollHeight)
                                 .foregroundColor(Color.white)
                                 .background(Color.white.opacity(0.25))
                                 .background(scrollOffsetMarkerView)
                         }
                         .frame(width: viewGeometry.size.width, height: self.scrollHeight)
                         .padding([.top, .bottom], self.spacing)

                         Circle()
                             .fill(Color.white.opacity(0.5))
                             .scaledToFit()
                            .frame(width: self.circleDiameter, height: self.circleDiameter)

                        Spacer(minLength: self.spacing)
                     }
                     .frame(width: viewGeometry.size.width)
                 }
                 .background(Color.orange)
             }
             .frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
             .cornerRadius(radius)
             .background(Color.black)
         }
}
like image 22
Fabrice Leyne Avatar answered Nov 20 '22 03:11

Fabrice Leyne


I messed around with several solutions involving a ScrollView with one or more GeometryReaders, but ultimately I found everything easier if I just ignored ScrollView and rolled my own using View.offset(x:y:) and a DragGesture:

This allows the LinearGradient to be panned by either dragging it like a ScrollView, or by updating the binding, in the case via a Slider

struct SliderBinding: View {
    @State var position = CGFloat(0.0)
    @State var dragBegin: CGFloat?

    var body: some View {
        VStack {
            Text("\(position)")
            Slider(value: $position, in: 0...400)
            ZStack {
                LinearGradient(gradient: Gradient(colors: [.blue, .red]) , startPoint: .leading, endPoint: .trailing)
                    .frame(width: 800, height: 200)
                    .offset(x: position - 400 / 2)
            }.frame(width:400)
            .gesture(DragGesture()
                        .onChanged { gesture in
                            if (dragBegin == nil) {
                                dragBegin = self.position
                            } else {
                                position = (dragBegin ?? 0) + gesture.translation.width
                            }
                        }
                        .onEnded { _ in
                            dragBegin = nil
                        }
            )
        }
        .frame(width: 400)
    }
}

Clamping the drag operation to the size of the scrolled area is omitted for brevity. This code allows horizontal scrolling, use CGPoints instead of CGSize to implement it horizontally and vertically.

like image 2
p10ben Avatar answered Nov 20 '22 03:11

p10ben