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 ;-)