一款將你的文件夾或文件隱藏起來的效率軟件AVHider開發記錄

軟件介紹

AVHider (oh NO) FileHider是一款將你的文件夾或文件隱藏起來的效率軟件,適用於macOS X 10.10及之後的macOS版本百度網盤下載地址,歡迎你們試用,並提出改進建議!有開發能力的朋友也能夠去Github將項目fork後contribute您的code。git

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

軟件的使用也很是簡單,基本能夠實現文件/文件夾的可見/不可見一鍵切換,錄了一個gif動畫。macos

軟件使用demoswift

開發初衷

開發這款軟件的初衷將xxx.mp4/xxx.avi/xxx.mkv在白天藏起來,省得被其餘人發現。 在Apple store上發現了一款相似的軟件,售價163元,並且賣的不錯。做爲一個工程師,我是不肯意掏這份冤枉錢的,由於我以爲這東西一天內能夠搞出來,因而就花了一夜作出了功能相似的軟件FileHider(認真臉)。數組

在Mac App Store訂價爲163元的Secret Folderapp

與Secret Folder不一樣的地方在於它的TableView中有兩列,而我認爲顯示當前文件可見/不可見的列跟右邊的NSSegmentedControl信息重複了,所以我就除去了該列。ide

還有一點不一樣是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,讓我有了靈感和動力去作一個相似的軟件。

參考

  1. Github
  2. stackoverflow
  3. FileManager Class Tutorial for macOS
  4. APPLE STORE:Secret Folder
相關文章
相關標籤/搜索