Logo of blog.sannewald.comblog.sannewald.com
Image of the article

QuickLook Thumbnailing ist ein relativ neues Framework von Apple, das hauptsächlich für zwei Anwendungsfälle verwendet wird:

  1. Erstellen von Vorschauen ("Thumbnails") für diverse Objekte, darunter unter anderem Bilder, Textdateien, PDF-Dateien, Audio-Dateien und Video-Dateien. Mit dieser API lassen sich also Vorschauen von üblichen Dateien wie in der Finder-App implementieren.
  2. Bereitstellung von Thumbnail-Extensions für eigene Dateitypen. Angenommen ein Entwickler programmiert eine Pages/Word-Alternative, dann ist es sehr wahrscheinlich, dass dieser auch einen eigenen Dateityp zum persistieren der Daten entwerfen wird. Solche für Finder unbekannten Dateitypen erhalten dann natürlich auch keine Vorschau, stattdessen wird lediglich ein generisches Datei-Icon angezeigt. Der Entwickler kann für seinen eigenen Dateityp nun aber eine Thumbnail-Extension bereitstellen, wodurch das Betriebssystem und dritte Apps Vorschauen des unbekannten Dateityps anfordern und anzeigen können.

In diesem Artikel werde ich mich auf Anwendungsfall 1 konzentrieren und nur diesen praktisch in Swift demonstrieren. Anwendungsfall 2 ist bei Document-Based-Apps aber durchaus relevant, weswegen ich dazu in Zukunft vielleicht einen weiteren Artikel schreiben werde.

Leider ist dieses wunderbare Framework noch immer ziemlich unbekannt und meiner Meinung nach auch absolut unterschätzt - es bietet schließlich eine Vielzahl an Möglichkeiten. Ich habe mich zwar selbst durch die Apple-Developer-Dokumentation gewühlt und so das Framework gefunden, in Online-Foren wird meiner Erfahrung nach aber noch immer viel zu wenig auf dieses Framework verwiesen. Bei meiner Kurzsuche habe ich lediglich eine StackOverflow-Antwort gefunden, die auf QuickLook Thumbnailing verweist. Dagegen stehe eine Vielzahl an Foren-Threads, bei denen dieses Framework die perfekte Lösung wäre, aber kein Verweis darauf zu finden ist:

Schaut man sich diese Beiträge an, werden meistens deprecated APIs, externe Frameworks, Hacks oder sogar eigene Implementierungen mit Fallunterscheidungen je nach Dateityp vorgeschlagen. Das gipfelt darin, für jeden zu berücksichtigenden Dateityp eine andere Lösungsstrategie zu verwenden, für PDFs beispielsweise PDFKit. Das ist logischerweise völliger Unsinn. Natürlich spielt auch das Deployment-Target beim Lösungsansatz eine wichtige Rolle, aber grundsätzlich sollte iOS 13.0, macOS 10.15 und Mac Catalyst 13.0 heute kein Hindernis mehr darstellen. Warum? Einfach mal in die Statistiken der App-Stores sehen: iOS- und iPadOS-Nutzung.

Werfen wir nun aber endlich einen Blick auf den notwendigen Swift-Code, der für Version 5.4.2 lauffähig ist. Wie bereits oben beschrieben, habe ich ein NSCollectionView ähnlich zu dem im Titelbild nachgebaut. Jedes NSCollectionViewItem ist selbst dafür verantwortlich, eine Vorschau seines zugehörigen Dokuments anzuzeigen. Nachfolgend ist der stark vereinfachte Aufbau dieser NSCollectionViewItems dargestellt:

import Cocoa

final class DocumentBrowserItem: NSCollectionViewItem {
    
    // MARK: - Properties
    
    // ...
    @IBOutlet private weak var documentName: NSTextField!
    @IBOutlet private weak var documentThumbnail: NSImageView!
    // ...
    
    private var document: DocumentManager.DocumentInformation? {
        didSet {
            guard self.isViewLoaded else { return }
            guard let document = self.document else { return }
            
            self.documentName.stringValue = document.name + document.extensionName
            DocumentManager.shared.getThumbnail(for: document.location, withHeight: 150, andWidth: 150) { error, image in
                guard !error else {
                    // ...
                    return
                }
                self.documentThumbnail.image = image
            }
        }
    }
    
    // MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
    }
    
    // ...
    
}

Sobald das DocumentBrowserItem das Dokument "eingesetzt" bekommt, wird der didSet-Block ab Zeile 13 ausgeführt. Für diesen Artikel ist primär Zeile 18 bis Zeile 24 innerhalb dieses Blocks relevant. Die Funktion getThumbnail(...) ist eine Wrapper-Funktion der Singleton-Klasse DocumentManager, die unter anderem die Verwendung von Dokument-Vorschauen innerhalb der macOS-App vereinfachen und zentralisieren soll. Natürlich wäre an dieser Stelle auch der direkte Aufruf der entsprechenden Funktion des Frameworks QuickLook Thumbnailing möglich. Ob das sinnvoll ist, hängt stark vom Kontext der Anwendung ab. Die Singleton-Klasse enthält zudem die Strukturdefinition DocumentInformation, die ein Dokument innerhalb der macOS-App repräsentiert. Dementsprechend sind darin alle relevanten Informationen über das Dokument gespeichert.

Als nächstes werfen wir einen Blick auf die interessantere Implementierung von DocumentManager. Natürlich ist auch das lediglich ein stark vereinfachter Ausschnitt:

import Cocoa
import QuickLookThumbnailing

final class DocumentManager: NSObject {
    
    // MARK: - Singleton Definition
    
    static let shared = DocumentManager()
    private override init() { }
    
    // MARK: - Type Definitions
    
    // ...
    struct DocumentInformation: Codable {
        let fileSystemName: String
        let location: URL
        let name: String
        let extensionName: String
        let description: String
        let creationDate: Date
    }
    // ...
    
    // MARK: - Functions
    
    // ...
    func getThumbnail(for location: URL, withHeight height: Int, andWidth width: Int, completion: @escaping (Bool, NSImage?) -> ()) {
        let request = QLThumbnailGenerator.Request(fileAt: location, size: CGSize(width: width, height: height), scale: NSScreen.main!.backingScaleFactor, representationTypes: .thumbnail)
        
        QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { thumbnail, error in
            DispatchQueue.main.async {
                if thumbnail == nil || error != nil {
                    return completion(true, nil)
                }
                return completion(false, thumbnail!.nsImage)
            }
        }
    }
    // ...
    
}

Aus Komplexitätsgründen habe ich das gesamte Sandbox-Management aus der Funktion getThumbnail(...) entfernt. macOS-Apps, die über den App-Store vertrieben werden sollen, müssen in einer Sandbox "leben", wodurch Zugriffe auf das Dateisystem (außerhalb des eigenen Containers) erst durch den Benutzer genehmigt werden müssen. Mit Security-Scoped-Bookmarks ist es dann möglich, die durch den Benutzer genehmigten Orte dauerhaft zu speichern. Mit startAccessingSecurityScopedResource() und stopAccessingSecurityScopedResource() kann dann dementsprechend die Sandbox "geöffnet" und "geschlossen" werden.

Die Wrapper-Funktion getThumbnail(...) versteckt somit die gesamte Komplexität der Vorschau-Erstellung und des Sandbox-Managements nach außen, wodurch die Erstellung von Vorschauen innerhalb der macOS-App trivial wird. Die Klasse QLThumbnailGenerator bietet mehrere Funktionen zum Erzeugen von Vorschauen an. Diese unterscheiden sich in ihrer Funktionsweise sehr stark. Die Funktion generateBestRepresentation(...) bot für meine Anforderungen die ideale Lösung, da sie gleich die bestmögliche Vorschau zurückgibt und das Callback nicht mehrfach aufgerufen wird. Auf die Verwendung von generateRepresentations(...) habe ich bewusst verzichtet, da die Dauer, bis die erste Vorschau verfügbar ist, in meinem Anwendungsfall nebensächlich war und das mehrfache Aufrufen des Callbacks andere Nebeneffekte gehabt hätte.

Grundsätzlich ist die Verwendung des Frameworks QuickLook Thumbnailing intuitiv und selbsterklärend. Außerdem ist QuickLook Thumbnailing sehr gut dokumentiert, sodass eigentlich keine Fragen mehr offen bleiben. Leider ist das bei vielen APIs von Apple nicht der Fall, das Reverse-Engineeren bleibt einem hier zumindest mal erspart 😉

Mein Ziel war es, mit diesem Artikel zu zeigen, wie schnell und einfach mit QuickLook Thumbnailing schöne Ergebnisse erzielt werden können. Das ist auch der Grund, warum ich nur wenig des gezeigten Codes erklärt habe. Die wirklich gute Dokumentation sollte für das Verständnis absolut ausreichend sein: Apple Developer Documentation.