How to trigger SwiftUI animation via change of non-State variable - animation

It's easy to have an animation begin when a view appears, by using .onAppear(). But I'd like to perform a repeating animation whenever one of the view's non-State variables changes. An example:
Here is a view that I would like to "throb" whenever its throbbing parameter, set externally, is true:
struct MyCircle: View {
var throbbing: Bool
#State var scale = 1.0
var body: some View {
Circle()
.frame(width: 100 * scale, height: 100 * scale)
.foregroundColor(.blue)
.animation(.easeInOut.repeatForever(), value: scale)
.onAppear { scale = 1.2 }
}
}
Currently the code begins throbbing immediately, regardless of the throbbing variable.
But imagine this scenario:
struct ContentView: View {
#State var throb: Bool = false
var body: some View {
VStack {
Button("Throb: \(throb ? "ON" : "OFF")") { throb.toggle() }
MyCircle(throbbing: throb)
}
}
}
It looks like this:
Any ideas how I can modify MyCircle so that the throbbing starts when the button is tapped and ends when it is tapped again?

You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:
struct MyCircle: View {
var throbbing: Bool
#State private var scale : CGFloat = 0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(1 + scale)
.foregroundColor(.blue)
.onChange(of: throbbing) { newValue in
if newValue {
withAnimation(.easeInOut.repeatForever()) {
scale = 0.2
}
} else {
withAnimation {
scale = 0
}
}
}
}
}

There're two interesting things that i've just found when trying to achieve yours target.
Animation will be added when the value changed
Animation that added to the view cannot be removed, and we need to drop the animated view from the view hiearachy by remove its id, new view will be created with zero animations.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var throbbling = false
#State var circleId = UUID()
var body: some View {
VStack {
Toggle("Throbbling", isOn: $throbbling)
circle.id(circleId)
.scaleEffect(throbbling ? 1.2 : 1.0)
.animation(.easeInOut.repeatForever(), value: throbbling)
.onChange(of: throbbling) { newValue in
if newValue == false {
circleId = UUID()
}
}
}.padding()
}
#ViewBuilder
var circle: some View {
Circle().fill(.green).frame(width: 100, height: 100)
}
}
PlaygroundPage.current.setLiveView(ContentView())

Related

SwiftUI: Animate resizing of .sheet (macOS)

Is it possible to animate a size change of a sheet? If I animate a size change of the sheet's contents, the sheet itself will immediately assume the new size without animation. See video (white background of sheet should never be visible):
Code:
struct ContentView: View {
#State var isPresented = false
#State var sheetContentHeight = 100.0
var body: some View {
VStack {
Button("Show Sheet") {
isPresented = true
}
}
.frame(width: 500, height: 300)
.sheet(isPresented: $isPresented) {
sheetContent
}
}
var sheetContent: some View {
ZStack {
Rectangle()
.frame(width: 300, height: sheetContentHeight)
.foregroundColor(Color.gray)
Button("Change Content Height") {
sheetContentHeight = 200
}
}
.animation(.easeOut(duration: 1), value: sheetContentHeight)
}
}

Fading Text in with Duration in SwiftUI

I have been using the following UIView extension for quite a while to fade text in and out. I have been trying to figure out how to implement this with SwiftUI but so far haven't found anything that exactly addresses this for me. Any help would be greatly appreciated.
extension UIView {
func fadeKey() {
// Move our fade out code from earlier
UIView.animate(withDuration: 3.0, delay: 2.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
func fadeIn1() {
// Move our fade out code from earlier
UIView.animate(withDuration: 1.5, delay: 0.5, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
I assume this is what you wanted. Try this below code:
struct FadeView: View {
#State var isClicked = false
#State var text = "Faded Text"
var body: some View {
VStack {
Text(text) //fade in and fade out
.opacity(isClicked ? 0 : 1)
Button("Click to animate") {
withAnimation {
isClicked.toggle()
}
}
}
}
}
You could use withAnimation function and manipulate the duration, options and delay
struct ContentView: View {
#State var isActive = false
var body: some View {
VStack {
Button("Fade") {
withAnimation(.easeIn(duration: 3.0).delay(2)){
isActive.toggle()
}
}
Rectangle()
.frame(width: 222.0, height: 222.0)
.opacity(isActive ? 0 : 1)
}
}
}

Reload SwiftUI view from within it

I have some text inside a View and I want this text to change it's position on timer. I have the following:
struct AlphabetView: View {
#State var timer: Publishers.Autoconnect<Timer.TimerPublisher> = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var refreshMe: Bool = false // this is a hack to refresh the view
var relativePositionX: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var relativePositionY: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var body: some View {
GeometryReader { geometry in
Text("Hello World!")
.position(x: geometry.size.width * self.relativePositionX, y: geometry.size.height * self.relativePositionY)
.onReceive(timer) { _ in
// a hack to refresh the view
self.refreshMe = !self.refreshMe
}
}
}
}
I suppose that the View should be reloaded every time self.refreshMe changes, because it is #State. But what I see is that the text position changes only when the parent view of the view in question is reloaded. What am I doing wrong (except hacking around)? Is there a better way to do this?
Instead of hack just update position directly.
Here is corrected variant. Tested with Xcode 12 / iOS 14.
struct AlphabetView: View {
let timer: Publishers.Autoconnect<Timer.TimerPublisher> = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var point: CGPoint = .zero
var relativePositionX: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var relativePositionY: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var body: some View {
GeometryReader { geometry in
Text("Hello World!")
.position(self.point)
.onReceive(timer) { _ in
self.point = CGPoint(x: geometry.size.width * self.relativePositionX, y: geometry.size.height * self.relativePositionY)
}
}
}
}

How to change color of a shape after a time x?

Sorry if it's a simple fix, I am a still bit new to swift/swiftui
I have been trying to accomplish a change of color after a second.
struct ContentView: View {
#State private var hasTimeElapsed = false
var colorCircle: [Color] = [.red, .green]
var body: some View {
Circle()
.fill(self.colorCircle.randomElement()!)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.hasTimeElapsed = true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There's no need to use the hasTimeElapsed variable. Instead you can add a #State variable to store your current colour and set it in delayCircle():
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor = Color.red
var body: some View {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.currentColor = self.colorCircle.randomElement()!
}
}
}
If you want to start with a random colour (instead of #State var currentColor = Color.red) you can set it in init:
#State var currentColor: Color
init() {
_currentColor = .init(initialValue: self.colorCircle.randomElement()!)
}
UPDATE due to OP's comment
If you want to set your circle colour in a loop you can use a Timer:
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor: Color = .red
// firing every 1 second
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
Button("Stop timer") {
self.timer.upstream.connect().cancel() // <- stop the timer
}
}
.onReceive(timer) { _ in // <- when the timer fires perfom some action (here `randomizeCircleColor()`)
self.randomizeCircleColor()
}
}
private func randomizeCircleColor() {
self.currentColor = self.colorCircle.randomElement()!
}
}

Animate the width of a Rectangle over time

In SwiftUI on WatchOS, how can I animate the width of a Rectangle (or any View for that matter) so that it starts at a certain value and over a specified time animates to a different value?
Specifically, I want to animate a Rectangle to indicate the time left to the next full minute or the next 30 seconds after a minute.
All the examples I've seen are based on Timer.scheduledTimer firing at relatively high speed and setting a #State variable, but my understanding is that especially on WatchOS this should be avoided. Is there a better way?
This is the timer/state based code I have but I feel like there should be a more efficient way:
import SwiftUI
func percentage() -> CGFloat {
1 - CGFloat(fmod(Date().timeIntervalSince1970, 30) / 30)
}
struct ContentView: View {
#State var ratio: CGFloat = percentage()
let timer = Timer.publish(every: 1 / 60, on:.main, in:.common).autoconnect()
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(Color.gray)
.frame(width:geometry.size.width, height:5)
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width:geometry.size.width * self.ratio, height:5)
Spacer()
}
}
}.onReceive(self.timer) { _ in
self.ratio = percentage()
}
}
}
I think a "more efficient way" to use animation:
struct AnimationRectangle: View {
struct AnimationRectangle: View {
#State private var percentage: CGFloat = 0.0
// count, how much time left to nearest 30 seconds
#State private var animationDuration = 30 - Double(fmod(Date().timeIntervalSince1970, 30))
private var repeatedAnimationFor30Seconds: Animation {
return Animation.easeInOut(duration: 30)
.repeatForever(autoreverses: false)
}
var body: some View {
VStack {
// just showing duration of current animation
Text("\(self.animationDuration)")
ZStack {
Rectangle()
.foregroundColor(.gray)
GeometryReader { geometry in
HStack {
Rectangle()
.foregroundColor(.green)
.frame(width: geometry.size.width * self.percentage)
Spacer()
}
}
}
.frame(height: 5)
.onAppear() {
// first animation without repeating
withAnimation(Animation.easeInOut(duration: self.animationDuration)) {
self.percentage = 1.0
}
// other repeated animations
DispatchQueue.main.asyncAfter(deadline: .now() + self.animationDuration) {
self.percentage = 0.0
self.animationDuration = 30.0
withAnimation(self.repeatedAnimationFor30Seconds) {
self.percentage = 1.0
}
}
}
}
}
}
struct AnimationRectangle_Previews: PreviewProvider {
static var previews: some View {
AnimationRectangle()
}
}

Resources