@State initial value not resetting variable on init()

Viewed 366

I have a segmented control that I am using as a tabs in the toolbar of my app linked to the selectedTab variable. when I switch tabs the accounts list changes but it does not reset the lines list. I tried using initialValue on the selected to make sure it reset to 0 but this did not effect it. I tried a print in the init to make sure that the value of selected was 0, after the init. it was every time but it still did not refresh the lines foreach list.

What am I missing?

import SwiftUI
import SQLite3

struct ContentView:View {

    @EnvironmentObject var shared:SharedObject

    var body: some View {
        VStack {
            if shared.selectedTab == 0 {
                LedgerView(ledger: .Accounts)
            } else if shared.selectedTab == 1 {
                LedgerView(ledger: .Budgets)
            } else if shared.selectedTab == 2 {
                ReportsView()
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }

}

struct LedgerView:View {

    @EnvironmentObject var shared:SharedObject

    let ledger:LedgerType
    @State var selected:Int = 0

    init(ledger:LedgerType) {
        self.ledger = ledger
        self._selected = State(initialValue: 0)
    }

    var body:some View {
        HStack {
            VStack(alignment: HorizontalAlignment.leading) {
                ForEach(shared.accounts.filter({$0.ledger == ledger})) { account in
                    Text(account.name)
                        .background(account.id == self.selected ? Color.accentColor : Color.clear)
                        .onTapGesture {self.selected = account.id}
                }
            }
            Divider()
            VStack(alignment: HorizontalAlignment.leading) {
                ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
                    Text("Line#\(line.id)")
                }
            }
        }
    }

}

struct ReportsView:View {
    var body:some View {
        Text("Under Construction ...")
    }
}

class SharedObject:ObservableObject {

    @Published var accounts:[Account] = []
    @Published var journalLines:[JournalLine] = []
    @Published var selectedTab:Int = 0

    init() {
        loadData()    
    }

}

enum LedgerType:Int {
    case Accounts=0,Budgets=1
    var name:String {
        switch(self) {
        case .Accounts: return "Accounts"
        case .Budgets: return "Budgets"
        }
    }
}
struct Account:Identifiable {
    var id:Int
    var name:String
    var ledger:LedgerType
}
struct Payee:Identifiable {
    var id:Int
    var name:String
}
struct Journal:Identifiable {
    var id:Int
    var date:Date
    var payeeID:Int
    var memo:String?
}
struct JournalLine:Identifiable {
    var id:Int
    var journalID:Int
    var accountID:Int
    var amount:Double
}

edit abridged demo code to try to isolate the problem

import SwiftUI

struct ContentView: View {

    @EnvironmentObject var shared:SharedObject

    var body: some View {
        VStack {
            Picker(selection: $shared.selectedTab, label: Text("")) {
                Text("Accounts").tag(0)
                Text("Budgets").tag(1)
            }.pickerStyle(SegmentedPickerStyle())
            Divider()
            if shared.selectedTab == 0 || shared.selectedTab == 1 {
                LedgerView()
            }
            Spacer()
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

}

struct LedgerView:View {

    @State var selected:Int = 0

    init() {
        self._selected = State(initialValue: 0)
        print("LedgerView.init()")
    }

    var body:some View {
        VStack(alignment: HorizontalAlignment.leading) {
            Text("Selected: \(selected)")
            Picker(selection: $selected, label: Text("")) {
                Text("Account#1").tag(1)
                Text("Account#2").tag(2)
                Text("Account#3").tag(3)
            }
        }
    }

}

class SharedObject: ObservableObject {
    @Published var selectedTab:Int = 0
}
2 Answers

Based on your recent comment, what you need is @Binding and not @State. To explain: The struct LedgerView is just an extract of the picker in a separate view. When you call LedgerView(), this doesn't instantiate a new object. It simply adds the picker view in that place. Therefore, when you need the picker to reset on switching tabs, you need to use binding to reset the picker. Here's the working code. Hope it helps.

struct ContentView: View {

    @EnvironmentObject var shared:SharedObject
    @State var selected: Int = 0

    var body: some View {
        VStack {
            Picker(selection: $shared.selectedTab, label: Text("")) {
                Text("Accounts").tag(0)
                Text("Budgets").tag(1)
            }.pickerStyle(SegmentedPickerStyle())
            Divider()
            if shared.selectedTab == 0 || shared.selectedTab == 1 {
                LedgerView(selected: $selected)
            }
            Spacer()
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onReceive(shared.$selectedTab) { newValue in
            self.selected = 0
        }
    }
}

struct LedgerView:View {

    @Binding var selected: Int

    var body:some View {
        VStack(alignment: HorizontalAlignment.leading) {
            Text("Selected: \(selected)")
            Picker(selection: $selected, label: Text("")) {
                Text("Account#1").tag(1)
                Text("Account#2").tag(2)
                Text("Account#3").tag(3)
            }
        }
    }
}

[Earlier answer containing alternate solution]

I've modified your code to make the lines work. I've added sample data to get the code to work. I doubt if the problem is with your data. Also I have modified the enum LedgerType to make it iterable. Here's the working code.

I've modified the following in code:

  1. Removed passing ledgerType to LedgerView as the source of truth is

    @EnvironmentObject var shared: SharedObject

  2. Added code to select the first account by default when switching tabs. This refreshes the lines when switching between tabs. See the code in .onReceive

Hope this helps. If you need anything, let me know.

struct ContentView:View {

    @EnvironmentObject var shared: SharedObject

    var body: some View {
        VStack {
            Picker(selection: $shared.selectedTab, label: Text("")) {
                ForEach(0 ..< LedgerType.allCases.count) { index in
                    Text(LedgerType.allCases[index].rawValue).tag(index)
                }
            }
            .pickerStyle(SegmentedPickerStyle())

            if shared.selectedTab == 0 || shared.selectedTab == 1 {
                LedgerView()
            } else if shared.selectedTab == 2 {
                ReportsView()
            }
            Spacer()
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }

}

struct LedgerView:View {
    @EnvironmentObject var shared:SharedObject
    @State var selected: Int = 0

    var body:some View {
        HStack {
            VStack(alignment: HorizontalAlignment.leading) {
                ForEach(shared.accounts.filter({ $0.ledger == LedgerType.allCases[shared.selectedTab] })) { account in
                    Text(account.name)
                        .background(account.id == self.selected ? Color.accentColor : Color.clear)
                        .onTapGesture {self.selected = account.id}
                }
            }
            Divider()
            VStack(alignment: HorizontalAlignment.leading) {
                ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
                    Text("Line#\(line.id)")
                }
            }
        }
        .onReceive(shared.$selectedTab) { newValue in
            if let id = self.shared.getInitialAccountId(tabIndex: newValue) {
                self.selected = id
            }
        }
    }
}

struct ReportsView:View {
    var body:some View {
        Text("Under Construction ...")
    }
}

class SharedObject:ObservableObject {

    @Published var accounts:[Account] = []
    @Published var journalLines:[JournalLine] = []
    @Published var selectedTab:Int = 0

    func getInitialAccountId(tabIndex: Int) -> Int? {
        if tabIndex == 0 {
            return accounts.filter({
                $0.ledger == LedgerType.Accounts
            }).first?.id
        }
        else if tabIndex == 1 {
            return accounts.filter({
                $0.ledger == LedgerType.Budgets
            }).first?.id
        }
        else {
            return accounts.filter({
                $0.ledger == LedgerType.Reports
            }).first?.id
        }
    }

    init() {
        accounts = [
            Account(id: 1, name: "Sales", ledger: .Accounts),
            Account(id: 2, name: "Purchase", ledger: .Accounts),
            Account(id: 3, name: "Forecast", ledger: .Budgets)
        ]
        journalLines = [
            // Line for sales
            JournalLine(id: 1, journalID: 10, accountID: 1, amount: 200),
            JournalLine(id: 2, journalID: 20, accountID: 1, amount: 400),
            // Line for purchase
            JournalLine(id: 3, journalID: 30, accountID: 2, amount: 600),
            JournalLine(id: 4, journalID: 40, accountID: 2, amount: 800)
        ]
    }

}

enum LedgerType: String, CaseIterable {
    case Accounts = "Accounts"
    case Budgets = "Budgets"
    case Reports = "Reports"
}

struct Account:Identifiable {
    var id:Int
    var name:String
    var ledger:LedgerType
}

struct Payee:Identifiable {
    var id:Int
    var name:String
}

struct Journal:Identifiable {
    var id:Int
    var date:Date
    var payeeID:Int
    var memo:String?
}

struct JournalLine:Identifiable {
    var id:Int
    var journalID:Int
    var accountID:Int
    var amount:Double
}

shared.accounts doesn't exist. you have never initialized it

[...]
  if shared.selectedTab == 0 {
        LedgerView(ledger: .Accounts).environmentObject(SharedObject())
  } else if shared.selectedTab == 1 {
        LedgerView(ledger: .Budgets).environmentObject(SharedObject())

[...]

Edit: Make sure your SceneDelgate also initiates SharedObject otherwise it wont work on physical/simulator device

--

Edit: After running your code and you confirmed you are loading, I ran your code with no change and I don't see any issue, can you look at the gif below and comment what's wrong in what I am seeing?

enter image description here

Related