RxSwift - start and update count down timer - ios

I'm trying to build a timer that starts at 15 seconds and count down until 0.
The thing is that I'll want to update that timer by 2 seconds more based on an event.
This is what I've tried to do so far:
struct ViewModel {
struct Input {
let add_time: Observable<Void>
}
struct Output {
let current_time: Observable<String>
let timer_over: Observable<Void>
}
private let current_time = BehaviorRelay(value: 15)
private let timer_over = PublishSubject<Void>()
func transform(input: Input) -> Output {
let current_time = self.current_time
.flatMapLatest { time in
Observable<Int>
.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.take(self.current_time.value + 1)
.map { "\(self.current_time.value - $0)" }
.do(onCompleted: { self.timer_over.onNext(()) })
}
return Output(
current_time: current_time,
timer_over: timer_over
)
}
}
let disposeBag = DisposeBag()
let add_time_subject = PublishSubject<Void>()
let input = ViewModel.Input(
add_time: add_time_subject.asObservable()
)
let output = ViewModel().transform(input: input)
output.current_time
.subscribe(onNext: { (time) in
print(time)
})
.disposed(by: disposeBag)
output.timer_over
.subscribe(onNext: {
print("timer_over")
})
.disposed(by: disposeBag)
The thing is when I run add_time_subject.onNext(()) I'd want to update the timer by 2 seconds more only if the timer hasn't reached 0 seconds.
How should I do that?

What you're trying to achieve kinda sounds like a state machine.
You could achieve it by splitting the timer actions into actual "Actions" and merging them based on the trigger (one being the manual "add two seconds", and thee other is automated as "reduce one second").
I haven't fully tested it, but this can be a good starting ground:
enum TimerAction {
case tick
case addTwoSeconds
}
let trigger = PublishRelay<Void>()
let timer = Observable<Int>
.interval(.seconds(1), scheduler: MainScheduler.instance)
.map { _ in TimerAction.tick }
let addSeconds = trigger.map { TimerAction.addTwoSeconds }
Observable
.merge(timer, addSeconds)
.scan(into: 15) { totalSeconds, action in
totalSeconds += action == .addTwoSeconds ? 2 : -1
}
.takeUntil(.inclusive) { $0 == 0 }
.subscribe()
.disposed(by: disposeBag)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
trigger.accept(()) // increase the timer by two seconds after 5 seconds
}

Related

Synchronous start of the event

Is there a way to run the code simultaneously on different devices? Let's say I want that when I click on a button on one of the devices, the function starts simultaneously on both the first and the second device? I tried to use a timer with a time check for 3 seconds ahead, but the function is triggered with a delay of 0.5 seconds on the second device
func getEventTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = (UInt64(interval) + (3)) * 1000
return result
}
func getCurrentTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = UInt64(interval * 1000)
return result
}
func startTimer(time : UInt64){
Timer.scheduledTimer(withTimeInterval: 0.0001, repeats: true) { timer in
switch getCurrentTime() {
case time - 1000 :
DispatchQueue.main.async {
countdownImageTimer.image = UIImage(named: "Start")
}
break
case time :
DispatchQueue.main.async {
countdownImageTimer.removeFromSuperview()
}
self.setShips()
timer.invalidate()
break
default:
break
}
}
}

Binding BehaviorRelay with Observable - RxSwift

I have such code as below. How can I achieve it in one chain, without using subscribe on timer? I would like to attach 'timerInterval' to 'timer' and then call subscribe.
var timerInterval: BehaviorRelay<String> = BehaviorRelay(value: "")
...
func doLogic() {
let timer = Observable<Int>.interval(0.05, scheduler: MainScheduler.instance)
timer.subscribe({ [weak self] value in
let doubleValue = Double(value.element ?? 0)
let dividedValue = doubleValue / 20.0
let text = String(format: "%.2f", dividedValue)
self?.timerInterval.accept(text)
}).disposed(by: disposeBag)
}
You'd go for map operator. I'm not sure why you need BehaviourRelay but I'd do something even more simpler:
let timer = Observable<Int>.interval(0.05, scheduler: MainScheduler.instance)
var timerInterval: Observable<String> {
return timer.map { value -> String in
let doubleValue = Double(value.element ?? 0)
let dividedValue = doubleValue / 20.0
let text = String(format: "%.2f", dividedValue)
return text
}
}

iOS (Swift): Changing UILabel text with a for loop

I have a for loop as follows:
#objc private func resetValue() {
for i in stride(from: value, to: origValue, by: (value > origValue) ? -1 : 1) {
value = i
}
value = origValue
}
And when value is set it updates a label:
private var value = 1 {
didSet {
updateLabelText()
}
}
private func updateLabelText() {
guard let text = label.text else { return }
if let oldValue = Int(text) { // is of type int?
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
} else {
label.text = "\(value)"
}
}
I was hoping that if value=5 and origValue=2, then the label would flip through the numbers 5,4,3,2. However, this is not happening - any suggestions why, please?
I've tried using a delay function:
func delay(_ delay:Double, closure: #escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
and then placing the following within the stride code:
delay(2.0) { self.value = i }
However, this doesn't seem to work either.
Thanks for any help offered.
UIKit won't be able to update the label until your code is finished with the main thread, after the loop completes. Even if UIKit could update the label after each iteration of the loop, the loop is going to complete in a fraction of a second.
The result is that you only see the final value.
When you attempted to introduce the delay, you dispatched the update to the label asynchronously after 0.5 second; Because it is asynchronous, the loop doesn't wait for the 0.5 second before it continues with the next iteration. This means that all of the delayed updates will execute after 0.5 seconds but immediately one after the other, not 0.5 seconds apart. Again, the result is you only see the final value as the other values are set too briefly to be visible.
You can achieve what you want using a Timer:
func count(fromValue: Int, toValue: Int) {
let stride = fromValue > toValue ? -1 : 1
self.value = fromValue
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats:true) { [weak self] (timer) in
guard let strongSelf = self else {
return
}
strongSelf.value += stride
if strongSelf.value == toValue {
timer.invalidate()
}
}
}
I would also update the didSet to send the oldValue to your updateLabelText rather than having to try and parse the current text.
private var value = 1 {
didSet {
updateLabelText(oldValue: oldValue, value: value)
}
}
private func updateLabelText(oldValue: Int, value: Int) {
guard oldValue != value else {
self.label.text = "\(value)"
return
}
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
}

Argument labels "(attributes:)" do not match any available overloads in DispatchQueue

I am trying to make the enemy in my game move using DispatchQueue. I tried fixing this error and it keeps telling me attributes do not match any available overloads.
func makeAIMove() {
let strategistTime = CFAbsoluteTimeGetCurrent()
DispatchQueue.global(attributes: .qosUserInitiated).async { [unowned self] in
guard let move = self.strategist.bestMoveForActivePlayer() as? Move else { return }
let delta = CFAbsoluteTimeGetCurrent() - strategistTime
DispatchQueue.main.async{ [unowned self] in
self.rows[move.row][move.col].setPlayer(.choose)
let aiTimeCeiling = 2.0
let delay = min(aiTimeCeiling - delta, aiTimeCeiling)
DispatchQueue.main.after(when: .now() + delay) { [unowned self] in
self.makeMove(row: move.row, col: move.col)
}
}
}
}
I think what you want is DispatchQueue.global(qos: .userInitiated); indeed, there is no overload taking an attributes argument.

Delay for Retry - RxSwift

Is there elegant solution for creating delay for retry? When error occurs I want to wait for 5 seconds and restart Observable (retry)
Just create a PrimitiveSequence extension that wraps around retry().
(Swift5.1 RxSwift 4.3.1)
extension PrimitiveSequence{
func retry(maxAttempts: Int, delay: TimeInterval) -> PrimitiveSequence<Trait, Element> {
return self.retryWhen { errors in
return errors.enumerated().flatMap{ (index, error) -> Observable<Int64> in
if index < maxAttempts {
return Observable<Int64>.timer(RxTimeInterval(delay), scheduler: MainScheduler.instance)
} else {
return Observable.error(error)
}
}
}
}
}
Usage example: (retry 3 times, with 2 sec delay each)
yourRxStream.retry(maxAttempts: 3, delay: 2)
Here is the code for swift 4.0+
// sample retry function with delay
private func sampleRetryWithDelay(){
let maxRetry = 3
let retryDelay = 1.0 // seconds
let _ = sampleStreamWithErrors() // sample observable stream, replace with the required observable
.retryWhen { errors in
return errors.enumerated().flatMap{ (index, error) -> Observable<Int64> in
return index < maxRetry ? Observable<Int64>.timer(RxTimeInterval(retryDelay), scheduler: MainScheduler.instance) : Observable.error(error)
}
}.subscribe(onNext:{ value in
print("Result:\(value)")
})
}
// Sample stream with errors, helper function only - generating errors 70% of the time
private func sampleStreamWithErrors() -> Observable<Int>{
return Observable.create { observer in
let disposable = Disposables.create() {} // nothing to cancel, we let it complete
let randomInt = Int(arc4random_uniform(100) + 1)
if randomInt > 70 {
observer.on(.next(randomInt))
observer.on(.completed)
} else {
let sampleError = NSError(domain: "SampleDomain", code: randomInt, userInfo: nil)
print("Result:Error:\(randomInt)")
observer.on(.error(sampleError))
}
return disposable
}
}

Resources