I am new to swift .. I am trying to crate delay and print the delay time like 1 seconds and start printing the next item into list . I got the result but I am not sure make delay of each iteration and print it ..
Here is the struct .
struct CartProductResult {
var id: Int
var title: String
var quantity: Int
}
let cartProducts = [
CartProductResult(id: 1, title: "nike shoe 1", quantity: 5),
CartProductResult(id: 2, title: "nike shoe 2", quantity: 2),
CartProductResult(id: 3, title: "soap", quantity: 6)
]
Here is the function ..
func printWithDelay(product1: CartProductResult, product2:
CartProductResult, completion: (#escaping ()-> Void)) {
completion()
}
Here is the call to the function ..
printWithDelay(product1: cartProducts[0], product2: cartProducts[1])
{
let seconds = 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
print(" Wait 1 second")
for cartProduct in cartProducts {
print(cartProduct.id)
}
print("Done printing products")
}
}
Here is the result ,I got..
Here is the expected result ..
You do NOT want to use sleep() on the main thread, as it will appear to "lock up" your app.
Instead, use a repeating Timer -- try this, tap anywhere to start the printWithDelay():
class ViewController: UIViewController {
struct CartProductResult {
var id: Int
var title: String
var quantity: Int
}
let cartProducts = [
CartProductResult(id: 1, title: "nike shoe 1", quantity: 5),
CartProductResult(id: 2, title: "nike shoe 2", quantity: 2),
CartProductResult(id: 3, title: "soap", quantity: 6)
]
func printOneProduct(_ index: Int) {
let p = cartProducts[index]
print("id:", p.id)
}
func printWithDelay(products: [CartProductResult], completion: (#escaping ()-> Void)) {
var idx: Int = 0
// we want to print the first product immediately,
// then step through the rest 1-second at a time
printOneProduct(idx)
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
idx += 1
if idx == products.count {
timer.invalidate()
completion()
}
self.printOneProduct(idx)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
printWithDelay(products: cartProducts) {
print("Done printing products")
}
// execution continues while products are printing
}
}
func printWithDelay(products: [CartProductResult], completion: (#escaping ()-> Void)) {
for product in products {
let seconds = 1.0
sleep(1)
print(" Wait 1 second")
print(product.id)
}
completion()
}
printWithDelay(products: cartProducts) {
print("Done printing products")
}
Related
I have two views. The Main View and a Break View. I have a timer running in the Main View which counts down to zero. When the timer reaches zero, I want to be able to switch the screen to Break View. I am using MVVM to keep track of the timers. Using .onReceive to make it look like the timer is running in the background.
I tried using a boolean to check if the timer has reached zero and based on that changed the view, but it's not working and is giving an error saying the result of the view is not used anywhere. I have a navigation view in the Content View if that's of any help.
Thanks in advance.
A snippet of the code :
Main View :
struct MainView: View {
var body: some View {
VStack(alignment: .center, spacing: 50, content: {
Button(action: {
if !fbManager.isTimerStarted {
fbManager.start()
fbManager.isTimerStarted = true
}
else {
fbManager.pause()
fbManager.isTimerStarted = false
}
}, label: {
Image(systemName: fbManager.isTimerStarted == true ? "pause.fill" : "play.fill")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundColor(Color(red: 1.00, green: 1.00, blue: 1.00))
})
.onReceive(NotificationCenter.default.publisher(
for: UIScene.didEnterBackgroundNotification)) { _ in
if fbManager.isTimerStarted {
movingToBackground()
}
}
.onReceive(NotificationCenter.default.publisher(
for: UIScene.willEnterForegroundNotification)) { _ in
if fbManager.isTimerStarted {
movingToForeground()
}
}
})
}
}
func movingToBackground() {
print("Moving to the background")
notificationDate = Date()
fbManager.pause()
}
func movingToForeground() {
print("Moving to the foreground")
let deltaTime: Int = Int(Date().timeIntervalSince(notificationDate))
fbManager.secondsElapsed -= deltaTime
fbManager.start()
}
}
View Model :
class FocusBreakManager: ObservableObject {
var timer: Timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] _ in
self.secondsElapsed -= 1
self.focusfill += 0.01667
focusTime = String(secondsElapsed)
focusTime = formatCounter()
if secondsElapsed <= 0 {
stop()
}
}
}
func formatCounter() -> String {
let minutes = Int(secondsElapsed) / 60 % 60
let seconds = Int(secondsElapsed) % 60
return String(format : "%02i : %02i", minutes, seconds)
}
}
Hey to keep up with your solution here is an example of how that could work you would need to use #ObservedObject property wrapper in order to monitor updates from your view.
struct ContentView: View {
#ObservedObject private var focusBreakManager = FocusBreakManager()
var body: some View {
VStack {
Text("\(focusBreakManager.elapsedSeconds)")
Text(focusBreakManager.timerRunningMessage)
Button("Start timer", action: focusBreakManager.start)
}
.padding()
}
}
class FocusBreakManager: ObservableObject {
var timer: Timer = Timer()
#Published var elapsedSeconds = 0
var timerRunningMessage: String {
timerRunning
? "Timer is running"
: "Timer paused"
}
private var timerRunning: Bool {
timer.isValid
}
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self else { return }
self.elapsedSeconds += 1
if self.elapsedSeconds > 5 {
self.timer.invalidate()
}
}
}
}
You can also take a look at the autoconnect api here's a great tutorial:
https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer
I have written a generic ViewPager with TabView and it works perfectly. However, I want to pause the timer (auto swipe) when user starts dragging and resume it when user finishes the dragging. Is there anyway to do that?
This is my ViewPager:
struct ViewPager<Data, Content> : View
where Data : RandomAccessCollection, Data.Element : Identifiable, Content : View {
private var timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#Binding var currentIndex: Int
private let data: [Data.Element]
private let content: (Data.Element) -> Content
private let isTimerEnabled: Bool
private let showIndicator: PageTabViewStyle.IndexDisplayMode
init(_ data: Data,
currentIndex: Binding<Int>,
isTimerEnabled: Bool = false,
showIndicator: PageTabViewStyle.IndexDisplayMode = .never,
#ViewBuilder content: #escaping (Data.Element) -> Content) {
_currentIndex = currentIndex
self.data = data.map { $0 }
self.content = content
self.isTimerEnabled = isTimerEnabled
self.showIndicator = showIndicator
}
private var totalCount: Int {
data.count
}
var body: some View {
TabView(selection: $currentIndex) {
ForEach(data) { item in
self.content(item)
.tag(item.id)
}
}.tabViewStyle(PageTabViewStyle(indexDisplayMode: showIndicator))
.onReceive(timer) { _ in
if !isTimerEnabled {
timer.upstream.connect().cancel()
} else {
withAnimation {
currentIndex = currentIndex < (totalCount - 1) ? currentIndex + 1 : 0
}
}
}
}
}
To "pause" while user is dragging, you can exchange .common with .default in the Timer. But what you probably also want is setting the timer back to 2 secs once the dragging is over ...
I got this to work but I use a global var, so the timer stays around and this feels wrong – can someone help further?
// global var – this seems wrong, but works
var timer = Timer.publish(every: 2, on: .main, in: .default).autoconnect()
struct ViewPager<Data, Content> : View
where Data : RandomAccessCollection, Data.Element : Identifiable, Content : View {
#Binding var currentIndex: Int
private let data: [Data.Element]
private let content: (Data.Element) -> Content
private let isTimerEnabled: Bool
private let showIndicator: PageTabViewStyle.IndexDisplayMode
init(_ data: Data,
currentIndex: Binding<Int>,
isTimerEnabled: Bool = false,
showIndicator: PageTabViewStyle.IndexDisplayMode = .never,
#ViewBuilder content: #escaping (Data.Element) -> Content) {
_currentIndex = currentIndex
self.data = data.map { $0 }
self.content = content
self.isTimerEnabled = isTimerEnabled
self.showIndicator = showIndicator
}
private var totalCount: Int {
data.count
}
var body: some View {
TabView(selection: $currentIndex) {
ForEach(data) { item in
self.content(item)
.tag(item.id)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: showIndicator))
.onReceive(timer) { _ in
if !isTimerEnabled {
timer.upstream.connect().cancel()
} else {
print("received")
withAnimation {
currentIndex = currentIndex < (totalCount - 1) ? currentIndex + 1 : 0
print(currentIndex)
}
}
}
.onChange(of: currentIndex) { _ in
timer = Timer.publish(every: 2, on: .main, in: .default).autoconnect()
}
}
}
same principle but used selection for TabView (and tag for view) and timer as not global var
struct MotivationTabView: View {
// MARK: - PROPERTIES
#State private var selectedItem = "Adolf Dobr’aňskŷj"
#State private var isTimerEnabled: Bool = true
#State private var timer = Timer.publish(every: 10, on: .main, in: .default).autoconnect()
let items: KeyValuePairs = ["Adolf Dobr’aňskŷj": "Svij narod treba ľubyty i ne haňbyty s’a za ňoho!",
"Fjodor Mychailovič Dostojevskŷj": "Chto ne maje narod, tot ne maje any Boha! Buďte sobi istŷ, že všŷtkŷ totŷ, što perestanuť rozumity svomu narodu i stračajuť z nym perevjazaňa, stračajuť jednočasno viru otc’ovsku, stavajuť buď ateistamy, abo cholodnŷma.",
"Pau del Rosso": "Je barz važnŷm uchovaty sobi vlastnu identičnosť. To naš unikatnŷj dar pro druhŷch, unikatnŷj v cilim kozmosi.",
"Viktor Hugo": "Velykosť naroda ne mir’ať s’a kiľkosťov, tak jak i velykosť čolovika ne mir’ať s’a vŷškov.",
"Lewis Lapham":"Strata identitŷ je vŷhoda pro biznis... pokľa bŷ jem znav, chto jem, čom bŷ jem si bezprestajno kupovav novŷ značkŷ vodŷ po holiňu?"]
private var totalCount: Int {
items.count
}
private func nextItem(currItem: String) -> String{
switch currItem {
case "Adolf Dobr’aňskŷj": return "Fjodor Mychailovič Dostojevskŷj"
case "Fjodor Mychailovič Dostojevskŷj": return "Pau del Rosso"
case "Pau del Rosso": return "Viktor Hugo"
case "Viktor Hugo": return "Lewis Lapham"
default: return "Adolf Dobr’aňskŷj"
}
}
// MARK: - BODY
var body: some View {
GroupBox {
TabView(selection: $selectedItem){
ForEach(items, id: \.self.key) { item in
VStack(alignment: .trailing){
Text(item.value)
Text(item.key)
.font(.caption)
.padding(.top, 5)
}.tag(item.key)
}
} //: TABS
.tabViewStyle(PageTabViewStyle())
.frame(height: 240)
.onReceive(timer) { _ in
if !isTimerEnabled {
timer.upstream.connect().cancel()
} else {
print("received")
withAnimation() {
selectedItem = nextItem(currItem: selectedItem)
print(selectedItem)
}
}
}
.onAppear{
isTimerEnabled = true
timer = Timer.publish(every: 10, on: .main, in: .default).autoconnect()
}
.onDisappear{
isTimerEnabled = false
}
} //: BOX
}
I want to animate a text that starts by default from 0, to a variable.
For example, for x = 80, I want my text to display all the numbers between 0 and 80 very fast, until it hits 80.
I found examples with progress indicators, but I cannot apply the methods to this.
Do you have any ideas for doing this?
Thanks, Diocrasis.
Here I've created a little function called runCounter which takes a binding to the counter variable, a start value, the end value, and the speed. When called, it sets the bound variable to the start value, and then starts a Timer which runs every speed seconds and increments the counter until it reaches end at which point it invalidates the timer.
This standalone example shows two counters running at different speeds, both of which start when they first appear using .onAppear().
struct ContentView: View {
#State private var counter1 = 0
#State private var counter2 = 0
var body: some View {
VStack {
Text("\(self.counter1)")
.onAppear {
self.runCounter(counter: self.$counter1, start: 0, end: 80, speed: 0.05)
}
Text("\(self.counter2)")
.onAppear {
self.runCounter(counter: self.$counter2, start: 0, end: 10, speed: 0.5)
}
}
}
func runCounter(counter: Binding<Int>, start: Int, end: Int, speed: Double) {
counter.wrappedValue = start
Timer.scheduledTimer(withTimeInterval: speed, repeats: true) { timer in
counter.wrappedValue += 1
if counter.wrappedValue == end {
timer.invalidate()
}
}
}
}
You can use a Timer.Publisher to trigger incrementing of your counter at regular intervals.
To stop incrementing once you reach your desired count, whenever your Timer fires, you can check if count has reached end, if not, increment it, otherwise remove the subscription and hence stop incrementing.
class Counter: ObservableObject {
#Published var count = 0
let end: Int
private var timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
private var subscriptions = Set<AnyCancellable>()
init(end: Int) {
self.end = end
}
func start() {
timer.sink { [weak self] _ in
guard let self = self else { return }
if self.count <=self.end {
self.count += 1
} else {
self.subscriptions.removeAll()
}
}.store(in: &subscriptions)
}
}
struct AnimatedText: View {
#ObservedObject var counter: Counter
var body: some View {
Text("\(counter.count)")
.onAppear() {
self.counter.start()
}
}
}
struct AnimatedText_Previews: PreviewProvider {
static var previews: some View {
AnimatedText(counter: Counter(end: 80))
}
}
Adding to the answer by #vacawama.
func runCounter(counter: Binding<Int>, start: Int, end: Int, speed: Double) {
let maxSteps = 20
counter.wrappedValue = start
let steps = min(abs(end), maxSteps)
var increment = 1
if steps == maxSteps {increment = end/maxSteps}
Timer.scheduledTimer(withTimeInterval: speed, repeats: true) { timer in
counter.wrappedValue += increment
if counter.wrappedValue >= end {
counter.wrappedValue = end
timer.invalidate()
}
}
}
I am curious if there is a way to get the timers to update with the UI (as they are now) even while someone is scrolling. Additionally, I want to make sure that as the UI updates each second the screen does not freeze. This question has been updated in response to the helpful answers I received previously.
struct ContentView: View {
var body: some View {
NavigationView{
NavigationLink(destination: ScrollTest()){
HStack {
Text("Next Screen")
}
}
}
}
}
struct ScrollTest: View {
#ObservedObject var timer = SectionTimer(duration: 60)
var body: some View {
HStack{
List{
Section(header: TimerNavigationView(timer: timer)){
ForEach((1...50).reversed(), id: \.self) {
Text("\($0)…").onAppear(){
self.timer.startTimer()
}
}
}
}.navigationBarItems(trailing: TimerNavigationView(timer: timer))
}
}
}
struct TimerNavigationView: View {
#ObservedObject var timer: SectionTimer
var body: some View{
HStack {
Text("\(timer.timeLeftFormatted) left")
Spacer()
}
}
}
class SectionTimer:ObservableObject {
private var endDate: Date
private var timer: Timer?
var timeRemaining: Double {
didSet {
self.setRemaining()
}
}
#Published var timeLeftFormatted = ""
init(duration: Int) {
self.timeRemaining = Double(duration)
self.endDate = Date().advanced(by: Double(duration))
self.startTimer()
}
func startTimer() {
guard self.timer == nil else {
return
}
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
self.timeRemaining = self.endDate.timeIntervalSince(Date())
if self.timeRemaining < 0 {
timer.invalidate()
self.timer = nil
}
})
}
private func setRemaining() {
let min = max(floor(self.timeRemaining / 60),0)
let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
}
func endTimer() {
self.timer?.invalidate()
self.timer = nil
}
}
Although SwiftUI has a timer, I don't think using it is the right approach in this case.
Your model should be handling the timing for you.
It also helps if your view observes its model object directly rather than trying to observe a member of an array in a property of your observable.
You didn't show your SectionTimer, but this is what I created:
class SectionTimer:ObservableObject {
private var endDate: Date
private var timer: Timer?
var timeRemaining: Double {
didSet {
self.setRemaining()
}
}
#Published var timeLeftFormatted = ""
init(duration: Int) {
self.timeRemaining = Double(duration)
self.endDate = Date().advanced(by: Double(duration))
self.startTimer()
}
func startTimer() {
guard self.timer == nil else {
return
}
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
self.timeRemaining = self.endDate.timeIntervalSince(Date())
if self.timeRemaining < 0 {
timer.invalidate()
self.timer = nil
}
})
}
private func setRemaining() {
let min = max(floor(self.timeRemaining / 60),0)
let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
}
func endTimer() {
self.timer?.invalidate()
self.timer = nil
}
}
It uses a Date rather than subtracting from a remaining counter; this is more accurate as timer's don't tick at precise intervals. It updates a timeLeftFormatted published property.
To use it I made the following changes to your TimerNavigationView -
struct TimerNavigationView: View {
#ObservedObject var timer: SectionTimer
var body: some View{
HStack {
Text("\(timer.timeLeftFormatted) left")
Spacer()
}
}
}
You can see how putting the timer in the model vastly simplifies your view.
You would use it via .navigationBarItems(trailing: TimerNavigationView(timer: self.test.timers[self.test.currentSection]))
Update
The updated code in the question helped demonstrate the issue, and I found the solution in this answer
When the scrollview is scrolling the mode of the current RunLoop changes and the timer is not triggered.
The solution is to schedule the timer in the common mode yourself rather than relying on the default mode that you get with scheduledTimer -
func startTimer() {
guard self.timer == nil else {
return
}
self.timer = Timer(timeInterval: 0.2, repeats: true) { (timer) in
self.timeRemaining = self.endDate.timeIntervalSince(Date())
if self.timeRemaining < 0 {
timer.invalidate()
self.timer = nil
}
}
RunLoop.current.add(self.timer!, forMode: .common)
}
After spending some time creating a #Debounced property wrapper I'm not happy with the readability of the code. To understand what's going on you really need to understand how a Property wrapper works and the concept of the wrappedvalue and projectedvalue. This is the Property Wrapper:
#propertyWrapper
class Debounced<Input: Hashable> {
private var delay: Double
private var _value: Input
private var function: ((Input) -> Void)?
private weak var timer: Timer?
public init(wrappedValue: Input, delay: Double) {
self.delay = delay
self._value = wrappedValue
}
public var wrappedValue: Input {
get {
return _value
}
set(newValue) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { [weak self] _ in
self?._value = newValue
self?.timer?.invalidate()
self?.function?(newValue)
})
}
}
public var projectedValue: ((Input) -> Void)? {
get {
return function
}
set(newValue) {
function = newValue
}
}
}
The property wrapper is being used like this:
#Debounced(delay: 0.4) var text: String? = nil
override func viewDidLoad() {
super.viewDidLoad()
self.$text = { text in
print(text)
}
}
It works as it should. Every time the text property is being set, the print function is being called. And if the value is updated more than once within 0.4 seconds then the function will only be called once.
BUT in terms of simplicity and readability, I think its better just creating a Debouncer class like this: https://github.com/webadnan/swift-debouncer.
What do you think? Is there a better way to create this property wrapper?
It works as it should ... In that case, just use it!
Hm ... but how to use it? In reality, it is not very flexible, especially till compiler claims "Multiple property wrappers are not supported" :-)
If your goal is to use it in UIKit or SwiftUI app, I suggest you different approach.
Lets try some minimalistic, but fully working SwiftUI example
//
// ContentView.swift
// tmp031
//
// Created by Ivo Vacek on 26/01/2020.
// Copyright © 2020 Ivo Vacek. NO rights reserved.
//
import SwiftUI
import Combine
class S: ObservableObject {
#Published var text: String = ""
#Published var debouncedText: String = ""
private var store = Set<AnyCancellable>()
init(delay: Double) {
$text
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.sink { [weak self] (s) in
self?.debouncedText = s
}.store(in: &store)
}
}
struct ContentView: View {
#ObservedObject var model = S(delay: 2)
var body: some View {
List {
Color.clear
Section(header: Text("Direct")) {
Text(model.text).font(.title)
}
Section(header: Text("Debounced")) {
Text(model.debouncedText).font(.title)
}
Section(header: Text("Source")) {
TextField("type here", text: $model.text).font(.title)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You still can subscribe to model.$debouncedText which is Publisher as many times, as you need. And if you like to use your own action to be performed, no problem as well!
model.$debouncedText
.sink { (s) in
doSomethingWithDebouncedValue(s)
}
Example application usage
UPDATE: if you not able to use Combine, but you like similar syntax ...
First define the protokol
protocol Debounce: class {
associatedtype Value: Hashable
var _value: Value { get set }
var _completions: [(Value)->Void] { get set}
var _delay: TimeInterval { get set }
var _dw: DispatchWorkItem! { get set }
func debounce(completion: #escaping (Value)->Void)
}
and default implementation of debounce function. The idea is, to use debounce the same way, as .publisher.sink() on Combine. _debounce is "internal" implementation of debouncing functionality. It compare current and "delay" old value and if they are equal, do the job.
extension Debounce {
func debounce(completion: #escaping (Value)->Void) {
_completions.append(completion)
}
func _debounce(newValue: Value, delay: TimeInterval, completions: [(Value)->Void]) {
if _dw != nil {
_dw.cancel()
}
var dw: DispatchWorkItem!
dw = DispatchWorkItem(block: { [weak self, newValue, completions] in
if let s = self, s._value == newValue {
for completion in completions {
completion(s._value)
}
}
dw = nil
})
_dw = dw
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: dw)
}
}
Now we have all componets of our property wrapper.
#propertyWrapper class Debounced<T: Hashable> {
final class Debouncer: Debounce {
typealias Value = T
var _completions: [(T) -> Void] = []
var _delay: TimeInterval
var _value: T {
willSet {
_debounce(newValue: newValue, delay: _delay, completions: _completions)
}
}
var _dw: DispatchWorkItem!
init(_value: T, _delay: TimeInterval) {
self._value = _value
self._delay = _delay
}
}
var wrappedValue: T {
get { projectedValue._value }
set { projectedValue._value = newValue }
}
var projectedValue: Debouncer
init(wrappedValue: T, delay: TimeInterval) {
projectedValue = Debouncer(_value: wrappedValue, _delay: delay)
}
deinit {
print("deinit")
}
}
lets try it
do {
struct S {
#Debounced(delay: 0.2) var value: Int = 0
}
let s = S()
print(Date(), s.value, "initial")
s.$value.debounce { (i) in
print(Date(), i, "debounced A")
}
s.$value.debounce { (i) in
print(Date(), i, "debounced B")
}
var t = 0.0
(0 ... 8).forEach { (i) in
let dt = Double.random(in: 0.0 ... 0.6)
t += dt
DispatchQueue.main.asyncAfter(deadline: .now() + t) { [t] in
s.value = i
print(s.value, t)
}
}
}
which prints something like
2020-02-04 09:53:11 +0000 0 initial
0 0.46608517831539165
2020-02-04 09:53:12 +0000 0 debounced A
2020-02-04 09:53:12 +0000 0 debounced B
1 0.97078412234771
2 1.1756938500918692
3 1.236562020385944
4 1.4076127046937024
2020-02-04 09:53:13 +0000 4 debounced A
2020-02-04 09:53:13 +0000 4 debounced B
5 1.9313412744029004
6 2.1617775513150366
2020-02-04 09:53:14 +0000 6 debounced A
2020-02-04 09:53:14 +0000 6 debounced B
7 2.6665465865810205
8 2.9287734023206418
deinit