macOSでディレクトリ内の変更を監視する方法

# 概要
- 特定のディレクトリにファイルが作成されたり削除される場合の監視をしたい
- `Timer`を使って定期的にポーリングしてもいいが非効率
- 代わりに`DispatchSourceFileSystemObject`を使うといい

# 参考
- [Detecting changes to a folder in iOS using Swift](https://medium.com/over-engineering/monitoring-a-folder-for-changes-in-ios-dc3f8614f902)
    - ほぼこちらを参考
- [DispatchSource\.FileSystemEvent](https://developer.apple.com/documentation/dispatch/dispatchsource/filesystemevent)
    - `eventMask`はディレクトリに対してのものであり、ディレクトリ内の個々のファイルに対するものではないので混同しないこと
    - 例えば`.rename`の場合、ディレクトリ自体のリネームは検知されるが、ディレクトリ内のリネームは検知されない

```swift
 folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor,
                                                                 eventMask: .write,
                                                                 queue: folderMonitorQueue)
```

- [Swift System is Now Open Source](https://swift.org/blog/swift-system/)
    - `open(url.path, O_EVTONLY)`がSwiftらしくないので、`system`を使えばSwiftらしく書けるという話
    - ファイルはともかくディレクトリに対応していない？のか、`FileDescriptor`の作成時に例外エラー

```swift
// 下記インポートでFileDescriptorを使用できる
import system
```

# 実装
## Sandboxの設定
- 今回ダウンロードフォルダを対象とするので、下記の通りSandboxでアクセスの許可を行っておく

<img width="800" alt="image" src="https://i.imgur.com/m9wsGzs.png">

## FolderMonitor.swift

```swift
import Foundation


class FolderMonitor {
    
    // MARK: Properties
    
    /// ディレクトリを監視するためのFileDescriptor
    private var monitoredFolderFileDescriptor: CInt = -1
    
    /// ディレクトリ内のファイル変更を処理するためのDispatchQueue
    private let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
    
    /// ファイル記述子に関連付けられたイベントを監視するDispatchSource
    private var folderMonitorSource: DispatchSourceFileSystemObject?
    
    /// 監視するディレクトリのURL
    let url: Foundation.URL
    
    /// ディレクトリに変更があった際に呼ばれるClosure
    var folderDidChange: (() -> Void)?
    
    // MARK: Initializers
    
    init(url: Foundation.URL) {
        self.url = url
    }
    
    // MARK: Monitoring
    
    /// Listen for changes to the directory (if we are not already).
    
    func startMonitoring() {
        guard folderMonitorSource == nil &&
                monitoredFolderFileDescriptor == -1
        else {
            return
        }
        // Open the directory referenced by URL for monitoring only.
        monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
        // Define a dispatch source monitoring the directory for additions, deletions, and renamings.
        folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor,
                                                                                                     eventMask: .write,
                                                                                                     queue: folderMonitorQueue)
        // Define the block to call when a file change is detected.
        folderMonitorSource?.setEventHandler { [weak self] in
            self?.folderDidChange?()
        }
        
        // Define a cancel handler to ensure the directory is closed when the source is cancelled.
        folderMonitorSource?.setCancelHandler { [weak self] in
            guard let strongSelf = self else {
                return
            }
            close(strongSelf.monitoredFolderFileDescriptor)
            strongSelf.monitoredFolderFileDescriptor = -1
            strongSelf.folderMonitorSource = nil
        }
        
        // Start monitoring the directory via the source.
        folderMonitorSource?.resume()
    }
    
    /// Stop listening for changes to the directory, if the source has been created.
    func stopMonitoring() {
        folderMonitorSource?.cancel()
    }
}
```

## ViewController.swift
- 呼び出し方の例

```swift
import Cocoa

class ViewController: NSViewController {

    var folderMonitor: FolderMonitor!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        
        let downloadsDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!
        let targetFolder = downloadsDirectory.appendingPathComponent("Sample")  // e.g. /Users/<user name>/Downloads/Sample
        folderMonitor = FolderMonitor(url: targetFolder)
        
        folderMonitor.folderDidChange = { [weak self] in
            self?.handleChanges()
        }        
        folderMonitor.startMonitoring()
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
    
    override func viewDidDisappear() {
        super.viewDidDisappear()
        folderMonitor.stopMonitoring()
    }

    private func handleChanges() {
        print("DEBUG: handleChanges")
    }

}
```