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!
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
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)
}
}
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 CGPoint
s instead of CGSize
to implement it horizontally and vertically.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With