Calculate/Predict the future center of a frame with SwiftUI - animation

Swift 5.2, iOS 13
I want to predict/calculate the centre point of a frame after it has been scaled so I can move a shape to it while scaling it. If I try to scale and center an view in an animation/dynamically it doesn't work, with the end result a combination of center points I suspect. So in the images below, the blue box starts in the top right hand corner, I scale and move it to the center. But as you can see from the green box, the scaling has messed up the point it needs to get to...
struct SwiftUIView2: View {
#State var relocate = Alignment.topTrailing
#State var zoom:CGFloat = 1.0
#State var tag:Bool = true
#State var tag2:Bool = false
#State var centerPoint: CGPoint = .zero
var body: some View {
ZStack {
ZStack(alignment: relocate) {
Rectangle()
.stroke(Color.blue, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
withAnimation {
if self.tag {
self.zoom = 2.0
self.relocate = Alignment.center
} else {
self.relocate = Alignment.topTrailing
self.zoom = 1.0
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tag.toggle()
}
}
.scaleEffect(zoom, anchor: .topLeading)
}.frame(width: 256, height: 256, alignment: relocate)
.border(Color.red)
if !tag {
ZStack {
Rectangle()
.stroke(Color.green, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
self.tag.toggle()
}
}.frame(width: 256, height: 256, alignment: .center)
.scaleEffect(zoom, anchor: .center)
}
}
}
}
GeoReader I thought would be the answer, but I get garbage from it too. Spent more than a week trying custom alignments, position, everything I can think of. Eyeballed a solution with screen site percentages for now, but obviously it doesn't work too well with different sized screens.

Here is fix (tested with Xcode 11.4 / iOS 13.4)
}
.scaleEffect(zoom) // << here !! (remove topLeading anchor)
}.frame(width: 256, height: 256, alignment: relocate)

Related

SwiftUI synchronizing multiple SwiftUI animations with delays

I am trying to build a view with multiple items that should animate in sync with each other; for example, if one item is at 25%, the other three should be 50%, 75%, and 100%.
The naive option of just using .delay(delay) seems to work perfectly at first, but once I switch apps, the different animations are all reset to be perfectly offset (without any offsets anymore).
I've tried a number of hacky workarounds, including:
Toggling the whole animated container when the view is backgrounded/restored; this doesn't work. (I think because the container is kept around along with animation states even if it's not visible.)
Using DispatchQueue.main.asyncAfter(); this doesn't work any differently to the animation (since I presume it's the internal animation state itself being lost.)
Note that I am not at all invested in this particular solution; any suggestions on a better way to accomplish this problem are very welcome.
Before switching apps
After switching apps
struct MyRepeatingItemView: View {
var delay: Double
#State
var isTranslated = false
var body: some View {
Color.red
.opacity(0.2)
.frame(width: 256, height: 256)
.scaleEffect(isTranslated ? 1 : 0.01)
.onAppear {
withAnimation(Animation.linear(duration: 4).repeatForever(autoreverses: false).delay(delay)) {
isTranslated.toggle()
}
}
}
}
struct MyRepeatingContainerView: View {
var body: some View {
ZStack {
Color.blue
.frame(width: 256, height: 2)
Color.blue
.frame(width: 2, height: 256)
MyRepeatingItemView(delay: 0.0)
MyRepeatingItemView(delay: 1.0)
MyRepeatingItemView(delay: 2.0)
MyRepeatingItemView(delay: 3.0)
}
}
}
The fix for the out of sync views is to put them all in one view and that view syncs everything simultaneously. It took me some time to work out the pattern, but here it is:
struct MyRepeatingItemView: View {
#State var isTranslated = false
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 256, height: 256)
.opacity(isTranslated ? 0 : 0.25)
.scaleEffect(isTranslated ? 1 : 0.75)
Rectangle()
.fill(Color.red)
.frame(width: 256, height: 256)
.opacity(0.25)
.scaleEffect(isTranslated ? 0.75 : 0.5)
Rectangle()
.fill(Color.red)
.frame(width: 256, height: 256)
.opacity(0.25)
.scaleEffect(isTranslated ? 0.5 : 0.25)
Rectangle()
.fill(Color.red)
.frame(width: 256, height: 256)
.opacity(0.25)
.scaleEffect(isTranslated ? 0.25 : 0)
}
.onAppear {
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
isTranslated.toggle()
}
}
}
}
struct MyRepeatingContainerView: View {
var body: some View {
ZStack {
Color.blue
.frame(width: 256, height: 2)
Color.blue
.frame(width: 2, height: 256)
MyRepeatingItemView()
}
}
}

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)

SwiftUI pin Image to top right of screen and resize based on device size

I just learning Apple's SwiftUI and it is a bit frustrating even doing the basics. I am trying to get my image to the top right of my screen but the default has it showing up in the center of my screen. Anyone know how I can accomplish this. In addition, I tried to get the image to resize based on the screen size but the old self.frame.width that I used to use in regular Swift doesn't work in Swift UI. Sorry, but this new way of writing code in SwiftUI is very odd to me.
var body: some View {
ZStack {
//Define a screen color
LinearGradient (gradient: Gradient(colors:[Color(ColorsSaved.gitLabDark),Color(ColorsSaved.gitLabLight)]),startPoint: .leading,endPoint: .trailing)
//Extend the screen to all edges
.edgesIgnoringSafeArea(.all)
VStack(alignment: .trailing) {
Image("blobVectorDark")
.resizable()
.edgesIgnoringSafeArea(.top)
.frame(width: 60, height: 60)
// .offset(x: , y: 20)
}
}
}
}
You need to add a frame for the outer container, here is the VStack.
Then assign the alignment for this container.
The width and height in the frame of VStack needs to use geometryProxy.
GeometryReader{ (proxy : GeometryProxy) in // New Code
VStack(alignment: .trailing) {
Image("blobVectorDark")
.resizable()
.edgesIgnoringSafeArea(.top)
.frame(width: 60, height: 60)
// .offset(x: , y: 20)
}
.frame(width: proxy.size.width, height:proxy.size.height , alignment: .topLeading) // New Code
}
I'm learning too so this might not be the ideal way to do it, but I did manage to get the image to pin to the top right of the screen.
You can get the screen width from GeometryReader. The alignment was driving me nuts for a while, until I realized I could give a frame to a Spacer to take up the other side of an HStack, and then it worked.
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
Spacer()
.frame(width: geometry.size.width / 2)
Image("blobVectorDark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geometry.size.width / 2)
}
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
}

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!
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.

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