SwiftUI Document App mit Paketdateien

Unter macOS gibt es zwei verschiedene Datei-Formen. Einmal die einfache flache Datei. Diese besteht aus einer Folge von Bytes. Darüber hinaus gibt es noch das Paket-Format. Dies ist ein Dateiverzeichnisbaum, der aber von aussen wie eine einzelne Datei aussieht. Um die Inhalte zu sehen kann man die Paketdatei mit dem Dateiexplorer auswählen und im Kontexten Paketinhalt zeigen auswählen.

Solche Paketdateien lassen sich auch mit SwiftUI erstellen, allerdings ist dies vom Programmierer selber zu realisieren, da das in XCode vorliegende Projekttemplate  nur einen Projektrahmen für Dokumente für flache Dateien erzeugt.

Im folgenden Text wird vorgestellt, wie man aus dem Standardbeispiel von Xcode für die Dokumentenapp eine Version mit Paketdateien als Speicherformat erstellt.

Wir beginnen dabei mit dem Standard-Projekt von Xcode. Dies ist beim Erzeugen eines neuen Projektes die Document App im Multiplattform-Tab.

 Wir betätigen die Next-Taste und geben im folgenden Dialog den Projektnamen ein und betätigen die Next-Taste.

Abschließend wählen wir noch den Speicherort aus und betätigen die Create-Taste

Als Ergebnis haben wir nun das einfache SwiftUI- Document-Example erstellt. Dieses wollen wir im folgenden anpassen. 

Die zu ändernden Daten sind

- das Documenten-Struct

- die Info.plist-Dateien für iOS und macOS

Ausserdem ändern wir, um die Ergebnisse der Änderung sehen zu können, den ContentView.

Beginnen wir mit den Info.plist-Dateien. Die nachfolgenden Änderungen sind sowohl in der Info.plist unter iOS wie unter macOS durchzuführen.

Am einfachsten ist es, wenn wir die Info.plist als Source Code öffnen und die passenden XML-Tags ändern oder hinzukopieren.

Als erstes passen wir den LSItemContentTypes-key an. Dieser ist aktuell noch com.example.plain-text. Wir ändern in in einen anderen, eindeutigen Schlüssel, in der Regel beginnend mit der eigenen Domain-Namen

            <key>LSItemContentTypes</key>
            <array>
                <string>de.h-mayer.demo-doc</string>
            </array> 

Und wir geben hier bekannt, dass es sich um ein Paketformat handelt, in dem wir den LSTypeIsPackageKey auf True setzen


   <key>LSTypeIsPackage</key>
            <true/> 

Der gesamte Block sieht dann wie folgt aus:

<key>CFBundleDocumentTypes</key>
	<array>
		<dict>
			<key>LSItemContentTypes</key>
			<array>
				<string>de.h-mayer.demo-doc</string>
			</array>
			<key>NSUbiquitousDocumentUserActivityType</key>
			<string>$(PRODUCT_BUNDLE_IDENTIFIER).example-document</string>
            <key>LSTypeIsPackage</key>
                        <true/>
		</dict>
	</array> 

 Nun müssen wir die UTImportedTypeDeclarations anpassen.

Hier müssen zwei Einträge geändert werden :

  • UTTypeConformsTo
  • UTTypeIdentifier


UITypeConformsTo muss auf com.apple.package geändert werden und der UTTypeIdentifier wird auf den schon zuvor festgelegten LSItemContentTypes gesetzt.

Die UTImportedTypeDeclarations sieht dann so aus

	<key>UTImportedTypeDeclarations</key>
	<array>
		<dict>
			<key>UTTypeConformsTo</key>
			<array>
				<string>com.apple.package</string>
			</array>
			<key>UTTypeDescription</key>
			<string>Example Text</string>
			<key>UTTypeIdentifier</key>
			<string>de.h-mayer.demo-doc</string>
			<key>UTTypeTagSpecification</key>
			<dict>
				<key>public.filename-extension</key>
				<array>
					<string>exampletext</string>
				</array>
			</dict>
		</dict>
	</array> 

Nun öffnen wir die Document-Datei und suchen die UIType-Extension. Hier müssen wir den String des UTTypes in unseren Gewählten ändern. Das sieht dann dort wie folgt aus:

extension UTType {
    static var exampleText: UTType {
        UTType(importedAs: "de.h-mayer.demo-doc")
    }
}
 

Und nun müssen das Speichern und Laden des Dokumentes anpassen. In dieser Demo erstellen wir dazu ein etwas erweitertes Datenmodell. Statt einem Beispieltext (Variable text) erstellen wir zwei (Variablen text1 und text2) und passen die Init-Routine an.

    var text1: String
    var text2: String

   
   init(text1: String = "Hello, HAL!", text2: String = "Hello, Dave!") {
        self.text1 = text1
        self.text2 = text2
    } 

Nun müssen wir die Init-Routine für das Lesen des Dokumentes anpassen. Hier erstellen wir zwei neue Filewrapper die die Dateien in der Paketdatei repräsentieren. Jede Datei enthält dann einen String (text1 oder text2).

       let fileWrapper = configuration.file
        
        guard let text1FileWrapper = fileWrapper.fileWrappers![firstFileName],
              let text1Data = text1FileWrapper.regularFileContents,
              let string1 = String(data: text1Data, encoding: .utf8)
              else {
            throw CocoaError(.fileReadCorruptFile)
        }
        
        guard let text2FileWrapper = fileWrapper.fileWrappers![secondFileName],
              let text2Data = text2FileWrapper.regularFileContents,
              let string2 = String(data: text2Data, encoding: .utf8)
              else {
            throw CocoaError(.fileReadCorruptFile)
        }

 
        text1 = string1
        text2 = string2 

Jetzt müssen wir die beiden Konstanten für die Dateinamen innerhalb der Paketdatei festlegen. Dies machen wir in der Struct damit wir sie bei Schreiben auch verwenden können.

  let firstFileName = "TextA.data"
  let secondFileName = "TextB.data" 

Und nun schreiben wir die Daten mittels Filewrapper. Dazu schaffen wir uns noch eine private Hilfsroutine wrapString und ersetzen den Speichercode durch folgenden Text:

 func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
               
        let contentsFileWrapper = FileWrapper(directoryWithFileWrappers: [:])
  
        wrapString(text1, fileName: firstFileName, contentsFileWrapper: contentsFileWrapper)

        wrapString(text2, fileName: secondFileName, contentsFileWrapper: contentsFileWrapper)

        return contentsFileWrapper
    }
    
    private func wrapString(_ string : String, fileName : String, contentsFileWrapper : FileWrapper){
        let data = string.data(using: .utf8)!

        let textFileWrapper = FileWrapper(regularFileWithContents: data)
        textFileWrapper.preferredFilename = fileName
        contentsFileWrapper.addFileWrapper(textFileWrapper)
    } 

Abschliessend müssen wir noch den CanvasView anpassen. Hier ersetzen wird die einzelne Textausgabe durch folgenden Abschnitt:

        VStack {
            TextEditor(text: $document.text1)
            TextEditor(text: $document.text2)
        }.padding() 

Wenn wir nun das Programm kompilieren und laufen lassen können wir zwei Texte eingeben, ändern und speichern.

Wenn wir nun die Datei speichern und im Dateiexplorer anwählen, können wir die Paketdatei öffnen. Wir sehen nun im Dateiexplorer die Inhalte.


Der Sourcecode kann unter https://github.com/Holger-Mayer/SwiftUIDocument heruntergeladen werden. Er befindet sich im ValueDocumentPacked-Branch .

Notifications mit Combine in Swift
Emoji in ListItems eines UICollectionViews

Ähnliche Beiträge

By accepting you will be accessing a service provided by a third-party external to http://h-mayer.de/