Layering iOS Animations - ios

I'm having some trouble layering iOS animations for my video recoding button. I've modeled it after other standard recording buttons and I'd like for it to begin as a solid circle and then on press, transform to a rounded rectangle where its opacity pulses.
The issue is that I've ordered the animations in a way such that their all applied at the same time.
https://imgur.com/a/xi7PxtH
I've tried reordering these animations but can't figure out which ordering with achieve the scope I want.
import SwiftUI
struct ContentView: View {
#State var recordComplete = false
#State private var rCorner: CGFloat = 100
#State private var rWidth: CGFloat = 70
#State private var rHeight: CGFloat = 70
#State private var opacityVal = 1.0
var body: some View {
HStack{
Button(action: {
self.rCorner = self.rCorner == 100 ? 12 : 100
self.rWidth = self.rWidth == 70 ? 45 : 70
self.rHeight = self.rHeight == 70 ? 45 : 70
self.recordComplete.toggle()
}) {
ZStack{
Circle()
.stroke(Color.red, lineWidth: 6)
.frame(width:85, height: 85)
RoundedRectangle(cornerRadius: rCorner)
.fill(Color.red)
.frame(width: rWidth, height: rHeight)
.opacity(opacityVal)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)).onAppear{ self.opacityVal = 0.3 }
}
.padding(.vertical, 25)
}
}.animation(.spring(response: 0.5, dampingFraction: 2, blendDuration: 0.5))
}
}
Thank you as always for the help with this issue!

Here is a possible solution - the main idea is to separate animations by value and make them serial. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var recordComplete = false
#State private var rCorner: CGFloat = 100
#State private var rWidth: CGFloat = 70
#State private var rHeight: CGFloat = 70
#State private var opacityVal = 1.0
var body: some View {
HStack{
Button(action: {
self.rCorner = self.rCorner == 100 ? 12 : 100
self.rWidth = self.rWidth == 70 ? 45 : 70
self.rHeight = self.rHeight == 70 ? 45 : 70
self.recordComplete.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.opacityVal = self.recordComplete ? 0.3 : 1.0
}
}) {
ZStack{
Circle()
.stroke(Color.red, lineWidth: 6)
.frame(width:85, height: 85)
RoundedRectangle(cornerRadius: rCorner)
.fill(Color.red)
.frame(width: rWidth, height: rHeight)
.opacity(opacityVal)
.animation(
self.recordComplete ?
Animation.easeInOut(duration: 1).repeatForever(autoreverses: true) :
Animation.default,
value: opacityVal)
}
.padding(.vertical, 25)
}
}.animation(.spring(response: 0.5, dampingFraction: 2, blendDuration: 0.5), value: rCorner)
}
}

Related

SwiftUI pop-out animation causes characters to shake on physical device

I'm attempting to do a simple pop-out animation with some text and an image in SwiftUI. In the preview on the simulator it looks fine, but when using a physical device to preview or when building and running on a physical device the characters in the text of the animation shake and it looks awful. Interestingly, the image moves fine.
I'm still new to SwiftUI so any advice or input would be very appreciated.
struct SplashScreenContent: View {
#State private var blurRadius: CGFloat = 10
#State private var movingShadowX: CGFloat = 4
#State private var movingShadowY: CGFloat = 4
#State private var imageSize: CGFloat = 50
#State private var fontSize: CGFloat = 55
#State private var bottomInset: CGFloat = -5
#State private var trailingInset: CGFloat = -3
var body: some View {
VStack {
Button("Animate", action: buttonPress)
Spacer()
VStack(spacing: 45) {
Image(systemName: "house")
.resizable()
.foregroundColor(.white)
.scaledToFill()
.frame(width: imageSize, height: imageSize)
.shadow(color: .black, radius: 1.5)
.shadow(color: .yellow, radius: 5)
.shadow(color: .black, radius: 3, x: movingShadowX, y: movingShadowY)
Text("I'm so\nShakey")
.font(.custom("NewYork-regular", size: fontSize))
.fontWeight(.semibold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 70.0)
.shadow(color: .black, radius: 1.5)
.shadow(color: .yellow, radius: 5)
.shadow(color: .black, radius: 3, x: movingShadowX, y: movingShadowY)
}
.padding(.init(top: 0, leading: 0, bottom: bottomInset, trailing: trailingInset))
.blur(radius: blurRadius)
.onAppear(perform: animate)
Spacer()
}
}
}
extension SplashScreenContent {
func buttonPress() {
blurRadius = 10
movingShadowX = 4
movingShadowY = 4
imageSize = 50
fontSize = 55
bottomInset = -5
trailingInset = -3
animate()
}
func animate() {
withAnimation(.easeIn(duration: 0.4).delay(0.8)) {
blurRadius = 0
withAnimation(.easeIn(duration: 4).delay(0.3)) {
movingShadowX = 6
movingShadowY = 9.5
imageSize = 53
fontSize = 58
bottomInset = 13
trailingInset = 3
withAnimation(.easeIn(duration: 0.5).delay(4.3)) {
blurRadius = 20
}
}
}
}
}
I tried removing the shadows, changing how much everything was animating, and so on. The text remains to be shakey.

SwiftUI .delay animation timing is incorrect after leaving and coming back to view

This animation runs as expected when first viewed but the timing gets messed up at random when the app is exited and then entered (or if you navigate away from the screen and back). How can this code be changed to keep the animation timing consistent after leaving the screen and coming back?
The error can be recreated by exiting and coming back to the app a couple times.
import SwiftUI
struct ExampleView: View {
#State var isAnimating: Bool = false
let timing = 4.0
let maxCounter: Int = 3
var body: some View {
ZStack {
Circle()
.stroke(
Color.blue.opacity(isAnimating ? 0.0 : 1.0),
style: StrokeStyle(lineWidth: isAnimating ? 0.0 : 15.0))
.scaleEffect(isAnimating ? 1.0 : 0.0)
.animation(
Animation.easeOut(duration: timing)
.repeatForever(autoreverses: false)
.delay(Double(0) * timing / Double(maxCounter) / Double(maxCounter)), value: isAnimating)
Circle()
.stroke(
Color.blue.opacity(isAnimating ? 0.0 : 1.0),
style: StrokeStyle(lineWidth: isAnimating ? 0.0 : 15.0))
.scaleEffect(isAnimating ? 1.0 : 0.0)
.animation(
Animation.easeOut(duration: timing)
.repeatForever(autoreverses: false)
.delay(Double(1) * timing / Double(maxCounter) / Double(maxCounter)), value: isAnimating)
Circle()
.stroke(
Color.blue.opacity(isAnimating ? 0.0 : 1.0),
style: StrokeStyle(lineWidth: isAnimating ? 0.0 : 15.0))
.scaleEffect(isAnimating ? 1.0 : 0.0)
.animation(
Animation.easeOut(duration: timing)
.repeatForever(autoreverses: false)
.delay(Double(2) * timing / Double(maxCounter) / Double(maxCounter)), value: isAnimating)
}
.frame(width: 200, height: 200, alignment: .center)
.onAppear {
isAnimating = true
}
}
}
Here is a right way for you, it needs to use DispatchQueue and scenePhase:
PS: I noticed lots of complaining from Xcode with your original code! I refactored your code and solved those complaining as well.
struct ExampleView: View {
var body: some View {
ZStack {
CustomCircleAnimationView(delayValue: 0.0)
CustomCircleAnimationView(delayValue: 1.0)
CustomCircleAnimationView(delayValue: 2.0)
}
.frame(width: 200, height: 200)
}
}
struct CustomCircleAnimationView: View {
#Environment(\.scenePhase) private var scenePhase
let delay: Double
private let timing: Double
private let maxCounter: Double
#State private var startAnimation: Bool = false
init(timing: Double = 4.0, maxCounter: Double = 3.0, delayValue: Double) {
self.timing = timing
self.maxCounter = maxCounter
self.delay = (delayValue*timing)/(maxCounter*maxCounter)
}
var body: some View { if (scenePhase == .active) { circle } }
var circle: some View {
return Circle()
.stroke(Color.blue, style: StrokeStyle(lineWidth: startAnimation ? 0.001 : 15.0))
.scaleEffect(startAnimation ? 1.0 : 0.001)
.opacity(startAnimation ? 0.001 : 1.0)
.onAppear { DispatchQueue.main.async { startAnimation = true } }
.onDisappear { startAnimation = false }
.animation(Animation.easeOut(duration: timing).repeatForever(autoreverses: false).delay(delay), value: startAnimation)
}
}

SwiftUI - stacking views like the notifications on the Lockscreen

I am trying to stack views of my app like the notifications on the lockscreen. (See attachment) I want them to be able to stack by clicking the button and expand on a click on the view. I have tried many things on VStack and setting the offsets of the other views but without success.
Is there maybe a standardized and easy way of doing this?
Somebody else have tried it and has a tip for me programming this?
Thanks
Notifications stacked
Notifications expanded
EDIT:
So this is some Code that I've tried:
VStack{
ToDoItemView(toDoItem: ToDoItemModel.toDo1)
.background(Color.green)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.zIndex(2.0)
ToDoItemView(toDoItem: ToDoItemModel.toDo2)
.background(Color.blue)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.zIndex(1.0)
.offset(x: 0.0, y: offset)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.yellow)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.gray)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.pink)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
}
Button(action: {
if(!stacked){
stacked.toggle()
withAnimation(.easeOut(duration: 0.5)) { self.offset = -100.0 }
withAnimation(.easeOut(duration: 0.5)) { self.offset1 = -202.0 }
}
else {
stacked.toggle()
withAnimation(.easeOut(duration: 0.5)) { self.offset = 0 }
withAnimation(.easeOut(duration: 0.5)) { self.offset1 = 0 }
}
}, label: {
Text("Button")
})
So the first problem is that view 4 and 5 are not stacked on z behind view 3.
The second problem is that the button is not getting under the 5th view. (the frame/VStack of the views is still the old size)
Views stacked vertical
Views stacked on z
Another way using ZStack if your interested.
Note-:
I have declared variables globally in view, you can create your own model if you wish.
struct ContentViewsss: View {
#State var isExpanded = false
var color :[Color] = [Color.green,Color.gray,Color.blue,Color.red]
var opacitys :[Double] = [1.0,0.7,0.5,0.3]
var height:CGFloat
var width:CGFloat
var offsetUp:CGFloat
var offsetDown:CGFloat
init(height:CGFloat = 100.0,width:CGFloat = 400.0,offsetUp:CGFloat = 10.0,offsetDown:CGFloat = 10.0) {
self.height = height
self.width = width
self.offsetUp = offsetUp
self.offsetDown = offsetDown
}
var body: some View {
ZStack{
ForEach((0..<color.count).reversed(), id: \.self){ index in
Text("\(index)")
.frame(width: width , height:height)
.padding(.horizontal,isExpanded ? 0.0 : CGFloat(-index * 4))
.background(color[index])
.cornerRadius(height/2)
.opacity(isExpanded ? 1.0 : opacitys[index])
.offset(x: 0.0, y: isExpanded ? (height + (CGFloat(index) * (height + offsetDown))) : (offsetUp * CGFloat(index)))
}
Button(action:{
withAnimation {
if isExpanded{
isExpanded = false
}else{
isExpanded = true
}
}
} , label: {
Text(isExpanded ? "Stack":"Expand")
}).offset(x: 0.0, y: isExpanded ? (height + (CGFloat(color.count) * (height + offsetDown))) : offsetDown * CGFloat(color.count * 3))
}.padding()
Spacer().frame(height: 550)
}
}
I find it a little easier to get exact positioning using GeometryReader than using ZStack and trying to fiddle with offsets/positions.
This code has some numbers hard-coded in that you might want to make more dynamic, but it does function. It uses a lot of ideas you were already doing (zIndex, offset, etc):
struct ToDoItemModel {
var id = UUID()
var color : Color
}
struct ToDoItemView : View {
var body: some View {
RoundedRectangle(cornerRadius: 20).fill(Color.clear)
.frame(height: 100)
}
}
struct ContentView : View {
#State private var stacked = true
var todos : [ToDoItemModel] = [.init(color: .green),.init(color: .pink),.init(color: .orange),.init(color: .blue)]
func offsetForIndex(_ index : Int) -> CGFloat {
CGFloat((todos.count - index - 1) * (stacked ? 4 : 104) )
}
func horizontalPaddingForIndex(_ index : Int) -> CGFloat {
stacked ? CGFloat(todos.count - index) * 4 : 4
}
var body: some View {
VStack {
GeometryReader { reader in
ForEach(Array(todos.reversed().enumerated()), id: \.1.id) { (index, item) in
ToDoItemView()
.background(item.color)
.cornerRadius(20)
.padding(.horizontal, horizontalPaddingForIndex(index))
.offset(x: 0, y: offsetForIndex(index))
.zIndex(Double(index))
}
}
.frame(height:
stacked ? CGFloat(100 + todos.count * 4) : CGFloat(todos.count * 104)
)
.border(Color.red)
Button(action: {
withAnimation {
stacked.toggle()
}
}) {
Text("Unstack")
}
Spacer()
}
}
}

Expand Swiftui view to cover screen

Hello I'm looking to replicate a swiftui animation that I see around specifically to animate a view (like a button) from where it is in the hierarchy to full screen and then back.
Here is a demo
https://imgur.com/a/qG5iaAB
What I've tried
I don't have a lot to go on with this. I basically tried manipulating frame dimensions with gesture inputs, but this breaks down pretty quickly once you have anything else on the screen.
the below works with a single element and nothing else 🤷🏻‍♀️.
struct Expand: View {
#State private var dimy = CGFloat(100)
#State private var dimx = CGFloat(100)
#State private var zz = 0.0
#State private var offset = CGFloat(0)
let w = UIScreen.main.bounds.size.width
let h = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Image(systemName: "play")
.foregroundColor(Color.red)
.frame(width: dimx, height: dimy)
.background(Color.red.opacity(0.5))
.border(Color.red)
.offset(y: offset)
.gesture(DragGesture()
.onChanged { gesture in
let y = gesture.translation.height
offset = y
}
.onEnded { _ in
offset = 0
dimx = dimx == 100 ? UIScreen.main.bounds.size.width : 100
dimy = dimy == 100 ? UIScreen.main.bounds.size.height : 100
zz = zz == 0 ? 3 : 0
}
)
.animation(.default)
.position(x: w / 2, y: h / 2)
.zIndex(zz)
}
}
.edgesIgnoringSafeArea(.vertical)
}
}

SwiftUI Circle line width change stops trim animation

I want to animate the trim attribute of a circle. However, if the animation is running and I change the line width, the animation finishes immediately (and unexpectedly).
Does somebody have an idea what I am doing wrong? I'm sure it's a very simple thing.
Thanks for answering!
import SwiftUI
struct ContentView: View {
#State var progress: Double = 0.01
#State var lineWidth: CGFloat = 20
var body: some View {
VStack{
CircleView(progress: progress, lineWidth: lineWidth)
.foregroundColor(.orange)
.onAppear{
withAnimation(.linear(duration: 20)){
progress = 1
}
}
.padding()
Button(action: {
withAnimation{
lineWidth = 40
}
}, label: {Text("Change Line Width")})
}
}
}
struct CircleView: View {
var progress: Double
var lineWidth: CGFloat
var body: some View {
Circle()
.trim(from: 0, to: CGFloat(progress))
.stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
.padding()
}
}
I'm not sure you can directly do what you hope to do. The stroking of the line uses the lineWidth, so I don't believe you can animate it with a separate time interval.
What you can do is change the lineWidth over time so that while the circle animation is running and redrawing the circle, it will use the new values.
With that in mind, I created the function changeValueOverTime(value:newValue:duration) to do this:
struct ContentView: View {
#State var progress: Double = 0.01
#State var lineWidth: CGFloat = 20
var body: some View {
VStack{
CircleView(progress: progress, lineWidth: $lineWidth)
.foregroundColor(.orange)
.onAppear{
withAnimation(.linear(duration: 20)){
progress = 1
}
}
.padding()
Button(action: {
changeValueOverTime(value: $lineWidth, newValue: 40, duration: 0.5)
}, label: {Text("Change Line Width")})
}
}
func changeValueOverTime(value: Binding<CGFloat>, newValue: CGFloat, duration: Double) {
let timeIncrements = 0.02
let steps = Int(duration / timeIncrements)
var count = 0
let increment = (newValue - value.wrappedValue) / CGFloat(steps)
Timer.scheduledTimer(withTimeInterval: timeIncrements, repeats: true) { timer in
value.wrappedValue += increment
count += 1
if count == steps {
timer.invalidate()
}
}
}
}
Just remove second animation and all will work. Tested with Xcode 12 / iOS 14
Button(action: {
lineWidth = 40 // << no animation here !!
}, label: {Text("Change Line Width")})

Resources