Learn Courses My Dashboard

Importing Data Store Using fileImporter

Hi Guys,

Below is my code where I am trying to use fileImporter to import my data store.

The export function works perfectly and currently I have no errors in my code and the app runs fine but when I select the file for import nothing actually updates in my data store.

Can some please help me work out what I am missing?

FYI - [BudgetDetails] is my data model.

If you have any questions please sing out.

Thanks

import SwiftUI
import UniformTypeIdentifiers

struct MainMenuView: View {
    
    @StateObject private var store = DataStore()
    
    @State private var showingAlert = false
    
    @State private var showingExporter = false
    
    @State private var showingImporter = false
    
    @State var fileName = ""
    
    let selectedFont = "DINAlternate-Bold"
    
    func load() async throws -> [BudgetDetails] {
        try await withCheckedThrowingContinuation { continuation in
            load {result in
                switch result {
                case .failure(let error):
                    continuation.resume(throwing: error)
                case .success(let budget):
                    continuation.resume(returning: budget)
                }
            }
        }
    }
    
    func load(completion: @escaping (Result<[BudgetDetails], Error>)->Void) {
        DispatchQueue.global(qos: .background).async {
            do {
                let fileURL = URL(string: fileName)
                guard let file = try? FileHandle(forReadingFrom: fileURL!) else {
                    DispatchQueue.main.async {
                        completion(.success([]))
                    }
                    return
                }
                let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }

    
    var body: some View {
        ZStack {
            ZStack {
                Color.BackgroundColor.ignoresSafeArea()
                List{
                    Section{
                        HStack{
                            Image(systemName: "arrow.up.doc").foregroundColor(Color.TextColorSecondary)
                            Text("Export").font(.custom(selectedFont, size: 15)).foregroundColor(Color.TextColorPrimary)
                        }
                        .overlay(
                            Button("", action: {
                                showingExporter.toggle()
                                   })
                            )
                                }
                    .listRowBackground(Color.BackgroundColorList)
                    Section{
                        HStack{
                            Image(systemName: "arrow.down.doc").foregroundColor(Color.TextColorSecondary)
                            Text("Import").font(.custom(selectedFont, size: 15)).foregroundColor(Color.TextColorPrimary)
                        }
                        .overlay(
                            Button("", action: {
                                showingImporter.toggle()
                                            })
                            )
                    }
                    .listRowBackground(Color.BackgroundColorList)
                }
                .navigationTitle("Menu")
                .toolbar {
                    HStack{
                    EditButton()
                }
                }
                }
        }
        
        .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.json]) { (result) in
            
            do {
                
                let fileURL = try result.get()
                print(fileURL)
                
                fileName = fileURL.lastPathComponent
                
                Task {
                    try await load()
                }
            }
            
            catch {
                
                print("Error Reading Documents")
                print(error.localizedDescription)
            }
        }
    
        .fileExporter(isPresented: $showingExporter, document: Doc(url: try! DataStore.fileURL()), contentType: .json) { (result) in
            
            do {
                
                let fileURL = try result.get()
                
                print(fileURL)
                
            }
            catch {
                
                print("Cannot Save Document")
                print(error.localizedDescription)
            }
        }
        
    }
}

struct Doc: FileDocument {
    
    var url: URL
    
    static var readableContentTypes: [UTType]{[.json]}
    
    init(url: URL) {
        self.url = url
    }
    
    init(configuration: ReadConfiguration) throws {
        
        url = try DataStore.fileURL()
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        
        let file = try! FileWrapper(url: url, options: .immediate)
        
        return file
    }
}

struct MainMenuView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView{
            MainMenuView()
        }
    }
}

@scott_foley

Have you set breakpoints in your load function code to check that at each step you are seeing what you expect to see?

Also check that your BudgetDetails data model defines exactly what you have in your json code OR if you are using CodeingKeys to remap the json code to your model, be sure that there are no errors in your CodingKeys.

Thanks @Chris_Parker I have been doing some breakpoints to try identify what the issue is and I have updated some code, but where I am still having problem is the following code is just being skipped when the func runs:

let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }

In this whole run:

    func load() async throws -> [BudgetDetails] {
        try await withCheckedThrowingContinuation { continuation in
            load {result in
                switch result {
                case .failure(let error):
                    continuation.resume(throwing: error)
                case .success(let budget):
                    continuation.resume(returning: budget)
                }
            }
        }
    }
    
    func load(completion: @escaping (Result<[BudgetDetails], Error>)->Void) {
        DispatchQueue.global(qos: .background).async {
            do {
                let fileURL = URL(string: fileName)
                guard let file = try? FileHandle(forReadingFrom: fileURL!) else {
                    DispatchQueue.main.async {
                        completion(.success([]))
                    }
                    return
                }
                let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }

Any ideas why this would be?

Thanks

Are you absolutely sure that your data model matches your JSON file (or the other way around)?

Pretty hard to figure out what the issue might be without being able to observe the entire program operating and watch the code as it steps through. If you have the project on GitHub or you are prepared to share it via Private DM as a zip file then debugging might be simpler.

Cheers

@Chris_Parker I have been playing around with this now and worked out a couple of issues.

I know 100% that the data is loading correctly, my issue now is it doesn’t appear to be updating my views.

I have put my code below, but if you have any idea why this would be I appreciate the help.

import SwiftUI
import UniformTypeIdentifiers

struct MainMenuView: View {
    
    @StateObject var store = DataStore()
    
    @State private var showingAlert = false
    
    @State private var showingExporter = false
    
    @State private var showingImporter = false
    
    @State var fileName = ""
    
    let selectedFont = "DINAlternate-Bold"
    
    func load() async throws -> [BudgetDetails] {
            try await withCheckedThrowingContinuation { continuation in
                load {result in
                    switch result {
                    case .failure(let error):
                        continuation.resume(throwing: error)
                    case .success(let budget):
                        continuation.resume(returning: budget)
                    }
                }
            }
        }
    
    func load(completion: @escaping (Result<[BudgetDetails], Error>)->Void) {
        DispatchQueue.main.async {
            do {
                let fileURL = URL(string: fileName)
                guard let file = try? FileHandle(forReadingFrom: fileURL!) else {
                    DispatchQueue.main.async {
                        completion(.success([]))
                    }
                    return
                }
                let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }

    
    var body: some View {
        ZStack {
            ZStack {
                Color.BackgroundColor.ignoresSafeArea()
                List{
                    Section{
                        HStack{
                            Image(systemName: "arrow.up.doc").foregroundColor(Color.TextColorSecondary)
                            Text("Export").font(.custom(selectedFont, size: 15)).foregroundColor(Color.TextColorPrimary)
                        }
                        .overlay(
                            Button("", action: {
                                showingExporter.toggle()
                                   })
                            )
                                }
                    .listRowBackground(Color.BackgroundColorList)
                    Section{
                        HStack{
                            Image(systemName: "arrow.down.doc").foregroundColor(Color.TextColorSecondary)
                            Text("Import").font(.custom(selectedFont, size: 15)).foregroundColor(Color.TextColorPrimary)
                        }
                        .overlay(
                            Button("", action: {
                                showingImporter.toggle()
                            })
                            )
                    }
                    .listRowBackground(Color.BackgroundColorList)
                }
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                    HStack{
                        Text("Menu").font(.custom(selectedFont, size: 30))  .font(.title).fontWeight(.bold)
                            .foregroundColor(Color.TextColorSecondary)
                        Spacer()
                    EditButton()
                }
                }
        }
            }
        }
        
        .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.json], onCompletion: { result in
            
            switch result {
                    case .success(let url):
                fileName = url.absoluteString
                Task {
                    load { result in
                        switch result {
                        case .failure(let error):
                            fatalError(error.localizedDescription)
                        case .success(let budget):
                            store.budget = budget
                        }
                    }
                }
                    case .failure(let error):
                        print(error.localizedDescription)
                }
        })
    
        .fileExporter(isPresented: $showingExporter, document: Doc(url: try! DataStore.fileURL()), contentType: .json) { (result) in
            
            do {
                
                let fileURL = try result.get()
                
                print(fileURL)
                
            }
            catch {
                
                print("Cannot Save Document")
                print(error.localizedDescription)
            }
        }
        
    }
}

struct Doc: FileDocument {
    
    var url: URL
    
    static var readableContentTypes: [UTType]{[.json]}
    
    init(url: URL) {
        self.url = url
    }
    
    init(configuration: ReadConfiguration) throws {
        
        url = try DataStore.fileURL()
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        
        let file = try! FileWrapper(url: url, options: .immediate)
        
        return file
    }
}

struct MainMenuView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView{
            MainMenuView()
        }
        .previewInterfaceOrientation(.landscapeLeft)
    }
}

import Foundation
import SwiftUI

class DataStore: ObservableObject {
    @Published var budget: [BudgetDetails] = []
}

Let me know if you need the budget details one as well.

Thanks

@scott_foley

Hi Scott,

Yeah there are too many errors to make any sense of what is going on.
Screenshot:

It would be useful to have the BudgetDetails definition and also you appear to have defined a whole bunch of custom colours and they are showing up as, for example, Type 'Color' has no member 'BackgroundColor'

DataStore fileURL() is also missing.

@Chris_Parker sorry about that, here are the other bits of code for you (for the colors I have just removed these below to stop any errors).

import SwiftUI
import UniformTypeIdentifiers

struct MainMenuView: View {
    
    @StateObject var store = DataStore()
    
    @State private var showingAlert = false
    
    @State private var showingExporter = false
    
    @State private var showingImporter = false
    
    @State var fileName = ""
    
    let selectedFont = "DINAlternate-Bold"
    
    func load() async throws -> [BudgetDetails] {
            try await withCheckedThrowingContinuation { continuation in
                load {result in
                    switch result {
                    case .failure(let error):
                        continuation.resume(throwing: error)
                    case .success(let budget):
                        continuation.resume(returning: budget)
                    }
                }
            }
        }
    
    func load(completion: @escaping (Result<[BudgetDetails], Error>)->Void) {
        DispatchQueue.main.async {
            do {
                let fileURL = URL(string: fileName)
                guard let file = try? FileHandle(forReadingFrom: fileURL!) else {
                    DispatchQueue.main.async {
                        completion(.success([]))
                    }
                    return
                }
                let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }

    
    var body: some View {
        ZStack {
            ZStack {
                Color.white.ignoresSafeArea()
                List{
                    Section{
                        HStack{
                            Image(systemName: "arrow.up.doc").foregroundColor(Color.black)
                            Text("Export").font(.custom(selectedFont, size: 15)).foregroundColor(Color.black)
                        }
                        .overlay(
                            Button("", action: {
                                
                                Task{
                                DataStore.save(budget: store.budget) { result in
                                    if case .failure(let error) = result {
                                        fatalError(error.localizedDescription)
                                    }
                                }
                            }
                                
                                showingExporter.toggle()
                                   })
                            )
                                }
                    .listRowBackground(.white)
                    Section{
                        HStack{
                            Image(systemName: "arrow.down.doc").foregroundColor(.black)
                            Text("Import").font(.custom(selectedFont, size: 15)).foregroundColor(.black)
                        }
                        .overlay(
                            Button("", action: {
                                showingImporter.toggle()
                            })
                            )
                    }
                    .listRowBackground(.white)
                }
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                    HStack{
                        Text("Menu").font(.custom(selectedFont, size: 30))  .font(.title).fontWeight(.bold)
                            .foregroundColor(.black)
                        Spacer()
                    EditButton()
                }
                }
        }
            }
        }
        
        .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.json], onCompletion: { result in
            
            switch result {
                    case .success(let url):
                fileName = url.absoluteString
                Task {
                    load { result in
                        switch result {
                        case .failure(let error):
                            fatalError(error.localizedDescription)
                        case .success(let budget):
                            store.budget = budget
                        }
                    }
                }
                    case .failure(let error):
                        print(error.localizedDescription)
                }
        })
    
        .fileExporter(isPresented: $showingExporter, document: Doc(url: try! DataStore.fileURL()), contentType: .json) { (result) in
            
            do {
                
                let fileURL = try result.get()
                
                print(fileURL)
                
            }
            catch {
                
                print("Cannot Save Document")
                print(error.localizedDescription)
            }
        }
        
    }
}

struct Doc: FileDocument {
    
    var url: URL
    
    static var readableContentTypes: [UTType]{[.json]}
    
    init(url: URL) {
        self.url = url
    }
    
    init(configuration: ReadConfiguration) throws {
        
        url = try DataStore.fileURL()
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        
        let file = try! FileWrapper(url: url, options: .immediate)
        
        return file
    }
}

struct MainMenuView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView{
            MainMenuView()
        }
    }
}
import Foundation
import SwiftUI

class DataStore: ObservableObject {
    @Published var budget: [BudgetDetails] = []
    
    static func fileURL() throws -> URL {
        try FileManager.default.url(for: .documentDirectory,
                                       in: .userDomainMask,
                                       appropriateFor: nil,
                                       create: false)
            .appendingPathComponent("BudgetMaster.data")
    }
    
    static func load() async throws -> [BudgetDetails] {
        try await withCheckedThrowingContinuation { continuation in
            load {result in
                switch result {
                case .failure(let error):
                    continuation.resume(throwing: error)
                case .success(let budget):
                    continuation.resume(returning: budget)
                }
            }
        }
    }
    
    static func load(completion: @escaping (Result<[BudgetDetails], Error>)->Void) {
        DispatchQueue.global(qos: .background).async {
            do {
                let fileURL = try fileURL()
                guard let file = try? FileHandle(forReadingFrom: fileURL) else {
                    DispatchQueue.main.async {
                        completion(.success([]))
                    }
                    return
                }
                let budgetMaster = try JSONDecoder().decode([BudgetDetails].self, from: file.availableData)
                DispatchQueue.main.async {
                    completion(.success(budgetMaster))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }
    
    @discardableResult
    static func save(budget: [BudgetDetails]) async throws -> Int {
        try await withCheckedThrowingContinuation { continuation in
            save(budget: budget) { result in
                switch result {
                case .failure(let error):
                    continuation.resume(throwing: error)
                case .success(let budgetSaved):
                    continuation.resume(returning: budgetSaved)
                }
            }
        }
    }
    
    static func save(budget: [BudgetDetails], completion: @escaping (Result<Int, Error>)->Void) {
        DispatchQueue.global(qos: .background).async {
            do {
                let data = try JSONEncoder().encode(budget)
                let outfile = try fileURL()
                try data.write(to: outfile)
                DispatchQueue.main.async {
                    completion(.success(budget.count))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }
}


import Foundation

struct BudgetDetails: Identifiable, Hashable, Equatable, Codable {
    let id: UUID
    var categoryDetails: String
    var incomeDetails: String
    var incomeCategory: String
    var incomeFrequency: Period
    var incomeAmount: Int
    var incomeAnnual: Int {
        switch incomeFrequency {
        case .Daily:
            return incomeAmount * 365
        case .Weekly:
            return incomeAmount * 52
        case .Fortnightly:
            return incomeAmount * 26
        case .Monthly:
            return incomeAmount * 12
        case .Quarterly:
            return incomeAmount * 4
        case .Annually:
            return incomeAmount
        case .Annually1:
            return incomeAmount
        }
    }
    var expenseDetails: String
    var expenseCategory: String
    var expenseFrequency: Period
    var expenseAmount: Int
    var expenseAnnual: Int {
        switch expenseFrequency {
        case .Daily:
            return expenseAmount * 365
        case .Weekly:
            return expenseAmount * 52
        case .Fortnightly:
            return expenseAmount * 26
        case .Monthly:
            return expenseAmount * 12
        case .Quarterly:
            return expenseAmount * 4
        case .Annually:
            return expenseAmount
        case .Annually1:
            return expenseAmount
        }
    }
    var budgetType: String {
        if incomeCategory == "" && expenseCategory == "" {
            return "Category"
        }
        else if incomeCategory == "" && categoryDetails == "" {
            return "Expense"
        }
        else {
            return "Income"
        }
    }
    
init(id: UUID = UUID(),categoryDetails: String, incomeDetails: String, incomeCategory: String, incomeFrequency: Period, incomeAmount: Int, incomeAnnual: Int, expenseDetails: String, expenseCategory: String, expenseFrequency: Period, expenseAmount: Int, expenseAnnual: Int, budgetType: String) {
        self.id = id
        self.categoryDetails = categoryDetails
        self.incomeDetails = incomeDetails
        self.incomeCategory = incomeCategory
        self.incomeFrequency = incomeFrequency
        self.incomeAmount = incomeAmount
        self.expenseDetails = expenseDetails
        self.expenseCategory = expenseCategory
        self.expenseFrequency = expenseFrequency
        self.expenseAmount = expenseAmount
    }
}

extension BudgetDetails {

struct Data {
    var categoryDetails: String = ""
    var incomeDetails: String = ""
    var incomeCategory: String = ""
    var incomeFrequency: Period = .Annually1
    var incomeAmount: Int = 0
    var incomeAnnual: Int = 0
    var expenseDetails: String = ""
    var expenseCategory: String = ""
    var expenseFrequency: Period = .Annually1
    var expenseAmount: Int = 0
    var expenseAnnual: Int = 0
    var budgetType: String = ""
    }

var bData: Data {
    Data(categoryDetails: categoryDetails, incomeDetails: incomeDetails, incomeCategory: incomeCategory, incomeFrequency: incomeFrequency, incomeAmount: incomeAmount, incomeAnnual: incomeAnnual, expenseDetails: expenseDetails, expenseCategory: expenseCategory, expenseFrequency: expenseFrequency, expenseAmount: expenseAmount, expenseAnnual: expenseAnnual, budgetType: budgetType)
}
    
    mutating func update(from bData: Data) {
        categoryDetails = bData.categoryDetails
        incomeDetails = bData.incomeDetails
        incomeCategory = bData.incomeCategory
        incomeFrequency = bData.incomeFrequency
        incomeAmount = Int(bData.incomeAmount)
        expenseDetails = bData.expenseDetails
        expenseCategory = bData.expenseCategory
        expenseFrequency = bData.expenseFrequency
        expenseAmount = Int(bData.expenseAmount)
    }
    
    init(bData: Data) {
        id = UUID()
        categoryDetails = bData.categoryDetails
        incomeDetails = bData.incomeDetails
        incomeCategory = bData.incomeCategory
        incomeFrequency = bData.incomeFrequency
        incomeAmount = Int(bData.incomeAmount)
        expenseDetails = bData.expenseDetails
        expenseCategory = bData.expenseCategory
        expenseFrequency = bData.expenseFrequency
        expenseAmount = Int(bData.expenseAmount)
    }
}

extension BudgetDetails {
    static let sampleData: [BudgetDetails] =
    [
        BudgetDetails(categoryDetails: "Work", incomeDetails: "", incomeCategory: "", incomeFrequency: .Annually1, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "",expenseCategory: "", expenseFrequency: .Annually1, expenseAmount: 0, expenseAnnual: 0, budgetType: "Category"),
        BudgetDetails(categoryDetails: "Investments", incomeDetails: "", incomeCategory: "", incomeFrequency: .Annually1, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "",expenseCategory: "", expenseFrequency: .Annually1, expenseAmount: 0, expenseAnnual: 0, budgetType: "Category"),
        BudgetDetails(categoryDetails: "Food", incomeDetails: "", incomeCategory: "", incomeFrequency: .Annually1, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "",expenseCategory: "", expenseFrequency: .Annually1, expenseAmount: 0, expenseAnnual: 0, budgetType: "Category"),
        BudgetDetails(categoryDetails: "House", incomeDetails: "", incomeCategory: "", incomeFrequency: .Annually1, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "",expenseCategory: "", expenseFrequency: .Annually1, expenseAmount: 0, expenseAnnual: 0, budgetType: "Category"),
        BudgetDetails(categoryDetails: "", incomeDetails: "Wages", incomeCategory: "Work", incomeFrequency: .Weekly, incomeAmount: 1000, incomeAnnual: 52000, expenseDetails: "",expenseCategory: "", expenseFrequency: .Weekly, expenseAmount: 0, expenseAnnual: 0, budgetType: "Income"),
        BudgetDetails(categoryDetails: "", incomeDetails: "Dividends", incomeCategory: "Investments", incomeFrequency: .Quarterly, incomeAmount: 500, incomeAnnual: 2000, expenseDetails: "",expenseCategory: "", expenseFrequency: .Weekly, expenseAmount: 0, expenseAnnual: 0, budgetType: "Income"),
        BudgetDetails(categoryDetails: "", incomeDetails: "", incomeCategory: "", incomeFrequency: .Weekly, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "Rent",expenseCategory: "House", expenseFrequency: .Weekly, expenseAmount: 500, expenseAnnual: 26000, budgetType: "Expense"),
        BudgetDetails(categoryDetails: "", incomeDetails: "", incomeCategory: "", incomeFrequency: .Weekly, incomeAmount: 0, incomeAnnual: 0, expenseDetails: "Coffee",expenseCategory: "Food", expenseFrequency: .Daily, expenseAmount: 5, expenseAnnual: 1825, budgetType: "Expense")
    ]
}

@Chris_Parker it may look a little messy, I am trying to have persistent data so when opened and closed it will keep / track any changes.

But then also have the ability to export the data (maybe use it on another device) and then import any changes from that file to over right.

Let me know how you go.

Thanks

@Chris_Parker I just realised you may also need this:

import SwiftUI

enum Period: String, CaseIterable, Identifiable, Codable {
    case Daily
    case Weekly
    case Fortnightly
    case Monthly
    case Quarterly
    case Annually
    case Annually1
    
    
    var name: String {
        rawValue.capitalized
    }
    var id: String {
        name
    }
}

Cheers. Errors now gone.

Just taking a few moments to understand what you are trying to achieve.

1 Like

@scott_foley

Scott,

Do you have an example of what you are expecting to see?

If I change the Preview orientation to .portrait this is the view I get:

then if I select Import this is the view I get.

If I had BudgetMaster.data in my documents folder, would that change what I see?

@Chris_Parker thanks for the help, I did a lot of research and worked out I had to clear the data that was in the array and then insert the decoded data from my import.

Thanks