Skip to content

Instantly share code, notes, and snippets.

@shaps80
Last active February 16, 2022 03:14
Show Gist options
  • Save shaps80/6289a8ecffc970add9a75916ec3f670d to your computer and use it in GitHub Desktop.
Save shaps80/6289a8ecffc970add9a75916ec3f670d to your computer and use it in GitHub Desktop.
NSNotification Scheduling Service in Swift. (the only required file is `SchedulingService.swift`)
//
// AppDelegate.swift
// Scheduling
//
// Created by Shaps Benkau on 19/02/2018.
// Copyright © 2018 152percent Ltd. All rights reserved.
//
import UIKit
extension Notification.Name {
public static let CountdownDidUpdate = Notification.Name(rawValue: "CountdownDidUpate")
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var store: SchedulingServiceStore!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
store = SchedulingServiceStore()
store.service.delegate = self
if store.service.scheduledRequests.isEmpty == true {
scheduleRequests(service: store.service)
}
store.service.resume()
print("\(store.service)")
return true
}
private func scheduleRequests(service: SchedulingService) {
/**
Closure based example
*/
let request = service.schedule(date: Date().addingTimeInterval(5))
service.cancel(request: request)
/**
Notification based example
*/
service.schedule(date: Date().addingTimeInterval(20), notification: .CountdownDidUpdate)
}
}
extension AppDelegate: SchedulingServiceDelegate {
func schedulingServiceDidChange(_ service: SchedulingService) {
print("Changed: \(service)")
}
func schedulingService(_ service: SchedulingService, didAdd request: SchedulingRequest) {
print("Added: \(request)")
}
func schedulingService(_ service: SchedulingService, didComplete request: SchedulingRequest) {
print("Completed: \(request)")
}
func schedulingService(_ service: SchedulingService, didCancel request: SchedulingRequest) {
print("Cancelled: \(request)")
}
}
//
// SchedulingService.swift
// Scheduling
//
// Created by Shaps Benkau on 19/02/2018.
// Copyright © 2018 152percent Ltd. All rights reserved.
//
import Foundation
public protocol SchedulingServiceDelegate: class {
func schedulingService(_ service: SchedulingService, didAdd request: SchedulingRequest)
func schedulingService(_ service: SchedulingService, didComplete request: SchedulingRequest)
func schedulingService(_ service: SchedulingService, didCancel request: SchedulingRequest)
func schedulingServiceDidChange(_ service: SchedulingService)
}
public final class SchedulingService {
deinit {
cancelAllRequests()
}
public weak var delegate: SchedulingServiceDelegate?
private var _requests: [SchedulingRequest] = [] {
didSet {
NotificationCenter.default.post(name: .SchedulingServiceDidChange, object: self)
delegate?.schedulingServiceDidChange(self)
}
}
private var _scheduledRequests: [SchedulingRequest: Timer] = [:]
public private(set) var isPaused: Bool = true
public var scheduledRequests: [SchedulingRequest] {
return _requests.sorted(by: { $0.date < $1.date })
}
@discardableResult
public func schedule(identifier: String = UUID().uuidString, date: Date, using closure: ((SchedulingRequest) -> Void)? = nil) -> SchedulingRequest {
var request = SchedulingRequest(identifier: identifier, date: date, notification: .closure)
request.handler = closure
schedule(request: request)
return request
}
@discardableResult
public func schedule(identifier: String = UUID().uuidString, date: Date, notification: Notification.Name) -> SchedulingRequest {
let request = SchedulingRequest(identifier: identifier, date: date, notification: notification)
schedule(request: request)
return request
}
private func schedule(request: SchedulingRequest) {
if _requests.contains(request) {
// if we have an existing request, invalidate its timer
_scheduledRequests[request]?.invalidate()
} else {
// otherwise append it
_requests.append(request)
delegate?.schedulingService(self, didAdd: request)
}
guard !isPaused else { return }
// if we're not paused, start the timer
enqueueTimer(for: request)
}
fileprivate func handleRequest(_ request: SchedulingRequest) {
removeRequest(request)
// execute the handler if it was set, and post the notification
request.handler?(request)
if request.notification != .closure {
// if a valid name was specified, post a notification
NotificationCenter.default.post(name: request.notification, object: request)
}
// send a catch-all notification as well for all requests
NotificationCenter.default.post(name: .SchedulingServiceDidCompleteRequest, object: request)
delegate?.schedulingService(self, didComplete: request)
}
public func cancel(request: SchedulingRequest) {
removeRequest(request)
NotificationCenter.default.post(name: .SchedulingServiceDidCancelRequest, object: request)
delegate?.schedulingService(self, didCancel: request)
}
public func cancelAllRequests() {
for request in scheduledRequests {
cancel(request: request)
}
}
public func resume() {
isPaused = false
_requests.forEach {
enqueueTimer(for: $0)
}
}
public func pause() {
isPaused = true
_scheduledRequests.values.forEach {
$0.invalidate()
}
}
private func enqueueTimer(for request: SchedulingRequest) {
guard request.date >= Date() else {
// if the date is in the past, handle it immediately
handleRequest(request)
return
}
let timer = Timer(fire: request.date, interval: 0, repeats: false) { [weak self] _ in
self?.handleRequest(request)
}
_scheduledRequests[request] = timer
RunLoop.main.add(timer, forMode: .defaultRunLoopMode)
}
private func removeRequest(_ request: SchedulingRequest) {
guard let index = _requests.index(of: request) else { return }
_requests.remove(at: index)
_scheduledRequests[request]?.invalidate()
_scheduledRequests[request] = nil
}
}
extension SchedulingService: CustomStringConvertible {
public var description: String {
return "\(type(of: self)) (\(isPaused ? "Paused" : "Running")) | \(scheduledRequests.count) pending request(s)"
}
}
extension SchedulingService: CustomDebugStringConvertible {
public var debugDescription: String {
var string = description
for request in _requests {
string.append("\n▹ \(request)")
}
return string
}
}
extension SchedulingService: Codable {
public convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.singleValueContainer()
_requests = try container.decode(Array<SchedulingRequest>.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(_requests)
}
}
public struct SchedulingRequest: Codable {
public enum CodingKeys: String, CodingKey {
case identifier
case date
case notification
}
public let identifier: String
public let date: Date
public let notification: Notification.Name
fileprivate var handler: ((SchedulingRequest) -> Void)? = nil
fileprivate init(identifier: String = UUID().uuidString, date: Date, notification: Notification.Name) {
self.identifier = identifier
self.date = date
self.notification = notification
}
}
extension SchedulingRequest: Hashable {
public var hashValue: Int {
return identifier.hashValue
}
public static func ==(lhs: SchedulingRequest, rhs: SchedulingRequest) -> Bool {
return lhs.identifier == rhs.identifier
}
}
extension SchedulingRequest: CustomStringConvertible {
public var description: String {
return "Request (\(identifier)) | \(date)"
}
}
extension Notification.Name: Codable {
public static let SchedulingServiceDidCompleteRequest = Notification.Name(rawValue: "SchedulingServiceDidHandleRequest")
public static let SchedulingServiceDidCancelRequest = Notification.Name(rawValue: "SchedulingServiceDidCancelRequest")
public static let SchedulingServiceDidChange = Notification.Name(rawValue: "SchedulingServiceDidChange")
// used to specify an empty notification for closure based requests
fileprivate static let closure = Notification.Name(rawValue: "")
}
//
// ScheduleStore.swift
// Scheduling
//
// Created by Shaps Benkau on 20/02/2018.
// Copyright © 2018 152percent Ltd. All rights reserved.
//
import Foundation
public final class SchedulingServiceStore {
public let service: SchedulingService
public init(service: SchedulingService? = nil) {
if let service = service {
self.service = service
} else {
if let data = try? Data(contentsOf: SchedulingServiceStore.storeUrl),
let service = try? JSONDecoder().decode(SchedulingService.self, from: data) {
self.service = service
} else {
self.service = SchedulingService()
}
}
NotificationCenter.default.addObserver(forName: .SchedulingServiceDidChange, object: nil, queue: .main) {
[weak self] note in
self?.save()
}
}
public func save() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(service)
try data.write(to: SchedulingServiceStore.storeUrl)
} catch {
print(error)
}
}
private static var storeUrl: URL {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
return URL(fileURLWithPath: path).appendingPathComponent("SchedulingService.json")
}
}
@shaps80
Copy link
Author

shaps80 commented Feb 20, 2018

This micro-library also provides nice output for debugging purposes.

screen shot 2018-02-20 at 16 49 08

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment