更新於 2022 年 09 月 27 日

取得寫入檔案的 URL

在應用程式執行的時候,有的時候會需要將檔案存放使用者的裝置中。我們只要將資料轉成 Data,便能使用它的方法.write(to: URL) 來寫入,不過,這個 URL 該從何而來?

在不同的裝置,我們能讀取、寫入的資料夾位址都有所不同,所以會需要先透過 API 來取得資料夾目錄 URL,接著再組合成我們要寫入的 URL

取得資料夾 URL

 URL 靜態屬性

iOS16 新增了方便又可靠的取得可用的資料夾位址的方式。你可以直接透過 URL 的靜態屬性 來取得想要資料夾位址。
如果你不確定要選哪一個的話,以下是幾個常用的資料夾路徑。

// 希望使用者看到的資料放這
URL.documentsDirectory 

// 不希望使用者看到的放這
URL.applicationSupportDirectory 

// 暫存、可拋棄的檔案放這
URL.cachesDirectory 

 FileManager

如果是在 iOS16 以前,或者是在 macOS 上的話,我們會透過 FileManager 來取得 URL。
你會需要選擇你要找的「資料夾(Directory)」和「區域(Domain)」。

💡 iOS 上的選擇建議
  • 資料夾:
    • documentDirectory:使用者的文件夾,主要會使用這個儲存資料,是使用者看得到的。
    • applicationSupportDirectory:用來存一些不該被使用者看到的檔案。
    • cachesDirectory:暫存資料。不會被備份。
  • iOS 上,區域只能有「userDomainMask」可用。

有了這兩個資訊,就可以透過下面兩種方法取得 URL。

// 1. 回傳一個符合要求的 URL Array,可能會是空 Array。通常會用這個選第一個。
FileManager.default.urls(for: .applicationSupportDirectory, 
                         in: .userDomainMask).first

// 2. 基本上和上面的一樣,會直接回傳找到的第一個 URL,但是找不到任何 URL 時會報錯。
try FileManager.default.url(for: .applicationSupportDirectory, 
                            in: .userDomainMask, 
                            appropriateFor: .none, 
                            create: false)

結合成的完整 URL

有了基本的資料夾 URL 之後,我們就能透過 URL 的方法來組合出完整的 URL。這裡一樣可以分成 iOS16 的新方法,和之前的方法。

* iOS16 推出了 .append(),將會取代舊有的 .appendingPathComponent() 方法。用法上大同小異,故以下僅介紹新方法。

 append()

  • .append(path: String)
  • .append(component: String)
  • .appendappend(components: String...)

💡 參數 path 和 component 的差別只有 component 會先進行 URL 編碼。
⚠️ 注意 .append() 是 mutating 方法,不是回傳新的 URL。

// 以下三種寫法都是同樣到「文件資料夾/newFolder/file.txt」。
var dir = URL.documentsDirectory
dir.append(path: "newFolder/file.txt")

var dir2 = URL.documentsDirectory
dir2.append(component: "newFolder/file.txt")

var dir3 = URL.documentsDirectory
dir3.append(components: "newFolder", "file.txt")

 DirectoryHint

append 中有個可選參數 DirectoryHint,這個 enum 用來明確表示 URL 是否是資料夾。

  • 預設值為「inferFromPath」,也就是我們需要自己管理好 URL 的結尾。
public enum DirectoryHint {
    case isDirectory        // 是資料夾
    case isNotDirectory     // 不是資料夾
    case checkFileSystem    // 應檢查檔案系統來確認是不是資料夾
    case inferFromPath      // 檢查 URL 最後是否以 / 結束來判斷是不是資料夾
}

 appendingPathExtension()

有時候我們只是想加上副檔名,就可以使用這個方法。不會變成繼續接下一層「/」。

let dir = URL.documentsDirectory.appendingPathExtension("txt")
print(dir)
// ~/Documents.txt/

檢查檔案、資料夾是否已存在

在直接使用 URL 之前,我們通常會先檢查一下檔名是否已被使用、資料夾是否已經建立好。

FileManager.default.fileExists(atPath: dir.path())

新增資料夾

try FileManager.default.createDirectory(
        at: dir,
        withIntermediateDirectories: false
    )
        // 這個參數如果設為 true,會自動把中間不存在的資料夾也一起新增。

你可能會想先確認資料夾是否存在,如果不存在就新增資料夾。但是你沒辦法保證在「確認~新增」這段期間沒有其他地方同時在這個位置新增一樣的資料夾。
所以直接用 do catch 處理會是更安全的做法。

do {
    try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false)
} catch CocoaError.fileWriteFileExists {
   // 資料夾已存在
} catch {
   // 處理其他錯誤
}

結語

經過「取得資料夾 URL」=> 「設定完整路徑」=>「確認資料夾與檔案是否存在」,這幾個步驟之後你就可以安全地使用這個 URL 了。
把這些步驟整理進你自己的 Manager,或是加進 FileManager 的 extension 中會讓你的程式碼更好用、整潔。