SwiftUI Animation Speed Modifier for Rotation Not Changing from the View - animation

I have been trying to change the rotation animation speed of the fan dynamically from SwiftUI code. But once the animation has been started, it seems that onChange it is not possible to change it to operate at different speed i.e not possible to modify .speed modifier. The idea is that animation speed changes when the speed variable changes.
Any ideas how to make this work?
struct FanTest: View {
#State var rotationAngle : Double = 0.0
#State var speed : Int = 0
var body: some View {
HStack {
Image(systemName: "fanblades")
.rotationEffect(.degrees(rotationAngle))
Text("\(speed)")
}
.scaleEffect(4)
.onAppear {
Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { Timer in
speed += 1
if speed > 2 { speed = 0 }
})
}
.onChange(of: speed) { _ in
if speed == 0 {
withAnimation(.linear(duration: 1).speed(1).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
if speed == 1 {
withAnimation(.linear(duration: 1).speed(2).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
if speed == 2 {
withAnimation(.linear(duration: 1).speed(3).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
}
}
}

Your fan already has an animation and the rotation angle has already been changed to 360. You need to remove the existing animation before applying a new one.
One simple way to accomplish this is to tell SwiftUI to replace the fan with a new View that is not yet animating. This can be done by giving the fan an id and changing the id when you want a new fan speed. Also, reset the rotationAngle back to 0 so that it will animate when set to 360 inside the withAnimation block.
struct FanTest: View {
#State var rotationAngle : Double = 0.0
#State var speed : Int = 0
#State var fanID = UUID() // here
var body: some View {
HStack {
Image(systemName: "fanblades")
.id(fanID) // here
.rotationEffect(.degrees(rotationAngle))
Text("\(speed)")
}
.scaleEffect(4)
.onAppear {
Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { timer in
speed += 1
if speed > 2 { speed = 0 }
})
}
.onChange(of: speed) { _ in
rotationAngle = 0 // here
fanID = UUID() // here
if speed == 0 {
withAnimation(.linear(duration: 1).speed(1).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
if speed == 1 {
withAnimation(.linear(duration: 1).speed(2).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
if speed == 2 {
withAnimation(.linear(duration: 1).speed(3).repeatForever(autoreverses: false))
{ rotationAngle = 360.0 }
}
}
}
}

Related

SwiftUI DragGesture list content Leaks memory

I am trying to implement Uber trip like screen, and I have used the code below
public struct OverlappingPanView<BGView, PanContentView>: View
where BGView: View, PanContentView: View {
public var backgroundView: () -> BGView
public var panContentView: () -> PanContentView
public init(backgroundView: #escaping () -> BGView, panContentView: #escaping () -> PanContentView) {
self.backgroundView = backgroundView
self.panContentView = panContentView
}
enum OffsetError : Error{
case TopBoundExceded
case BottomBoundExceded
}
//MARK: - State Property Variables
#GestureState private var dragOffset: CGFloat = 0
#State private var lastEndedOffset: CGFloat = 0
#State private var panFrame: CGSize = .zero
//MARK: - Getters
var staticTopPadding: CGFloat{
let screenPadding = UIScreen.main.bounds.height * 0.4
let actualFrame = (panFrame.height - screenPadding)
let extraCorrection = actualFrame * 0.5
return screenPadding + extraCorrection
}
private var currentPaddingValue: CGFloat{
return dragOffset + lastEndedOffset + staticTopPadding
}
private var difference: CGFloat{
return 200
}
//MARK: - Body
public var body: some View {
ZStack {
backgroundView()
.edgesIgnoringSafeArea(.all)
panControllerView()
}
}
func panControllerView() -> some View{
panContentView()
.background(
GeometryReader { geo -> Color in
DispatchQueue.main.async {
self.panFrame = geo.size
}
return Color.clear
}
)
.frame(minHeight: 0, maxHeight: .infinity)
.offset(y: dragOffset + lastEndedOffset + staticTopPadding)
.animation(.interactiveSpring())
.highPriorityGesture(
DragGesture(coordinateSpace: CoordinateSpace.global)
.updating($dragOffset) { (value, state, _) in
if let endValue = try? self.canDrag(value.translation.height){
state = endValue
}
}
.onEnded { value in
self.onDragEnd(value.translation.height)
}
)
}
func onDragEnd(_ value: CGFloat){
withAnimation {
do{
let endValue = try self.canDrag(value)
self.lastEndedOffset = lastEndedOffset + endValue
}catch OffsetError.BottomBoundExceded{
self.lastEndedOffset = 0
}catch OffsetError.TopBoundExceded{
self.lastEndedOffset = -(self.panFrame.height - difference)
}catch{
self.lastEndedOffset = 0
}
}
}
func canDrag(_ value: CGFloat) throws -> CGFloat{
let newValue = value + lastEndedOffset + staticTopPadding
// stop going below
guard newValue <= staticTopPadding else{
throw OffsetError.BottomBoundExceded
}
let topCalculation = (self.panFrame.height + (value + self.lastEndedOffset)) - difference
// stop going top
guard topCalculation >= 0 else{
throw OffsetError.TopBoundExceded
}
return value
}
}
and Used the above code to show mapview in the background and ForEach content on the front.
OverlappingPanView {
GoogleMapView(delegate: presenter)
} panContentView: {
contentView() // ForEach looped data Views
}
The list that I have provided in content View is leaking memory and uses high CPU (above 100%) when dragging leading to UI Performance issues.

In SwiftUI how do I animate changes one at a time when they occur in a called method?

Although I get an animation when I tap the button, it's not the animation I want.
The entire view is being replaced at once, but I want to see each element change in sequence. I tried in both the parent view and in the called method. Neither produces the desired result.
(this is a simplified version of the original code)
import SwiftUI
struct SequencedCell: Identifiable {
let id = UUID()
var value: Int
mutating func addOne() {
value += 1
}
}
struct AQTwo: View {
#State var cells: [SequencedCell]
init() {
_cells = State(initialValue: (0 ..< 12).map { SequencedCell(value: $0) })
}
var body: some View {
VStack {
Spacer()
Button("+") {
sequencingMethod(items: $cells)
}
.font(.largeTitle)
Spacer()
HStack {
ForEach(Array(cells.enumerated()), id: \.1.id) { index, item in
// withAnimation(.linear(duration: 4)) {
Text("\(item.value)").tag(index)
// }
}
}
Spacer()
}
}
func sequencingMethod(items: Binding<[SequencedCell]>) {
for cell in items {
withAnimation(.linear(duration: 4)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
// cell.wrappedValue.addOne()
}
}
}
}
struct AQTwoPreview: PreviewProvider {
static var previews: some View {
AQTwo()
}
}
So I want the 0 to turn into a 1, the 1 then turn into a 2, etc.
Edit:
Even though I have accepted an answer, it answered my question, but didn't solve my issue.
I can't use DispatchQueue.main.asyncAfter because the value I am updating is an inout parameter and it makes the compiler unhappy:
Escaping closure captures 'inout' parameter 'grid'
So I tried Malcolm's (malhal) suggestion to use delay, but everything happens immediately with no sequential animation (the entire block of updated items animate as one)
Here's the recursive method I am calling:
static func recursiveAlgorithm(targetFill fillValue: Int, in grid: inout [[CellItem]],
at point: (x: Int, y: Int), originalFill: Int? = nil, delay: TimeInterval) -> [[CellItem]] {
/// make sure the point is on the board (or return)
guard isValidPlacement(point) else { return grid }
/// the first time this is called we don't have `originalFill`
/// so we read it from the starting point
let tick = delay + 0.2
//AnimationTimer.shared.tick()
let startValue = originalFill ?? grid[point.x][point.y].value
if grid[point.x][point.y].value == startValue {
withAnimation(.linear(duration: 0.1).delay(tick)) {
grid[point.x][point.y].value = fillValue
}
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y - 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y + 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x - 1, point.y), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x + 1, point.y), originalFill: startValue, delay: tick)
}
return grid
}
Further comments/suggestions are welcome, as I continue to wrestle with this.
As mentioned in the comments, the lowest-tech version is probably just using a DisatpchQueue.main.asyncAfter call:
func sequencingMethod(items: Binding<[SequencedCell]>) {
var wait: TimeInterval = 0.0
for cell in items {
DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
withAnimation(.linear(duration: 1)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
}
wait += 1.0
}
}
You could use delay(_:) for that, e.g.
func sequencingMethod(items: Binding<[SequencedCell]>) {
var delayDuration = 0.0
for cell in items {
withAnimation(.linear(duration: 4).delay(delayDuration)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
delayDuration += 0.5
}
}

How to create a dynamic calculation based off more than 1 State

I am brand new to Swift (and coding in general). I am working on an app that will output a calculation based off the tracking of two states. The two states are brewModel and waterAmount. I am able to successfully create a function that will return one calculation based on the two states. However, now I am trying to create a Picker that will toggle the calculation between two measurements - grams and tablespoons. This is where I am having trouble.
I tried to write a series of conditionals in different ways such as if and else if as well as switch cases, but it doesn't work. When I build the simulator, Xcode will just think for a long time until I stop it. Sometimes I get error messages after I manually stop it and sometimes I don't. Today I got "Command CompileSwiftSources failed with a nonzero exit code."
Does anyone have any suggestions?
Also, I apologize if my code is messy, I have a bunch of things commented out that I am playing with. The func computeGrinds does work but just for the one calculation. Thank you!
import SwiftUI
struct Water: View {
// #EnvironmentObject var favorites: Favorites
#State var animationInProgress = true
#State var brewModel: BrewModel
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
var grindOptions = ["tbsp", "grams"]
// var resultGrindCalc: Double {
//
// var value = Double(0)
// }
// switch grindsSelection {
// case "tbsp" || brewModel.frenchPress:
// value = Double(waterAmount) * 2.5
//
// }
//
// func computeGrinds () -> Double {
// switch brewModel {
// case .frenchPress, .chemex:
// return (2.5 * Double(waterAmount))
// case .drip :
// return Double(2 * Double(waterAmount))
// case .mokaPot:
// return Double(1 * Double(waterAmount))
// case .aeroPress:
// return Double(1.6 * Double(waterAmount))
// // default:
// // return(1 * Double(waterAmount))
// }
// }
var body: some View {
VStack (spacing: 5) {
Spacer()
HStack {
// Text("").padding(20)
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
// Spacer()
}.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
// gif/image conditionals
if (brewModel == .frenchPress) {
LottieView(name: "frenchpress", loopMode: .loop)
} else if brewModel == .chemex {
LottieView(name: "pourover", loopMode: .loop)
} else if brewModel == .aeroPress {
LottieView(name: "aeropress", loopMode: .loop)
} else if brewModel == .mokaPot {
LottieView(name: "mokapot", loopMode: .loop)
} else if brewModel == .drip {
Image("Drip")
.resizable()
.scaledToFit()
}
// I would have more conditionals but testing with just these two for now
var testingCalcCond = Double
if (brewModel == .frenchPress)||(grindsSelection=="tbsp") {
testingCalcCond = (2.5 * Double(waterAmount))
} else if (brewModel == .frenchPress)||(grindsSelection=="grams") {
testingCalcCond = (16 * Double(waterAmount))
}
let formatted = String(format: "%.2f", testingCalcCond)
// let formatted = String(format: "%.2f", computeGrinds())
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
}).onChange(of: grindsSelection) { _ in computeGrinds() }
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
}
Spacer()
}
}
struct Water_Previews: PreviewProvider {
static var previews: some View {
Water(brewModel: .drip)
}
}
}
*I'm using Xcode 13.2.1
*I'm using swiftUI
There are 2 aspects you want to think over:
1. How to update the result value based on the inputs.
Your result value is based on two inputs: brewModel and waterAmount. Both are #State vars and changed by a picker.
I changed your computeGrinds func to a computed property, because this will be automatically called when one of the two base values changes. Then there is no need for .onchange anymore, you can just use the var value – it will always be up to date.
2. recalculating from tbsp to grams.
This is more of a math thing: As I understand, for .frenchPress you need either 2.5 tbsp – or 16 grams per cup. So 1 tbsp = 16 / 2.5 = 6.4 grams. Once you know that you just have to go through the switch case once, and use the unitValue to recalculate. I integrated that too ;)
Here is my simplified code:
enum BrewModel {
case frenchPress
case chemex
case drip
case mokaPot
case aeroPress
}
struct ContentView: View {
#State var animationInProgress = true
#State var brewModel: BrewModel = .frenchPress
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
let grindOptions = ["tbsp", "grams"]
// computed var instead of func, does the same
var computeGrinds: Double {
// transforms tbsp = 1 to grams (= 6.4 ?)
var unitValue: Double = 1.0
if grindsSelection == "grams" {
unitValue = 6.4
}
switch brewModel {
case .frenchPress, .chemex:
return (2.5 * unitValue * Double(waterAmount))
case .drip :
return Double(2 * unitValue * Double(waterAmount))
case .mokaPot:
return Double(1 * unitValue * Double(waterAmount))
case .aeroPress:
return Double(1.6 * unitValue * Double(waterAmount))
}
}
var body: some View {
VStack (spacing: 5) {
HStack {
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
.padding(.bottom)
let formatted = String(format: "%.2f", computeGrinds)
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
})
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
}
}
}

How to add a modifier to any specific buttons inside a ForEach loop for an array of buttons in SwiftUI?

How can I add modifiers according to my needs to individual buttons?
I have a ForEach loop running in a range of 0..<5 & I need to add an animation of .rotation3DEffect in clicked button and another .opacity on remaining un-clicked buttons, according to their state.
Note: I can watch for any state value inside buttons action, and then change it to have the animation, but since the modifier is applied on
the Button itself inside the ForEach loop, I am having the animation
applied to every buttons inside the foreach.
#State private var rotationDegree = 0.0
#State private var selectedNum = 0
#State private var correctAnswer = 0 // comes from saved data
var body: some View {
ForEach(0..<5) { num in // image as a button, loops through their prefix name
Button {
selectedNum = (num == correctAnswer) ? num : 0
withAnimation {
// once clicked, will animate all 5 buttons, need only this clicked button to animate.
if (num == correctAnswer) {
rotationDegree += 360
}
}
} label: {
Image("imageName\(num)")
}
.rotation3DEffect((.degrees((selectedNum == correctAnswer) ? rotationDegree : 0.0)), axis: (x: 0, y: (selectedNum == correctAnswer) ? 1 : 0, z: 0)) // animation I want to add to a specific button
}
}
import SwiftUI
struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an #State for each element of the ForEach
#State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}
Try adding this to your view.
#State var selectedNum: Int = 0
Then change your button to look something like this.
Button {
selectedNum = num
withAnimation {
rotationDegree += 360
}
} label: {
Image("imageName\(num)")
}
// Check if the selected button is equal to the num
.rotation3DEffect((.degrees(selectedNum == num ? rotationDegree : 0.0)), axis: (x: 0, y: 1, z: 0))
Not sure how the rotation3DEffect works. But here is another example
.background(selectedNum == num ? .red : .blue)

Animating the letters in a word w the app opens - SwiftUI

I'm trying to get a dancing letters effect when my app first opens.
I'm close. The coding below almost does what I want. I use a ForEach loop to loop through the letters of the word and apply an animation to each letter. And I use the onAppear function to set the drag amount when the app opens.
With this coding I can get the 'forward' motion but I can't get the animation to reverse so that the letters end up in their original position. I've tried adding a repeat with reverse, but, again, the letters never return to their original position
Does anyone have any idea how to do this?
struct ContentView: View {
let letters = Array("Math Fun!")
#State private var enabled = false
#State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num)/20).repeatCount(3, autoreverses: true))
}
}
.onAppear {
self.dragAmount = CGSize(width: 0, height: 80)
self.enabled.toggle()
}
}
}
Update: with Xcode 13.4 / iOS 15.5
Animation is based on changed states, we switched states and view animated to the new states, so to rollback we need to switch the states back.
Here is the possible approach (might still require tuning, but is ok for demo)
struct ContentView: View {
let letters = Array("Math Fun!")
#State private var enabled = false
#State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count, id: \.self) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num)/20), value: enabled)
}
}
.onAppear {
self.dragAmount = CGSize(width: 0, height: 80)
self.enabled.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.dragAmount = .zero
self.enabled.toggle()
}
}
}
}
You can use AnimatableModifier to achieve this effect. here is a sample code:
extension Double {
var rad: Double { return self * .pi / 180 }
var deg: Double { return self * 180 / .pi }
}
struct ContentView: View {
#State private var flag = false
var body: some View {
VStack {
Spacer()
Color.clear.overlay(WaveText("Your Text That Need Animate", waveWidth: 6, pct: flag ? 1.0 : 0.0).foregroundColor(.blue)).frame(height: 40)
Spacer()
}.onAppear {
withAnimation(Animation.easeInOut(duration: 2.0).repeatForever()) {
self.flag.toggle()
}
}
}
}
struct WaveText: View {
let text: String
let pct: Double
let waveWidth: Int
var size: CGFloat
init(_ text: String, waveWidth: Int, pct: Double, size: CGFloat = 34) {
self.text = text
self.waveWidth = waveWidth
self.pct = pct
self.size = size
}
var body: some View {
Text(text).foregroundColor(Color.clear).modifier(WaveTextModifier(text: text, waveWidth: waveWidth, pct: pct, size: size))
}
struct WaveTextModifier: AnimatableModifier {
let text: String
let waveWidth: Int
var pct: Double
var size: CGFloat
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.0) { (n, ch) in
Text(String(ch))
.scaleEffect(self.effect(self.pct, n, self.text.count, Double(self.waveWidth)))
}
}
}
func effect(_ pct: Double, _ n: Int, _ total: Int, _ waveWidth: Double) -> CGFloat {
let n = Double(n)
let total = Double(total)
return CGFloat(1 + valueInCurve(pct: pct, total: total, x: n/total, waveWidth: waveWidth))
}
func valueInCurve(pct: Double, total: Double, x: Double, waveWidth: Double) -> Double {
let chunk = waveWidth / total
let m = 1 / chunk
let offset = (chunk - (1 / total)) * pct
let lowerLimit = (pct - chunk) + offset
let upperLimit = (pct) + offset
guard x >= lowerLimit && x < upperLimit else { return 0 }
let angle = ((x - pct - offset) * m)*360-90
return (sin(angle.rad) + 1) / 2
}
}
}
You can find refrence and compelete answer here

Resources