Skip to content

Instantly share code, notes, and snippets.

@woodycatliu
Last active March 7, 2022 07:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save woodycatliu/6565d8e50dcaac12048d3d64fcb31a44 to your computer and use it in GitHub Desktop.
Save woodycatliu/6565d8e50dcaac12048d3d64fcb31a44 to your computer and use it in GitHub Desktop.
Countdown Timer Publisher (Swift5.2)

Countdown Timer Publisher (Swift5.2)

In the past we had to set the countdown by Timer.TimePublisher.

We maybe coding to like bellow.

let timer = Timer.publish(every: 0.01, on: .main, in: .common)
    .autoconnect()
    .map { _ in return 0.01 }
    .scan(10, -)
    .share()
timer
    .map { dateFormatterTransform($0) }
    .assign(to: \.text, on: label)
    .store(in: &bag)
timer
    .map { $0 <= 0 }
    .filter { $0}
    .sink(receiveValue: toDoCancel)
    .store(in: &bag)

It works perfect but not clean on really.

Let's build our publisher for this feature.

Subscription

A protocol representing the connection of a subscriber to a publisher.

I prefer to call it flow producer.
The source of the whole resource.

Building a Countdown Subscription I build a custom subscription for countdown feature. It has two important properties, everyTimeInterval and duration, and Output type is TimeInterval, will call finished in time out.

extension Timer {
    private class CountdownSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == TimeInterval{
        private var subscriber: SubscriberType?
        private let duration: TimeInterval
        private let everyTimeInterval: TimeInterval
        
        private var timeTask: AnyCancellable?
        
        private var bag: Set<AnyCancellable> = []
        
        init(every: TimeInterval = 0.01, _ duration: TimeInterval, _ subscriber: SubscriberType) {
            self.duration = duration
            self.subscriber = subscriber
            self.everyTimeInterval = every
            start()
        }
        
        func request(_ demand: Subscribers.Demand) {}
        
        func cancel() {
            subscriber?.receive(completion: .finished)
            subscriber = nil
        }
        
        private func start() {
            let cancel: (Bool)-> () = { [weak self] bool in
                guard bool else { return }
                self?.cancel()
            }
            
            let every = everyTimeInterval
            let task = Timer.publish(every: every, on: .main, in: .common)
                .autoconnect()
                .map {  _ in return every }
                .scan(duration, -)
                .share()
            task.sink(receiveValue: { [weak self] in
                _ = self?.subscriber?.receive($0 > 0 ? $0 : 0)
                                         }).store(in: &bag)
            task.map { $0 <= 0 }.sink(receiveValue: cancel).store(in: &bag)
        }
    }
 }   

Publisher

Declares that a type can transmit a sequence of values over time..

A simple job in today's feature. Building a Countdown Publisher It just a simple case help caller to observe and bind.

 struct CountdownPublisher: Combine.Publisher {
    
        typealias Output = TimeInterval
        
        typealias Failure = Never
        
        let duration: TimeInterval
        
        func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, TimeInterval == S.Input {
            let scr = CountdownSubscription(duration, subscriber)
            subscriber.receive(subscription: scr)
        }
    }

Final

Create a Timer extension to build our Countdown Timer Publisher.

extension Timer {
   
    static func counrdownPublish(_ duration: TimeInterval)-> CountdownPublisher {
        return CountdownPublisher(duration: duration)
    }
}

Test

It work perfectly.

Timer.counrdownPublish(10)
    .print()
    .sink(receiveValue: { _ in })
    .store(in: &bag)

Full version

import Combine
import Foundation
extension Timer {
private class CountdownSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == TimeInterval{
private var subscriber: SubscriberType?
private let duration: TimeInterval
private let everyTimeInterval: TimeInterval
private var timeTask: AnyCancellable?
private var bag: Set<AnyCancellable> = []
init(every: TimeInterval = 0.01, _ duration: TimeInterval, _ subscriber: SubscriberType) {
self.duration = duration
self.subscriber = subscriber
self.everyTimeInterval = every
start()
}
func request(_ demand: Subscribers.Demand) {}
func cancel() {
subscriber?.receive(completion: .finished)
subscriber = nil
}
private func start() {
let cancel: (Bool)-> () = { [weak self] bool in
guard bool else { return }
self?.cancel()
}
let every = everyTimeInterval
let task = Timer.publish(every: every, on: .main, in: .common)
.autoconnect()
.map { _ in return every }
.scan(duration, -)
.share()
task.sink(receiveValue: { [weak self] in
_ = self?.subscriber?.receive($0 > 0 ? $0 : 0)
}).store(in: &bag)
task.map { $0 <= 0 }.sink(receiveValue: cancel).store(in: &bag)
}
}
struct CountdownPublisher: Combine.Publisher {
typealias Output = TimeInterval
typealias Failure = Never
let duration: TimeInterval
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, TimeInterval == S.Input {
let scr = CountdownSubscription(duration, subscriber)
subscriber.receive(subscription: scr)
}
}
static func counrdownPublish(_ duration: TimeInterval)-> CountdownPublisher {
return CountdownPublisher(duration: duration)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment