Last active
June 3, 2022 06:18
-
-
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
This file contains hidden or 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
// | |
// 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) | |
} | |
} |
This file contains hidden or 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
// | |
// 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 | |
} | |
} |
This file contains hidden or 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
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