Getting and setting the position of content in a SwiftUI ScrollView - uiscrollview

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 CGPoints instead of CGSize to implement it horizontally and vertically.

Related

SwiftUI: Animated rotation in different sides

At first look question can seen ordinary but after spend few hour try out different techniques I still failed to achieve expected behavior... I want to control the rotation side of View depending in #State. If view in default position (chevron up) it should rotate down from the right side. If chevron is down it should rotate up from the left side.
And one more thing I discovered: in my work project I use Image instead SF Symbol with the same modification like in code example and by default it always rotates from the right side, but using SF Symbol it's going from left. I will be grateful if you explain why
struct Arrow: View {
#State var showView = false
var body: some View {
VStack {
Image(systemName: "chevron.up.circle")
.resizable()
.frame(width: 50, height: 50)
.rotationEffect(.degrees(showView ? 180 : 360))
.animation(Animation.easeInOut(duration: 0.3), value: showView)
}
.onTapGesture { showView.toggle() }
}
}
To always rotate in the same direction, maintain a rotationAngle that always increases (or always decreases) while animating:
struct ContentView: View {
#State private var showView = false
#State private var rotationAngle: Double = 0
var body: some View {
VStack {
Image(systemName: "chevron.up.circle")
.resizable()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: rotationAngle))
}
.onTapGesture {
showView.toggle()
withAnimation(.easeInOut(duration: 0.3)) {
rotationAngle += 180
}
rotationAngle = showView ? 180 : 0
}
}
}
Note: Use rotationAngle -= 180 to rotate in the other direction.

Add a circular image view with corner radius for an image in swiftUI

How can I add a circular view similar to the attachment below. In the attachment, the check is the image icon and I want to add the green background color in circular shape. I have a solution in Swift but, couldn't implement the same in swiftUI.
Related posts to my question: Add a border with cornerRadius to an Image in SwiftUI Xcode beta 5. But, this doesn't solve my issue.
Swift code to this implemention:
var imageView = UIImageView()
override init(theme: Theme) {
super.init(theme: theme)
imageView.clipsToBounds = true
setLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
let cornerRadius = frame.height / 2
imageView.setCornerRadius(cornerRadius)
setCornerRadius(cornerRadius)
}
You could create this image like...
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.white)
.padding(20)
.background(Color.green)
.clipShape(Circle())
Or alternatively...
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 40, height: 40) // put your sizes here
.foregroundColor(.green)
This is not the simplest thing to come up with. Use this struct as a separate view. It will return the image properly sized on the circle.
struct ImageOnCircle: View {
let icon: String
let radius: CGFloat
let circleColor: Color
let imageColor: Color // Remove this for an image in your assets folder.
var squareSide: CGFloat {
2.0.squareRoot() * radius
}
var body: some View {
ZStack {
Circle()
.fill(circleColor)
.frame(width: radius * 2, height: radius * 2)
// Use this implementation for an SF Symbol
Image(systemName: icon)
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.frame(width: squareSide, height: squareSide)
.foregroundColor(imageColor)
// Use this implementation for an image in your assets folder.
// Image(icon)
// .resizable()
// .aspectRatio(1.0, contentMode: .fit)
// .frame(width: squareSide, height: squareSide)
}
}
}

Animating changes to #FocusState SwiftUI

I am using the new #FocusState to control how my views react to the user deciding to start inputting information into text fields. My current need is to wrap an animation around my top view leaving the screen as the keyboard moves up. Usually this kind of thing can be accomplished by simply wrapping withAnimation() around a boolean toggle, but since Swift is toggling my focus state bool under the hood, I can't wrap an animation around it in this way. How else should I do it?
Here is a minimal reproducible example. Basically I want to animate the top (red) view leaving / coming back into view with changes to my focus state isFocused var.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
var body: some View {
ZStack {
VStack {
if !isFocused {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
.animation(.easeInOut(duration: 5), value: isFocused)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}
I don't think the animation modifier that's currently on the top view is doing anything, but I imagine that that's where I'll put some animation code.
Here is something that works. I've done this before to make an animation happen upon an #FocusState property changing its value. Can't really tell you why though, it's just something I figured out with trial and error.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
#State private var showRedView = false
var body: some View {
ZStack {
VStack {
if !showRedView {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
.onChange(of: isFocused) { bool in
withAnimation(.easeInOut(duration: 5)) {
showRedView = bool
}
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}

Can't Animate or Transition SwiftUI Images in Sequence

I have an app with a view where I show images taken with the device's camera (stored
with Core Data) and I want to create the effect of time lapse - say you take photos
of a flower over a period of time and want to show those images in a stream. I have
been able to do that - but I want to have some transition effects between the images
and have not been able to do so. I can apply rotation and a static scale but cannot
fade from 0 to 1 opacity, nor scale up from zero to full frame. I must be missing
something really simple. Here's the code using an array of photos instead of Core Data
for simplicity.
struct ContentView: View {
#State private var activeImageIndex = 0
#State private var startTimer = false
#State private var myAnimationBool = true
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
let myShots = ["CoolEquipment", "FishAndChipsMedium", "FlatheadLake1", "GlacierBusMedium", "Hike", "HuckALaHuckMedium", "JohnAndRiverMedium", "MacDonaldLodgeLakesideMedium"
]
var body: some View {
GeometryReader { geo in
VStack {
Text("Sequence")
.foregroundColor(.blue)
.font(.system(size: 25))
Image(self.myShots[self.activeImageIndex])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
.cornerRadius(20)
.shadow(radius: 10, x: 10, y: 10)
//None of these do anything
//.animation(.easeInOut(duration: 0.5))
//.animation(.easeInOut)
//.transition(.opacity)
//.transition(self.myAnimationBool ? .opacity : .slide)
//.transition(.scale)
//.transition(AnyTransition.opacity.combined(with: .slide))
//this works but is static
//.scaleEffect(self.myAnimationBool ? 0.5 : 1.0)
//this works but again is static
//.rotationEffect(.degrees(self.myAnimationBool ? 90 : 0))
.onReceive(self.timer) { t in
print("in fixed timer")
if self.startTimer {
self.activeImageIndex = (self.activeImageIndex + 1) % self.myShots.count
}
}
HStack {
Button(action: {
self.startTimer.toggle()
}) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 200, height: 40)
Text(self.startTimer ? "Stop" : "Start").font(.headline)
}
}
}
.padding()
}//top VStack
}.onDisappear{ self.startTimer = false }
}
}
Any guidance would be appreciated. Xcode 11.3 (11C29)

GeometryReader Size Calculations SwiftUI

I am attempting to use the GeometryReader to place a view. I used the GeometryReader to
modify a Rectangle() and it is placed as I expected. However, I want to programmatically
set the y value to line up with the top of the VStack frame. I thought I could read
the geometry height of the text field and use it, but I cannot assign the geometry
dimension of the textField to a variable, even though I use the same concept to move
the Rectangle.
Pictorially:
The simple code:
struct ContentView: View {
#State private var textHeight = 0.0
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Text("Title Text")
}
//self.textHeight = CGFloat(geometry.size.height)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50,
y: 0, width: geometry.size.width / 2.0,
height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
Xcode Beta 7, Catalina Beta 7
Any guidance would be appreciated.
The problem is that you're using a VStack with two GeometryReader inside. The VStack positions its children vertically, one above the other. The y=0 of your second GeometryReader is the first y after the first GeometryReader. I suggest you change the way you're designing your UI, for example you can get the same result this way:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack {
ZStack {
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: 0, width: geometry.size.width / 2.0, height: geometry.size.height / 4))
.fill(Color.red)
Text("Title Text")
.frame(width: geometry.size.width, height: geometry.size.height/2.0)
.border(Color.green)
}
Spacer()
}
}
.frame(width: 150, height: 300).border(Color.black)
}
}
The result is:
To push things up and down or to the left/right you can use the really useful Spacer view.
This is my advice, but if you really need to use two GeometryReader you have to do this way:
struct ContentView2: View {
#State private var textHeight = CGFloat(0)
private func firstContentView(geometry: GeometryProxy) -> some View {
textHeight = geometry.size.height
return Text("Title Text")
.position(x: geometry.size.width/2.0, y: geometry.size.height/2.0)
}
var body: some View {
VStack(spacing: 0) {
GeometryReader { geometry in
self.firstContentView(geometry: geometry)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: -self.textHeight, width: geometry.size.width / 2.0, height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
But it's a pretty dirty solution.

Resources