I want to prevent a WatchKit app from sleeping while a view updates itself for a period of time. The view is updated by a Timer, and I want it to stay visible until the Timer is cancelled. For example:
struct TestView: View {
#State var seconds: Int = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(seconds)")
.onReceive(timer) { input in
seconds = (seconds + 1)
if seconds >= 60 {
stopTimer()
}
}
}
func stopTimer() {
self.timer.upstream.connect().cancel()
}
}
If I run an app with this view on a physical watch configured with a short wake duration, the screen sleeps before the 60 seconds are up.
There must be a way to do this (the built in Mindfulness application stays visible for example), but I can't figure out how.
Related
I have SwiftUI code which computes the time duration between two times (startTime and endTime) and rounds up to nearest 15 minutes. But how do I calculate the currency rate of $220 per hour from this duration?
I also seem to be struggling with organizing my code into view code (for SwiftUI) and also including the numerical code that runs in the background.
But here's my code I have so far with comments where I need to include this code.
import SwiftUI
struct ContentView: View {
#State private var startTime = Date().zeroSeconds
#State private var endTime = Date().zeroSeconds
#State private var number15Intervals = 0
#State private var amountDue = 0.0
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Case Times:")) {
DatePicker("Start Time", selection: $startTime , displayedComponents: .hourAndMinute)
DatePicker("End Time", selection: $endTime, in: startTime..., displayedComponents: .hourAndMinute)
}
Section(header: Text("Case Duration:")) {
Text("duration = \(self.duration) min")
Text("duration (15m) = \(self.duration15) min")
}
Section(header: Text("Amount Due:")) {
// What code do I put here to calculate currency (US dollars)
// which equals time (rounded up by 15 min) times a rate of $220 per hour?
Text(amountDue, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
}
.navigationTitle("DDA Rates Calculator")
}
}
var duration: TimeInterval {
guard endTime > startTime else {
return 0
}
let dateIntervalMinutes = DateInterval(start: startTime, end: endTime).duration / 60
return dateIntervalMinutes
}
var duration15: TimeInterval {
return (self.duration/15.0).rounded(.up)*15
}
}
extension Date {
var zeroSeconds: Date {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.hour, .minute], from: self)
return calendar.date(from: dateComponents) ?? self
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sorry if I'm not too clear here. I've tried all sorts of ways to do this but always seem to get all sorts of errors from XCode. I think it would be more confusing to show what I've tried so far since I tried it so many ways without success. I'm not understanding the scope and how to reference variables properly in SwiftUI.
You can replace your amountDue #State variable with a computed property:
var amountDue: Double {
duration15 / 60 * 220
}
(You can also remove the unused number15Intervals)
I'm implementing a Timer, to show an Alert after 10 seconds, using Combine's Publisher and #ObservedObject, #StateObject or #State to manage states in a screen A. The problem is when I navigate to screen B through a NavigationLink the Alert still show up.
Is there a way to process the state changes of a view only when it's on top?
struct NavigationView: View {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.receive(on: DispatchQueue .main)
.scan(0) { counter, _ in
counter + 1
}
#State private var counter = "Seconds"
#State private var alert: AlertConfiguration?
var body: some View {
ZStack {
HStack(alignment: .top) {
Text(counterText)
Spacer()
}
NavigationLink(
destination: destinationView
) {
Button(Strings.globalDetails1) {
navigationAction()
}
}
}
.onReceive(timer) { count in
if count == 10 {
makeAlert()
}
setSeconds(with: count)
}
.setAlert(with: $alert) // This is just a custom ViewModifier to add an Alert to a view
}
private func makeAlert() {
alert = AlertConfiguration()
}
private func setSeconds(with count: Int) {
counter = "seconds_counter".pluralLocalization(count: count)
}
}
You could add a variable that you set true if your View is in foreground and will be set false if you switch to a different View. Then you only increase the timer if said variable is true
.onReceive(timer) {
if count == 10 {
makeAlert()
}
if (timerIsRunning) {
count += 1
}
}
So basically the solution is to have a publisher that supports the lifecycle of the screen (onAppear & onDisappear).
This Answer helped: SwiftUI Navigation Stack pops back on ObservableObject update
I'm coding an app targeted at iOS but would be nice if it would still work (as is the case now) with iPadOS and macOS. This is a scientific app that performs matrices computations using LAPACK. A typical "step" of computation takes 1 to 5 seconds and I want the user to be able to run multiple computations with different parameters by clicking a single time on a button. For example, with an electric motor, the user clicks the button and it virtually performs the action of turning the motor, time step after time step until it reaches a final value. With each step, the “single step” computation returns a value, and the “multiple step” general computation gathers all these single step values in a list, the aim being to make a graph of these results.
So, my issue is making a progress bar so that the user knows where he is with the computation he issued. Using a for loop, I can run the multistep but it won’t refresh the ProgressView until the multistep computation is over. Thus, I decided to try DispatchQueue.global().async{}, in which I update the progress of my computation. It seems to work on the fact of refreshing the ProgressView but I get warnings that tell me I’m doing it the wrong way:
[SwiftUI] Publishing changes from background threads is not allowed;
make sure to publish values from the main thread (via operators like
receive(on:)) on model updates.
I do not know how to publish on ProgressView, also because all the examples I come across on the Internet show how to update ProgressView with a timer, and that is not what I want to do, I want each computation to be achieved, send ProgressView the fact that it can update, and continue with my computation.
Also, what I did does not update correctly the final values. As the computation is asynchronous, the multistep function finishes instantaneously, showing a value of 0.0 while when the tasks finishes, the final value should be 187500037500.0. I show you an example code with these values, it’s a dummy and I put a huge for loop to slow down the code and ressemble a computation so you can see the update of the ProgressView.
import SwiftUI
class Params: ObservableObject {
/// This is a class where I store all my user parameters
#Published var progress = 0.0
#Published var value = 0.0
}
struct ContentView: View {
#StateObject var params = Params()
#FocusState private var isFocused: Bool
var body: some View {
NavigationView {
List {
Section("Electromagnetics") {
NavigationLink {
Form {
ViewMAG_MultiStep(isFocused: _isFocused)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isFocused = false
}
}
}
} label: {
Text("Multi-step")
}
}
}
.navigationTitle("FErez")
}
.environmentObject(params)
}
}
struct ViewMAG_MultiStep: View {
#FocusState var isFocused: Bool
#EnvironmentObject var p: Params
#State private var showResults = false
#State private var induction = 0.0
var body: some View{
List{
Button("Compute") {
induction = calcMultiStep(p: p)
showResults.toggle()
}
.sheet(isPresented: $showResults) {
Text("\(induction)")
ProgressView("Progress...", value: p.progress, total: 100)
}
}
.navigationTitle("Multi-step")
}
}
func calcSingleStep(p: Params) -> Double {
/// Long computation, can be 1 to 5 seconds.
var induction = p.value
for i in 0...5000000 {
induction += Double(i) * 0.001
}
return induction
}
func calcMultiStep(p: Params) -> Double{
/// Usually having around 20 steps, can be up to 400.
var induction = 0.0
DispatchQueue.global().async {
for i in 0...5 {
induction += Double(i) * calcSingleStep(p: p)
p.progress += 10.0
}
print("Final value of induction: \(induction)")
}
return induction
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You have used the wrong thread, you should update the UI only on the main thread:
DispatchQueue.main.async { //main thread not global.
Please note: You should move those functions to your View struct or to a shared class, as using global functions does not align with best practices.
I'm currently building out a project where a user could create multiple timers however I'm running into the following error. "Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher'". I just can't figure out how to get Countdown Timer to conform to Publisher.
Here is the code for my timer object:
struct CountdownTimer: Identifiable {
let id = UUID()
let name: String
var minutes: Int
var seconds: Int
var countdown: Int {
let totalTime = seconds + (minutes * 60)
return totalTime
}
}
func timeString(time: Int) -> String {
return String(format: "%01i:%02i", minutes, seconds)
}
class CountdownTimers: ObservableObject {
#Published var timers = [CountdownTimer]()
}
My Content View:
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var countdownTimers = CountdownTimers()
#State private var showingAddSheet = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Color.pastelGreen
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView {
ForEach(countdownTimers.timers) { timer in
ZStack {
CardView()
.overlay(
HStack {
VStack(alignment: .leading) {
Text("\(timer.countdown)")
.font(.custom("Quicksand-Bold", size: 30))
Text(" \(timer.timeString(time: timer.countdown))")
.font(.custom("Quicksand-Bold", size: 40))
.onReceive(timer){ _ in
if timers.countdown > 0 {
timers.countdown -= 1
}
}
The error is happening on the .OnReceive(timer) line. the minutes and seconds are created on a different "AddView" but I don't believe that the issue is related to that section.
Any help would be greatly appreciated.
You're shadowing the name timer. It appears first here:
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Then, later, you use it again here:
ForEach(countdownTimers.timers) { timer in
Swift is going to assume you always mean the timer in the most-recently-defined scope, so when you use .onReceive, it assumes you mean timer from the ForEach (which is of type CountdownTimer) and not the Timer defined earlier.
To fix this, use a different name in your ForEach and adjust your code (such as the Text elements) to use the new name.
I am animating the remaining time for some scenes in my app. I have the issue that when the user interrupts this scene by going to the next scene, the animation doesn't stop
This code is a simplified version of what is happening:
struct ContentView: View {
#State private var remaining: Int = 40
#State private var sceneIndex: Int = 0
var body: some View {
VStack(spacing: 30) {
Text("Remaining:")
EmptyView().modifier(DoubleModifier(value: remaining))
Button(action: {
self.sceneIndex += 1
if self.sceneIndex == 1 {
withAnimation(.linear(duration: 10)) {
self.remaining = 30
}
} else {
withAnimation(.linear(duration: 0)) {
self.remaining = 30 // changing this to any other value will stop the animation
}
}
}) {
Text("Go to next scene")
}
}
}
}
struct DoubleModifier: AnimatableModifier {
private var value: Double
init(value: Int) {
self.value = Double(value)
}
var animatableData: Double {
get { value }
set { value = newValue }
}
func body(content: Content) -> some View {
Text("\(Int(value)) seconds remaining")
}
}
What is happening is that when the user clicks the countdown animation is initialised with a duration of 10 seconds and will set the remaining time from 40 to 30 seconds. Now when the user clicks the skip button, the animation should stop and go to the 30 seconds remaining. However, it still shows the animation from 40 to 30 (somewhere in between).
If I change the self.remaining to 30 in the else statement, the value is not updated since it is already set to 30, but internally it is still animating.
If I change the self.remaining to any other valid value than 30, for example self.remaining = 29 the animation is indeed immediately stopped.
I could replace the content in the else statement by:
withAnimation(.linear(duration: 0)) {
self.remaining = 29
DispatchQueue.main.async {
self.remaining = 30
}
}
Which does work but feels very messy and in my case it is not always possible to use DispatchQueue.main.async
Is there another, better, way to stop the animation and skip to the destination value?
Is it maybe somehow possible to assign some sort of identifier to the self.remaining = 30 in the else statement such that swiftUI knows the value actually did change even though it factually didn't so that swiftUI stops the animation?
It is not clear what's the logic planned by step from scene-to-scene, but goal as it is postulated now in question can be achieved with the following body:
var body: some View {
VStack(spacing: 30) {
Text("Remaining:")
EmptyView().modifier(DoubleModifier(value: remaining)).id(sceneIndex)
Button(action: {
self.sceneIndex += 1
withAnimation(.linear(duration: 10)) {
self.remaining = 30
}
}) {
Text("Go to next scene")
}
}
}