【Swift】その URL が ファイル なのか ディレクトリ なのか 存在しないのか

これはディレクトリ。


URL.documentsDirectory

では、これは何を指しているのか。


URL.documentsDirectory.appending(component: "xxx")

ファイルなのか、ディレクトリなのか、存在しないのか。


appendingPathComponent(_:) が Deprecated なので、

👉 appendingPathComponent(_:) | Apple Developer Documentation hatena-bookmark

appending(component:directoryHint:) としています。

👉 appending(component:directoryHint:) | Apple Developer Documentation hatena-bookmark

 

🤔 ディレクトリなのか


print(
  URL.documentsDirectory
    .appending(component: "xxx")
    .hasDirectoryPath
)

// false

👉 hasDirectoryPath | Apple Developer Documentation hatena-bookmark

「ディレクトリではない」のですが、

そのURLにファイルがあるのか、

または、何も存在しないのか、

分かりません。

 

🤔 URL.resourceValues(forKeys:) を使う

👉 resourceValues(forKeys:) | Apple Developer Documentation hatena-bookmark

この形でよく使われています。


extension URL {
  var isDirectory: Bool {
    (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
  }
}


print(
  URL.documentsDirectory
    .isDirectory,
  URL.documentsDirectory
    .appending(component: "xxx")
    .isDirectory
)

// true false

extension 内の条件式の左辺は、


URL先にリソースが存在してディレクトリのとき
→ true

URL先にリソースが存在してディレクトリでないとき
→ false

URL先にリソースが存在しないとき 
→ nil

となります。

URLの指すリソースが、ファイルとディレクトリのみであるとすれば、以下のように書くことができますね!


extension URL {
  private var resourceIsDirectory: Bool? {
    (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory
  }

  var exists: Bool {
    resourceIsDirectory != nil
  }

  var isFile: Bool {
    resourceIsDirectory == false
  }

  var isDirectory: Bool {
    resourceIsDirectory == true
  }
}


let documents = URL.documentsDirectory

print(
  documents.exists,
  documents.isDirectory,
  documents.isFile
)
// true true false

let documentsXXX = URL.documentsDirectory
  .appending(component: "xxx")

print(
  documentsXXX.exists,
  documentsXXX.isDirectory,
  documentsXXX.isFile
)
// false false false

 

🤔 まとめ

URLResourceValues が扱う値はいろいろです。

👉 URLResourceValues | Apple Developer Documentation hatena-bookmark

以下に、まとめておきます。

そもそもは、

「ファイルの存在の確認時に、制限のゆるい String に置き換えてからの FileManager.fileExists(atPath:) を使うのが面倒すぎる。」

ということがきっかけでした。

👉 fileExists(atPath:) | Apple Developer Documentation hatena-bookmark

実体とURLは直感と違います。

👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark


【Apple WWDC24】Swift ネタをサンプルコードとともに眺めていくページ

いよいよです。



👉 WWDC24 - Apple Developer hatena-bookmark

Apple 公式の GitHub では、

「sample」

の prefix をつけてリポジトリを公開していますよね。

洗練されていて、Swift の方向性も示されてるようで。

👉 Repository search results hatena-bookmark

いや、もしかしたら、GitHub 公開は遅れるかもしれん。

(更新中 ...)



【Swift】String と Data の変換

という bytes っぽい型があるんですね !

👉 Data | Apple Developer Documentation hatena-bookmark

String との変換です。

 

🧑‍💻 String から Data


let string = "Hello, world!"

// String to Data

let data = Data(string.utf8)
let optionalData = string.data(using: .utf8)

print(data)
// 13 bytes

// Expression implicitly coerced from 'Data?' to 'Any'
print(optionalData)
// Optional(13 bytes)

 

🧑‍💻 Data から String

上のコードからのつづきです。


// Data to String

// Expression implicitly coerced from 'String?' to 'Any'
print(String(data: data, encoding: .utf8))
// Optional("Hello, world!")

print(String(data: data, encoding: .utf8) ?? "")
// Hello, world!

print(String(data: data, encoding: .utf8)!)
// Hello, world!

print(String(decoding: data, as: UTF8.self))
// Hello, world!

 

🧑‍💻 参考

いろいろ見たけど、良さげなエントリーのみ。

👉 How to convert Data to a String - free Swift example code and tips hatena-bookmark
👉 How to convert a String to Data - free Swift example code and tips hatena-bookmark
👉 Swift Tip: String to Data and Back · objc.io hatena-bookmark
👉 Converting between String and Data without optionals | Swift by Sundell hatena-bookmark
👉 Swift String to Data | Convert Data to String | Viking Skull Apps hatena-bookmark

Swift 5からはStringの内部データがUTF-8になったのでした

👉 [Swift] string.data(using: .utf8)ってnilになるの? #Swift - Qiita hatena-bookmark

今の Swift では、String 内部エンコーディングが「UTF-8」ってこと大事。


【Swift】FileManager を使いたい

最初、無駄にややこしくて使いづらい気がした。

しかし、整理できるとそうでもない。


let fileManager = FileManager.default
let documents = URL.documentsDirectory

Button("create file") {
  // new.txt
  let text = "Hello!"
  let to = documents.appending(component: "new.txt")
  try? text.write(to: to, atomically: true, encoding: .utf8)
}

Button("read file") {
  // new.txt
  let file = documents.appending(component: "new.txt")
  let text = (try? String(contentsOf: file, encoding: .utf8)) ?? "ERROR"
  print(text)
}

Button("create directory") {
  // some/
  let directory = documents.appending(component: "some/")
  try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}

Button("copy file") {
  // new.txt - copy -> some/copied.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/copied.txt")
  try? fileManager.copyItem(at: at, to: to)
}

Button("move file") {
  // new.txt - move -> some/moved.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/moved.txt")
  try? fileManager.moveItem(at: at, to: to)
}

Button("copy directory") {
  // some/ - copy -> another/
  let at = documents.appending(component: "some/")
  let to = documents.appending(component: "another/")
  try? fileManager.copyItem(at: at, to: to)
}

Button("delete file") {
  // some/moved.txt
  let dir = documents.appending(component: "some/")
  let file = dir.appending(component: "moved.txt")
  try? fileManager.removeItem(at: file)
}

Button("delete directory") {
  // some/ and another/
  let dir1 = documents.appending(component: "some/")
  let dir2 = documents.appending(component: "another/")
  try? fileManager.removeItem(at: dir1)
  try? fileManager.removeItem(at: dir2)
}

つづいて、ディレクトリを指定するだけで、

そのディレクトリ内に存在するディレクトリとファイルの状態を表示できるようにしておきます。

実体の確認を頻繁にしやすくしておくこと大事。



extension FileManager {
  private func contents(directory url: URL) -> [URL] {
    (try? contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])) ?? []
  }

  func showContents(_ url: URL = .documentsDirectory) {
    print(
      String(format: "%@ %@", url.shortPath(), url.isFile ? "[\(url.fileSize)]" : "")
    )

    if url.isDirectory {
      contents(directory: url)
        .sorted(by: {
          let lr = [$0, $1].map {
            ($0.path().components(separatedBy: "/").dropLast().joined(), $0.path())
          }
          return lr[0] < lr[1]
        })
        .forEach { content in
          showContents(content)
        }
    }
  }
}


FileManager.default.showContents(.documentsDirectory)

// /HOME/Documents/ 
// /HOME/Documents/new.txt [6 bytes]
// /HOME/Documents/another/ 
// /HOME/Documents/another/copied.txt [6 bytes]
// /HOME/Documents/some/ 
// /HOME/Documents/some/copied.txt [6 bytes]
// /HOME/Documents/some/moved.txt [6 bytes]

👉 contentsOfDirectory(at:includingPropertiesForKeys:options:) | Apple Developer Documentation hatena-bookmark

これぐらいでいいか。

Permission とか mask の話しはまたそのうちやりたいです。



 

🤔 まとめ

コツとしては、URL の取り扱い。


ファイルやディレクトリは URL を使って指す。


URL を path() などを使って String に変換するのは表示直前のみ。


URL は実体の情報を保持していない。

という当たり前の言葉が思いつく。

しかし、最初は混乱したし、すると思う。

String のほうが直感的で人間に近いし、

古い API を見てると String ベースでファイルを操作してる。

フツーに検索しても古い記述がヒットすることが多い。

 

🤔 参考

👉 【Swift】ファイルやディレクトリ操作するための extension をまずは作った hatena-bookmark
👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark


【Swift】よく使われている zip ファイルユーティリティ 2つ

よく似た Star数の2つ。

どっちが使いやすそうでしょうか。

 

🙆🏻‍♂️ ZIPFoundation


import ZIPFoundation


try fileManager.zipItem(at: sourceURL, to: destinationURL)


try fileManager.unzipItem(at: sourceURL, to: destinationURL)

共に、利用する FileManager の extension になっています。

👉 weichsel/ZIPFoundation: Effortless ZIP Handling in Swift hatena-bookmark

 

🙆🏻‍♂️ Zip


import Zip


let zipFilePath = try Zip.quickZipFiles([filePath], fileName: "archive")


let unzipDirectory = try Zip.quickUnzipFile(filePath)

Zip クラスの static な関数を利用するようです。

👉 marmelroy/Zip: Swift framework for zipping and unzipping files. hatena-bookmark

 

🙆🏻‍♂️ まとめ

共に、ファイル位置は「URL」を引数として使います。

もしかして、ビルトインで何か関数あるの?