Published on 2021-09-08 · About 5 min read

Anwendungsbeispiel des noch wenig bekannten Frameworks QuickLook Thumbnailing

Zur Zeit entwickle ich eine macOS-App, die spĂ€ter als Mac-Client fĂŒr ein ERP-System in einem Unternehmen eingesetzt werden soll. Aus DatenschutzgrĂŒnden kann ich in diesem Artikel leider keine Bilder dieser App verwenden, weswegen ich den Finder als exemplarisches Beispiel nutzen werde. Im Rahmen dieses Auftrags war es notwendig, ein NSCollectionView, Ă€hnlich wie im Titelbild, nachzubauen. Jedes NSCollectionViewItem sollte dementsprechend eine Vorschau seines zugehörigen Dokuments anzeigen. FĂŒr die Realisierung dieser Anforderung bot sich das Framework QuickLook Thumbnailing von Apple an, das mit iOS 13.0, macOS 10.15 und Mac Catalyst 13.0 eingefĂŒhrt wurde. Dieser Artikel soll das Framework kurz vorstellen und exemplarisch zeigen, wie damit oben genannte Anforderung elegant umgesetzt werden kann.

Article Thumbnail

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.