软件介绍
AVHider (oh NO) FileHider 是一款将你的文件夹或文件隐藏起来的效率软件, 适用于 macOS X 10.10 及以后的 macOS 版本百度网盘下载地址, 欢迎大家试用, 并提出改进建议! 有开发能力的朋友也可以去 Github 将项目 fork 后 contribute 您的 code
Specially thanks to unfamous Designer Joseph, who designed the exquisite logo for this Application!
软件的使用也非常简单, 基本可以实现文件 / 文件夹的可见 / 不可见一键切换, 录了一个 gif 动画
软件使用 demo
开发初衷
开发这款软件的初衷是将 xxx.mp4/xxx.avi/xxx.mkv 在白天藏起来, 免得被其他人发现 在 Apple store 上发现了一款类似的软件, 售价 163 元, 而且卖的不错作为一个工程师, 我是不愿意掏这份冤枉钱的, 因为我觉得这东西一天内可以搞出来, 于是就花了一晚上做出了功能类似的软件 FileHider(认真脸)
在 Mac App Store 定价为 163 元的 Secret Folder
与 Secret Folder 不同的地方在于它的 TableView 中有两列, 而我认为显示当前文件可见 / 不可见的列跟右边的 NSSegmentedControl 信息重复了, 因此我就除去了该列
还有一点不同是 Secret Folder 设置了 Require Password 这个选项, 这个我觉得可以不加, 因为如果一个人在用户不在的时候能够进入到系统中, 那么 user 的密码也是多余的, FileHider 的目的是对有机会看到你电脑屏幕却没有机会操作你电脑的人隐藏文件
起初我还想在用户切换文件可见性的时候发送一个 Notification, 但是觉得过度设计了, 因为这些通知如果不手动删除, 将会在通知中心保留下来, 这显然会增加别人知道有文件隐藏起来的可能性
开发过程
界面部分
界面部分完全模仿了 Secret Folder 的布局, 是一个 single-Page 的应用, 依然采用了 StoryBoard 构造界面
项目 storyboard 截图
左右分为垂直的两栏, 使用了 NSSplitView, 并调整左右两栏的大小比例, 左边显示文件列表和对列表的增加 / 删除按钮; 右边是文件的详细信息与文件隐藏 / 可见之间切换的 NSSegmentedControl 对各个组件定好布局, 确保在窗口 resize 后依然保持着相对较好的样式
TableView 部分
文件列表是放到 TableView 中进行显示的, 它也是本应用的核心部分默认的 TableView Cell 高度只有 17px, 每个 Cell 要塞进去一个文件缩略图 icon 和文件名, 显然过于小了, 因此需要定制 Cell 在本项目中, 我将 Cell 设置为了 30px, 其中文件缩略图为 24 X 24 px, 我觉得大小是比较合适的
一个 TableView 要想成功显示需要知道两件事:**1. 显示几行 2. 每行显示什么 ** 和其他应用一样, 驱动这个 TableView 的是一个数组, filesList : [URL] 请注意这里是一个 URL 的数组, 文件路径的 URL 都是定义为 file://+ 文件路径这种格式的 URL 在 Swift 中有相当多的方法, 方便拿到文件名路径名根据完整路径拿到对应文件的缩略图文件的 detail 信息等等具体的使用可以参考官方 API 文档
数据持久化
对于本应用, 用户对某个文件的操作并不是一次性隐藏就完事了的, 它需要保留恢复为可见的权力, 显然让用户记住哪些文件被隐藏甚至隐藏在哪个路径下是很不现实的, 因此需要数据持久化, 保证用户下次打开应用的时候可以知道哪些文件是有过隐藏历史的因为有过前科的文件很可能需要二次隐藏
数据持久化的选择很多, 最典型的有比较重的 core data 和比较轻量级的 userDefaults 由于文件列表的路径通常不会很长, 因此我选用了相对轻量级的 userDefaults
在使用 userDefaults 存储前面提到的 URL 类型的 filesList 数组的时候, 我发现会报一个错误, Attempt to set a non-property-list object as an NSUserDefaults 后面在网上发现了一些 solution, 主要的原因是 NSUserDefaults 只支持 NSArray, NSDictionary, NSString, NSData, NSNumber, 和 NSDate 的数据类型, 对于 URL 这种类型, 网上大多数的 solution 都是建议将数组编码为 NSData, 然后进行存储我考虑到 URL 和 String 之间的互转比较方便, 因此我将其转换为了 string 类型的数组进行存储
- // String -> URL
- override func viewDidLoad() {
- let defaults = UserDefaults.standard
- if let filesListFromUserDefaults = defaults.array(forKey: "filesPath"){
- var tmpFilePath : [String] = filesListFromUserDefaults as! [String]
- for str in tmpFilePath{
- self.filesList.append(URL(string: str)!)
- }
- }
- }
- // URL -> String
- override func viewWillDisappear() {
- let defaults = UserDefaults.standard
- var array : [String] = []
- for url in filesList{
- array.append(url.absoluteString)
- }
- defaults.set(array, forKey: "filesPath")
- }
URL 与 String 数组之间的互转
转换的时机很重要, 这会提高应用的性能 String->URL 这个方向仅在应用打开时, view 加载完毕后进行; 而 URL->String 这个方向是在应用关闭后, view 消失的时候触发一次
文件列表的增加
文件的增加目前是靠比较简单的 NSOpenPanel 来实现的, 显然这很不 Mac, 后面需要做的是 drag-and-drop, 一种更为优雅的 solution
- @IBAction func selectFile(_ sender: Any) {
- let openPanel = NSOpenPanel()
- openPanel.message = "Please select file to Hide"
- openPanel.canChooseDirectories = true
- // openPanel.allowsMultipleSelection = true
- openPanel.beginSheetModal(for: view.window!, completionHandler: {(result) in
- if result == NSModalResponseOK{
- self.selectedFolder = openPanel.url!
- }
- })
- }
文件列表的删除
文件列表的删除依然是对上文提到的 filesList 进行操作, 通过 tableviewDelegate 中的 tableViewSelectionDidChange 方法得到需要删除的元素 index 需要注意的是, 需要增加判断, 确保当前有元素被选中 (如果没有元素被选中, index 值会是 - 1, 这很可能引起应用的崩溃)
无论是文件列表的增加还是删除, 都需要调用 tableview.reloadData() 方法对视图进行更新
隐藏和非隐藏的实现
Unix 系统中实现一个文件隐藏的方法很多, 甚至可以给该文件进行加密我能想到的最简单的方法是在原文件前面加一个., 并用 mv xxx.mp4 .xxx.mp4 将该文件就地在原路径下进行隐藏这也符合了本软件的设计初衷, 将文件从有机会从你电脑边路过, 但却没有机会真正操作你电脑的人隐藏
模拟 console 执行命令, 是通过 Process() 来完成的这里有一些坑, 不幸的被我全踩了
第一个坑是普通文件和文件夹的 URL 是不同的, 文件夹是以 / 结尾的, 而普通文件则不是, 为了得到 path 和文件名, 我调用了 String.components(separatedBy: /) 方法, 那么文件夹的文件名就存在了方法得到数组的倒数第二项中; 而其他普通文件的文件名存在了数组的倒数第一项中
第二个是当用户不是第一次打开应用时, 执行 mv 的参数设置方式需要分四种情况讨论, 这也是前面为了应用的效率, 不及时 update fileList 挖下的坑果然凡事都是有两面性的~
Drag & drop in FileHider
FileHider 只需要实现 Drag & drop 的一半, 因为它只需要接收外部拖拽进来的文件, 并获取文件路径, 将文件添加到隐藏文件列表中即可
Drag & drop in FileHider
通过研究 Drag & drop 的 API 文档发现它的设计和 D3JS 的设计有类似之处, 都提供了对动作完整生命周期进行控制的钩子但是似乎 macOS 中提供了更多的钩子, 比如监控拖拽东西进来没有释放便移出去的情况 (draggingExited)
- override func draggingExited(_ sender: NSDraggingInfo?) {
- isReceivingDrag = false
- }
相对应的, 有刚进来时的钩子 (draggingEntered)
- override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
- }
对于 FileHider 来说, 我们需要指定 TableView 为 Drag & drop 事件的终点, 并指定可接受的文件类型, 并在 drag 结束后, 获取文件的完整路径, 添加到 tableView 的 datasource 对应的数组中
具体实现如下: 首先生成 DragDestinationView 类, 继承自 NSView 子类由于 NSView 天然地实现了 NSDraggingDestination 协议, 因此直接 override 相应的方法即可然后在 stroyboard 页面指定 Drag & drop 事件的终点对应的 NSView 为 DragDestinationView
- protocol FileDragDelegate : class{
- func didFinishDrag(_ filePath:String)
- }
- class DragDestinationView: NSView {
- weak var delegate: FileDragDelegate?
- override func awakeFromNib() {
- super.awakeFromNib()
- // 注册可接受文件类型
- self.register(forDraggedTypes: [NSFilenamesPboardType])
- }
- // 文件进入 NSView
- override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
- let sourceDragMask = sender.draggingSourceOperationMask()
- let pboard = sender.draggingPasteboard()
- let dragTypes = pboard.types! as NSArray
- if dragTypes.contains(NSFilenamesPboardType) {
- if sourceDragMask.contains([.link]) {
- return .link
- }
- if sourceDragMask.contains([.copy]) {
- return .copy
- }
- }
- return .generic
- }
- // 获取数据, 触发代理事件的方法
- override func performDragOperation(_ sender: NSDraggingInfo?)-> Bool {
- let pboard = sender?.draggingPasteboard()
- let dragTypes = pboard!.types! as NSArray
- if dragTypes.contains(NSFilenamesPboardType) {
- let files = (pboard?.propertyList(forType: NSFilenamesPboardType))! as! Array<String>
- let numberOfFiles = files.count
- if numberOfFiles > 0 {
- let filePath = files[0] as String
- if let delegate = self.delegate {
- NSLog("filePath \(filePath)")
- delegate.didFinishDrag(filePath)
- }
- }
- }
- return true
- }
- }
在主 ViewController 中生成该 NSView 对应的 Outlet, 并实现 FileDragDelegate 协议, 实现协议中的方法, 即 Drag & drop 事件完成后需执行的逻辑即可
- extension ViewController: FileDragDelegate {
- func didFinishDrag(_ filePath:String) {
- let url = NSURL(fileURLWithPath: filePath)
- filesList.append(url as URL)
- print(url)
- tableview.reloadData()
- }
- }
致谢结束语
首先感谢非著名设计师 Joseph 给我提供的精美 logo, 感谢 Secret Folder, 让我有了灵感和动力去做一个类似的软件
参考
- Github
- stackoverflow
- FileManager Class Tutorial for macOS
- APPLE STORE:Secret Folder
来源: https://juejin.im/post/5aa23f8d6fb9a028c06a6c74