garysimpson.dev
Mobile development with swift and flutter

Import/Export JSON in SwiftUI (Part 3: iOS)

December 09, 2024 at 6:30PM


Spent some time getting a new feature implemented in one of my existing apps. As both a stop gap until I get iCloud sync setup and a method of backup I wanted to be able to export and import all setting and data as json. Turns out to be pretty simple to do since JSON encode and decode has been a native feature of Swift for a few years now.

Import



    // MARK: - Import Functionality
    func importSettings(from url: URL, completion: @escaping (Budgets?) -> Void) {
        do {
            let jsonData = try Data(contentsOf: url)
            let decoder = JSONDecoder()
            let settings = try decoder.decode(Budgets.self, from: jsonData)
            completion(settings)
        } catch {
            print(“Error decoding JSON: \(error)”)
            completion(nil)
        }
    }

Export



    // MARK: - Export Functionality
    func exportSettings(settings: Budgets, completion: @escaping (URL?) -> Void) {
        // Create a temporary file to store the exported JSON
        let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
        let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(getFileName())
        
        // Convert settings to JSON data
        let jsonEncoder = JSONEncoder()
        jsonEncoder.outputFormatting = .prettyPrinted
        do {
            let jsonData = try jsonEncoder.encode(settings)
            try jsonData.write(to: temporaryFileURL)
            completion(temporaryFileURL)
        } catch {
            print(“Error encoding JSON: \(error)”)
            completion(nil)
        }
    }

     // MARK: - Helper Functions
    func getFileName() -> String {
        return “\(Bundle.main.appName) (\(Date().fileDescription)).json”
    }   

Extenstions

I use a Date and Bundle extension to help with creating file name for export.



extension Bundle {
    var appName: String {
        return object(forInfoDictionaryKey: “CFBundleName”) as? String ?? “App”
    }
}

extension Date {
    /// Returns a **String** dateFormatted as “yyyy-MM-dd_HHmm”.
    var fileDescription: String {
        let formatter = DateFormatter()
        formatter.dateFormat = “yyyy-MM-dd_HHmm”
        return formatter.string(from: self)
    }
}

FileDocument

I also create a document to easily handle document management.



import SwiftUI
import UniformTypeIdentifiers

// MARK: - JSON File Document for File Import/Export
struct JsonDocument: FileDocument {
    static var readableContentTypes: [UTType] { [.json] }
    var text: String
    
    init(text: String = “”) {
        self.text = text
    }
    
    init(configuration: ReadConfiguration) throws {
        if let data = configuration.file.regularFileContents {
            self.text = String(decoding: data, as: UTF8.self)
        } else {
            throw CocoaError(.fileReadCorruptFile)
        }
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = text.data(using: .utf8)!
        return FileWrapper(regularFileWithContents: data)
    }
}

Use

With all of that setup and ready to go my next step was updating my SwiftUI views.



    @State private var showFileExporter = false
    @State private var showFileImporter = false
    

Settings”)
        
            .fileImporter(
                isPresented: $showFileImporter,
                allowedContentTypes: [.json],
                allowsMultipleSelection: false
            ) { result in
                switch result {
                case .success(let url):
                    guard let selectedURL = url.first else { return }
                    manager.importSettings(from: selectedURL) { importedBudgets in
                        if let budgets = importedBudgets {
                            self.budgets = budgets
                            
                            let json: String = budgets.jsonRepresentation
                            defaults.setBudgets(json, .budgetsString)
                            
                            manager.alertMessage = “Budgets imported successfully!”
                        } else {
                            manager.alertMessage = “Failed to import budgets.”
                        }
                        manager.showAlert = true
                    }
                case .failure(let error):
                    print(“Import failed: \(error)”)
                }
            }
        
            .fileExporter(
                isPresented: $showFileExporter,
                document: JsonDocument(text: budgets.jsonRepresentation),
                contentType: .json,
                defaultFilename: manager.getFileName()
            ) { result in
                switch result {
                case .success(let url):
                    print(“Exported to: \(url)”)
                case .failure(let error):
                    print(“Export failed: \(error)”)
                }
            }

And finally a couple helper functions.



    func showImport() {
        showFileImporter = true
    }
    
    func showExport() {
        let budgets: Budgets = budgets
        
        manager.exportSettings(settings: budgets) { url in
            if url != nil {
                // Trigger File Exporter
                showFileExporter = true
            } else {
                manager.alertMessage = “Failed to export budgets.”
                manager.showAlert = true
            }
        }
    }

Improvements 🤔

I think the next steps will be to turn the .fileImporter(… and .fileExporter(… calls into SwiftUI view modifiers to encapsulate the inner workings. Ideally I’d like this to be modular enough to be called easily from any SwiftUI view.


Happy Coding ;-)