(Swift)DispatchSourceを利用してファイルの作成・名前変更・削除を監視する
はじめに
以下の環境で動作確認しました。
- Xcode 13.2.1 (13C100)
- iOS 15.1
実装例
DispatchSourceを利用することでファイルの作成・名前変更・削除イベントを監視できます。ここでは例としてDocumentsディレクトリ内のファイル作成・名前変更・削除を監視する実装例を示します。
実装例はSwiftUIを利用しています。Xcodeを開いて新規プロジェクトを作成したら、ContentView.swiftを以下のコードで置き換えてください。
import SwiftUI
protocol FileWatcherDelegate {
var targetURL: URL { get }
func onChange()
func onCancel()
}
class FileWatcher {
var delegate: FileWatcherDelegate? = nil
private let queue = DispatchQueue.global(qos: .default)
private var fileDescriptor: Int32 = -1
private var source: DispatchSourceFileSystemObject? = nil
deinit {
self.stop()
}
func stop() {
if self.source != nil {
self.source!.cancel()
self.source = nil
}
if self.fileDescriptor < 0 {
close(self.fileDescriptor)
self.fileDescriptor = -1
}
}
func start() {
if self.source != nil {
return
}
guard let url = self.delegate?.targetURL else {
fatalError("failed to read targetURL property")
}
let fileDescriptor = open(url.path, O_EVTONLY)
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileDescriptor, eventMask: .write, queue: self.queue)
source.setEventHandler {
if self.delegate != nil {
self.delegate!.onChange()
}
}
source.setCancelHandler {
if self.delegate != nil {
self.delegate!.onCancel()
}
}
if self.delegate != nil {
self.delegate!.onChange()
}
source.activate()
self.fileDescriptor = fileDescriptor
self.source = source
}
}
class FileList: ObservableObject, FileWatcherDelegate {
@Published var urls: [URL] = []
private let url: URL
var targetURL: URL {
return self.url
}
init(url: URL) {
self.url = url
}
func onChange() {
guard
let urls =
try? FileManager.default.contentsOfDirectory(
at: self.targetURL, includingPropertiesForKeys: nil,
options: .skipsSubdirectoryDescendants)
else {
return
}
DispatchQueue.main.async {
self.urls = urls
}
}
func onCancel() {
DispatchQueue.main.async {
self.urls = []
}
}
}
extension URL: Identifiable {
public var id: String {
return self.absoluteString
}
}
struct ContentView: View {
@ObservedObject var fileList: FileList
let watcher = FileWatcher()
var body: some View {
VStack {
List(self.fileList.urls) { url in
Text(url.lastPathComponent)
}
Spacer()
HStack {
Button("Start") {
self.watcher.start()
}
Button("Stop") {
self.watcher.stop()
}
Button("Create") {
self.create()
}
Button("Remove") {
self.remove()
}
}
}
}
init() {
guard
let documentURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
else {
fatalError("failed to get Document URL")
}
self.fileList = FileList(url: documentURL)
self.watcher.delegate = self.fileList
}
func create() {
let text = "Hello, World!"
let n = Int.random(in: 0..<1_000_000)
let name = "text-\(n).txt"
let url = self.fileList.targetURL.appendingPathComponent(name)
do {
try text.write(to: url, atomically: true, encoding: .utf8)
} catch {
fatalError("failed to create file: \(url): \(error)")
}
}
func remove() {
for url in self.fileList.urls {
do {
try FileManager.default.removeItem(at: url)
} catch {
fatalError("failed to remove: \(error)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
実装の肝はDispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: self.queue)
です。
まず、ファイルディスクリプタにはopen("/path/to/file", O_EVTONLY)
で取得した値を設定する必要があります。iOS 14以降であればCのopen関数ではなくFileDescriptor型を利用して実装することも可能です。
イベントマスクについては.write
という名前に反して作成だけではなく名前の変更や削除イベントも監視することができます。詳しくはDispatchSourceのAPIドキュメントを参照してください。
試運転
実装したアプリの使い方を説明します。
- ファイルの一覧は画面の上半分に表示されます。
- Startボタンを押すと監視が始まります。
- Createボタンを押すとランダムな名前の
*.txt
ファイルが作成されます。 - Removeボタンを押すと監視対象のディレクトリ内のテキストファイルをすべて削除します。
- Stopボタンを押すと監視が終ります。
アプリが起動したら、まずはStartボタンを押してください。その後、Createボタンを押すと、ファイルが作成されると同時にファイル一覧が更新されます。
さらに、iOS組み込みのファイルアプリを開いて、ファイルの名前変更や削除を試してみてください。ファイルアプリの変更が即座に反映されます。
なお、ファイルアプリからアプリ内のDocumentsディレクトリが参照できるように変更するにはInfo.plistの設定が必要になります。詳細については以下の記事をご覧ください。
Discussion