How to allow timer to count down in background on Watch OS - swift

Made a pretty simple timer app, but I've found that Apple Watch documentation is no where near as good as the phone. It's been difficult to find any answers on this.
Here is my content view. The seconds for the timer is kept in '''timeVal'''
struct ContentView: View {
#State var timeVal = 0.0
#State var timerScreenShow:Bool = false
var body: some View {
VStack {
HStack{
Spacer()
Text("\(self.timeVal.hourMinute)")
Spacer()
}
//various buttons to set timer
NavigationLink(
destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: Double(Int(self.timeVal)), initialTime: Int(self.timeVal)),
isActive: $timerScreenShow,
label: {
Text("Start")
}).background(Color.green).cornerRadius(22)
}
}
}
Then, here is the bit that keeps time:
struct TimerView: View {
#Binding var timerScreenShow:Bool
#State var timeVal:Double
let initialTime:Int
var body: some View {
if timeVal > -1 {
VStack {
ZStack {
Text("\(self.timeVal.hourMinuteSecond)").font(.system(size: 20))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
//Text("\(self.timeVal.hourMinuteSecond)")
ProgressBar(progress: Int(self.timeVal), initial: self.initialTime).frame(width: 90.0, height: 90.0)
}
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Cancel")
.foregroundColor(Color.red)
})
.padding(.top)
}
} else {
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
}
}
}
What do I need to do to keep the timer running when the watch app is closed?

Don't keep track of how much time is left on the counter. Instead, store the startTime = Date(). Use the timer only as a signal to update the UI. When the timer ticks, compute the time left as startAmount - (Date().timeInterval(since: startTime)). Store startAmount and startTime and Bool isTimerRunning in persistent storage such as #AppStorage. When the app restarts, pick up where you left off.

Related

Is there a way to get my Apple Watch App to vibrate when the screen sleeps?

I own a Apple Watch series 3 and I made an app for it that vibrates every specified amount of seconds. I know haptics drain your battery but, regardless, its the purpose of my app. I tried to test it on my watch and I encountered a problem. When the screen is on the app functions like its supposed to but as soon as I put my wrist down and the screen sleeps, the app runs in the background but it doesn't vibrate like supposed to. Is there a way around this? Like I said I have a series 3 therefore I don't have access to the always on screen display function.
My app code is below:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
WKInterfaceDevice.current().play(.failure)
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}

Cannot get Haptic Feedback to work with Swift

I'm a CS major in college who had an idea for an Apple Watch app that vibrates every time it reaches a certain time interval, for example the user picks intervals of 20s through a picker, taps "start" and the app starts a timer from 0 to 20 and once it reaches 20 I want the watch to vibrate and start counting up to 20 again until the user chooses to stop it. I want to use it for runners to have an idea of consistent paces to run. The problem is that I can't get UIImpactFeedbackGenerator to work and none of the other methods of haptic feedback either. I have no swift experience, only python and Java but I am basically done except for this haptic part. The one error I get is UIImpactFeedbackGenerator is out of scope where the generator is declared under SecondView. Thank you in advance! Here's my code:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
WatchOs does have the play(_:) - method for notification.
try WKInterfaceDevice.current().play(.click)
for more information take a look at the documentation -- https://developer.apple.com/documentation/watchkit/wkinterfacedevice/1628128-play

Dismiss picker in SwiftUI and reset picker selection to initial value

I am making a view in SwiftUI with a picker-popover. When picking a value and dismissing the view everything works fine.
But I need to be able to dismiss the picker WITHOUT setting the newly selected value, and have it go back to the initial value it had when being opened.
You can see the code here:
import SwiftUI
struct ContentView: View {
#State var showPicker = false
#State var selectedPickerOption = 0
let pickerOptions = ["Hello", "World", "Yes"]
var body: some View {
ZStack {
VStack {
Text("Selected Option: \(pickerOptions[selectedPickerOption])")
Button(
action: {
showPicker = true
},
label: {
Text("Open Picker")
.padding()
}
)
}
if showPicker {
PickerPopover(
pickerOptions: pickerOptions,
width: 300,
height: 300,
showPicker: $showPicker,
selectedPickerOption: $selectedPickerOption,
initialPickerOption: selectedPickerOption
)
.background(Color.red)
}
}
}
}
struct PickerPopover: View {
var pickerOptions: [String]
var width: CGFloat
var height: CGFloat
#Binding var showPicker: Bool
#Binding var selectedPickerOption: Int
var initialPickerOption: Int // This one doesn't work yet
func selectOption() {
withAnimation {
showPicker.toggle()
}
}
func cancel() {
// ######### THIS LINE HERE ISN'T WORKING ##############
selectedPickerOption = initialPickerOption
withAnimation {
showPicker.toggle()
}
}
var body: some View {
VStack {
Picker(
selection: $selectedPickerOption,
label: Text("")
) {
ForEach(0 ..< pickerOptions.count) {
Text(self.pickerOptions[$0])
}
}
.pickerStyle(WheelPickerStyle())
Button(action: cancel) {
Text("Cancel")
}
Button(action: selectOption) {
Text("Select")
}
}
.transition(.move(edge: .bottom))
}
}
I believe the first line in the cancel() function should do the trick - if selectedPickerOption is set to 0 (or 1, etc) that will reset the picker to that index specifically.
I have been unable to set it dynamically though. I have tried passing in an additional value (intialPickerOption), but resetting selectedPickerOption = initialPickerOption does seem to set it to the actual currently selected selectedPickerOption, and the picker behaves as if that was chosen correctly.
What am I possibly missing here?
The problem occurs as you are modifying selectedPickerOption which will cause your ContentView to reload whenever the picker changes. Hence, you will pass the selected value as initialPickerOption. selectedPickerOption will always be the same like your initial value.
Here is a solution with using local State in your PickerView and then sync the Binding on Select or don't sync it. I comment the code at these parts
struct PickerPopover: View {
var pickerOptions: [String]
var width: CGFloat
var height: CGFloat
#Binding var showPicker: Bool
#Binding var selectedPickerOption: Int
#State var localState : Int = 0 //<< Here your local State
func selectOption() {
self.selectedPickerOption = localState //<< Sync the binding with the local State
withAnimation {
showPicker.toggle()
}
}
func cancel() {
//<< do nothing here
withAnimation {
showPicker.toggle()
}
}
var body: some View {
VStack {
Picker(
selection: $localState,
label: Text("")
) {
ForEach(0 ..< pickerOptions.count) {
Text(self.pickerOptions[$0])
}
}
.pickerStyle(WheelPickerStyle())
Button(action: cancel) {
Text("Cancel")
}
Button(action: selectOption) {
Text("Select")
}
}
.transition(.move(edge: .bottom))
.onAppear {
self.localState = selectedPickerOption // << set inital value here
}
}
}

Get offset of element while moving using animation

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)
}
}
}
}

SwiftUI: Stop an Animation that Repeats Forever

I would like to have a 'badge' of sorts on the screen and when conditions are met, it will bounce from normal size to bigger and back to normal repeatedly until the conditions are no longer met. I cannot seem to get the badge to stop 'bouncing', though. Once it starts, it's unstoppable.
What I've tried:
I have tried using a few animations, but they can be classified as animations that use 'repeatForever' to achieve the desired effect and those that do not. For example:
Animation.default.repeatForever(autoreverses: true)
and
Animation.spring(response: 1, dampingFraction: 0, blendDuration: 1)(Setting damping to 0 makes it go forever)
followed by swapping it out with .animation(nil). Doesn't seem to work. Does anyone have any ideas? Thank you so very much ahead of time! Here is the code to reproduce it:
struct theProblem: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation( active ? Animation.default.repeatForever(autoreverses: true): nil )
.frame(width: 100, height: 100)
.onTapGesture {
self.active = !self.active
}
}
}
I figured it out!
An animation using .repeatForever() will not stop if you replace the animation with nil. It WILL stop if you replace it with the same animation but without .repeatForever(). ( Or alternatively with any other animation that comes to a stop, so you could use a linear animation with a duration of 0 to get a IMMEDIATE stop)
In other words, this will NOT work: .animation(active ? Animation.default.repeatForever() : nil)
But this DOES work: .animation(active ? Animation.default.repeatForever() : Animation.default)
In order to make this more readable and easy to use, I put it into an extension that you can use like this: .animation(Animation.default.repeat(while: active))
Here is an interactive example using my extension you can use with live previews to test it out:
import SwiftUI
extension Animation {
func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
if expression {
return self.repeatForever(autoreverses: autoreverses)
} else {
return self
}
}
}
struct TheSolution: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation(Animation.default.repeat(while: active))
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct TheSolution_Previews: PreviewProvider {
static var previews: some View {
TheSolution()
}
}
As far as I have been able to tell, once you assign the animation, it will not ever go away until your View comes to a complete stop. So if you have a .default animation that is set to repeat forever and auto reverse and then you assign a linear animation with a duration of 4, you will notice that the default repeating animation is still going, but it's movements are getting slower until it stops completely at the end of our 4 seconds. So we are animating our default animation to a stop through a linear animation.
How about using a Transaction
In the code below, I turn off or turn on the animation depending on the state of the active
Warning: Be sure to use withAnimation otherwise nothing will work
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect(active ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true), value: active)
.frame(width: 100, height: 100)
.onTapGesture {
useTransaction()
}
}
func useTransaction() {
var transaction = Transaction()
transaction.disablesAnimations = active ? true : false
withTransaction(transaction) {
withAnimation {
active.toggle()
}
}
}
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)
}
}
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, so it is back to its original state.
Again this might not be the best solution but it works for my case.
There is nothing wrong in your code, so I assume it is Apple's defect. It seems there are many with implicit animations (at least with Xcode 11.2). Anyway...
I recommend to consider alternate approach provided below that gives expected behaviour.
struct TestAnimationDeactivate: View {
#State var active: Bool = false
var body: some View {
VStack {
if active {
BlinkBadge()
} else {
Badge()
}
}
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct Badge: View {
var body: some View {
Circle()
}
}
struct BlinkBadge: View {
#State private var animating = false
var body: some View {
Circle()
.scaleEffect(animating ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true))
.onAppear {
self.animating = true
}
}
}
struct TestAnimationDeactivate_Previews: PreviewProvider {
static var previews: some View {
TestAnimationDeactivate()
}
}
Aspid comments on the accepted solution that an Xcode update broke it. I was struggling with a similar problem while playing around with an example from Hacking with Swift, and
.animation(active ? Animation.default.repeatForever() : Animation.default)
was not working for me either on Xcode 13.2.1. The solution I found was to encapsulate the animation in a custom ViewModifier. The code below illustrates this; the big button toggles between active and inactive animations.
`
struct ContentView: View {
#State private var animationAmount = 1.0
#State private var animationEnabled = false
var body: some View {
VStack {
Button("Tap Me") {
// We would like to stop the animation
animationEnabled.toggle()
animationAmount = animationEnabled ? 2 : 1
}
.onAppear {
animationAmount = 2
animationEnabled = true
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
)
.modifier(AnimatedCircle(animationAmount: $animationAmount, animationEnabled: $animationEnabled))
}
}
}
struct AnimatedCircle: ViewModifier {
#Binding var animationAmount: Double
#Binding var animationEnabled: Bool
func body(content: Content) -> some View {
if animationEnabled {
return content.animation(.easeInOut(duration: 2).repeatForever(autoreverses: false),value: animationAmount)
}
else {
return content.animation(.easeInOut(duration: 0),value: animationAmount)
}
}
}
`
It may not be the best conceivable solution, but it works. I hope it helps somebody.