SwiftUI animation - animation

I am trying to animate a rectangle only in the x scale without modifying the height using scaleEffect but without any success. Does scaleEffect apply only to both dimension x, y or can we apply it to one dimension only? I tried to apply scaleEffect (x,y) but it doesn't work.
Rectangle()
.fill(Color.pink)
.frame(width: 100, height: 100)
.scaleEffect( self.isAnimating ? 1 : 2)
.animation(Animation.linear(duration: 1))
.onAppear {
self.isAnimating = true
}

You can use scaleEffect with CGSize:
func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View
which will give you the desired effect:
Rectangle()
.fill(Color.pink)
.frame(width: 100, height: 100)
.scaleEffect(isAnimating ? CGSize(width: 2.0, height: 1.0) : CGSize(width: 1.0, height: 1.0))
.animation(Animation.linear(duration: 1))
.onAppear {
isAnimating = true
}

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()
}
}
}

Why is this rotating animation moving vertically?

I am trying to implement a Loader that rotates in the center of the screen:
struct Loader : View {
#State var isRotated = false
var animation: Animation {
Animation.linear
.repeatForever(autoreverses: false)
}
var body: some View {
VStack {
Circle()
.trim(from: 0, to: 0.8)
.stroke(AngularGradient(gradient: .init(colors: [Color(#colorLiteral(red: 0.9069682956, green: 0.7839415669, blue: 0.9612388015, alpha: 1)),.white]), center: .center), style: StrokeStyle(lineWidth: 8, lineCap: .round))
.frame(width: 45, height: 45)
.rotationEffect(Angle.degrees(isRotated ? 360 : 0))
.animation(animation)
}
.onAppear{
self.isRotated.toggle()
}
}
}
I want the loader to rotate in the middle of the screen, but when I run my code the circle moves up the y axis with each rotation. Why is it doing this?

Calculate/Predict the future center of a frame with SwiftUI

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)

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