Skip to content

Instantly share code, notes, and snippets.

@bok-
Last active June 3, 2022 06:18
Show Gist options
  • Save bok-/520cb7a737ef41f12c0e5f4e7c69874e to your computer and use it in GitHub Desktop.
Save bok-/520cb7a737ef41f12c0e5f4e7c69874e to your computer and use it in GitHub Desktop.
A simple Combine Publisher that adds a `URL.publisher` to monitor for file changes
//
// FileMonitorPublisher.swift
// Longinus
//
// Created by Rob Amos on 3/3/20.
//
import Foundation
import Combine
public struct FileMonitorPublisher: Publisher {
public typealias Output = Data
public typealias Failure = Swift.Error
// MARK: - Properties and Initialisation
public let url: URL
public init (url: URL) {
self.url = url
}
// MARK: - Supported URLs
public static func isSupported (url: URL) -> Bool {
return url.isFileURL == true
}
// MARK: - Publisher Implementation
public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
guard FileMonitorPublisher.isSupported(url: self.url) else {
subscriber.receive(completion: .failure(Error.urlNotSupported(self.url)))
return
}
let path = self.url.standardizedFileURL.path
guard FileManager.default.fileExists(atPath: path) else {
subscriber.receive(completion: .failure(Error.fileNotFound(self.url)))
return
}
guard FileManager.default.isReadableFile(atPath: path) else {
subscriber.receive(completion: .failure(Error.accessDenied(self.url)))
return
}
let subscription = FileMonitorSubscription(subscriber: subscriber, url: self.url)
subscriber.receive(subscription: subscription)
}
// MARK: - Error
enum Error: Swift.Error {
case accessDenied(URL)
case fileNotFound(URL)
case fileNotReadable(URL)
case urlNotSupported(URL)
}
}
// MARK: - Convenience Publisher
extension URL {
public var isPublisherSupported: Bool {
return FileMonitorPublisher.isSupported(url: self)
}
public var publisher: FileMonitorPublisher {
return FileMonitorPublisher(url: self)
}
}
//
// FileMonitorSubscription.swift
// Longinus
//
// Created by Rob Amos on 3/3/20.
//
import Foundation
import Combine
final public class FileMonitorSubscription<SubscriberType>: NSObject, Subscription, NSFilePresenter where SubscriberType: Subscriber, SubscriberType.Input == Data, SubscriberType.Failure == Swift.Error {
// MARK: - Properties and Initialisation
public let presentedItemURL: URL?
public var presentedItemOperationQueue = OperationQueue()
private var subscriber: SubscriberType?
private var demand: Subscribers.Demand?
private var registered = false
private var coordinator = NSFileCoordinator(filePresenter: nil)
public init (subscriber: SubscriberType, url: URL) {
self.subscriber = subscriber
self.presentedItemURL = url
}
deinit {
if self.registered {
self.unregister()
}
}
// MARK: - Subscription Management
public func request (_ demand: Subscribers.Demand) {
self.demand = demand
if demand == .none {
self.unregister()
} else {
self.register()
self.send()
}
}
public func cancel() {
self.unregister()
}
// MARK: - Managing Registration
private func register () {
self.registered = true
NSFileCoordinator.addFilePresenter(self)
}
private func unregister () {
guard self.registered == true else { return }
NSFileCoordinator.removeFilePresenter(self)
}
// MARK: - Responding to Changes
private func send () {
guard let subscriber = self.subscriber, let url = self.presentedItemURL else { return }
do {
let data = try self.coordinator.coordinate(readingItemAt: url)
_ = subscriber.receive(data)
} catch {
subscriber.receive(completion: .failure(error))
self.unregister()
}
}
public func presentedItemDidChange() {
guard let demand = self.demand, demand > 0 else { return }
self.send()
}
public func accommodatePresentedItemDeletion(completionHandler: @escaping (Swift.Error?) -> Void) {
self.subscriber?.receive(completion: .failure(Error.fileWasDeleted))
self.unregister()
completionHandler(nil)
}
// MARK: - Errors
enum Error: Swift.Error {
case fileWasDeleted
}
}
extension NSFileCoordinator {
func coordinate(readingItemAt url: URL, options: NSFileCoordinator.ReadingOptions = []) throws -> Data {
var coordinationError: NSError?
var thrownError: Swift.Error?
var data: Data?
self.coordinate(readingItemAt: url, options: options, error: &coordinationError) { internalURL in
do {
data = try Data(contentsOf: internalURL)
} catch {
thrownError = error
}
}
if let error = coordinationError {
throw error
} else if let error = thrownError {
throw error
}
if let data = data {
return data
}
throw FileMonitorSubscriptionCoordinationError.unableToCoordinateFileRead
}
private enum FileMonitorSubscriptionCoordinationError: Swift.Error {
case unableToCoordinateFileRead
}
}
let cancellable = URL(fileURLWithPath: "/path/to/file")
.publisher
.assertNoFailure()
.sink { data in
// do something with your updated file
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment