Observe if my value reached zero in Swift? [duplicate] - ios

It is possible to pass a date to Text() in SwiftUI, then format it as a timer using the style argument. However, a countdown like this never stops, it just keeps incrementing after zero. How to make it stop at 0?
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
return date ?? Date()
}
Above is the function I use to start a countdown, then I pass it as follows:
Text(nextRollTime(in: 20), style: .timer)

Here is a demo of possible approach - as .timer run from now for ever (by design), the idea is to replace it with regular text once specified period is over.
Tested with Xcode 12b3 / iOS 14.
struct DemoView: View {
#State private var run = false
var body: some View {
VStack {
if run {
Text(nextRollTime(in: 10), style: .timer)
} else {
Text("0:00")
}
}
.font(Font.system(.title, design: .monospaced))
.onAppear {
self.run = true
}
}
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
DispatchQueue.main.asyncAfter(deadline: .now() + Double(seconds)) {
self.run = false
}
return date ?? Date()
}
}

Related

How do I animate a number in swift?

I want to animate a number. The animation I want to achieve is going from 0 increasing all the way up to the current number (at high speed). In this project, the number is the number of steps a user has taken. Is there a way this can be achieved?
LazyVStack{
ForEach(steps, id: \.id) { step in
//Here is the number I want to be animated
Text("\(step.count)")
.font(.custom(customFont, size: 50))
Text("Steps")
.font(.custom(customFont, size: 25))
.multilineTextAlignment(.center)
}
}
I believe I have a function along the right lines, I just need to apply it! Here is the function:
func addNumberWithRollingAnimation() {
withAnimation {
// Decide on the number of animation tasks
let animationDuration = 1000 // milliseconds
let tasks = min(abs(self.enteredNumber), 100)
let taskDuration = (animationDuration / tasks)
// add the remainder of our entered num from the steps
total += self.enteredNumber % tasks
// For each task
(0..<tasks).forEach { task in
// create the period of time when we want to update the number
// I chose to run the animation over a second
let updateTimeInterval = DispatchTimeInterval.milliseconds(task * taskDuration)
let deadline = DispatchTime.now() + updateTimeInterval
// tell dispatch queue to run task after the deadline
DispatchQueue.main.asyncAfter(deadline: deadline) {
// Add piece of the entire entered number to our total
self.total += Int(self.enteredNumber / tasks)
}
}
}
}
Here is a utility function called Timer.animateNumber() which takes a Binding<Int> to animate, a Binding<Bool> busy which indicates if the value is currently animating, and Int start value, an Int end value, and a Double duration in seconds.
To use it, you need to define an #State private var number: Int to animate, and #State private var busy: Bool to keep track of the animation's state. This can also be used to terminate the animation early by just setting busy to false. Pass in your start value, end value, and duration in seconds.
This demo shows two animated numbers. The first counts up from 1 to 10000 in 1 second. The second counts down from 20 to 0 in 20 seconds. The Stop All button can be used to stop both animations.
extension Timer {
static func animateNumber(number: Binding<Int>, busy: Binding<Bool>, start: Int, end: Int, duration: Double = 1.0) {
busy.wrappedValue = true
let startTime = Date()
Timer.scheduledTimer(withTimeInterval: 1/120, repeats: true) { timer in
let now = Date()
let interval = now.timeIntervalSince(startTime)
if !busy.wrappedValue {
timer.invalidate()
}
if interval >= duration {
number.wrappedValue = end
timer.invalidate()
busy.wrappedValue = false
} else {
number.wrappedValue = start + Int(Double(end - start)*(interval/duration))
}
}
}
}
struct ContentView: View {
#State private var number: Int = 0
#State private var number2: Int = 0
#State private var busy: Bool = false
#State private var busy2: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(String(number))
Button("Go") {
if !busy {
Timer.animateNumber(number: $number, busy: $busy, start: 1, end: 10000, duration: 1)
}
}
Text(String(number2))
Button("Go") {
if !busy2 {
Timer.animateNumber(number: $number2, busy: $busy2, start: 20, end: 0, duration: 20)
}
}
Button("Stop All") {
busy = false
busy2 = false
}
}
}
}

Countdown timer SwiftUI

How to make a countdown timer daily at a specific time. When I open the application again, the timer is reset and the countdown starts again, I'm trying to figure out how to make the timer start again after its time has elapsed..
For example, so that this timer starts over every day at 6 pm
struct TimerView: View {
//MARK: - PROPERTIES
#State var timeRemaining = 24*60*60
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
//MARK: - BODY
var body: some View {
Text("\(timeString(time: timeRemaining))")
.font(.system(size: 60))
.frame(height: 80.0)
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.black)
.onReceive(timer){ _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}else{
self.timer.upstream.connect().cancel()
}
}
}
//Convert the time into 24hr (24:00:00) format
func timeString(time: Int) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
}
the timer is reset and the countdown starts again
You could try to play with UserDefaults to store variables in the device's memory.
Here is the Documentation : https://developer.apple.com/documentation/foundation/userdefaults
Take a look at TimelineView and AppStorage e.g.
#AppStorage("StartDate") var startDate: Date
...
TimelineView(.periodic(from: startDate, by: 1)) { context in
AnalogTimerView(date: context.date)
}

Basing a StopWatch off of Date() - SwiftUI

I am wanting to have a stopwatch in my app that runs completely off the device's time. I have my code below which takes the time in which the start button is pressed, and then every second updates the secondsElapsed to be the difference between the startTime and current. I am getting stuck on implementing a pause function. If I just invalidate the update timer, then the timer will restart having pretty much carried on from where it left off. Any ideas on how this could be done?
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var timer = Timer()
func startWatch(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
timer.invalidate()
}
}
I display the stopwatch using this code below:
struct ContentView: View {
#ObservedObject var stopWatchManager = StopWatchManager()
var body: some View{
HStack{
Button("Start"){
stopWatchManager.startWatch()
}
Text("\(stopWatchManager.secondsElapsed)")
Button("Pause"){
stopWatchManager.pauseWatch()
}
}
}
}
Yes. Here is how to do it:
When pause is pressed, note the current time and compute the elapsed time for the timer. Invalidate the update timer.
When the timer is resumed, take the current time and subtract the elapsed time. Make that the startTime and restart the update timer.
Here's the updated code:
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var elapsedTime = 0.0
var paused = false
var running = false
var timer = Timer()
func startWatch(){
guard !running else { return }
if paused {
startTime = Date().addingTimeInterval(-elapsedTime)
} else {
startTime = Date()
}
paused = false
running = true
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
guard !paused else { return }
timer.invalidate()
elapsedTime = Date().timeIntervalSince(startTime)
paused = true
running = false
}
}
Things to note:
I changed the timer interval to 0.1 from 1 to avoid missing updates.
I added paused and running state variables to keep the buttons from being pressed more than once in a row.

SwiftUI State updates in Xcode 11 Playground

I have not been able to find anything through the standard Google search, but is there any reason why the ContentView is not updating through the ObservableObject? Feel like I am missing something but I am not quite sure what.
import SwiftUI
import PlaygroundSupport
let start = Date()
let seconds = 10.0 * 60.0
func timeRemaining(minutes: Int, seconds: Int) -> String {
return "\(minutes) minutes \(seconds) seconds"
}
class ViewData : ObservableObject {
#Published var timeRemaining: String = "Loading..."
}
// View
struct ContentView: View {
#ObservedObject var viewData: ViewData = ViewData()
var body: some View {
VStack {
Text(viewData.timeRemaining)
}
}
}
let contentView = ContentView()
let viewData = contentView.viewData
let hosting = UIHostingController(rootView: contentView)
// Timer
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .seconds(1))
timer.setEventHandler {
let diff = -start.timeIntervalSinceNow
let remaining = seconds - diff
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
let timeRemaning = timeRemaining(minutes: mins, seconds: secs)
viewData.timeRemaining = timeRemaning
print(timeRemaning)
}
timer.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
timer.cancel()
PlaygroundPage.current.finishExecution()
}
PlaygroundPage.current.setLiveView(contentView)
PlaygroundPage.current.needsIndefiniteExecution = true
The reason is that GCD based timer works on own queue, so here is the fix - view model have to be updated on main, UI, queue as below
DispatchQueue.main.async {
viewData.timeRemaining = timeRemaning
}
The main utility of GCD timers over standard timers is that they can run on a background queue. Like Asperi said, you can dispatch the updates to the main queue if your GCD timer isn’t using the main queue, itself.
But, you might as well just schedule your GCD timer on the main queue from the get go, and then you don’t have to manually dispatch to the main queue at all:
let timer = DispatchSource.makeTimerSource(queue: .main)
But, if you’re going to run this on the main thread, you could just use a Timer, or, in SwiftUI projects, you might prefer Combine’s TimerPublisher:
import Combine
...
var timer: AnyCancellable? = nil
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let remaining = start.addingTimeInterval(seconds).timeIntervalSince(Date())
guard remaining >= 0 else {
viewData.timeRemaining = "done!"
timer?.cancel()
return
}
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
viewData.timeRemaining = timeRemaining(minutes: mins, seconds: secs)
}
When you incorporate your timer within your SwiftUI code (rather than a global like here), it’s nice to stay within the Combine Publisher paradigm.
I also think it’s probably cleaner to cancel the timer when it expires in the timer handler, rather than doing a separate asyncAfter.
Unrelated, but you might consider using DateComponentsFormatter in your timeRemaining function, e.g.:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .full
return formatter
}()
func timeRemaining(_ timeInterval: TimeInterval) -> String {
formatter.string(from: timeInterval) ?? "Error"
}
Then,
It gets you out of the business of calculating minutes and seconds yourself;
The string will be localized; and
It will ensure grammatically correct wording; e.g. when there are 61 seconds left and the existing routine will report a grammatically incorrect “1 minutes, 1 seconds”.
DateComponentsFormatter gets you out of the weeds of handling these sorts of edge cases, where you want singular instead of plural or languages other than English.

Constructing a data model for Time Periods

I'm building an app that offers a service with something similar to dog walking. The people who will walk the dogs can upload the days and times they are available. Instead of them picking an actual date like Mon Jan 1st I let them just pick whatever days of the week and whatever times they are avail.
The problem I'm having is I can't figure out how to construct a data model for it.
What's in the photo is a collectionView with a cell and in each cell I show the available day and times slots that they can pick. Each day of the week has the same 7 time slots that a user who wants to be the dog walker can pick from.
The thing is if someone picks Sun 6am-9am, 12pm-3pm, and 6pm-9pm but they also pick Mon 6am-9m, how can I construct a data model that can differentiate between the days and times. For eg Sunday at 6am - 9am and Mon 6am-9am, how to tell the difference? Should those time slots be Doubles or Strings?
This is what I'm currently using for the collectionView data source and the cell:
// the collectionView's data source
var tableData = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
//cellForItem
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: availabilityCell, for: indexPath) as! AvailabilityCell
cell.clearCellForReuse()
cell.dayOfWeek = tableData[indexPath.item]
// inside the AvailabilityCell itself
var dayOfWeek: String? {
didSet {
dayOfWeekLabel.text = dayOfWeek
}
}
func clearCellForReuse() {
dayOfWeekLabel.text = nil
// deselect whatever radio buttons were selected to prevent scrolling issues
}
For a little further explanation what will eventually happen is when the user who wants their dog walks scrolls through to see who is avail, if the day and time their scrolling isn't on any of the days and times the person who posted (Sun & Mon with the chosen hours) isn't available, then their post shouldn't appear in the feed but if it is one of those days and one of those hours then their post will appear in the feed (in the example if someone is scrolling on Sunday at 10pm this post shouldn't appear). Whatever is in the data model will get compared to whatever day and time the posts are currently getting scrolled . I'm using Firebase for the backend.
What I came up with is rather convoluted and that's why I need something more reasonable.
class Availability {
var monday: String?
var tuesday: String?
var wednesday: String?
var thursday: String?
var friday: String?
var saturday: String?
var sunday: String?
var slotOne: Double? // sunday 6am-9am I was thinking about putting military hours here that's why I used a double
var slotTwo: Double? // sunday 9am-12pm
var slotTwo: Double? // sunday 12pm-3pm
// these slots would continue all through saturday and this doesn't seem like the correct way to do this. There would be 49 slots in total (7 days of the week * 7 different slots per day)
}
I also thought about maybe separating them into different data models like a Monday class, Tuesday class etc but that didn't seem to work either because they all have to be the same data type for the collectionView datasource.
UPDATE
In #rob's answer he gave me some insight to make some changes to my code. I'm still digesting it but I still have a a couple of problems. He made a cool project that shows his idea.
1- Since I’m saving the data to Firebase database, how should the data get structured to get saved? There can be multiple days with similar times.
2- I'm still wrapping my head around rob's code because I've never dealt with time ranges before so this is foreign to me. I'm still lost with what to sort against especially the time ranges against inside the callback
// someone is looking for a dog walker on Sunday at 10pm so the initial user who posted their post shouldn't appear in the feed
let postsRef = Database().database.reference().child("posts")
postsRef.observe( .value, with: { (snapshot) in
guard let availabilityDict = snapshot.value as? [String: Any] else { return }
let availability = Availability(dictionary: availabilityDict)
let currentDayOfWeek = dayOfTheWeek()
// using rob;s code this compares the days and it 100% works
if currentDayOfWeek != availability.dayOfWeek.text {
// don't add this post to the array
return
}
let currentTime = Calendar.current.dateComponents([.hour,.minute,.second], from: Date())
// how to compare the time slots to the current time?
if currentTime != availability.??? {
// don't add this post to the array
return
}
// if it makes this far then the day and the time slots match up to append it to the array to get scrolled
})
func dayOfTheWeek() -> String? {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.stringFromDate(self)
}
There are lots of ways to skin the cat, but I might define the availability as day enumeration and time range:
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
}
Your day of the week might be:
enum DayOfWeek: String, CaseIterable {
case sunday, monday, tuesday, wednesday, thursday, friday, saturday
}
Or you could also do:
enum DayOfWeek: Int, CaseIterable {
case sunday = 0, monday, tuesday, wednesday, thursday, friday, saturday
}
Their are pros and cons of both Int and String. The string representation is easier to read in the Firestore web-based UI. The integer representation offers easier sorting potential.
Your time range:
typealias Time = Double
typealias TimeRange = Range<Time>
extension TimeRange {
static let allCases: [TimeRange] = [
6 ..< 9,
9 ..< 12,
12 ..< 15,
15 ..< 18,
18 ..< 21,
21 ..< 24,
24 ..< 30
]
}
In terms of interacting with Firebase, it doesn’t understand enumerations and ranges, so I’d define an init method and dictionary property to map to and from [String: Any] dictionaries that you can exchange with Firebase:
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
self.dayOfWeek = dayOfWeek
self.timeRange = timeRange
}
init?(dictionary: [String: Any]) {
guard
let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
let startTime = dictionary["startTime"] as? Double,
let endTime = dictionary["endTime"] as? Double
else {
return nil
}
self.dayOfWeek = dayOfWeek
self.timeRange = startTime ..< endTime
}
var dictionary: [String: Any] {
return [
"dayOfWeek": dayOfWeek.rawValue,
"startTime": timeRange.lowerBound,
"endTime": timeRange.upperBound
]
}
}
You could also define a few extensions to make this easier to work with, e.g.,
extension Availability {
func overlaps(_ availability: Availability) -> Bool {
return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
}
}
extension TimeRange {
private func string(forHour hour: Int) -> String {
switch hour % 24 {
case 0: return NSLocalizedString("Midnight", comment: "Hour text")
case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
case 12: return NSLocalizedString("Noon", comment: "Hour text")
default: return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
}
}
var text: String {
return string(forHour: Int(lowerBound)) + "-" + string(forHour: Int(upperBound))
}
}
extension DayOfWeek {
var text: String {
switch self {
case .sunday: return NSLocalizedString("Sunday", comment: "DayOfWeek text")
case .monday: return NSLocalizedString("Monday", comment: "DayOfWeek text")
case .tuesday: return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
case .thursday: return NSLocalizedString("Thursday", comment: "DayOfWeek text")
case .friday: return NSLocalizedString("Friday", comment: "DayOfWeek text")
case .saturday: return NSLocalizedString("Saturday", comment: "DayOfWeek text")
}
}
}
If you don’t want to use Range, you can just define TimeRange as a struct:
enum DayOfWeek: String, CaseIterable {
case sunday, monday, tuesday, wednesday, thursday, friday, saturday
}
extension DayOfWeek {
var text: String {
switch self {
case .sunday: return NSLocalizedString("Sunday", comment: "DayOfWeek text")
case .monday: return NSLocalizedString("Monday", comment: "DayOfWeek text")
case .tuesday: return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
case .thursday: return NSLocalizedString("Thursday", comment: "DayOfWeek text")
case .friday: return NSLocalizedString("Friday", comment: "DayOfWeek text")
case .saturday: return NSLocalizedString("Saturday", comment: "DayOfWeek text")
}
}
}
struct TimeRange {
typealias Time = Double
let startTime: Time
let endTime: Time
}
extension TimeRange {
static let allCases: [TimeRange] = [
TimeRange(startTime: 6, endTime: 9),
TimeRange(startTime: 9, endTime: 12),
TimeRange(startTime: 12, endTime: 15),
TimeRange(startTime: 15, endTime: 18),
TimeRange(startTime: 18, endTime: 21),
TimeRange(startTime: 21, endTime: 24),
TimeRange(startTime: 24, endTime: 30)
]
func overlaps(_ availability: TimeRange) -> Bool {
return (startTime ..< endTime).overlaps(availability.startTime ..< availability.endTime)
}
}
extension TimeRange {
private func string(forHour hour: Int) -> String {
switch hour % 24 {
case 0: return NSLocalizedString("Midnight", comment: "Hour text")
case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
case 12: return NSLocalizedString("Noon", comment: "Hour text")
default: return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
}
}
var text: String {
return string(forHour: Int(startTime)) + "-" + string(forHour: Int(endTime))
}
}
struct Availability {
let dayOfWeek: DayOfWeek
let timeRange: TimeRange
init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
self.dayOfWeek = dayOfWeek
self.timeRange = timeRange
}
init?(dictionary: [String: Any]) {
guard
let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
let startTime = dictionary["startTime"] as? Double,
let endTime = dictionary["endTime"] as? Double
else {
return nil
}
self.dayOfWeek = dayOfWeek
self.timeRange = TimeRange(startTime: startTime, endTime: endTime)
}
var dictionary: [String: Any] {
return [
"dayOfWeek": dayOfWeek.rawValue,
"startTime": timeRange.startTime,
"endTime": timeRange.endTime
]
}
}
extension Availability {
func overlaps(_ availability: Availability) -> Bool {
return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
}
}

Resources