How to start an animation after the previous one has finished - ios

When I press the button, I want to make the blue rectangle smaller and then move it to the right.
In the current code, the animation to make it smaller and the animation to move it to the right happen at the same time.
How do you start an animation after the previous one has finished?
sample code:
import SwiftUI
struct ContentView: View {
#State private var scale: CGFloat = 1
#State private var offsetX: CGFloat = 1
var body: some View {
VStack {
Button(
action: {
withAnimation {
scale = scale - 0.1
}
withAnimation {
offsetX = offsetX + 25
}
},
label: {
Text("Tap Me")
}
)
RectangleView().scaleEffect(
scale
)
.offset(
x: offsetX,
y: 0
)
}
}
}
struct RectangleView: View {
var body: some View {
Rectangle().fill(
Color.blue
)
.frame(
width: 200,
height: 150
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

You can adjust by duration and delay.
struct ContentView: View {
#State private var scale: CGFloat = 1
#State private var offsetX: CGFloat = 1
var body: some View {
VStack {
Button(
action: {
withAnimation(.linear(duration: 0.2)) { //<-- Here
scale = scale - 0.1
}
withAnimation(Animation.default.delay(0.2)) { //<-- Here
offsetX = offsetX + 25
}
},
label: {
Text("Tap Me")
}
)
RectangleView().scaleEffect(
scale
)
.offset(
x: offsetX,
y: 0
)
}
}
}

Related

how to infinite rotation in circular way by SwiftUI?

I could not rotate the green part in the circular path.
I want to set dynamically infinite time.
I created a struct for ProgressView and want to set it as ProgressView in my project.
how to implement the red and black colour curve design in the rotation
path
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.onAppear{
self.incrementProgress()
}
}
func incrementProgress() {
let randomValue = Float([0.012, 0.022, 0.034, 0.016, 0.11, 0.012].randomElement()!)
self.progressValue += randomValue
}
}
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 2.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.green)
.rotationEffect(Angle(degrees: 270))
.animation(.linear)
}
}
}
This is the Preview struct.
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
Thanks in advance for helping me.
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
#State private var isLoaderVisible: Bool = false
var degree = 90
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.overlay(LoaderView(showLoader: isLoaderVisible))
.onAppear {
isLoaderVisible.toggle()
}
}
}
struct ProgressBar: View {
#Binding var progress: Float
#State var degree:Double = 90
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.foregroundColor(Color.green)
}
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
// enum for LoaderView
public enum LoaderAnimation {
case low, medium, high
var animationSpeed: Double {
switch self {
case .low: return 1.0
case .medium: return 0.8
case .high: return 10.2
}
}
}
This this the LoaderView which we set on overlay and animate
public struct LoaderView: View {
var loaderAnimationSpeed: LoaderAnimation? = .high
var showLoader: Bool = false
public var body: some View {
GeometryReader { reader in
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.yellow)
Circle()
.trim(from: 0.6, to: 1)
.stroke(.green, lineWidth: 20)
.rotationEffect(.degrees(showLoader ? 360 : 0))
.animation(Animation.easeInOut(duration: showLoader ? loaderAnimationSpeed?.animationSpeed ?? 0.0 : 1).repeatForever(autoreverses: false), value: showLoader)
}
}
}
public init( loaderAnimationSpeed: LoaderAnimation? = .medium, showLoader: Bool) {
self.loaderAnimationSpeed = loaderAnimationSpeed
self.showLoader = showLoader
}
}

SwiftUI. Subview animation is not working if subview's View #State was changed while parent View is animating. iOS 16

I have a structure, where the parent view is a hidden pop-up with content. On some user action parent View starts to slide up. I need all its content to slide up with the parent View. It was working perfectly before iOS 16, but now it's broken. If the child's View subview #State is changed during the animation, then this View appears instantly on a screen, i.e. not sliding. As I understand, because View's #State was changed SwiftUI redraws this particular View and disables its animation, so it appears in its final position without animation. I was trying to force the animation of this particular View using .animation or withAnimation. Nothing of it helped. How to fix this bug in iOS 16?
Minimal reproducible example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var shouldShow: SlideCardPosition = .bottom
#State var text1: String = ""
var body: some View {
VStack {
Button {
shouldShow = .top
} label: {
Text("Show PopUp")
.padding(.top, 100)
}
PopUpUIView(shouldShow: $shouldShow)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SlideOverCard<Content: View>: View {
#GestureState private var dragState = SlideDragState.inactive
#Binding private var position: SlideCardPosition
#State private var highlightedBackground = false
private var contentHeight: CGFloat
private var backgroundColor: Color?
private var withCorners: Bool
private var isHandleHidden: Bool
private var overlayOpacity: CGFloat
init(position: Binding<SlideCardPosition>,
contentHeight: CGFloat,
backgroundColor: Color? = nil,
withCorners: Bool = true,
isHandleHidden: Bool = false,
overlayOpacity: CGFloat = 0.75,
content: #escaping () -> Content) {
_position = position
self.content = content
self.contentHeight = contentHeight
self.backgroundColor = backgroundColor
self.withCorners = withCorners
self.isHandleHidden = isHandleHidden
self.overlayOpacity = overlayOpacity
}
var content: () -> Content
var body: some View {
return Rectangle()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.foregroundColor(Color.black.opacity(highlightedBackground ? overlayOpacity : 0))
.position(x: UIScreen.screenWidth / 2, y: (UIScreen.screenHeight) / 2)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay(
Group {
VStack(spacing: 0) {
if !isHandleHidden {
Handle()
}
self.content()
Spacer()
}
}
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.background(backgroundColor != nil ? backgroundColor! : Color.black)
.cornerRadius(withCorners ? 40.0 : 0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: position(from: position) + dragState.translation.height)
.animation(dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0), value: UUID())
.edgesIgnoringSafeArea([.top, .bottom])
.onTapGesture {}
.onChange(of: position) { _ in
withAnimation(.easeInOut) {
highlightedBackground.toggle()
}
}
)
.onTapGesture {
position = position == .bottom ? .top : .bottom
}
}
private func position(from cardPosition: SlideCardPosition) -> CGFloat {
switch cardPosition {
case .top: return UIScreen.screenHeight - contentHeight - UIScreen.topSafeAreaHeight
case .bottom: return 1000
}
}
}
enum SlideCardPosition {
case top
case bottom
}
private enum SlideDragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
private struct Handle: View {
private let handleThickness: CGFloat = 5
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 34, height: handleThickness)
.foregroundColor(.white)
.padding(.top, 8)
}
}
import UIKit
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
private static let window = UIApplication.shared.windows[0]
private static let safeFrame = window.safeAreaLayoutGuide.layoutFrame
static var topSafeAreaHeight: CGFloat {
safeFrame.minY
}
static var bottomSafeAreaHeight: CGFloat {
window.frame.maxY - safeFrame.maxY
}
}
import SwiftUI
struct PopUpUIView: View {
#Binding var shouldShow: SlideCardPosition
#State var text1 = "some random text"
var body: some View {
SlideOverCard(position: $shouldShow,
contentHeight: 300) {
VStack(spacing: 10) {
Text(text1)
.foregroundColor(.white)
.padding(.top, 80)
}
}.onChange(of: shouldShow) { _ in
if shouldShow == .top {
text1 = UUID().uuidString
}
}
}
}
struct PopUpUIView_Previews: PreviewProvider {
static var previews: some View {
PopUpUIView(shouldShow: .constant(.bottom))
}
}
Example of incorrect animation with a dynamic text.
Example of what I want to achieve. It is working fine if text is static.

Programmatically drawn circles are not appearing SwiftUI

I am trying to create a test application using SwiftUI where the user can draw on the screen when they drag on the screen. However, I am having some difficulties getting the Circles that I am using to represent the pen to appear.
Here is the ContentView.swift code that I am using.
import SwiftUI
var list_of_points = [CGPoint]()
struct ContentView: View {
var body: some View {
ZStack{
Rectangle().fill(Color.gray)
Text("Hello!")
}.gesture(DragGesture().onChanged({
value in
drag_responder(point: value.location)
}))
}
}
func drag_responder(point: CGPoint){
print("Drawing at \(point)")
list_of_points.append(point)
let pen = Circle().size(CGSize(width:10, height:10)).position(point)
pen
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct Drawing {
var points: [CGPoint] = [CGPoint]()
}
struct ContentView: View {
#State private var currentDrawing: Drawing = Drawing()
#State private var drawings: [Drawing] = [Drawing]()
#State private var color: Color = Color.black
#State private var lineWidth: CGFloat = 3.0
var body: some View {
VStack(alignment: .center) {
DrawingPad(currentDrawing: $currentDrawing,
drawings: $drawings,
color: $color,
lineWidth: $lineWidth)
}
}
}
struct DrawingPad: View {
#Binding var currentDrawing: Drawing
#Binding var drawings: [Drawing]
#Binding var color: Color
#Binding var lineWidth: CGFloat
var body: some View {
GeometryReader { geometry in
Path { path in
for drawing in self.drawings {
self.add(drawing: drawing, toPath: &path)
}
self.add(drawing: self.currentDrawing, toPath: &path)
}
.stroke(self.color, lineWidth: self.lineWidth)
.background(Color(white: 0.95))
.gesture(
DragGesture(minimumDistance: 0.1)
.onChanged({ (value) in
let currentPoint = value.location
if currentPoint.y >= 0
&& currentPoint.y < geometry.size.height {
self.currentDrawing.points.append(currentPoint)
}
})
.onEnded({ (value) in
self.drawings.append(self.currentDrawing)
self.currentDrawing = Drawing()
})
)
}
.frame(maxHeight: .infinity)
}
private func add(drawing: Drawing, toPath path: inout Path) {
let points = drawing.points
if points.count > 1 {
for i in 0..<points.count-1 {
let current = points[i]
let next = points[i+1]
path.move(to: current)
path.addLine(to: next)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to animate a number that in wrapped in a View with SwiftUI?

Let say i have a simple view, and a number INSIDE another view:
struct ContentView: View {
#State private var counter = 0
var body: some View {
VStack {
Button(action: { self.counter += 1 }) {
Text("Add 1")
}
NumberView(currentValue: counter)
}
}
}
struct NumberView: View {
var currentValue: Int
var body: some View {
VStack {
Text("I don't want to be animated")
Text(currentValue.description)
.font(.largeTitle)
.foregroundColor(Color.black)
.padding(.all)
}
}
}
For every click I want the number to scale up for a second and go back as normal. (Using scale effect). How can i animate this scaling for a second?
try this:
struct ContentView: View {
#State private var counter = 0
#State var scale = false
var body: some View {
VStack {
Button(action: {
self.counter += 1
withAnimation(Animation.easeInOut(duration: 0.5)) {
self.scale.toggle()
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in
withAnimation(Animation.easeInOut(duration: 0.5)) {
self.scale.toggle()
}
}
}
}) {
Text("Add 1")
}
NumberView(currentValue: counter)
.scaleEffect(scale ? 1.5 : 1)
}
}
}
struct NumberView: View {
var currentValue: Int
var body: some View {
Text(currentValue.description)
.font(.largeTitle)
.foregroundColor(Color.black)
.padding(.all)
}
}
ok, here the new answer for your changed requirement
struct ContentView: View {
#State private var counter = 0
#State var scale : CGFloat = 1
var body: some View {
VStack {
Button(action: {
self.counter += 1
withAnimation(Animation.easeInOut(duration: 0.5)) {
self.scale = 1.5
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in
withAnimation(Animation.easeInOut(duration: 0.5)) {
self.scale = 1
}
}
}
}) {
Text ("add")
}
NumberView(currentValue: counter, scale: self.scale)
}
}
}
struct NumberView: View {
var currentValue: Int
var scale : CGFloat
var body: some View {
VStack {
Text("I don't want to be animated")
Text(currentValue.description)
.font(.largeTitle)
.foregroundColor(Color.black)
.padding(.all)
.scaleEffect(self.scale)
}
}
}

SwiftUI Card flip with two views

I am trying to create a card flip effect between two SwiftUI Views. When clicking on the original view, it 3D rotates on the Y axis like when flipping a card, and the second view should start being visible after 90 degrees have been made.
Using .rotation3DEffect() I can easily rotate a view, the issue is that with the animation() I don't know how to trigger the View change once the angle has reached 90 degrees...
#State var flipped = false
var body: some View {
return VStack{
Group() {
if !self.flipped {
MyView(color: "Blue")
} else {
MyView(color: "Red")
}
}
.animation(.default)
.rotation3DEffect(self.flipped ? Angle(degrees: 90): Angle(degrees: 0), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
.onTapGesture {
self.flipped.toggle()
}
}
How to achieve such a rotation between two views ?
Simple Solution
The approach you're taking can be made to work by putting your two views in a ZStack and then showing/hiding them as the flipped state changes. The rotation of the second view needs to be offset. But this solution relies on a cross-fade between the two views. It might be OK for some uses cases. But there is a better solution - though it's a bit more fiddly (see below).
Here's a way to make your approach work:
struct SimpleFlipper : View {
#State var flipped = false
var body: some View {
let flipDegrees = flipped ? 180.0 : 0
return VStack{
Spacer()
ZStack() {
Text("Front").placedOnCard(Color.yellow).flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("Back").placedOnCard(Color.blue).flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { self.flipped.toggle() }
Spacer()
}
}
}
extension View {
func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}
func placedOnCard(_ color: Color) -> some View {
return padding(5).frame(width: 250, height: 150, alignment: .center).background(color)
}
}
Better Solution SwiftUI has some useful animation tools - such as GeometryEffect - that can generate a really smooth version of this effect. There are some excellent blog posts on this topic at SwiftUI Lab. In particular, see: https://swiftui-lab.com/swiftui-animations-part2/
I've simplified and adapted one of examples in that post to provide the card flipping functionality.
struct FlippingView: View {
#State private var flipped = false
#State private var animate3d = false
var body: some View {
return VStack {
Spacer()
ZStack() {
FrontCard().opacity(flipped ? 0.0 : 1.0)
BackCard().opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.animate3d.toggle()
}
}
Spacer()
}
}
}
struct FlipEffect: GeometryEffect {
var animatableData: Double {
get { angle }
set { angle = newValue }
}
#Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}
let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}
struct FrontCard : View {
var body: some View {
Text("One thing is for sure – a sheep is not a creature of the air.").padding(5).frame(width: 250, height: 150, alignment: .center).background(Color.yellow)
}
}
struct BackCard : View {
var body: some View {
Text("If you know you have an unpleasant nature and dislike people, this is no obstacle to work.").padding(5).frame(width: 250, height: 150).background(Color.green)
}
}
Update
The OP asks about managing the flip status outside of the view. This can be done by using a binding. Below is a fragment that implements and demos this. And OP also asks about flipping with and without animation. This is a matter of whether changing the flip state (here with the showBack var) is done within an animation block or not. (The fragment doesn't include FlipEffect struct which is just the same as the code above.)
struct ContentView : View {
#State var showBack = false
let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."
var body : some View {
let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}
return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
Spacer()
}
}
}
struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {
var front : SomeTypeOfViewA
var back : SomeTypeOfViewB
#State private var flipped = false
#Binding var showBack : Bool
var body: some View {
return VStack {
Spacer()
ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}
struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView
var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}
A cleaned up and extendable solution
Note that you can easily change flip direction by changing axis parameter in .rotation3DEffect.
import SwiftUI
struct FlipView<FrontView: View, BackView: View>: View {
let frontView: FrontView
let backView: BackView
#Binding var showBack: Bool
var body: some View {
ZStack() {
frontView
.modifier(FlipOpacity(percentage: showBack ? 0 : 1))
.rotation3DEffect(Angle.degrees(showBack ? 180 : 360), axis: (0,1,0))
backView
.modifier(FlipOpacity(percentage: showBack ? 1 : 0))
.rotation3DEffect(Angle.degrees(showBack ? 0 : 180), axis: (0,1,0))
}
.onTapGesture {
withAnimation {
self.showBack.toggle()
}
}
}
}
private struct FlipOpacity: AnimatableModifier {
var percentage: CGFloat = 0
var animatableData: CGFloat {
get { percentage }
set { percentage = newValue }
}
func body(content: Content) -> some View {
content
.opacity(Double(percentage.rounded()))
}
}

Resources