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()
}
}
}
Related
I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...
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:
I want to show an image for one second when the player achieve the goal. I thought about putting an alert but it will slow down the game. I just want the image to stay for one second at the top of the screen and disappear until the next achievement.
Example code is below:
var TapNumber = 0
func ScoreUp() {
TapNumber += 1
if TapNumber == 100 {
showImage()
}
}
func showImage() {
// this is the function I want to create but. I do not know how
show image("YouEarnedAPointImage") for one second
}
Here is a demo of possible approach
struct DemoShowImage1Sec: View {
#State private var showingImage = false
var body: some View {
ZStack {
VStack {
Text("Main Content")
Button("Simulate") { self.showImage() }
}
if showingImage {
Image(systemName: "gift.fill")
.resizable()
.frame(width: 100, height: 100)
.background(Color.yellow)
}
}
}
private func showImage() {
self.showingImage = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showingImage = false
}
}
}
So in my ContentView.swift, I have this code:
import SwiftUI
extension UIScreen{
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
struct ContentView: View {
#State var Direction: Int = 0
#State var ButtonsShowing: Bool = true
#State var View: Int = 0
func MoveLeft() {
if ButtonsShowing == true {
ButtonsShowing = false
}
}
func MoveRight() {
if ButtonsShowing == true {
ButtonsShowing = false
}
Direction = 1
}
var body: some View {
ZStack {
Image("Space").resizable()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight + 50)
.edgesIgnoringSafeArea(.all)
VStack {
HStack {
Button(action: MoveLeft) {
if ButtonsShowing == true {
NavigationLink(destination: Playing()) {
Image("LeftClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
}
}
Spacer()
Button(action: MoveRight) {
if ButtonsShowing == true {
Image("RightClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I get a preview of this:
Preview of button with NavigationLink
If you look towards my LeftClick button, I want it to take me to a different view, but don't know how. I have a NavigationLink in it, with my destination being the different view (Playing.swift, if needed to know that, I call it using Playing())
Obviously I'm doing something wrong, and I [[hopefully]] know for sure that it's from the button, not from the other view:
NavigationLink(destination: Playing()) {
Image("LeftClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
Thanks in advance.
Add a NavigationView before your ZStack, NavigationLink won't work without a NavigationView.
var body: some View {
NavigationView {
ZStack {
...
}
}
}
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.