I have an image that I apply a 360 rotation on to have the effect of loading/spinning. It works fine until I add Text underneath, the image still spins but it bounces vertically.
Here is code to see it:
import SwiftUI
#main
struct SpinnerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var isAnimating = false
#State var text = ""
var animation: Animation {
Animation.linear(duration: 3.0)
.repeatForever(autoreverses: false)
}
var body: some View {
HStack {
Spacer()
VStack {
Circle()
.foregroundColor(Color.orange)
.frame(height: 100)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? animation : .default)
.onAppear { self.isAnimating = true }
.onDisappear { self.isAnimating = false }
if self.text != "" {
Text(self.text)
}
}
Spacer()
}
.background(Color.gray)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now()+4, execute: {
self.text = "Test"
})
})
}
}
I replaced the image with a Circle so you won't be able to see the spinning/animation, but you can see the circle bouncing vertically once we set the text. If the text was there from the beginning and it didn't change then all is fine. The issue only happens if the text is added later or if it got updated at some point.
Is there a way to fix this?
Just link animation to dependent state value, like
//... other code
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? animation : .default, value: isAnimating) // << here !!
//... other code
Try using explicit animations instead, with withAnimation. When you use .animation(), SwiftUI sometimes tries to animate the position of your views too.
struct ContentView: View {
#State var isAnimating = false
#State var text = ""
var animation: Animation {
Animation.linear(duration: 3.0)
.repeatForever(autoreverses: false)
}
var body: some View {
HStack {
Spacer()
VStack {
Circle()
.foregroundColor(Color.orange)
.overlay(Image(systemName: "plus")) /// to see the rotation animation
.frame(height: 100)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.onAppear {
withAnimation(animation) { /// here!
self.isAnimating = true
}
}
.onDisappear { self.isAnimating = false }
if self.text != "" {
Text(self.text)
}
}
Spacer()
}
.background(Color.gray)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now()+4, execute: {
self.text = "Test"
})
})
}
}
Result:
Related
I'm trying to change a view in response to changing a Toggle. I figure out a way to do it as long as I mirror the state value the toggle is using.
Is there a way to do it without creating another variable?
Here's my code. You can see that there are three animations triggered.
import SwiftUI
struct OverlayView: View {
#State var toggle: Bool = false
#State var animatedToggle: Bool = false
var body: some View {
VStack {
Image(systemName: "figure.arms.open")
.resizable()
.scaledToFit()
.foregroundColor(animatedToggle ? .green : .red) // << animated
.opacity(animatedToggle ? 1 : 0.2) // << animated
if animatedToggle { // << animated
Spacer()
}
Toggle("Overlay", isOn: $toggle)
.onChange(of: toggle) { newValue in
withAnimation {
animatedToggle = toggle
}
}
}
}
}
Just use the .animation view modifier
.animation(.default, value: toggle)
Then remove the second variable
struct OverlayView: View {
#State var toggle: Bool = false
var body: some View {
VStack {
Image(systemName: "figure.arms.open")
.resizable()
.scaledToFit()
.foregroundColor(toggle ? .green : .red)
.opacity(toggle ? 1 : 0.2)
if toggle {
Spacer()
}
Toggle("Overlay", isOn: $toggle)
}.animation(.default, value: toggle)
}
}
I am trying to run the following transition in a View but does not work as expected. The HELLO WORLD Text should be at first visible. Then on tap of the button it should scale based on the private var zoomOutTransition. The transition is triggered but i see a second Text staying on the screen. Any help appreciated!!!
import SwiftUI
public struct SimpleAnimation: View {
#State
private var scaleEightImage = false
private var zoomOutTransition: AnyTransition {
return .asymmetric(
insertion: .identity.animation(nil),
removal: .scale(
scale: 1000, anchor: UnitPoint(x: 0.494, y: 0.05))
.animation(
.easeInOut(duration: 10.0)
.delay(0.1)
)
)
}
public var body: some View {
ZStack {
Color.clear
VStack {
Spacer()
if scaleEightImage {
Text("HELLO WORLD!")
.foregroundColor(Color.black)
.transition(zoomOutTransition)
} else if !scaleEightImage {
Text("HELLO WORLD!")
.foregroundColor(Color.black)
}
}
VStack {
Button {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
viewModel.scaleEightImage.toggle()
}
}
} label: {
Text("tappppp me")
.foregroundColor(Color.black)
}
}
}
}
The text stays on the screen because you have 2 Text() views - one that shows when scaleEightImage is true, another one for when scaleEightImage is false.
You just need to show the text only when scaleEightImage is false:
VStack {
Spacer()
if !scaleEightImage {
Text("HELLO WORLD!")
.foregroundColor(Color.black)
.transition(zoomOutTransition)
}
}
I would like to scale some view with animation and go back to the original scale. The way I figured out to do it is by using DispatchQueue.main.asyncAfter with deadline matching the end of scale up animation. As shown in following code. My question is if there is better way to do that (more SwiftUI way, or just simpler).
import SwiftUI
struct ExampleView: View {
#State private var isPulse = false
var body: some View {
VStack {
Text("Hello")
.scaleEffect(isPulse ? 3 : 1)
ZStack {
Capsule()
.frame(width: 100, height: 40)
.foregroundColor(.pink)
Text("Press me")
}
.onTapGesture {
let animationDuration = 0.4
withAnimation(.easeInOut(duration: animationDuration)) {
isPulse.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
withAnimation(.easeInOut) {
isPulse.toggle()
}
}
}
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
Pulse animation
You can use two withAnimation statements with the second one that scales the text down using a .delay(animationDuration):
.onTapGesture {
let animationDuration = 0.4
withAnimation(.easeInOut(duration: animationDuration)) {
isPulse.toggle()
}
withAnimation(.easeInOut(duration: animationDuration).delay(animationDuration)) {
isPulse.toggle()
}
}
You could also replace the two calls to withAnimation with a for loop:
.onTapGesture {
let animationDuration = 0.4
for delay in [0, animationDuration] {
withAnimation(.easeInOut(duration: animationDuration).delay(delay)) {
isPulse.toggle()
}
}
}
I'm trying to animate a color change on some text but I can't seem to get it to change gradually. I've tried both an implicit and explicit animation as seen in the code below, but no dice....
struct Example: View {
#State var showing = false
var body: some View {
VStack {
Text("test text").foregroundColor(showing ? .red : .blue)
.animation(.easeIn(duration: 2))
Button(action: toggle) {
Text("Toggle")
}
}
}
func toggle() {
withAnimation(.easeIn(duration: 2)) {self.showing.toggle()}
}
}
Can anyone give me some pointers?
Unfortunately, you can't animate .foregroundColor. But you can animate .colorMultiply. So in your case this will work:
struct ColorChangeAnimation: View {
#State private var multiplyColor: Color = .blue
var body: some View {
VStack {
Text("test text")
.foregroundColor(.white)
.colorMultiply(multiplyColor)
Button(action: toggle) {
Text("Toggle")
}
}
}
func toggle() {
withAnimation(.easeIn(duration: 2)) {
self.multiplyColor = (self.multiplyColor == .red) ? .blue : .red
}
}
}
I am new to SwiftUI and have managed to build simple game.
All works except my plan to implement some sort of image animation / transition to introduce the image into the view.
The images are formatted correctly, but too abrupt and hoped to soften the image glow
I have tried by using the animation method, but has not mad a difference.
I hope someone can point me in the right direction.
var body: some View {
NavigationView {
ZStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
.scaledToFill()
.edgesIgnoringSafeArea(.all)
VStack {
Text("Tap Correct Image")
.font(.headline)
.foregroundColor(Color.blue)
ForEach((0...2), id: \.self) { number in
Image(self.naijaObject[number])
.resizable()
.frame(width: 100, height: 100)
.border(Color.black,width: 1)
.transition(.slide)
.animation(.easeInOut(duration: 1))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text(processCorrectAnswer))
.alert(isPresented: $showAlert) {
Alert(title: Text("\(alertTitle), Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}//End of VStack
}//End of
} //End of Naviagtion View
} //End of View
//Function to action which object is tapped
func pictureTapped(_ tag: Int) {
if tag == correctAnswer {
score += 1
alertTitle = "Correct"
} else {
score -= 1
alertTitle = "Wrong"
}
showAlert = true
}
//Function to continue the game
func askQuestion() {
naijaObject.shuffle()
correctAnswer = Int.random(in: 0...2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I think I understand, what you want to achieve, but in future better to present more code. Here I used DispatchQueue.main.asyncAfter(deadline: .now() + 1) where I change the variable showAlert to true. And it works smoothly, hope it'll help you:
struct QuestionGame: View {
#State private var showAlert = false
#State private var score: Int = 0
#State private var correctAnswer = 1
#State private var tapped = false
var body: some View {
NavigationView {
VStack {
Text("Tap Correct Image")
.font(.headline)
ForEach((0...2), id: \.self) { number in
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(self.tapped && number == self.correctAnswer ? .green : .black) // you can do anything for animation
.animation(.easeInOut(duration: 1.0))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text("answer the question"))
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}
}
}
func pictureTapped(_ tag: Int) {
tapped = true
if tag == correctAnswer {
score += 1
} else {
score -= 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showAlert = true
}
}
//Function to continue the game
func askQuestion() {
tapped = false
showAlert = false
correctAnswer = Int.random(in: 0...2)
}
}
struct QuestionGame_Previews: PreviewProvider {
static var previews: some View {
QuestionGame()
}
}
P.S. and no, I didn't find the way to show alert smoothly. For this case may be better to use ZStack and change the alert (or any other view) opacity from 0 to 1 (with animation), when you tap on image.