Reputation: 1156
I have a SwiftUI app that fetches some information from the backend when the view appears and then attempts to update the State by setting @Published
vars in an ObservableObject
. The problem I have is it doesn't update at first fetch (it remains empty since it was initialized with an empty array) but if I click to another view and come back it's updated (since the information was already fetched).
Obviously, the intended thing I'm going for with using @Published
is for the view to update once the information is fetched. This is part of a larger app but I have the reduced version of what I have below.
First, we have a parent view that contains the view I want to update.
struct ParentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
SummaryView()
// In real life I have various forms of summary
// but to simplify here I will just use this one SummaryView.
SummaryView()
SummaryView()
}
}
}
}
}
Here is the summary view itself:
struct SummaryView: View {
@ObservedObject var model = AccountsSummaryViewModel()
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Accounts")
.font(.title)
Spacer()
NavigationLink(
destination: AccountView(),
label: {
Image("RightArrow")
})
}
if model.accounts.count > 0 {
Divider()
}
// And if I add the following line for debugging
//Text(model.accounts.count)
// It remains 0.
ForEach(model.accounts, id: \.id) { account in
Text(account.account.text)
}
}
.padding()
.onAppear() {
model.onAppear()
}
}
}
Here is it's simple view model:
class AccountsSummaryViewModel: ObservableObject, Identifiable {
@Published var accounts: [AccountIdentifiable] = []
func onAppear() {
AccountsService.accounts { (success, error, response) in
DispatchQueue.main.async {
// This always succeeds
if let response = response {
// All AccountIdentifiable does is make a struct that is Identifiable (has an account and a var id = UUID())
self.accounts = Array(response.accounts.map { AccountIdentifiable(account: $0) }.prefix(3))
}
}
}
}
}
Here is the contents of the AccountsService also, I will note that the URL is a localhost but I'm not sure if that matters:
public struct AccountsService {
public static func accounts(completion: @escaping ((Bool, Error?, AccountsResponse?) -> Void)) {
guard let url = getAllAccountsURL() else {
completion(false, nil, nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = ["Content-Type": "application/json",
BusinessConstants.SET_COOKIE : CredentialsObject.shared.jwt]
let task = URLSession.shared.dataTask(with: request) { (data, urlResponse, error) in
guard let data = data else {
completion(false, error, nil)
return
}
guard let response = try? JSONDecoder().decode(AccountsResponse.self, from: data) else {
completion(false, error, nil)
return
}
// This does successfully decode and return here.
completion(true, nil, response)
return
}
task.resume()
}
private static func getAllAccountsURL() -> URL? {
let address = "\(BusinessConstants.SERVER)/plaid/accounts"
return URL(string: address)
}
}
I have read that there are issues with an empty ScrollView
, however, my ScrollView
is never empty as I have those static text elements. I also read that if you use a ForEach
without the id it can fail - but you can see I am using the id so I'm kind of at a loss.
I have print statements in the onAppear()
so I know it runs and successfully sets the @Published accounts
but looking at the UI and putting breakpoints in the ForEach
I can see the view does not update. However, if I navigate somewhere else in my app, and then come back to the ParentView
then since the @Published accounts
is non-empty (already fetched) it updates perfectly.
Upvotes: 3
Views: 870
Reputation: 1156
The reason it was not working was due to the fact that I was using @ObservedObject
instead of @StateObject
in SummaryView. Making the change fixed the issue.
Upvotes: 0
Reputation: 91
It looks like you're running into a problem because of the two levels of observed objects, with model: AccountsSummaryViewModel
containing accounts: [AccountIdentifiable]
.
SwiftUI will only watch one level, leading to your ParentView
not updating when accounts
is set more than one UI level down.
As discussed here, one option is to use PublishedObject via the Swift Package Manager in Xcode. Changing model
in your SummaryView
to @PublishedObject
may be all that's required to fix this.
Upvotes: 1