I am trying to update a SwiftUI Image very frequently. That image view is clickable and gets a highlight if selected.
When using an NSTimer with a short interval (0.25 seconds) for the image update, my SwiftUI view does not respond properly to user clicks anymore - clicks are only caught intermittently. If I set the timer interval to 1 second, things would work fine, however, that's not possible in my specific situation.
How can I ensure that my SwiftUI Image's onTapGesture works smoothly even with a high timer frequency?
The timer is declared as such:
let timer = Timer(timeInterval: 0.5, repeats: true, block: { [weak self] timer in
guard let strongSelf = self else {
timer.invalidate()
return
}
// updating an observable object here which will be propagated to the ScreenElement view below
})
timer.tolerance = 0.2
RunLoop.current.add(timer, forMode: .common)
Then I have the SwiftUI view declared as such:
struct ScreenElement: View {
var body: some View {
VStack(alignment: .center, spacing: 12)
{
Image(nsImage: screen.imageData)
.resizable()
.aspectRatio(174/105, contentMode: .fit)
.background(Asset.gray900.swiftUIColor)
.cornerRadius(12)
Text(screen.name)
}
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
.onTapGesture {
// modify data source and mark this current element as the highlighted (selected) one
}
}
}
What I have tried:
I tried to move the timer to a background thread which didn't really work and/or caused more problems than it solved. Additionally, I tried to increase the timer interval, which, however, is not feasible in my use case since it has to be a very high refresh rate.
Some further considerations I had but couldn't answer:
Is it maybe possible that SwiftUI just doesn't support a frequent refresh of 4x per second? Or did I maybe use the wrong UI Element to handle the tap gesture in my particular case? Or is it just not possible to have a timer with such frequent updates since it overloads the main thread?
Any help will be greatly appreciated!
The following works fine on an iPhone and Mac, all taps are recognised and it's updating at 10 times per second:
struct ContentView: View {
let imageNames = ["eraser", "lasso.and.sparkles", "folder.badge.minus", "externaldrive.badge.xmark", "calendar.badge.plus", "arrowshape.zigzag.forward", "newspaper.circle", "shareplay", "person.crop.square.filled.and.at.rectangle.fill"]
#State private var currentImageNames: [String] = ["", "", ""]
#State private var selected: Int?
var body: some View {
VStack {
HStack {
ForEach(0..<3, id: \.self) { index in
Image(systemName: currentImageNames[index])
.imageScale(.large)
.frame(width: 80, height: 60)
.contentShape(Rectangle())
.onTapGesture {
selected = index
}
.background(
Rectangle()
.fill(selected == index ? .red : .blue)
)
}
}
}
.onAppear {
addTimer()
}
.padding()
}
func addTimer() {
let timer = Timer(timeInterval: 0.1, repeats: true) { timer in
currentImageNames = [imageNames.randomElement()!, imageNames.randomElement()!, imageNames.randomElement()!]
}
RunLoop.current.add(timer, forMode: .common)
}
}
Is there something else you're possibly doing that is blocking the main queue?
Related
I want to make a mini game for kids that have Alphabet characters floating inside a bubble so I used text with overlay image and it's working but the problem is when I tap on the bubble I don't know how to make it disappear with animation and make the text stop on current location
struct ExtractedView: View {
#State var BubbleAnimate = false
#State var AlphbetArray: [String] = ["A", "B","C","D","E","F","G"]
#State var randomNumber = Int.random(in: 0...6)
var body: some View {
Button {
BubbleAnimate = true
print("Hi")
} label: {
Text("Test Me")
}
.offset(x:100)
Text(AlphbetArray[randomNumber])
.font(.system(size: 120))
.fontWeight(.bold)
.foregroundColor(.purple)
.padding()
.overlay {
Image("bubble").resizable().frame(width: 130.0, height: 130.0).scaledToFit()
}
.animation(.linear(duration: 5).repeatForever(autoreverses: false), value: BubbleAnimate)
.offset(y:BubbleAnimate ? -300 : 300)
.onTapGesture {
print("Hello")
BubbleAnimate.toggle()
// I tried toggle but it won't work probably
}
}
}
This is the result how can I make the bubble disappear with animation and make the letter stay for 1 second period to make more animation on it like scaling etc...
There are a few considerations:
The letter will never stop in a specific place, given that it has only two positions: offset 300, or offset -300. To make it stop somewhere, the offset must have intermediate values. Solution: use a timer that modifies the offset every n milliseconds.
The bubble is always shown. To make it disappear, you must have a condition and a specific variable to trigger its visibility. Solution: create a variable that conditionally shows the bubble, and animate it using the .transition() modifier.
Variables must start with lower case letter. Just use the convention, it makes it easier to understand the code.
Here's the code:
// Don't use binary values for the offset, use absolute values
#State private var bubbleOffset = 0.0
#State private var alphabetArray: [String] = ["A", "B","C","D","E","F","G"]
#State private var randomNumber = Int.random(in: 0...6)
// Trigger the visibility of the bubble separately from the animation
#State private var showBubble = false
let maxOffset = 300.0
let minOffset = -300.0
// Use a timer to change the offset
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Button {
// The button starts the animation; in this case,
// the animation happens only when the bubble is
// visible, but it does not have to be like that
showBubble = true
print("Hi")
} label: {
Text("Test Me")
}
.offset(x:100)
Text(alphabetArray[randomNumber])
.font(.system(size: 120))
.fontWeight(.bold)
.foregroundColor(.purple)
.padding()
.overlay {
// Show the bubble conditionally
if showBubble {
Image(systemName: "bubble.left").resizable().frame(width: 130.0, height: 130.0).scaledToFit()
// This transition will animate the disappearance of the bubble
.transition(.opacity)
}
}
// Absolute offset
.offset(y: bubbleOffset)
.onTapGesture {
withAnimation {
print("Hello")
// Trigger the disappearance of the bubble
showBubble = false
}
}
.onReceive(timer) { _ in
// In this case,
// the animation happens only when the bubble is
// visible, but it does not have to be like that
if showBubble {
// Change the offset at every emission of a new time
bubbleOffset -= 1
// Handle out-of-bounds
if bubbleOffset < minOffset {
bubbleOffset = maxOffset
}
}
}
}
}
I'm trying to detect a Long Press gesture on TabView that's swipable.
The issue is that it disables TabView's swipable behavior at the moment.
Applying the gesture on individual VStacks didn't work either - the long press doesn't get detected if I tap on the background.
Here's a simplified version of my code - it can be copy-pasted into Swift Playground:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var currentSlideIndex: Int = 0
#GestureState var isPaused: Bool = false
var body: some View {
let tap = LongPressGesture(minimumDuration: 0.5,
maximumDistance: 10)
.updating($isPaused) { value, state, transaction in
state = value
}
Text(isPaused ? "Paused" : "Not Paused")
TabView(selection: $currentSlideIndex) {
VStack {
Text("Slide 1")
Button(action: { print("Slide 1 Button Tapped")}, label: {
Text("Button 1")
})
}
VStack {
Text("Slide 2")
Button(action: { print("Slide 2 Button Tapped")}, label: {
Text("Button 2")
})
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(width: 400, height: 700, alignment: .bottom)
.simultaneousGesture(tap)
.onChange(of: isPaused, perform: { value in
print("isPaused: \(isPaused)")
})
}
}
PlaygroundPage.current.setLiveView(ContentView())
The overall idea is that this TabView will be rotating the slides automatically but holding a finger on any of them should pause the rotation (similar to Instagram stories). I removed that logic for simplicity.
Update: using DragGesture didn't work either.
The issue here is with the precedence of Animations in SwiftUI. Because TabView is a struct we are unable to change, its animation detection precedence cannot really be changed. The solution to this, however clunky, is to write our own custom tab view that has the expected behavior.
I apologize for how much code is here, but the behavior you described is surprisingly complex. In essence, we have a TimeLineView that is sending automatic updates to our view, telling it to change the pages, as you would see on Instagram. TimeLineView is a new feature, so if you want this to work old school, you could replace it with a Timer and its onReceive method, but I'm using this for brevity.
In the pages themselves, we are listening for this update, but only actually changing the page to the next one if there is room to do so and we are not long pressing the view. We use the .updating modifier on the LongPressGesture to know exactly when our finger is still on the screen or not. This LongPressGesture is combined in a SimultaneousGesture with a DragGesture, so that the drag can also be activated. In the drag gesture, we wait for the user's mouse/finger to traverse a certain percentage of the screen before animating the change in pages.
When sliding backwards, we initiate an async request to set the animation direction back to sliding forwards once the animation completes, so that updates received from the TimeLineView still animate in the correct direction, no matter which way we just swiped. Using custom gestures here has the added benefit that if you choose to do so, you can implement some fancy geometry effects to more closely emulate Instagram's animations. At the same time, our CustomPageView is still fully interactable, which means I can still click on button1 and see it's onTapGesture print message!
One caveat of passing in Views to a struct as a generic as I am doing in CustomTabView is that all of the views must be of the same type, which is part of the reason the pages are now reusable structs in their own right. If you have any questions about what you can / can't do with this methodology, let me know, but I've just run this in Playground same as you and it works exactly as described.
import SwiftUI
import PlaygroundSupport
// Custom Tab View to handle all the expected behaviors
struct CustomTabView<Page: View>: View {
#Binding var pageIndex: Int
var pages: [Page]
/// Primary initializer for a Custom Tab View
/// - Parameters:
/// - pageIndex: The index controlling which page we are viewing
/// - pages: The views to display on each Page
init(_ pageIndex: Binding<Int>, pages: [() -> Page]) {
self._pageIndex = pageIndex
self.pages = pages.map { $0() }
}
struct currentPage<Page: View>: View {
#Binding var pageIndex: Int
#GestureState private var isPressingDown: Bool = false
#State private var forwards: Bool = true
private let animationDuration = 0.5
var pages: [Page]
var date: Date
/// - Parameters:
/// - pageIndex: The index controlling which page we are viewing
/// - pages: The views to display on each Page
/// - date: The current date
init(_ pageIndex: Binding<Int>, pages: [Page], date: Date) {
self._pageIndex = pageIndex
self.pages = pages
self.date = date
}
var body: some View {
// Ensure that the Page fills the screen
GeometryReader { bounds in
ZStack {
// You can obviously change this to whatever you like, but it's here right now because SwiftUI will not look for gestures on a clear background, and the CustomPageView I implemented is extremely bare
Color.red
// Space the Page horizontally to keep it centered
HStack {
Spacer()
pages[pageIndex]
Spacer()
}
}
// Frame this ZStack with the GeometryReader's bounds to include the full width in gesturable bounds
.frame(width: bounds.size.width, height: bounds.size.height)
// Identify this page by its index so SwiftUI knows our views are not identical
.id("page\(pageIndex)")
// Specify the transition type
.transition(getTransition())
.gesture(
// Either of these Gestures are allowed
SimultaneousGesture(
// Case 1, we perform a Long Press
LongPressGesture(minimumDuration: 0.1, maximumDistance: .infinity)
// Sequence this Gesture before an infinitely long press that will never trigger
.sequenced(before: LongPressGesture(minimumDuration: .infinity))
// Update the isPressingDown value
.updating($isPressingDown) { value, state, _ in
switch value {
// This means the first Gesture completed
case .second(true, nil):
// Update the GestureState
state = true
// We don't need to handle any other case
default: break
}
},
// Case 2, we perform a Drag Gesture
DragGesture(minimumDistance: 10)
.onChanged { onDragChange($0, bounds.size) }
)
)
}
// If the user releases their finger, set the slide animation direction back to forwards
.onChange(of: isPressingDown) { newValue in
if !newValue { forwards = true }
}
// When we receive a signal from the TimeLineView
.onChange(of: date) { _ in
// If the animation is not pause and there are still pages left to show
if !isPressingDown && pageIndex < pages.count - 1{
// This should always say sliding forwards, because this will only be triggered automatically
print("changing pages by sliding \(forwards ? "forwards" : "backwards")")
// Animate the change in pages
withAnimation(.easeIn(duration: animationDuration)) {
pageIndex += 1
}
}
}
}
/// Called when the Drag Gesture occurs
private func onDragChange(_ drag: DragGesture.Value, _ frame: CGSize) {
// If we've dragged across at least 15% of the screen, change the Page Index
if abs(drag.translation.width) / frame.width > 0.15 {
// If we're moving forwards and there is room
if drag.translation.width < 0 && pageIndex < pages.count - 1 {
forwards = true
withAnimation(.easeInOut(duration: animationDuration)) {
pageIndex += 1
}
}
// If we're moving backwards and there is room
else if drag.translation.width > 0 && pageIndex > 0 {
forwards = false
withAnimation(.easeInOut(duration: animationDuration)) {
pageIndex -= 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
forwards = true
}
}
}
}
// Tell the view which direction to slide
private func getTransition() -> AnyTransition {
// If we are swiping left / moving forwards
if forwards {
return .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
}
// If we are swiping right / moving backwards
else {
return .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
}
}
}
var body: some View {
ZStack {
// Create a TimeLine that updates every five seconds automatically
TimelineView(.periodic(from: Date(), by: 5)) { timeLine in
// Create a current page struct, as we cant react to timeLine.date changes in this view
currentPage($pageIndex, pages: pages, date: timeLine.date)
}
}
}
}
// This is the view that becomes the Page in our Custom Tab View, you can make it whatever you want as long as it is reusable
struct CustomPageView: View {
var title: String
var buttonTitle: String
var buttonAction: () -> ()
var body: some View {
VStack {
Text("\(title)")
Button(action: { buttonAction() }, label: { Text("\(buttonTitle)") })
}
}
}
struct ContentView: View {
#State var currentSlideIndex: Int = 0
#GestureState var isPaused: Bool = false
var body: some View {
CustomTabView($currentSlideIndex, pages: [
{
CustomPageView(title: "slide 1", buttonTitle: "button 1", buttonAction: { print("slide 1 button tapped") })
},
{
CustomPageView(title: "slide 2", buttonTitle: "button 2", buttonAction: { print("slide 2 button tapped") })
}]
)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(width: 400, height: 700, alignment: .bottom)
}
}
PlaygroundPage.current.setLiveView(ContentView())
I found the best and cleanest solution to this is just to add a clear view on top of your tabView when the slide show is active and put the gesture recognizer on that.
I haven't shown the implementation of the start stop timer which depends on your design.
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var slideshowPlaying = false
#State var selection = 0
var body: some View {
ZStack {
TabView(selection: $selection) {
ForEach(modelArray.indices, id: \.self) { index in
SomeView()
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle())
.background(Color(.systemGroupedBackground))
.onReceive(self.timer) { _ in
if selection < modelArray.count + 1 {
selection += 1
} else {
selection = 0
}
}
if slideshowPlaying {
Color.clear
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0).onChanged { _ in
slideshowPlaying = false
})
}
}
}
Hello i wrote a simple code where i created an animation with a Circle which changes its position. I want the animation to stop when i tap on that Circle, but it doesn't stop, no matter that i set the animation to nil. Maybe my approach is wrong in some way. I will be happy if someone helps.
struct ContentView: View {
#State private var enabled = true
#State private var offset = 0.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onAppear {
withAnimation(self.enabled ? .easeIn(duration: 5) : nil) {
self.offset = Double(-UIScreen.main.bounds.size.height + 300)
}
}
.onTapGesture {
self.enabled = false
}
}
}
As #Schottky stated above, the offset value has already been set, SwiftUI is just animating the changes.
The problem is that when you tap the circle, onAppear is not called again which means enabled is not checked at all and even if it was it wouldn’t stop the offset from animating.
To solve this, we can introduce a Timer and do some small calculations, iOS comes with a built-in Timer class that lets us run code on a regular basis. So the code would look like this:
struct ContentView: View {
#State private var offset = 0.0
// 1
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var animationDuration: Double = 5.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onTapGesture {
// 2
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
// 3
animationDuration -= 0.1
if animationDuration <= 0.0 {
timer.upstream.connect().cancel()
} else {
withAnimation(.easeIn) {
self.offset += Double(-UIScreen.main.bounds.size.height + 300)/50
}
}
}
}
}
This creates a timer publisher which asks the timer to fire every 0.1 seconds. It says the timer should run on the main thread, It should run on the common run loop, which is the one you’ll want to use most of the time. (Run loops lets iOS handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)
When you tap the circle, the timer is automatically cancelled.
We need to catch the announcements (publications) by hand using a new modifier called onReceive(). This accepts a publisher as its first parameter (in this came it's our timer) and a function to run as its second, and it will make sure that function is called whenever the publisher sends its change notification (in this case, every 0.1 seconds).
After every 0.1 seconds, we reduce the duration of our animation, when the duration is over (duration = 0.0), we stop the timer otherwise we keep decreasing the offset.
Check out triggering-events-repeatedly-using-a-timer to learn more about Timer
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
#Binding var animate: Bool
#State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
#State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
What do you have to change?
make an offset as #Binding, provide from outside
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, and since you have offset as binding property, replaced view will have the correct offset.
Again this might not be the best solution but it works for my case.
What you need to understand is that only something that has already happened is being animated. It just takes longer to show. So in other words, when you set your offset to some value, it is going to animate towards that value, no matter what. What you, therefore, need to do is to set the offset to zero when tapping on the circle. The animation system is smart enough to deal with the 'conflicting' animations and cancel the previous one.
Also, a quick tip: You can do var offset: CGFloat = 0.0 so you don't have to cast it in the onAppear modifier
I’m building an Apple Watch app in SwiftUI that reads the user’s heart rate and displays it next to a heart symbol.
I have an animation that makes the heart symbol beat repeatedly. Since I know the actual user’s heart rate, I’d like to make it beat at the same rate as the user’s heart rate, updating the animation every time the rate changes.
I can determine how long many seconds should be between beats by dividing the heart rate by 60. For example, if the user’s heart rate is 80 BPM, the animation should happen every 0.75 seconds (60/80).
Here is example code of what I have now, where currentBPM is a constant, but normally that will be updated.
struct SimpleBeatingView: View {
// Once I get it working, this will come from a #Published Int that gets updated any time a new reading is avaliable.
let currentBPM: Int = 80
#State private var isBeating = false
private let maxScale: CGFloat = 0.8
var beatingAnimation: Animation {
// The length of one beat
let beatLength = 60 / Double(currentBPM)
return Animation
.easeInOut(duration: beatLength)
.repeatForever()
}
var body: some View {
Image(systemName: "heart.fill")
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(isBeating ? 1 : maxScale)
.animation(beatingAnimation)
.onAppear {
self.isBeating = true
}
}
}
I'm looking to make this animation behave more like Apple's built-in Heart Rate app. Instead of the heart constantly getting bigger or smaller, I'd like to have it beat (the animation in BOTH directions) then pause for a moment before beating again (the animation in both directions) then pause again, and so on.
When I add a one second delay, for example, with .delay(1) before .repeatForever(), the animation pauses half way through each beat. For example, it gets smaller, pauses, then gets bigger, then pauses, etc.
I understand why this happens, but how can I insert the delay between each autoreversed repeat instead of at both ends of the autoreversed repeat?
I'm confident I can figure out the math for how long the delay should be and the length of each beat to make everything work out correctly, so the delay length can be arbitrary, but what I'm looking for is help on how I can achieve a pause between loops of the animation.
One approach I played with was to flatMap the currentBPM into repeating published Timers every time I get a new heart rate BPM so I can try to drive animations from those, but I wasn't sure how I can actually turn that into an animation in SwiftUI and I'm not sure if manually driving values that way is the right approach when the timing seems like it should be handled by the animation, based on my current understanding of SwiftUI.
A possible solution is to chain single pieces of animation using DispatchQueue.main.asyncAfter. This gives you control when to delay specific parts.
Here is a demo:
struct SimpleBeatingView: View {
#State private var isBeating = false
#State private var heartState: HeartState = .normal
#State private var beatLength: TimeInterval = 1
#State private var beatDelay: TimeInterval = 3
var body: some View {
VStack {
Image(systemName: "heart.fill")
.imageScale(.large)
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(heartState.scale)
Button("isBeating: \(String(isBeating))") {
isBeating.toggle()
}
HStack {
Text("beatLength")
Slider(value: $beatLength, in: 0.25...2)
}
HStack {
Text("beatDelay")
Slider(value: $beatDelay, in: 0...5)
}
}
.onChange(of: isBeating) { isBeating in
if isBeating {
startAnimation()
} else {
stopAnimation()
}
}
}
}
private extension SimpleBeatingView {
func startAnimation() {
isBeating = true
withAnimation(Animation.linear(duration: beatLength * 0.25)) {
heartState = .large
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.25) {
withAnimation(Animation.linear(duration: beatLength * 0.5)) {
heartState = .small
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.75) {
withAnimation(Animation.linear(duration: beatLength * 0.25)) {
heartState = .normal
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength + beatDelay) {
withAnimation {
if isBeating {
startAnimation()
}
}
}
}
func stopAnimation() {
isBeating = false
}
}
enum HeartState {
case small, normal, large
var scale: CGFloat {
switch self {
case .small: return 0.5
case .normal: return 0.75
case .large: return 1
}
}
}
Before pawello2222 answered, I was still experimenting with trying to get this working and I came up with a solution that uses Timer publishers and the Combine framework.
I've included the code below, but it is not a good solution because every time currentBPM changes a new delay is added before the next animation starts. pawello2222's answer was better because it always allows the current beating animation to finish then starts the next one at the updated rate for the next cycle.
Also, I think my answer here is not as good because a lot of the animation work is done in the data store object rather than being encapsulated in the view, where it probably makes more sense.
import SwiftUI
import Combine
class DataStore: ObservableObject {
#Published var shouldBeSmall: Bool = false
#Published var currentBPM: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
let newLengthPublisher =
$currentBPM
.map { 60 / Double($0) }
.share()
newLengthPublisher
.delay(for: .seconds(0.2),
scheduler: RunLoop.main)
.map { beatLength in
return Timer.publish(every: beatLength,
on: .main,
in: .common)
.autoconnect()
}
.switchToLatest()
.sink { timer in
self.shouldBeSmall = false
}
.store(in: &cancellables)
newLengthPublisher
.map { beatLength in
return Timer.publish(every: beatLength,
on: .main,
in: .common)
.autoconnect()
}
.switchToLatest()
.sink { timer in
self.shouldBeSmall = true
}
.store(in: &cancellables)
currentBPM = 75
}
}
struct ContentView: View {
#ObservedObject var store = DataStore()
private let minScale: CGFloat = 0.8
var body: some View {
HStack {
Image(systemName: "heart.fill")
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(store.shouldBeSmall ? 1 : minScale)
.animation(.easeIn)
Text("\(store.currentBPM)")
.font(.largeTitle)
.fontWeight(.bold)
}
}
}
How can i get the Y offset of a a moving element at the same time while it's moving?
This is the code that I'm tring the run:
import SwiftUI
struct testView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
self.showPopup.toggle()
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
.animation(.easeInOut)
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
I want to get the Y value of the black squar when the button is clicked the squar will move to a 0 position however I want to detect when the squar did reach the 0 value how can i do that?
Default animation duration (for those animations which do not have explicit duration parameter) is usually 0.25-0.35 (independently of where it is started & platform), so in your case it is completely safe (tested with Xcode 11.4 / iOS 13.4) to use the following approach:
withAnimation(.spring()){
self.offset = .zero
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animationRunning = false
}
}
SwiftUI doesn't provide a callback with an animation completion, so there are two methods to accomplish detecting when the square has finished animating.
Method 1: Using AnimatableModifier. Here's a well-written Stack Overflow post on how to set that up.
Method 2: Using a Timer to run after the animation has completed. The idea here is to create a scheduled timer that runs after the timer has finished.
struct ContentView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
// add addition here with specified duration
withAnimation(.easeInOut(duration: 1), {
self.showPopup.toggle()
})
// set timer to run after the animation's specified duration
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
// run your completion code here eg.
withAnimation(.easeInOut(duration: 1)) {
self.showPopup.toggle()
}
timer.invalidate()
}
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
}
}
}
}