I want an image to rotate in place but the code I've written makes the image rotate but also move to a new place in the view. What am I doing wrong? I feel like I've tried everything:
struct loadingView: View {
#State private var spinArrow = false // Spinning Motion
#State private var dismissArrow = false // Opacity 1 - 0
#State private var displayBorder = false // StrokeBorder (lineWidth) 64 - 5
#State private var displayCheckmark = false // Trimpath from 1 - 0
var body: some View {
ZStack {
Image("arrowtocircle").resizable()
.frame(width:50, height: 50)
.rotationEffect(.degrees(spinArrow ? 720 : -360))
.animation(Animation.easeInOut(duration: 2))
.onAppear() {
self.spinArrow.toggle()
}
}.frame(width: 500, height:500)
}
}
Related
I am trying to achieve drag and magnification gestures take in place simultaneously. With the code below, I was able to achieve transitioning dragging to magnifying, however, when I release one finger after magnification, the drag gesture is killed even though one finger is still on the rectangle. In the Photos app in iOS, you can magnify and release one finger then you can still drag an image. I have looked at Simultaneous MagnificationGesture and DragGesture with SwiftUI. Has this been fixed since then? If not, is there any way to achieve this by using UIViewRepresentable?
import SwiftUI
struct DragAndMagnificationGestures: View {
#State private var currentAmount = 0.0
#State private var finalAmount = 1.0
#State private var dragGestureOffset: CGSize = .zero
var body: some View {
VStack {
let combined = dragGesture.simultaneously(with: magnificationGesture)
Rectangle()
.frame(width: 200, height: 200)
.scaleEffect(finalAmount + currentAmount)
.offset(dragGestureOffset)
.gesture(combined)
}
}
}
struct MultiGesture_Previews: PreviewProvider {
static var previews: some View {
DragAndMagnificationGestures()
}
}
extension DragAndMagnificationGestures {
private var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
if finalAmount != 0 {
withAnimation(.spring()) {
dragGestureOffset = value.translation
}
}
}
.onEnded({ value in
withAnimation(.spring()) {
dragGestureOffset = .zero
}
})
}
private var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { amount in
currentAmount = amount - 1
}
.onEnded({ amount in
finalAmount += currentAmount
currentAmount = 0
})
}
}
What I expect to happen
I have a meditation view that has an animation subview with a binding property inhaling that should appear when a button is pressed.
When the animation subview appears, it should start the animation from the beginning. It's the Apple meditation breathing animation basically: it starts as a small ball and gets bigger as inhaling is true, and then smaller as inhaling is false.
When the user presses the button again, the animation should disappear.
When the user then again presses the button, a second time, it should start the animation subview with a binding clean. Meaning the subview is a small ball and gets big again. Like the first time.
struct Meditation: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
} }
.onDisappear {
infoText = "Start your meditation" // Never executed?
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
startBreathingAnimation.toggle()
}
}
.padding()
}
}
What actually happens
The animation subview with a binding is not reset, newly initialized, but starts just where it left off after being "dismissed" with the button press.
When I don't add a binding property into the subview, it actually works as expected: it resets every time and gives me a "fresh" subview. But I do actually need to observe changes to the animation subview property inhaling in order to update the infoText property in the main view.
Reproducible example code, ready to copy into Xcode
Any help is greatly appreciated!
// Can be copied to Xcode directly
struct Meditation: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
} }
.onDisappear {
infoText = "Start your meditation" // Never executed?
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
startBreathingAnimation.toggle()
}
}
.padding()
}
}
private let gradientStart = Color.accentColor.opacity(0.9)
private let gradientEnd = Color.accentColor.opacity(1.0)
private let gradient = LinearGradient(gradient: Gradient(colors: [gradientStart, gradientEnd]), startPoint: .top, endPoint: .bottom)
private let maskGradient = LinearGradient(gradient: Gradient(colors: [.black]), startPoint: .top, endPoint: .bottom)
private let maxSize: CGFloat = 150
private let minSize: CGFloat = 30
private let inhaleTime: Double = 8
private let exhaleTime: Double = 8
private let pauseTime: Double = 1.5
private let numberOfPetals = 4
private let bigAngle = 360 / numberOfPetals
private let smallAngle = bigAngle / 2
private let ghostMaxSize: CGFloat = maxSize * 0.99
private let ghostMinSize: CGFloat = maxSize * 0.95
private struct Petals: View {
let size: CGFloat
let inhaling: Bool
var isMask = false
var body: some View {
let petalsGradient = isMask ? maskGradient : gradient
ZStack {
ForEach(0..<numberOfPetals) { index in
petalsGradient
.frame(maxWidth: .infinity, maxHeight: .infinity)
.mask(
Circle()
.frame(width: size, height: size)
.offset(x: inhaling ? size * 0.5 : 0)
.rotationEffect(.degrees(Double(bigAngle * index)))
)
.blendMode(isMask ? .normal : .screen)
}
}
}
}
struct BreathAnimation: View {
#State private var size = minSize
#Binding var inhaling: Bool
#State private var ghostSize = ghostMaxSize
#State private var ghostBlur: CGFloat = 0
#State private var ghostOpacity: Double = 0
var body: some View {
ZStack {
// Color.black
// .edgesIgnoringSafeArea(.all)
ZStack {
// ghosting for exhaling
Petals(size: ghostSize, inhaling: inhaling)
.blur(radius: ghostBlur)
.opacity(ghostOpacity)
// the mask is important, otherwise there is a color
// 'jump' when exhaling
Petals(size: size, inhaling: inhaling, isMask: true)
// overlapping petals
Petals(size: size, inhaling: inhaling)
Petals(size: size, inhaling: inhaling)
.rotationEffect(.degrees(Double(smallAngle)))
.opacity(inhaling ? 0.8 : 0.6)
}
.rotationEffect(.degrees(Double(inhaling ? bigAngle : -smallAngle)))
.drawingGroup()
}
.onAppear {
performAnimations()
}
.onDisappear {
size = minSize
inhaling = false
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0
}
}
func performAnimations() {
withAnimation(.easeInOut(duration: inhaleTime)) {
inhaling = true
size = maxSize
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0.8
Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
ghostBlur = 30
ghostOpacity = 0
}
}
withAnimation(.easeInOut(duration: exhaleTime)) {
inhaling = false
size = minSize
ghostSize = ghostMinSize
}
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in
// endless animation!
performAnimations()
}
}
private func performAnimations2() {
withAnimation(.easeInOut(duration: inhaleTime)) {
inhaling = true
size = maxSize
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
ghostSize = ghostMaxSize
ghostBlur = 0
ghostOpacity = 0.8
Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
ghostBlur = 30
ghostOpacity = 0
}
}
withAnimation(.easeInOut(duration: exhaleTime)) {
inhaling = false
size = minSize
ghostSize = ghostMinSize
}
}
Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in
// endless animation!
performAnimations()
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
Meditation()
}
}
Here is a possible approach by setting a specific .id for the view and changing it on reset, forcing a redraw of the subview:
struct ContentView: View {
#State var startBreathingAnimation = false
#State var inhaling = false
#State var infoText = "Start a mediation"
#State var viewID = UUID()
var body: some View {
VStack(spacing: 20) {
ZStack {
if startBreathingAnimation {
BreathAnimation(inhaling: $inhaling)
.id(viewID) // here
.onChange(of: inhaling) { newValue in
if newValue {
infoText = "Breath in..."
} else {
infoText = "Breath out..."
}
}
} else {
Circle()
.frame(height: 100)
.foregroundColor(.blue)
}
}
Text(infoText)
Button("Toggle") {
if startBreathingAnimation {
startBreathingAnimation = false
infoText = "Start your meditation"
inhaling = false
} else {
startBreathingAnimation = true
viewID = UUID()
}
}
}
.padding()
}
}
As an extension to ChrisR's answer, which helped give me a fresh subview, but created the problem of out-of-sync animation property values, I used the help of PreferenceKeys. PreferenceKeys are apparently not that known among many intermediate SwiftUI devs, so I thought I'd share it here briefly.
Swiftful Thinking has a great video on them: link to video
A binding to a subview and its parent creates a way to strong connection for my case. I only want to observe the inhaling property of BreathAnimation() on my MainView().
That's when PreferenceKeys come into play.
Here is the code that helped me solve my issue.
Create a property that can be accessed from all views if needed:
struct InhalingPreferenceKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue()
}
}
// Housekeeping that lets us update the preference key in our childview
extension View {
func updateInhalingPreferenceKey(_ isInhaling: Bool) -> some View {
preference(key: InhalingPreferenceKey.self, value: isInhaling)
}
}
Add this to the childview and connect it to the BreathAnimation property inhaling:
var body: some View {
VStack {
// Content of child view
}
.updateInhalingPreferenceKey(inhaling)
}
And finally, we can access the childview property by using this:
.onPreferenceChange(InhalingPreferenceKey.self, perform: { inhaling in
self.inhaling = inhaling
}) // self.inhaling is the parentview property
This together with ChrisR's solution for fresh child views helped me achieve what I wanted. Hope this might help someone else as well!
I am actually a beginner in SwiftUI. I want to know if there is a way to detect when the global position of a View is changing. For the moment I can get the global position of the view with GeometryReader, but I definitely can't detect when it is changing.
Thank you in advance for your help !
struct VideoView: View {
#State var player : AVPlayer // It is a video player
#State var isplaying = false
#State var showcontrols = false // Don't show controls
#State var value : Float = 0
var body: some View {
GeometryReader { geo in
VStack{
ZStack{
VideoPlayer(player: $player)
}
.frame(height: UIScreen.main.bounds.height)
}
.background(Color.black.edgesIgnoringSafeArea(.all))
#IF GLOBAL POSITION OF THIS VIEW IS CHANGING
if(geo.frame(in: .global).midY < 400){
self.player.play()
self.isplaying = true
}
}
#ENDIF
}
}
}
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
I need to implement something like an animated page control. And I don't want to use integration with UIKit if possible. I have pages array containing 4 views I need to switch between. I create the animation itself by changing the value of progress variable using timer. And I have the following code right now
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}
}
It is animating great - current page is moved to the left until it disappears, and the next page is revealed from the right taking its place. At the end of animation I add 1 to both indices and reset progress to 0. But once the animation (well not exactly an animation - I just change the value of progress using timer, and generate every state manually) is over, the page with index 1 is swapped back to page with index 0. If I check with debugger, currentIndex and nextIndex values are correct - 1 and 2, but the page displayed after animation is always the one I started with (with index 0). Does anybody know why this is happening?
The whole code follows
struct ContentView : View {
let limit: Double = 15
let step: Double = 0.3
let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}.edgesIgnoringSafeArea(.vertical)
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct PageView: View {
#State var title: String
#State var imageName: String
#State var content: String
let imageWidth: Length = 150
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(imageName)
.resizable()
.frame(width: imageWidth, height: imageWidth)
.cornerRadius(imageWidth/2)
.clipped()
Text(content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}
in SceneDelegate:
if let windowScene = scene as? UIWindowScene {
let pages = (0...3).map { i in
PageView(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView(pages:
pages))
self.window = window
window.makeKeyAndVisible()
}
The following solution works. I think the problem was switching out views while SwiftUI tries to diff and update them is not something SwiftUI is good at.
So just use the same two PageView views and swap out their content based on the current index.
import Foundation
import SwiftUI
import Combine
struct PagesView : View {
let limit: Double = 15
let step: Double = 0.3
#State var pages: [Page] = (0...3).map { i in
Page(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
static let timerSpeed: Double = 0.01
#State var timer = Timer.publish(every: timerSpeed, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
var body: some View {
ZStack {
Button(action: {
self.isAnimating.toggle()
self.timer = Timer.publish(every: Self.timerSpeed, on: .current, in: .common).autoconnect()
}) { self.shape
}.offset(y: 300)
PageView(page: pages[currentIndex])
.offset(x: -CGFloat(pow(2, self.progress)))
PageView(page: pages[nextIndex])
.offset(x: CGFloat(pow(2, (self.limit - self.progress))))
}.edgesIgnoringSafeArea(.vertical)
.onReceive(self.timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct Page {
var title: String
var imageName: String
var content: String
let imageWidth: CGFloat = 150
}
struct PageView: View {
var page: Page
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(page.title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(page.imageName)
.resizable()
.frame(width: page.imageWidth, height: page.imageWidth)
.cornerRadius(page.imageWidth/2)
.clipped()
Text(page.content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}