Programmieren mit Swift - Für macOS und iOS
Programmieren mit Swift - Für macOS und iOS
Ordner durchsuchen und löschen mit Swift

Im DerivedData-Verzeichnis eines Xcode-Projektes legt die Entwicklungsumgebung verschiedenen Dateien ab, die im Laufe der Entwicklung erzeugt werden. So finden wir dort unter anderem bereits kompilierte Dateien, die, sofern ihr Quellcode nicht verändert wurde, bei einem erneuten Programmstart nicht noch einmal erstellt werden müssen. So bleibt einem fleißigen Entwickler eine Menge an Wartezeit erspart.
Es ist jedoch nicht ungewöhnlich, wenn der DerivedData Verzeichnis bei fortschreitender Entwicklung immer größer wird und somit sehr viel Platz auf der Festplatte für sich beansprucht. Oftmals ist dieser Ordner das mit Abstand größte Verzeichnis im ganzen Projekt. Allerdings enthält das DerivedData Verzeichnis keine Dateien, die nicht durch eine erneute Kompilierung das ganzen Projektes wiederhergestellt werden können, so dass man dieses Verzeichnis auch problemlos löschen oder leeren könnte, ohne Informationen zu verlieren. Die Clean Funktion aus dem Xcode-Menü übernimmt genau diese Aufgabe und oftmals ist genau das nötig, wenn das Projekt von Xcode nicht mehr so erstellt wird, wie man das gerne hätte. Die nächste Kompilierung muss dann wieder für alle Dateien durchgeführt werden und dauert daher wieder ein wenig länger.
Besonders wenn man Sicherheitskopien der eigenen Projekte anlegen will, sollte man die DerivedData Ordner von der Sicherung ausschließen, denn der von ihnen belegte Platz kann sinnvoller verwendet werden. Die Verzeichnisse nach der Arbeit mit Clean zu bereinigen ist jedoch ein Vorgang, der von Entwicklern sehr selten ausgeführt wird und so hat man jetzt die Wahl, die DerivedData Ordner eben doch mit zu sichern, oder alle Projekte manuell zu bereinigen. Wie praktisch wäre ein Programm, das diese Bereinigung für uns erledigen würde?
Auf den nächsten Seiten wollen wir so ein Projekt umsetzten und das gibt uns gleichzeitig Gelegenheit, eine Funktion zu entwickeln, die vielleicht noch öfters in Programmen zum Einsatz kommen könnte: Die Ordner auf einem Datenträger sollen rekursiv durchsucht werden! Das klingt vielleicht im ersten Moment ein wenig kompliziert, doch zu unserer Entlastung gibt es in den Apple-Frameworks schon Klassen, die uns einen Großteil der Arbeit abnehmen. Ziel ist es, einer Ordner auszuwählen, zum Beispiel das Stammverzeichnis aller Projekte, und unser Programm sucht innerhalb dieses Ordners und seiner Unterordner alle Verzeichnisse, die mit DerivedData enden und entfernt diese.

Einen Ordner auswählen mit dem NSOpenPanel

Um den Aufwand der Programmierung gering zu halten, werden wir versuchen, möglichst viele der bereits vorhandenen Klassen aus den Apple-Frameworks zu verwenden und der erste Schritt ist dabei der Einsatz eines NSOpenPanel. Diese Klasse repräsentiert das standardisierte Fenster von OS X, welches von nahezu allen Programmen für den Datei-Öffnen Dialog verwendet wird. Für unsere Zwecke ist das Fenster ebenfalls gut geeignet, allerdings müssen wir noch einige der Eigenschaften anpassen. Durch die Konfiguration von canChooseDirectories = true und canChooseFiles = false kann der Anwender im Fenster ausschließlich Verzeichnisse auswählten und durch die Einstellung allowsMultipleSelection = false kann nur genau ein Ordner gewählt werden. Neue Verzeichnisse sollen nicht angelegt werden können und so setzten wir canCreateDirectories auf false. Durch den Aufruf von runModal wird das Fenster angezeigt. Die Auswahl ist gültig und wurde auch nicht vom Benutzer abgebrochen, wenn der Rückgabewert der runModal Methode NSOKButton entspricht.
Was nun folgt ist die eigentliche Programmlogik. Zunächst holen wir uns rekursiv sämtliche Ordner-Pfade unterhalb des ausgewählten Stammverzeichnissen und überprüfen anschließend, welche dieser Pfade mit DerivedData enden. Sämtliche Verzeichnisse, die dieser Vorgabe entsprechen, sollen dann gelöscht werden. Was die Methoden im Einzelnen tun, ist Thema der folgenden Seiten.
@IBAction func cleanUpButtonClicked(sender: AnyObject)
{
    var openPanel = NSOpenPanel()
    openPanel.canChooseDirectories = true
    openPanel.canChooseFiles = false
    openPanel.canCreateDirectories = false
    openPanel.allowsMultipleSelection = false
   
    if openPanel.runModal() == NSOKButton
    {
        // Das ausgewählte Stammverzeichniss
        var rootPath = openPanel.URLs[0] as NSURL
        // Alle Ordner-Pfade unterhalb des Stammverzeichnisses holen
        var enumerator = getFolderEnumerator(rootPath)
        // Alle Ordner die mit DerivedData enden ermitteln
        var folders = getDerivedDataFolders(enumerator)
        // Wenn es solche Ordner gibt, diese löschen
        if folders.count > 0
        {
            deleteFolders(folders)
        }
    }
}
Im Beispiel wird die Auswahl des Projektstammverzeichnisses durch eine Schaltfläche auf der grafischen Oberfläche ausgelöst und die Methode cleanUpButtonClicked ist daher über eine IBAction verbunden. Sollten Sie eine andere Vorgehensweise bevorzugen, können sie den Code ohne weitere Einschränkungen Ihren Bedürfnissen anpassen.

NSFileManager und NSDirectoryEnumerator

Sämtliche Dateien, Ordner und deren Unterordner von einem vorgegebenen Verzeichnis einzulesen, ist eigentlich eine sehr einfache Aufgabe. Alles, was wir benötigen, ist ein korrekt konfigurierter NSFileManger. Über den Parameter Keys geben wir an, dass zusätzlich zu den Dateien auch die Ordnernamen mit eingelesen werden sollen. Weil die enumerateAtUrl-Methode jedoch ein Array erwartet, müssen wir NSURLIsDirectoryKey in eckigen Klammern einkapseln.
Neben dem zu durchsuchendem Verzeichnis, als ein Objekt vom Typ NSURL, benötigen die Methoden zusätzlich einen Errorhandler. Dieser Block von Programmcode, in Swift als Closure bezeichnet, wird aufgerufen, wenn es bei der Ermittlung der Verzeichnisse zu Fehlern kommt. Das gibt uns Gelegenheit, die Fehlermeldung und den Pfad auszugeben. Einen Closure kann man sich als Variable vorstellen, die statt einem Wert ausführbaren Code enthält. Der Rückgabewert des Closure, hier true, bestimmt, dass nach einem Fehler trotzdem weitere Dateien und Ordner gesucht werden sollen. Das Ergebnis dieser Analyse erhalten wir als Instanz einer Klasse vom Typ NSDirectoryEnumerator. Sie enthält alle eingelesenen Dateien und Verzeichnisse und ist der Rückgabewert der Methode.
func getFolderEnumerator(directoryUrl:NSURL) -> NSDirectoryEnumerator?
{
    var fileManger = NSFileManager()
    var keys = [NSURLIsDirectoryKey]
       
    var handler = {
        (url:NSURL!,error:NSError!) -> Bool in
        println(error.localizedDescription)
        println(url.absoluteString)
        return true
    }
       
    var enumarator = fileManger.enumeratorAtURL(
        directoryUrl, includingPropertiesForKeys:
        keys, options: NSDirectoryEnumerationOptions(),
        errorHandler:handler)
       
    return enumarator
}

Ordnernamen analysieren

Als nächstes benötigen wir ein Array aus NSURL-Objekten, die dem von uns vorgegebenen Format entsprechen. Also ausschließlich Ordner, die mit DerivedData enden. Mit enumerator.nextObject() und einer Schleife können wir leicht über alle eingelesenen Daten iterieren und ein NSFileManager hilft uns bei der Auswertung. Die Methode fileExistsAtPath erhält einen Zeiger auf einen ObjCBool-Typen, über den wir anschließend prüfen können, ob es sich um ein Verzeichnis handelt oder nicht. Außerdem erhalten wir mit dem Rückgabewert vom Typ Bool der genannten Methode Auskunft darüber, ob die überprüfte Datei oder der Ordner noch existieren. Dass sollte eigentlich immer der Fall sein, haben wir die Daten doch erst kurz zuvor eingelesen. Trotzdem kann eine zusätzliche Kontrolle nicht schaden.
Wurde ein Verzeichnis gefunden, muss nun überprüft werden, ob dieses mit dem Text DerivedData endet. Die Methode hasSuffix der Swift String-Klasse macht so einen Test sehr einfach. Um Probleme mit der Groß- und Kleinschreibung zu vermeiden, wird der komplette Pfad und der zu prüfende Begriff mit lowercaseString zunächst in kleine Buchstaben umgewandelt. Das funktioniert auch bei Literalen, bei Zeichenketten, die direkt in den Programmcode geschrieben wurden. Wenn der Verzeichnisname den Anforderungen entspricht, wird er dem Array folders hinzugefügt. Erwähnenswert ist, dass der Ordnername der Eigenschaft url.absoluteString mit einem / abschließt, der gleiche Pfad bei der Eigenschaft url.path hingegen nicht.
func getDerivedDataFolders(enumerator:NSDirectoryEnumerator) ->Array<NSURL>
{
    var folders = NSURL[]()
       
    while let url = enumerator.nextObject() as? NSURL
    {
        var error:NSError?
        var isDirectory: ObjCBool = ObjCBool(0)
        var exists = NSFileManager.defaultManager().fileExistsAtPath(url.path, isDirectory: &isDirectory)
           
        // Gibt es diesen Pfad?
        if exists == true && isDirectory == true
        {
            if url.absoluteString.lowercaseString.hasSuffix("DerivedData/".lowercaseString)
            {
                println("Gefunden: \(url.path)")
                folders.append(url)
            }
        }
    }
    return folders
}

Ordner löschen

Die gefundenen Ordner zu löschen ist ebenfalls eine Aufgabe, die von einem NSFileManager erledigt werden kann. Die Methode removeItemAtPath erwartet bei jedem Aufruf ein NSURL-Objekt, von denen wir inzwischen ein ganzes Array haben. Ein Zeiger auf ein NSError-Objekt gibt uns Auskunft darüber, ob es beim Löschen der Verzeichnisse zu Fehlern gekommen ist.
func getDerivedDataFolders(enumerator:NSDirectoryEnumerator) ->Array<NSURL>
{
    var folders =  [NSURL]()
       
    while let url = enumerator.nextObject() as? NSURL
    {
        var error:NSError?
        var isDirectory = ObjCBool(false)
        var exists =
        NSFileManager.defaultManager().fileExistsAtPath(
            url.path!, isDirectory: &isDirectory)
           
        // Gibt es diesen Pfad?
        if exists == true && isDirectory.boolValue == true
        {
            if (url.absoluteString?.lowercaseString.hasSuffix(
                "DerivedData/".lowercaseString) != nil)
            {
                println("Gefunden: \(url.path)")
                folders.append(url)
            }
        }
    }
    return folders
}

Wissenswertes über den NSDirectoryEnumerator

Wenn Sie sich die zwei folgenden Anweisungen aus der cleanUpButtonClicked-Methoden ansehen, werden Sie sich vielleicht fragen, ob der Programmablauf hier optimal ist. Möglicherweise wäre es sinnvoll, zunächst zu prüfen, ob der NSDirectoryEnumerator überhaupt Informationen eingesammelt hat, bevor diese an die nächste Methode weiter gereicht werden.
// Alle Ordner unterhalb des Stammverzeichnisses holen
var enumerator = getFolderEnumerator(rootPath)
// Alle Ordner die mit DerivedData enden ermitteln
var folders = getDerivedDataFolders(enumerator)
Möchte man die Anzahl der Objekt eines NSDirectoryEnumerator, oder der Elternklasse NSEnumerator ermitteln, so ist das über die Eigenschaft allObjects ohne Weiteres möglich. Man erhält ein Array, dessen Größe leicht auszulesen ist. Allerdings würde der Aufruf von allObjects den Enumerator „verbrauchen“, „exhausted“ in englischen Dokumentationen, und ein folgender Aufruf von nextObject würde nil zurück geben. Um alle hinterlegten Objekte zurückzugeben muss der Enumerator seine komplette Auflistung durchgehen, bis er an dessen Ende angekommen ist. Es gibt jedoch keine Möglichkeit einen Schritt zurück zu gehen oder an den Anfang zurück zu kehren. Deshalb wird der NSDirectoryEnumerator, auch wenn er keine Objekte enthält, an die nächste Methode durchgereicht.