Last active
February 16, 2022 03:14
-
-
Save shaps80/6289a8ecffc970add9a75916ec3f670d to your computer and use it in GitHub Desktop.
NSNotification Scheduling Service in Swift. (the only required file is `SchedulingService.swift`)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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)") | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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: "") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This
micro-library
also provides nice output for debugging purposes.