一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

zhihaozhang發表於2018-03-09

軟體介紹

AVHider (oh NO) FileHider是一款將你的資料夾或檔案隱藏起來的效率軟體,適用於macOS X 10.10及以後的macOS版本百度網盤下載地址,歡迎大家試用,並提出改進建議!有開發能力的朋友也可以去Github將專案fork後contribute您的code。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

Specially thanks to unfamous Designer Joseph, who designed the exquisite logo for this Application!

軟體的使用也非常簡單,基本可以實現檔案/資料夾的可見/不可見一鍵切換,錄了一個gif動畫。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

軟體使用demo

開發初衷

開發這款軟體的初衷將xxx.mp4/xxx.avi/xxx.mkv在白天藏起來,免得被其他人發現。 在Apple store上發現了一款類似的軟體,售價163元,而且賣的不錯。作為一個工程師,我是不願意掏這份冤枉錢的,因為我覺得這東西一天內可以搞出來,於是就花了一晚上做出了功能類似的軟體FileHider(認真臉)。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

在Mac App Store定價為163元的Secret Folder

與Secret Folder不同的地方在於它的TableView中有兩列,而我認為顯示當前檔案可見/不可見的列跟右邊的NSSegmentedControl資訊重複了,因此我就除去了該列。

還有一點不同是Secret Folder設定了Require Password這個選項,這個我覺得可以不加,因為如果一個人在使用者不在的時候能夠進入到系統中,那麼user的密碼也是多餘的,FileHider的目的是對有機會看到你電腦螢幕卻沒有機會操作你電腦的人隱藏檔案。

起初我還想在使用者切換檔案可見性的時候傳送一個Notification,但是覺得過度設計了,因為這些通知如果不手動刪除,將會在通知中心保留下來,這顯然會增加別人知道有檔案隱藏起來的可能性。

開發過程

介面部分

介面部分完全模仿了Secret Folder的佈局,是一個single-Page的應用,依然採用了StoryBoard構造介面。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

專案storyboard截圖

左右分為垂直的兩欄,使用了NSSplitView,並調整左右兩欄的大小比例,左邊顯示檔案列表和對列表的增加/刪除按鈕;右邊是檔案的詳細資訊與檔案隱藏/可見之間切換的NSSegmentedControl。對各個元件定好佈局,確保在視窗resize後依然保持著相對較好的樣式。

TableView部分

檔案列表是放到TableView中進行顯示的,它也是本應用的核心部分。預設的TableView Cell高度只有17px,每個Cell要塞進去一個檔案縮圖icon和檔名,顯然過於小了,因此需要定製Cell。在本專案中,我將Cell設定為了30px,其中檔案縮圖為24 X 24 px,我覺得大小是比較合適的。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

一個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的一半,因為它只需要接收外部拖拽進來的檔案,並獲取檔案路徑,將檔案新增到隱藏檔案列表中即可。

一款將你的資料夾或檔案隱藏起來的效率軟體AVHider開發記錄

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,讓我有了靈感和動力去做一個類似的軟體。

參考

  1. Github
  2. stackoverflow
  3. FileManager Class Tutorial for macOS
  4. APPLE STORE:Secret Folder

相關文章