2
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-11-02

概要

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

参考

  • Detecting changes to a folder in iOS using Swift
    • ほぼこちらを参考
  • DispatchSource.FileSystemEvent
    • eventMaskはディレクトリに対してのものであり、ディレクトリ内の個々のファイルに対するものではないので混同しないこと
    • 例えば.renameの場合、ディレクトリ自体のリネームは検知されるが、ディレクトリ内のリネームは検知されない
 folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor,
                                                                 eventMask: .write,
                                                                 queue: folderMonitorQueue)
  • Swift System is Now Open Source
    • open(url.path, O_EVTONLY)がSwiftらしくないので、systemを使えばSwiftらしく書けるという話
    • ファイルはともかくディレクトリに対応していない?のか、FileDescriptorの作成時に例外エラー
// 下記インポートでFileDescriptorを使用できる
import system

実装

Sandboxの設定

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

FolderMonitor.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

  • 呼び出し方の例
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")
    }

}
2
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?