@State variable returns empty despite being set in .onAppear function

With the code below, JSON data is parsed and is stored in the variable data in the .onAppear function, however an empty set of data is passed to the Content view. How can that be fixed so that the JSON data passes to the DataView?

struct ContentView: View {
    @State var data: [Data]
    @State var index: Int = 0
    
    var body: some View {
        VStack {
            DataView(data: data[index])
        }
        .onAppear {
            let filePath = Bundle.main.path(forResource: "data", ofType: "json")
            let url = URL(fileURLWithPath: filePath!)
                
            data = getData(url: url)
        }
    }
    
    func getData(url: URL) -> [Data] {
        do {
            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data].self, from: data)
            
            return jsonDecoded
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
        
        return []
    }
}
Answered by Claude31 in 853587022

That's the error I told you in a previous post.

    @State var datum: Data

Replace with

    var datum: Data

If tha works, don't forget to close the thread by marking this answer as correct. Otherwise, explain.

Could you provide complete code as well as the json file ?

Is Data a structure you have defined ?

If it is an empty set, it should crash at:

            DataView(data: data[index])

Do you get Fail messages ?

Problem is not in onAppear. I tested by replacing getData by a dummy call .onAppear {

//            let filePath = Bundle.main.path(forResource: "data", ofType: "json")
//            let url = URL(fileURLWithPath: filePath!)
//                
//            data = getData(url: url)
            data = [Data()]
        }

And data DataView is not empty, as I could check with the following

struct DataView: View {
    
    @State var data: Data
    var body: some View {
        Text("DataView \(data)")
    }
}

So, it looks like you return from getData by line 29 and not 22.

So, could you change line 29, to return a custom single data to check ?

struct ContentView: View {
    @State var data: [Data]
    @State var index: Int = 0
    
    var body: some View {
        VStack {
           DataView(data: features[index])
        }
        .onAppear {
            print("on appear")
            
            let filePath = Bundle.main.path(forResource: "data", ofType: "json")
            let url = URL(fileURLWithPath: filePath!)
                
            data = getData(url: url)
            
            print(data)
        }
    }
    
    func getData(url: URL) -> [Data] {
        do {
            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data].self, from: data)
            
            return jsonDecoded
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
        
        print("test 1")
        
        return [Data(index: 0, str: "")]
    }
}
struct Data: Decodable, Hashable {
    let index: Int
    let str: String
}
[
    {
        "index": 0,
        "str": "String 1"
    },
    {
        "index": 1,
        "str": "String 2"
    }
]

Thanks.

But your code does not compile, with the following errors:

Does it compile for you ?

My apologies. The code below should compile.

struct ContentView: View {
    @State var sampleData: [SampleData]
    @State var index: Int = 0
    
    var body: some View {
        VStack {
           DataView(data: sampleData[index])
        }
        .onAppear {
            print("on appear")
            
            let filePath = Bundle.main.path(forResource: "data", ofType: "json")
            let url = URL(fileURLWithPath: filePath!)
                
            sampleData = getData(url: url)
            
            print(sampleData)
        }
    }
    
    func getData(url: URL) -> [SampleData] {
        do {
            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([SampleData].self, from: data)
            
            return jsonDecoded
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
        
        print("test 1")
        
        return [SampleData(index: 0, str: "")]
    }
}

It is a question of timing.

When onAppear returns, and DataView is called, the array is not yet populated, even though its count is already OK.

You have to wait for a short time in onAppear (0.01 s should be OK).

Here is the solution.

I had to rename Data to avoid the error above (Data is a Foundation type). I renamed to Data2

struct Data2: Decodable, Hashable {
    let index: Int
    let str: String
}

struct DataView: View {
    
    @State var data: Data2

    var body: some View {
        Text("DataView \(data.str)")
    }
}

struct ContentView: View {
    @State var showView = false // To wait for array to be populated before we display the view
    @State var data: [Data2]
    @State var index: Int = 0
    
    var body: some View {
        VStack {
            if showView {
                DataView(data: data[index])
                // Text("Hello \(data.count)") // To test if needed ; without async, data.count is 2. Seems OK, but…
                // Text("Hello \(data.count) index \(index) \(data[0].str)") // but without async, it will crash, as data is not populated yet, even if count is already 2
            }
        }
        .onAppear {
            let filePath = Bundle.main.path(forResource: "data", ofType: "json")
            let url = URL(fileURLWithPath: filePath!)
            
            data = getData(url: url)
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { //  <<-- THE KEY POINT 0.1 sec delay
                self.showView = true
            }
        }
    }
    
    func getData(url: URL) -> [Data2] {
        
        do {
            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: data)
            
            return jsonDecoded
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
        
        return []
    }
}

Credit: https://stackoverflow.com/questions/62355428/how-do-i-show-a-new-view-after-some-delay-amount-of-time-swiftui

A simpler option is to use an init:

struct ContentView: View {
    @State var data: [Data2]
    @State var index: Int = 0
    
    init() {
        let filePath = Bundle.main.path(forResource: "data", ofType: "json")
        let url = URL(fileURLWithPath: filePath!)
        
          //        data = getData(url: url) // We cannot call getData in init
        do {
            let dataRead = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: dataRead)
            
            data = jsonDecoded
            return
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }

        data = []
        return
    }
    
    var body: some View {
        VStack {
            DataView(data: data[index])
        }
    }
    
}

Another point.

In DataView, index should not be a State variable.

Add a button in ContentView

        VStack {
            if showView && (index >= 0) && (index < data.count) {
                DataView(datum: data[index])
            }
            
            Spacer()

            HStack {
                Text("Index: \(index) -> ")
                Button("Next")  {
                    index += 1
                    if index >= data.count { index = 0 }
                }
            }
        }

You will see that text in DataView does not change

        Text("DataView \(datum.str)")

always says DataView String 1

Now change DataView as follows to get it updated on index change by button press:

struct DataView: View {
    
    /*@State*/ var datum: Data2
    var body: some View {
        Text("DataView \(datum.str)")
    }
}

Note: I renamed data to datum for an array item, to differentiate from the array.

Now, I have to update my initial answer.

In fact, no need for showView and Dispatch (even though that made it work).

Just test index:

            if (index < data.count) {
                DataView(datum: data[index])

Without the index test, it crashes…

Swift/ContiguousArrayBuffer.swift:691: Fatal error: Index out of range

Reason is that DataView is not called immediately but only when State var data is populated by getData (then count changes) and has all its elements.

So the test has a similar effect as Dispatch to wait for data to be fetched.

I have a follow up question which you may have answered but I'm not sure. I am incrementing/decrementing a @State variable as a @Binding variable in another view:

IndexView(current: $index)
    .onChange(of: index) { newValue in
        print("new value \(newValue)")
       }

The variable newValue prints as it should (incrementing and decrementing) but the index variable doesn't seem to change.

DataView(datum: data[index])

The DataView only displays data of index 0 despite the index incrementing.

Did you change datum to a simple var and not a State var ?

Could you show the complete code, to see how Binding is used.

Logically,

@Binding var index: Int

should be in the ContentView and the

@State var index = 0

in the other view

But I would need to check.

PS: don't forget to close the thread, eventually starting a new one where you reference this one.

struct ContentView: View {
    @State var data: [Data2]
    @State var index: Int = 0
    
    init() {
        let filePath = Bundle.main.path(forResource: "data", ofType: "json")
        let url = URL(fileURLWithPath: filePath!)
        
          //        data = getData(url: url) // We cannot call getData in init
        do {
            let dataRead = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: dataRead)
            
            data = jsonDecoded
            return
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
 
        data = []
        return
    }
    
    var body: some View {
        VStack {
            DataView(data: data[index])
            SlideDots(current: $index)
        }
    }
    
}
struct SlideDots: View {
    @Binding var current: Int
    
    var body: some View {
        HStack {
            if current > 0 {
                Spacer()
                    .frame(width: 25)
                
                Button(action: { if current > 0 { current -= 1 }}) {
                    Image(systemName: "chevron.left")
                }
                .foregroundStyle(getColor(type: .main))
                .frame(width: 35)
            } else {
                Spacer()
                    .frame(width: 60)
            }
            
            Spacer()
            
            ForEach(0..<4) { index in
                Spacer()
                    .frame(width: 15)
                
                Circle()
                    .fill(index == current ? Color.gray : Color.white)
                    .stroke(Color.gray, lineWidth: 1)
                    .frame(width: 15, height: 15)
                
                Spacer()
                    .frame(width: 15)
            }
            
            Spacer()
            
            if current < 3 {
                Button(action: { if current < 3 { current += 1 }}) {
                    Image(systemName: "chevron.right")
                }
                .foregroundStyle(getColor(type: .main))
                .frame(width: 35)
                
                Spacer()
                    .frame(width: 25)
            } else {
                Spacer()
                    .frame(width: 60)
            }
        }
    }
}

Why is it data and not datum on line 28 of the first code ?

            DataView(data: data[index])

With this simple change, it works here.

            DataView(datum: data[index])

The screenshots show the change in String x

That was an error on my end. It should be datum.

Here is a revised version of code:

struct ContentView: View {
    @State var data: [Data2]
    @State var index: Int
    
    init() {
        index = 0
        
        let filePath = Bundle.main.path(forResource: "data", ofType: "json")
        let url = URL(fileURLWithPath: filePath!)
        
        do {
            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: data)
            
            data = jsonDecoded
            
            return
        } catch let error as NSError {
            print("Fail: \(error.localizedDescription)")
        } catch {
            print("Fail: \(error)")
        }
        
        data = []
        
        return
    }
    
    var body: some View {
        VStack {
            DataView(datum: data[index])
            
            Spacer()
            
            SlideDots(current: $index)
    }
}

This cannot compile. data is a constant that you try to modify.

            let data = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: data)
            
            data = jsonDecoded

The code I sent was:

            let dataRead = try Data(contentsOf: url)
            let jsonDecoded = try JSONDecoder().decode([Data2].self, from: dataRead)
            
            data = jsonDecoded

With these changes, your code works OK.

Could you show the DataView code ?

struct DataView: View {
    @State var datum: Data
    
    var body: some View {
        VStack {
            Text(datum.str)
        }
    }
}
Accepted Answer

That's the error I told you in a previous post.

    @State var datum: Data

Replace with

    var datum: Data

If tha works, don't forget to close the thread by marking this answer as correct. Otherwise, explain.

&#64;State variable returns empty despite being set in .onAppear function
 
 
Q