Print angle rotate gesture - swiftui

I want print value angle rotate, but
Text (self.finaldegree)
don’t working.
Code:
#State var degree: Angle = Angle(degrees: 0)
#State var finaldegree: Angle = Angle(degrees: 0)
var body: some View {
VStack {
Image("Encoder_White1")
.padding(100)
.scaleEffect(2)
.rotationEffect(degree+finaldegree)
.gesture(RotationGesture()
.onChanged{ value in
degree = value
}
.onEnded{ value in
finaldegree = value
degree = Angle(degrees: 0)
})
}
}
Text (self.finaldegree)

It's not quite clear what you're trying to do, as there's a a lot of extra code that's probably not needed.
Text(self.finaldegree)
won't work by itself, as Text expects a String, or a value that can formatted as a String (e.g. Int, Double, Date). If you convert your angle into a double (e.g using .degrees), you can then format that for display, e.g.
Text(angle.degrees, format: .number)
A simplified solution to your code might be:
struct ContentView: View {
#State var angle: Angle = .zero
var body: some View {
VStack {
Image(systemName: "questionmark")
.padding(100)
.scaleEffect(4)
.rotationEffect(angle)
.gesture(
RotationGesture()
.onChanged{ value in
angle = value
}
)
Text("\(Text(angle.degrees, format: .number))°")
}
}
}

Related

Animate an animation's speed

I want the effect of a rotating record that has some ease to it whenever it starts and stops rotating. In code below the trigger is the isRotating Bool.
But I guess it's not possible to animate the speed of an animation?
struct PausableRotatingButtonStyle: ButtonStyle {
var isRotating: Bool
#State private var speed: Double = 1.0
#State private var degrees: Double = 0.0
var foreverAnimation: Animation {
Animation.linear(duration: 2)
.repeatForever(autoreverses: false)
.speed(speed)
}
func makeBody(configuration: Configuration) -> some View {
VStack {
Text("speed: \(speed.description)")
configuration.label
.rotationEffect(Angle(degrees: degrees))
.animation(foreverAnimation)
.onAppear {
degrees = 360.0
}
.onChange(of: isRotating) { value in
withAnimation(.linear) {
speed = value ? 1 : 0
}
}
}
}
}
struct TestRotatingButtonStyle_Previews: PreviewProvider {
static var previews: some View {
TestRotatingButtonStyle()
}
struct TestRotatingButtonStyle: View {
#State private var isPlaying: Bool = true
var body: some View {
VStack {
Button {
isPlaying.toggle()
} label: {
Text("💿")
.font(.system(size: 200))
}
.buttonStyle(PausableRotatingButtonStyle(isRotating: isPlaying))
}
}
}
}
If .easeOut and .spring options don't cut it, you can make a timing curve. This function accepts x and y values for two points (c0, c1).
These points define anchors that (choose a mental model verb: stretch/form/define) a cubic animation timing curve between the start and end points of your animation. (Just like drawing a path between 0,0 and 1,1. If this still sounds like gibberish, look at the objc.io link below for visuals.)
Image("wheel")
.animation(.timingCurve(0, 0.5, 0.25, 1, duration: 2))
An ease-in-out type curve could be .timingCurve(0.17, 0.67, 0.83, 0.67)
https://cubic-bezier.com/#.42,0,.58,1
You can read more via the objc.io guys.
https://www.objc.io/blog/2019/09/26/swiftui-animation-timing-curves/
Edit re: comment on speed
While timing is the intended API, you might be able to change speed in response to a binding from a GeometryEffect progress reporter.
In the animation below, I apply or remove the shadow beneath the ball based on the progress of the vertical-sin-wave-travel GeometryEffect. The progress value is between 0 and 1. (Takeoff/flight/landing is achieved by another boolean and animation curve for x-axis offset.)
[
/// Ball
.modifier(BouncingWithProgressBinding(
currentEffect: $currentEffectSize, // % completion
axis: .vertical,
offsetMax: flightHeight,
interationProgress: iteration
).ignoredByLayout())
struct BouncingWithProgressBinding: GeometryEffect {
#Binding var currentEffect: CGFloat // % completion
var axis: Axis
var offsetMax: CGFloat
var interationProgress: Double
var animatableData: Double {
get { interationProgress }
set { interationProgress = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let progress = interationProgress - floor(interationProgress)
let curvePosition = cos(2 * progress * .pi)
let effectSize = (curvePosition + 1) / ( .pi * 1.25 )
let translation = offsetMax * CGFloat(1 - effectSize)
DispatchQueue.main.async { currentEffect = CGFloat(1 - effectSize) }
if axis == .horizontal {
return ProjectionTransform(CGAffineTransform(translationX: translation, y: 0))
} else {
return ProjectionTransform(CGAffineTransform(translationX: 0, y: translation))
}
}
}

How can i rotate an image in SwiftUI with one finger only?

I have an image in a view and i want to be able to rotate it with one finger only, how can i do that in SwiftUI?
I checked the RotationGesture but it works only with two fingers...
Thanks
Ok, i got it with this code :
https://gist.github.com/ts95/9f8e05380824c6ca999ab3bc1ff8541f
Ok fixed it with this code :
struct RotationGesture: View {
#State var totalRotation = CGSize.zero
#State var currentRotation = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-totalRotation.width)))
.gesture(
DragGesture()
.onChanged { value in
totalRotation.width = value.translation.width + currentRotation.width
}
.onEnded { value in
currentRotation = totalRotation
}
)
}
}
But now we have to fixed the vertical movement because this solution is only working when you move around the X axis...
I want to solution to work when you make circle movement around the view you want to rotate...
one finger rotation
struct RotationGesture: View {
#State var gestureValue = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-gestureValue.width)))
.gesture(
DragGesture().onChanged({ (value) in
self.gestureValue = value.translation
}).onEnded({ (value) in
self.gestureValue = .zero
})
)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
}
}
Using the solutions from above this is what I have for rotating with one finger. This will keep the position when you finish rotating as well as starting the next rotation where the last one left off.
struct SwiftUIView: View {
#State private var rotation: Angle = Angle(degrees: 0)
#State private var previousRotation: Angle?
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.frame(width: 760, height: 760)
.rotationEffect(rotation, anchor: .center)
.gesture(DragGesture()
.onChanged{ value in
if let previousRotation = self.previousRotation {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
let angle = fingerAngle - previousRotation
rotation += angle
self.previousRotation = fingerAngle
} else {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
previousRotation = fingerAngle
}
}
.onEnded{ _ in
previousRotation = nil
})
}
}
A dragGesture will give us the value of a location where user has dragged on screen (in our case, the circle). Here, the circle frame will be equal height and equal width.
Capturing the location point where the user tapped and subtracting height/2 from y point and width/2 from x point results in deltaX and deltaY. Once we have deltaX and deltaY, we can convert it into radians using the atan2 function (which is provided by the Swift Standard Library).
struct SwiftUIView: View {
#State var angle: Angle = .zero
var circleFrame: CGRect = CGRect(x: 0, y: 0, width: 300, height: 300)
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.padding()
.rotationEffect(angle)
.gesture(
DragGesture()
.onChanged { value in
let deltaY = value.location.y - (circleFrame.height / 2)
let deltaX = value.location.x - (circleFrame.width / 2)
angle = Angle(radians: Double(atan2(deltaY, deltaX)))
}
)
}
}

Triggering an animation when a boolean is true

I would like to make an animation that is like this: a ZStack with a view and over it, a purple rectangle, that initially has scale 0 and opacity 0 and is at the center of screen.
When the animation is triggered, two animations happen:
the rectangle's scale increases from 0 to 1 in 0.4 seconds.
the rectangle's opacity increases from 0 to 1 in 0.3 seconds and from 1 to 0 in 0.1 seconds.
This is me trying to do this:
struct ContentView : View {
#State private var scale: CGFloat = 0
#State private var scaleSeed: CGFloat = 0.1
#State private var counter: Int = 1
#State private var triggerFlashAnimation = false {
didSet {
scale = 0
counter = 1
}
}
var body: some View {
ZStack {
Text("Hello")
if triggerFlashAnimation {
Rectangle()
.background(Color.purple)
.scaleEffect(scale)
.onAppear {
scale += scaleSeed
counter += 1
}
}
}
I think this will animate the scale but this is very crude as I don't have any control over time.
I remember back in the day of CoreGraphics that you could define keyframes for time and values.
How do I do that with SwiftUI. I mean a precise animation defining keyframes and values for every parameter?
I googled around and found nothing.
SwiftUI doesn't have support for keyframes like we're used to in CoreAnimation (sources: https://stackoverflow.com/a/56908148/560942, https://swiftui-lab.com/swiftui-animations-part1/).
However, you can do a variation on chaining by using delay:
struct ContentView : View {
#State private var scale: CGFloat = 0
#State private var opacity: Double = 0
#State private var triggerFlashAnimation = false
func triggerAnimationActions() {
withAnimation(.linear(duration: 0.4)) {
scale = 1
}
withAnimation(.linear(duration: 0.3)) {
opacity = 1
}
withAnimation(Animation.linear(duration: 0.1).delay(0.3)) {
opacity = 0
}
// reset
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
triggerFlashAnimation = false
scale = 0
opacity = 0
}
}
var body: some View {
ZStack {
Text("Hello")
Button(action: { triggerFlashAnimation = true }) {
Text("Button")
}.onChange(of: triggerFlashAnimation) { (val) in
if val {
triggerAnimationActions()
}
}
if triggerFlashAnimation {
Rectangle()
.fill(Color.purple.opacity(opacity))
.frame(width: 100, height: 100)
.scaleEffect(scale)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
It's also probably worth looking into AnimatableModifier which lets you define an animatableData property where you can interpolate values, base exact frames on a certain state of the animation, etc, but, as far as I know, doesn't account for timing at all -- rather it just lets you set a state based on the precise value of the current animation. Good resource for reading about AnimatableModifier: https://www.hackingwithswift.com/quick-start/swiftui/how-to-animate-the-size-of-text

Is it possible to flip a SwiftUI View vertically?

Looks like you can rotate a view upside down with UIView, but I can't find anything saying it's possible to do the same thing with a SwiftUI View.
Any help will be appreciated :)
Actually approach is the same to referenced post
Text("Test").font(.largeTitle)
.scaleEffect(CGSize(width: 1.0, height: -1.0)) // << here !!
Here is a convenience extension:
extension View {
func flipped(_ axis: Axis = .horizontal, anchor: UnitPoint = .center) -> some View {
switch axis {
case .horizontal:
return scaleEffect(CGSize(width: -1, height: 1), anchor: anchor)
case .vertical:
return scaleEffect(CGSize(width: 1, height: -1), anchor: anchor)
}
}
}
Use it:
Text("Flip me")
.flipped(.vertical)
Rotate Image As Mirror image by SwiftUI:
struct Rotate3DView: View {
#State var imagename: String = "exImage"
var body: some View {
VStack {
ZStack{
Image(imagename).resizable().opacity(0.3)
.rotation3DEffect(.degrees(180), axis: (x: -10, y: 0, z: 0))
.rotationEffect(.radians(.pi))
.padding(.top,60)
.cornerRadius(5)
.frame(width: 180, height: 280)
Image(imagename).resizable().padding(.bottom,45).frame(width: 180, height: 280)
}
}
}
}
This is very easy for native Users.
Turns out I can just apply this to the surrounding Stack:
.rotationEffect(.degrees(-180))
To flip it vertically
SwiftUI’s rotationEffect() modifier lets us rotate views freely, using either degrees or radians.
For example, if you wanted to rotate some text by -90 degrees so that it reads upwards, you would use this:
Text("Up we go")
.rotationEffect(.degrees(-90))
If you prefer using radians, just pass in .radians() as your parameter, like this:
Text("Up we go")
.rotationEffect(.radians(.pi))
View rotation is so fast that it’s effectively free, so you could even make it interactive using a slider if you wanted:
struct ContentView: View {
#State private var rotation = 0.0
var body: some View {
VStack {
Slider(value: $rotation, in: 0...360)
Text("Up we go")
.rotationEffect(.degrees(rotation))
}
}
}
enter link description here
By default views rotate around their center, but if you want to pin the rotation from a particular point you can add an extra parameter for that. For example if you wanted to make the slider above pivoting the rotation around the view’s top-left corner you’d write this:
struct ContentView: View {
#State private var rotation = 0.0
var body: some View {
VStack {
Slider(value: $rotation, in: 0...360)
Text("Up we go")
.rotationEffect(.degrees(rotation), anchor: .topLeading)
}
}
}
enter link description here

SwiftUI pause/resume rotation animation

So far I've seen the following technique for stopping an animation, but what I'm looking for here is that the rotating view stops at the angle it was at the moment and not return to 0.
struct DemoView: View {
#State private var isRotating: Bool = false
var foreverAnimation: Animation {
Animation.linear(duration: 1.8)
.repeatForever(autoreverses: false)
}
var body: some View {
Button(action: {
self.isRotating.toggle()
}, label: {
Text("🐰").font(.largeTitle)
.rotationEffect(Angle(degrees: isRotating ? 360 : 0))
.animation(isRotating ? foreverAnimation : .linear(duration: 0))
})
}
}
It seems having the rotation angle to be either 360 or 0 doesn't let me freeze it at an intermediate angle (and eventually resume from there). Any ideas?
Pausing and resuming the animation in SwiftUI is really easy and you're doing it right by determining the animation type in the withAnimation block.
The thing you're missing is two additional pieces of information:
What is the value at which the animation was paused?
What is the value to which the animation should proceed while resumed?
These two pieces are crucial because, remember, SwiftUI views are just ways of expressing what you want your UI to look like, they are only declarations. They are not keeping the current state of UI unless you yourself provide the update mechanism. So the state of the UI (like the current rotation angle) is not saved in them automatically.
While you could introduce the timer and compute the angle values yourself in a discreet steps of X milliseconds, there is no need for that. I'd suggest rather letting the system compute the angle value in a way it feels appropriate.
The only thing you need is to be notified about that value so that you can store it and use for setting right angle after pause and computing the right target angle for resume. There are few ways to do that and I really encourage you to read the 3-part intro to SwiftUI animations at https://swiftui-lab.com/swiftui-animations-part1/.
One approach you can take is using the GeometryEffect. It allows you to specify transform and rotation is one of the basic transforms, so it fits perfectly. It also adheres to the Animatable protocol so we can easily participate in the animation system of SwiftUI.
The important part is to let the view know what is the current rotation state so that it know what angle should we stay on pause and what angle should we go to when resuming. This can be done with a simple binding that is used EXCLUSIVELY for reporting the intermediate values from the GeometryEffect to the view.
The sample code showing the working solution:
import SwiftUI
struct PausableRotation: GeometryEffect {
// this binding is used to inform the view about the current, system-computed angle value
#Binding var currentAngle: CGFloat
private var currentAngleValue: CGFloat = 0.0
// this tells the system what property should it interpolate and update with the intermediate values it computed
var animatableData: CGFloat {
get { currentAngleValue }
set { currentAngleValue = newValue }
}
init(desiredAngle: CGFloat, currentAngle: Binding<CGFloat>) {
self.currentAngleValue = desiredAngle
self._currentAngle = currentAngle
}
// this is the transform that defines the rotation
func effectValue(size: CGSize) -> ProjectionTransform {
// this is the heart of the solution:
// reporting the current (system-computed) angle value back to the view
//
// thanks to that the view knows the pause position of the animation
// and where to start when the animation resumes
//
// notice that reporting MUST be done in the dispatch main async block to avoid modifying state during view update
// (because currentAngle is a view state and each change on it will cause the update pass in the SwiftUI)
DispatchQueue.main.async {
self.currentAngle = currentAngleValue
}
// here I compute the transform itself
let xOffset = size.width / 2
let yOffset = size.height / 2
let transform = CGAffineTransform(translationX: xOffset, y: yOffset)
.rotated(by: currentAngleValue)
.translatedBy(x: -xOffset, y: -yOffset)
return ProjectionTransform(transform)
}
}
struct DemoView: View {
#State private var isRotating: Bool = false
// this state keeps the final value of angle (aka value when animation finishes)
#State private var desiredAngle: CGFloat = 0.0
// this state keeps the current, intermediate value of angle (reported to the view by the GeometryEffect)
#State private var currentAngle: CGFloat = 0.0
var foreverAnimation: Animation {
Animation.linear(duration: 1.8)
.repeatForever(autoreverses: false)
}
var body: some View {
Button(action: {
self.isRotating.toggle()
// normalize the angle so that we're not in the tens or hundreds of radians
let startAngle = currentAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2)
// if rotating, the final value should be one full circle furter
// if not rotating, the final value is just the current value
let angleDelta = isRotating ? CGFloat.pi * 2 : 0.0
withAnimation(isRotating ? foreverAnimation : .linear(duration: 0)) {
self.desiredAngle = startAngle + angleDelta
}
}, label: {
Text("🐰")
.font(.largeTitle)
.modifier(PausableRotation(desiredAngle: desiredAngle, currentAngle: $currentAngle))
})
}
}
Here's my idea. It may not be the smart way.
If you want to stop at an intermediate angle, there are ways to use counting and timer.
struct DemoView: View {
#State private var degree: Double = 0
#State private var amountOfIncrease: Double = 0
#State private var isRotating: Bool = false
let timer = Timer.publish(every: 1.8 / 360, on: .main, in: .common).autoconnect()
var body: some View {
Button(action: {
self.isRotating.toggle()
self.amountOfIncrease = self.isRotating ? 1 : 0
}) {
Text("🐰").font(.largeTitle)
.rotationEffect(Angle(degrees: self.degree))
}
.onReceive(self.timer) { _ in
self.degree += self.amountOfIncrease
self.degree = self.degree.truncatingRemainder(dividingBy: 360)
}
}
}
TLDR; Use a Timer!
I tried a bunch of solutions and this is what worked for me the best!
struct RotatingView: View {
/// Use a binding so parent views can control the rotation
#Binding var isLoading: Bool
/// The number of degrees the view is rotated by
#State private var degrees: CGFloat = 0.0
/// Used to terminate the timer
#State private var disposeBag = Set<AnyCancellable>()
var body: some View {
Image(systemName: "arrow.2.circlepath")
.rotationEffect(.degrees(degrees))
.onChange(of: isLoading) { newValue in
if newValue {
startTimer()
} else {
stopTimer()
}
}
}
private func startTimer() {
Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.receive(on: DispatchQueue.main)
.sink { _ in
withAnimation {
degrees += 20 /// More degrees, faster spin
}
}
.store(in: &disposeBag)
}
private func stopTimer() {
withAnimation {
/// Snap to the nearest 90 degree angle
degrees += (90 - (degrees.truncatingRemainder(dividingBy: 90)))
}
/// This will stop the timer
disposeBag.removeAll()
}
}
Using Your View
struct ParentView: View {
#State isLoading: Bool = false
var body: some View {
RotatingView(isLoading: $isLoading)
}
}