原文地址 翻译:DeveloperLx
在
macOS
中文件系统是每个app的基础 - 在
FileManager
类有很多来处理这个。你会在
Applications
目录下储存app,在
Documents
目录下储存文档,在
Library
目录下储存偏好和支持文件。
随着文件和数据在文件系统中传播,你的app该如何查找文件和目录,使用文件和目录的路径,甚至读取及写入数据到一个文件中?
那就要靠
FileManager
类了!
在这个教程中,你会学到如何管理目录路径,使用URL,使用通用的文件和目录对话框,展示文件和目录信息等等!
在本教程中,你会从playground开始,然后在掌握基础之后,移步到app中。
macOS
使用了一个分层的文件系统:目录中的文件和目录,目录内部。这意味着找到一个指定的文件是非常复杂的。每个文件都有它自己的地址,定义地址的结构被称为
URL
。
打开 Xcode 并在 Welcome to Xcode 窗口中单击 Get started with a playground ,或依次选择 File/New/Playground… 将playground的名称设为 Files ,确保平台被设置为 macOS 并单击 Next 。
选择你的 Desktop 并单击 Create 来保存playground。
打开开始的playground后,删除除
import Cocoa
外全部行的代码。
添加下面的代码到你的playground中,但不要担心现在改变了用户名:
let completePath = “/Users/sarah/Desktop/Files.playground”
completePath
现在包含了这个playground文件的地址或路径。由于
now contains the address, or path, of this playground file. Since
macOS
是基于Unix系统的,而
this is how
Unix
(以及它的所有变体)就是这么描述文件路径的。第一条斜杠表示根目录,在这个case中是你的启动磁盘。之后,每个斜杠都界定除了一个新的目录或文件。因此,
(and all its variants) describe file paths.
The first slash indicates the root directory,
which in this case is your startup disk.
After that, every slash delimits a new folder or file.
So
Files.playground
文件就位于启动驱动器的
Users
目录下的
sarah
目录下的
Desktop
目录中。
尽管这个字符串描述了这个文件的全路径,但它并不是处理地址的最佳方式。相代替的,你会通过添加下列代码,将地址替换为一个
URL
:
let completeUrl = URL(fileURLWithPath: completePath)
现在,在playground的结果面板中,你会看到:
file:///Users/sarah/Desktop/Files.playground
“稍等!”你喊道。“我以为
URL
是一个像
https://www.raywenderlich.com
这样的网址,而不是目录的路径!”
嗯,是的是的!
URL
代表了
Uniform Resource Locator
- 它也可以指向本地的文件和目录。并非
https://
,而是以
file://
开头来表示本地文件。在结果面板中,它看起来有3个斜杠,但这是因为这个路径本身就是以斜杠开头的。
你已经使用了一个
字符串
来指定一个文件路径,并将它转换为一个
URL
。但是,虽然它是一个有效的
URL
,但却无法工作 - 除非你的用户名也恰好就是
sarah
。因此,下面的一步就是来创建一个可以在任何人的电脑上work的
URL
了。
要做到这个,你就要使用
FileManager
类,它提供了在macOS中大多数的处理文件相关行为的方法。
第一个任务就是识别你的 Home 目录,并使用你自己的用户名来替换 sarah 。
添加下列的代码到你的playground中:
let home = FileManager.default.homeDirectoryForCurrentUser
default
返回了
FileManager
类的单例实例,而
homeDirectoryForCurrentUser
包含了当前用户的home目录的
URL
。
现在你已经有了指向你的home目录的
URL
,你就可以通过添加下列的代码来获取指向playground的路径:
let playgroundPath = "Desktop/Files.playground"
let playgroundUrl = home.appendingPathComponent(playgroundPath)
现在结果面板就会展示在你的家目录下的
URL
了。
添加下列的代码到playground中,来查询各种
URL
的property:
playgroundUrl.path
playgroundUrl.absoluteString
playgroundUrl.absoluteURL
playgroundUrl.baseURL
playgroundUrl.pathComponents
playgroundUrl.lastPathComponent
playgroundUrl.pathExtension
playgroundUrl.isFileURL
playgroundUrl.hasDirectoryPath
pathComponents
这个property非常有趣,它会将所有的目录和文件拆成一个数组。而
lastPathComponent
和
pathExtension
property在实践中都相当地有用。
下面是你应该在你的playground中有的:
注意:
property
hasDirectoryPath
的值被设为了
true
。这标记了
URL
是一个目录。但为何playground文件是被标记为一个目录?
这是因为 .playground 文件是“目录bundle”,就像 .app 文件一样。右击playground文件,并选择 Show Package Contents 就可以查看它的内部了。
URL
类使得编辑
URLs
非常得容易。
添加下列的代码到你的playground中:
var urlForEditing = home
urlForEditing.path
urlForEditing.appendPathComponent("Desktop")
urlForEditing.path
urlForEditing.appendPathComponent("Test file")
urlForEditing.path
urlForEditing.appendPathExtension("txt")
urlForEditing.path
urlForEditing.deletePathExtension()
urlForEditing.path
urlForEditing.deleteLastPathComponent()
urlForEditing.path
注意,每次你都会展示
path
property,因此很容易就会看到改变了什么。
尽管这些命令恰当地编辑
URL
,你也可以从已存在的创建一个新的
URL
。
为了了解如何操作,添加下列的命令到你的playground:
let fileUrl = home
.appendingPathComponent("Desktop")
.appendingPathComponent("Test file")
.appendingPathExtension("txt")
fileUrl.path
let desktopUrl = fileUrl.deletingLastPathComponent()
desktopUrl.path
这些方法会返回新的
URLs
,因此将它们连接到一个序列中效果会更好。
这三个
appending
方法实际上可以缩到一个方法中,但我在这里将它们拆分成了独立的步骤,以便清晰地展示给你。
你的playground现在应当看起来像下面的样子:
查看文件和目录
NSString
中有很多处理文件路径的方法,但在Swift的结构体
String
中则不是。相反地,随着苹果向着
Apple File System (APFS)
的转变,你应当使用
URLs
来处理文件路径。在这种方式下处理将会变得更重要,因为。
然而,在下面这个情形下,你仍然需要一个字符串来代表文件
URL
:检查是否这个文件或目录存在。获取一个
URL
的字符串版本的最近方式是通过
path
property。
添加下列的代码到你的playground中:
let fileManager = FileManager.default
fileManager.fileExists(atPath: playgroundUrl.path)
let missingFile = URL(fileURLWithPath: "this_file_does_not_exist.missing")
fileManager.fileExists(atPath: missingFile.path)
检查一个目录是否存在稍微有一点难懂,因为你必须这个
URL
既是一个有效的资源,又是一个目录。
这就要求我使用一个非常非Swift的机制 - OC版本的inout的Bool值。添加下列的代码:
var isDirectory: ObjCBool = false
fileManager.fileExists(atPath: playgroundUrl.path, isDirectory: &isDirectory)
isDirectory.boolValue
现在,你的playground看起来应当是下面的样子:
一个充满注释版本的 playground 可以到这里下载。
既然你已理解了如何使用
URL
来区别文件和目录,关闭playground。是时候来构建app了!
在教程的这一部分,你将要构建 文件间谍 app,你可以用它选择一个目录来查看其内部的每个文件和目录。选择其中任一项目,就可以看到更多的详情。
下载 起始的app项目 ,在 Xcode 中打开它并在toolbar单击 Play 按钮,或按 Command+R键 运行项目。它的UI已完成,但你需要添加文件管理位。
你第一个任务就是让用户选择一个目录,然后展示它的内容。你会在
Select Folder
按钮上添加一些代码,并使用
NSOpenPanel
类来选择一个目录。
在
ViewController.swift
中的
Actions
区,找到
selectFolderClicked
并插入下面的代码:
// 1
guard let window = view.window else { return }
// 2
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = false
// 3
panel.beginSheetModal(for: window) { (result) in
if result == NSFileHandlingPanelOKButton {
// 4
self.selectedFolder = panel.urls[0]
print(self.selectedFolder)
}
}
上面的代码完成了:
-
检查你可否获取window的引用,因为它是
NSOpenPanel
将要展示的地方。 -
创建一个新的
NSOpenPanel
,并设置一些property,使其值运行单选,且只能选择目录。 -
模态地在window中展示
NSOpenPanel
并使用一个闭包来等待结果。 -
如果result表明用户点击了
OK
按钮(实际看到的按钮上,将基于你的本地化带有不同的标签),获取被选择的
URL
并设置指定的ViewController
property。为了临时快速地测试,你会把选择的URL
输入到控制台。现在忽略这行代码上的警告。
运行项目,单击
Select Folder
按钮并选择一个目录。确认选择的目录的
URL
已打印到控制台上。
再次单击按钮来打开对话框,但这次单击
Cancel
按钮。这时就不会打印
URL
。
退出app并删除临时的
print
语句。
既然你能够选择目录,那接下来你的工作就是找出这个目录的内容并展示出来。
上一部分的代码填充了一个名为
selectedFolder
的property。滚动到
ViewController
定义的顶部,并查看
selectedFolder
的property。它使用了一个
didSet
的property观察者,它会在设置值的时候运行代码。
这里的关键代码是调用
contentsOf(folder:)
。滚动到这个方法这里,它当前返回了一个空的数组。用下面的代码来替换其中的内容:
func contentsOf(folder: URL) -> [URL] {
// 1
let fileManager = FileManager.default
// 2
do {
// 3
let contents = try fileManager.contentsOfDirectory(atPath: folder.path)
<span class="hljs-comment">// 4</span>
<span class="hljs-keyword">let</span> urls = contents.<span class="hljs-built_in">map</span> { <span class="hljs-keyword">return</span> folder.appendingPathComponent($<span class="hljs-number">0</span>) }
<span class="hljs-keyword">return</span> urls
} catch {
// 5
return []
}
}
一步步来看代码:
-
和之前一样,获取
FileManager
类的单例。 -
由于
FileManager
的方法可能抛出错误,因此你要使用do...catch
代码块。 -
尝试找到目录
contentsOfDirectory(atPath:)
的内容,并返回内部文件和目录名称的数组。 -
使用
map
处理返回的数组,并将每个名称,用其父目录转换成一个完整的URL
然后返回数组。 -
如果
contentsOfDirectory(atPath:)
抛出错误的话,返回一个空的数组。
selectedFolder
property将
filesList
property设置为被选择的目录的内容,但由于你使用了一个table view来展示内容,你就需要定义如何展示每个项目。
向下拖动到
NSTableViewDataSource
的extension。注意
numberOfRows
早已返回了
filesList
数组中
URLs
的数量。现在滚动到
NSTableViewDelegate
,并注意到
tableView(_:viewFor:row:)
返回的是
nil
。你需要在table中出现任何事之前改变这点。
使用下面的代码来替换这个方法:
func tableView(_ tableView: NSTableView, viewFor
tableColumn: NSTableColumn?, row: Int) -> NSView? {
// 1
let item = filesList[row]
// 2
let fileIcon = NSWorkspace.shared().icon(forFile: item.path)
// 3
if let cell = tableView.make(withIdentifier: "FileCell", owner: nil)
as? NSTableCellView {
// 4
cell.textField?.stringValue = item.lastPathComponent
cell.imageView?.image = fileIcon
return cell
}
// 5
return nil
}
你在代码中做的事有:
-
获取匹配行序号的
URL
。 -
获取这个
URL
的icon。NSWorkspace
是另一个非常有用的单例;这个方法对任何URL
都返回的是Finder的icon。 - 获取这个table中对于这个cell的引用。 FileCell 这个标识符是在 Storyboard 中被设置的。
- 如果cell存在,就设置它的text field来展示文件名,设置它的image view来展示文件的icon。
-
如果没有cell存在,返回
nil
。
运行项目,选择一个目录,你应当看到一个文件和目录的列表出现了 - 欢呼吧!
但点击一个文件或目录现在还没有给出有用的信息,因此继续下一步。
打开Finder并按
Command+I键
来打开一个关于文件信息的窗口:创建日期,修改日期,尺寸,权限等等。全部的这些信息,甚至更多,你都可以通过
FileManager
类来获取。
回到app,仍然在
ViewController.swift
中,查找
tableViewSelectionDidChange
。设置
ViewController
的property:
selectedItem
。
滚动回到顶部,并找到
selectedItem
被定义的地方。和
selectedFolder
一样,
didSet
观察者正在观察这个property的变化。当这个property改变时,如果新的值不为
nil
,观察者就会调用
infoAbout(url:)
。这里将是你检索的信息用来展示的地方。
找到
infoAbout
,当前它会返回一个无聊的静态字符串,用下面的代码来替换它:
func infoAbout(url: URL) -> String {
// 1
let fileManager = FileManager.default
// 2
do {
// 3
let attributes = try fileManager.attributesOfItem(atPath: url.path)
var report: [String] = ["(url.path)", ""]
<span class="hljs-comment">// 4</span>
<span class="hljs-keyword">for</span> (key, value) <span class="hljs-keyword">in</span> attributes {
<span class="hljs-comment">// ignore NSFileExtendedAttributes as it is a messy dictionary</span>
<span class="hljs-keyword">if</span> key.rawValue == <span class="hljs-string">"NSFileExtendedAttributes"</span> { <span class="hljs-keyword">continue</span> }
report.append(<span class="hljs-string">"<span class="hljs-subst">\(key.rawValue)</span>:\t <span class="hljs-subst">\(value)</span>"</span>)
}
<span class="hljs-comment">// 5</span>
<span class="hljs-keyword">return</span> report.joined(separator: <span class="hljs-string">"\n"</span>)
} catch {
// 6
return "No information available for (url.path)"
}
}
这里发生了一些不同的事,因此我们一次看一个:
按照惯例,获取一个FileManager
单例的引用。-
使用
do...catch
来捕获任何的错误。 -
使用
FileManager
类的
attributesOfItem(atPath:)
方法来获取文件的信息。如果成功的话,它就会返回一个[FileAttributeKey: Any]
类型的字典,FileAttributeKeys
是一个带有字符串rawValue
的结构体的成员。 -
将key的名称和value的值组装成一个tab分隔符字符创的数组。但会忽略掉
NSFileExtendedAttributes
键,因为它包含了一个复杂的但并不是确实有用的字段。 - 将整个数组组装成一个单独的字符串,返回它。
-
如果
try
语句抛出了错误,就返回一个默认的报告。
Build并再次运行,就像之前一样选择一个目录,然后点击列表中的任一文件或目录:
你现在已经获得了很多关于这个文件或目录有用的信息。但还有更多你可以做的事!
这个app正在变得更棒,但仍然缺少一些事情:
- 点击 Show Invisible Files 不会改变任何事。
- 双击一个目录应当进入它的内容。
- Move Up 按钮需要向上移动目录的层次结构。
- Save Info 应当可以把被选择文件的详情到一个文件中。
接下来你就会出来这些事情。
处理不可见的文件
在Unix系统中,名称以一个句点开头文件和目录将会不可见。你会添加代码来处理这个情况。
前往
contentsOf(folder:)
,并用下列代码替换
map
这行:
let urls = contents
.filter { return showInvisibles ? true : $0.characters.first != "." }
.map { return folder.appendingPathComponent($0) }
上面的代码添加了一个
filter
,当
showInvisibles
的property不为
true
时,就会拒绝隐藏的项目;否则
filter
会返回所有的项目,包括因此的。
找到
ViewController
中的
toggleShowInvisibles
方法,并插入下列代码到函数中:
// 1
showInvisibles = (sender.state == NSOnState)
// 2
if let selectedFolder = selectedFolder {
filesList = contentsOf(folder: selectedFolder)
selectedItem = nil
tableView.reloadData()
}
这些代码完成了:
-
根据sender的状态设置
showInvisibles
property。由于sender是NSButton
,所以它的state不是NSOnState
就是NSOffState
。由于它是一个checkbox按钮,NSOnState
就表示勾选。 -
如果当前有
selectedFolder
,就重新生成filesList
并更新UI。
运行项目,选择一个目录,并勾选及取消 Show Invisible Files 按钮。依赖于你正在观察的目录,你有可能会在当 Show Invisible Files 被勾选时,看到以一个圆点的文件。
处理双击目录的情况
在storyboard中,table view已被分配了一个叫做
tableViewDoubleClicked
的
双击动作
。找到
tableViewDoubleClicked
并用以下代码替换:
@IBAction func tableViewDoubleClicked(_ sender: Any) {
// 1
if tableView.selectedRow < 0 { return }
// 2
let selectedItem = filesList[tableView.selectedRow]
// 3
if selectedItem.hasDirectoryPath {
selectedFolder = selectedItem
}
}
一步一步地回顾上面的代码:
-
检查双击是否发生在一个已填充的行上。如果点击在table的空白的部分,就会将
tableView的
selectedRow设置为-1。 -
从
filesList
中获取匹配的URL
。 -
URL
是一个目录,设置ViewController的
selectedFolder
property。就像当你使用Select Folder按钮来选择一个目录一样,设置这个property就会触发property的观察者来读取目录的内容,并更新UI。如果URL
不是目录,则不执行任何事。
运行项目,选择一个包含其它目录的目录,然后双击这个列表中的目录进入它。
处理Move Up按钮
一旦你实现了双击进入目录,接下来显然就应该是移回到“树”上了。
找到空的
moveUpClicked
方法并用下列的代码来替换它:
@IBAction func moveUpClicked( sender: Any) {
if selectedFolder?.path == "/" { return }
selectedFolder = selectedFolder?.deletingLastPathComponent()
}
首先,检查
selectedFolder
是否是根目录。如果是的话,你就不能做任何事,如果不是的话,就使用
URL
的方法来去掉URL的最后一部分。编辑
selectedFolder
就会像之前一样触发更新。
Build并再次运行;确认你可以选择一个目录,双击并移动到一个子目录上,并点击 Move Up 来回到目录的层级中。只要你不在根目录上,你甚至可以在双击目录之前向上移动。
didSet
)非常得有用。用来更新界面的所有代码都在一个观察者中,因此无论是UI元素或方法来改变被观察的property,更新就发生了,无需做任何其它的事。Sweet!
保存信息
保存数据主要有两个办法:用户发起的保存以及自动保存。对于用户发起的保存,你的app应答让用户选择一个保存数据的位置,来将数据写入到这个位置中。对于自动保存,app自己会决定保存数据的位置。
在这一部分,你会处理当用户点击 Save Info 按钮来发起保存的case。
你已使用
NSOpenPanel
来提示用户选择一个目录。这次,你会使用
NSSavePanel
。
NSOpenPanel
和
NSSavePanel
都是
NSPanel
的子类,因此它们有很多共同的地方。
使用下列代码来替换空方法
saveInfoClicked
:
@IBAction func saveInfoClicked( sender: Any) {
// 1
guard let window = view.window else { return }
guard let selectedItem = selectedItem else { return }
// 2
let panel = NSSavePanel()
// 3
panel.directoryURL = FileManager.default.homeDirectoryForCurrentUser
// 4
panel.nameFieldStringValue = selectedItem
.deletingPathExtension()
.appendingPathExtension("fs.txt")
.lastPathComponent
// 5
panel.beginSheetModal(for: window) { (result) in
if result == NSFileHandlingPanelOKButton,
let url = panel.url {
// 6
do {
let infoAsText = self.infoAbout(url: selectedItem)
try infoAsText.write(to: url, atomically: true, encoding: .utf8)
} catch {
self.showErrorDialogIn(window: window,
title: "Unable to save file",
message: error.localizedDescription)
}
}
}
}
依次来看每个被标号的注释:
-
确认每样你需要的都存在:一个用来展示面板的窗口,已经你将要保存的
URL
。 -
创建一个
NSSavePanel
。 -
设置
directoryURL
property,它会指定展示在面板中的发起目录。 -
设置
nameFieldStringValue
property,来为文件设置一个默认的名称。
- 展示面板,并在一个闭包中来等待用户完成。
-
如果用户选择了一个有效的数据文件的路径(一个有效的
URL
)并点击 OK 按钮,获取文件信息并将它写入到被选择的文件中。如果出现错误,就展示一个对话框。注意如果用户在保持的对话框中点击 Cancel ,你只需忽略操作。
write(to:atomically:encoding)
是一个字符串的方法,它会将字符串写入到被提供的
URL
中。
atomically
选项意味着字符串将会写入到一个临时的文件中,并进行重命名,确保你不会在一个坏掉的文件上结束 - 即使系统在写入过程中崩溃了。在这个文件中,文本的编码方式被设置为
UTF8
,这是一个通用的标准。
运行项目,从列表中选择一个文件或目录,点击 Save Info 。选择一个待保存的位置,并点击 Save 。你会以一个文本文件结束,看起来就像下面这样:
NSSavePanel
的一个很好的特性,就是如果你尝试覆盖一个早已存在的文件,你的app会自动展示一个确认对话框,询问你是否想要替换文件。
这已经闭环了app的特性列表,但这里还有一个很好的特性可以添加:当app重新启动时,记录被选择的目录和项目,将最后被选择的目录再展示出来。
通常,我会把app状态的数据保存到
UserDefaults
中,它会为你自动保存到
Preferences
目录中。但不允许你对文件系统做任何事。相反,你会保存这个数据到
Application Support
目录中的专用的app目录中。
滚动到 ViewController.swift 的尾部,你会看到一个extension,专门用来保存和恢复用户的选择。
我已经提供了用来进行实际的读写的方法。写入会使用和保存info file相同的
write(to:atomically:encoding)
方法。读取则使用一个
String
构造器,从
URL
创建一个
String
。
一个非常有趣的事是如何决定去哪里保存数据。你会在
urlForDataStorage
来做这件事,它现在返回的是
nil
。
使用下列的代码替换
urlForDataStorage
:
private func urlForDataStorage() -> URL? {
// 1
let fileManager = FileManager.default
// 2
guard let folder = fileManager.urls(for: .applicationSupportDirectory,
in: .userDomainMask).first else {
return nil
}
// 3
let appFolder = folder.appendingPathComponent("FileSpy")
var isDirectory: ObjCBool = false
let folderExists = fileManager.fileExists(atPath: appFolder.path,
isDirectory: &isDirectory)
if !folderExists || !isDirectory.boolValue {
do {
// 4
try fileManager.createDirectory(at: appFolder,
withIntermediateDirectories: true,
attributes: nil)
} catch {
return nil
}
}
// 5
let dataFileUrl = appFolder.appendingPathComponent("StoredState.txt")
return dataFileUrl
}
这些代码都做了些什么?
-
又是你的老朋友
FileManager
。:] -
FileManager
有一个方法,可以返回对于指定用途的恰当的URL
的列表。在这个case中,你会在用户当前的目录下查找applicationSupportDirectory
。这个基本上是不大可能返回超过一个的URL的,并且你只想获取第一个元素。你可以用不同的参数来调用这个方法,来找到更多不同的目录。 -
就像你在playground中做的一样,添加一个路径成分来创建一个app指定目录的
URL
,并检查它是否存在。 -
如果这个目录不存在,尝试创建它,以及任何由路径决定的中间目录。如果创建失败的话,就返回
nil
。 -
添加另一个路径成分,来创建数据文件的完整的
URL
,并返回它。
.applicationSupportDirectory
是
FileManager.SearchPathDirectory.applicationSupportDirectory
的一个简写的方式。
.userDomainMask
则指向了
FileManager.SearchPathDomainMask.userDomainMask
。虽然简写的方式会更容易输入和阅读,但完整的方式会更有用于了解这些来自于哪里,因此如果需要的话,你可以在文档中找到他们。
运行项目,选择一个目录,然后点击一个目录或文件。使用 Quit 菜单项或 Command-Q 来关闭app。不要通过 Xcode 来退出,否则生命循环的方法就不会触发保存的动作。再次运行app,可以注意到它自动打开了你上次退出时正在查看的文件或目录。
你可以在 这里 下载最后的示例项目。
在
FileManager
类的教程中:
-
你学到了
URL
如何表示本地的文件和目录,以及展示关于文件或目录的有用的属性。 -
你学到了如何添加和删除一个
URL
中的 路径成分 。 -
你探究了
FileManager
类,如propertyhomeDirectoryForCurrentUser
,applicationSupportDirectory
,甚至attributesOfItem
,它包含了一个文件或目录的详细信息。 - 你学到了如何保持一个文件的相关信息。
- 你学到了如何查看一个文件或目录是否存在。
有关更多的信息,请访问
苹果FileManager API参考文档
,你可以找到更多关于
FileManager
类中可用的方法。
现在,你已经可以开始在你自己的app中应用关于文件和目录的知识了。