Search code examples
swiftuiswiftdataswiftui-navigationstack

How to pass data to a sub View from a SwiftData query


I'm learning Swift through a project. I'm creating an app where the user can create multiple accounts and register transactions within each account. The models and views that are relevant to my question are shown below.

When the user taps on an account, they are taken to the transactions view which shows all the transactions for that selected account. The user can then add a new transaction to the account.

What's not clear to me is how to pass the selected account to the TransactionView and the AddTransactionView when using a NavigationStack. The code I currently have throws the error: Cannot convert value of type 'Bindable<Account>' to expected argument type 'Binding<Account>'

If I change Bindable to .constant that seems to work, but I'm not sure if that's correct.

Would appreciate an explanation of why what I have doesn't work as well.

Models

import Foundation
import SwiftData

@Model
final class Account {
    var created: Date
    var name: String
    var transactions: [Transaction]

    init(created: Date, name: String, transactions: [Transaction] = []) {
        self.created = created
        self.name = name
        self.transactions = transactions
    }
}
import Foundation
import SwiftData

@Model
final class Transaction {
    var date: Date
    var value: Decimal?

    init(date: Date, value: Decimal? = nil) {
        self.date = date
        self.value = value
    }
}

Views

import SwiftData
import SwiftUI

struct AccountsView: View {
    @Query private var accounts: [Account]
    @State private var isAddingAccount = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(accounts) { account in
                    NavigationLink(destination: TransactionsView(account: Bindable(account))) {
                        Text(account.name)
                    }
                }
            }
            .navigationTitle("Accounts")
            .toolbar {
                ToolbarItem {
                    Button {
                        isAddingAccount = true
                    } label: {
                        Label("Add Account", systemImage: "plus")
                    }
                }
            }
        }
        .fullScreenCover(isPresented: $isAddingAccount) {
            AddAccountView(isPresented: $isAddingAccount)
        }
    }
}
import SwiftUI

struct TransactionsView: View {
    @Binding var account: Account
    var transactions: [Transaction] { account.transactions }
    @State private var isAddingTransaction = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(transactions) { transaction in
                    NavigationLink(destination: Text(transaction.date.description)) {
                        Text(transaction.value!.formatted())
                    }
                }
            }
            .toolbar {
                ToolbarItem {
                    Button {
                        isAddingTransaction = true
                    } label: {
                        Label("Add transaction", systemImage: "plus")
                    }
                }
            }
        }
        .fullScreenCover(isPresented: $isAddingTransaction) {
            AddTransactionView(isPresented: $isAddingTransaction, account: $account)
        }
    }
}
import SwiftUI

struct AddTransactionView: View {
    @Binding var isPresented: Bool
    @Binding var account: Account
    @State private var newTransaction = Transaction(date: Date())

    var body: some View {
        NavigationView {
            VStack {
                Form {
                    DatePicker(selection: $newTransaction.date, displayedComponents: .date) { Text("Date") }
                        .datePickerStyle(.compact)

                    TextField(value: $newTransaction.value, format: .number) {
                        Text("Value")
                    }
                }
                Button("Add") {
                    addTransaction(transaction: newTransaction)
                    isPresented = false
                }
            }
            .navigationTitle("Add transaction")
            .navigationBarItems(trailing: Button("Cancel") {
                isPresented = false
            })
        }
    }

    private func addTransaction(transaction: Transaction) {
        withAnimation {
            account.transactions.append(transaction)
        }
    }
}

Solution

  • I would suggest you read up more look at some tutorials on how to use @Bindable so you properly understand this.

    You should declare properties in your view using Bindable and not Binding so it should be

    @Bindable var account: Account
    

    and when calling a view with such a property you do not need to do anything special with the value passed

    NavigationLink(destination: TransactionsView(account: account))
    

    As for your actual code you do not need to use @Bindable at all when adding transactions to an account since the Account object isn't updated really. This might be a bit confusing but when adding a transaction to an account object you are updating the relationship between them and not the account object itself.

    So with that said we can remove @Bindable for the Account property in both the child views and change the declaration to

    let account: Account