Reputation: 423
I use combine to connect to a REST API which pulls some data into an ObservableObject. The whole setup is really just MVVM. The ObservableObject is then being Observed in a view. Now, I'm stuck with a bug I can't seem to get resolved. What seems to happen is that the View is drawn twice. The first time it updates with the values from the ObservedObject. Then it immediately re-draws, but now the pulled data is suddenly gone. I have confirmed that the code that pulls the data is not executed a second time. Also, the View Hierarchy Viewer seems to suggest that the Text Views which are supposed to be render the data in the view are somehow deleted on the second render run.
This is what the model looks like (the authentication related code is from Firebase):
class ItemViewModel: ObservableObject {
@Published var items = [ScheduleItem]()
private var publisher: AnyCancellable?
func fetchData() {
let currentUser = Auth.auth().currentUser
currentUser?.getIDTokenForcingRefresh(false, completion: { idToken, error in
let url = URL(string: "abc")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer "+String(idToken!), forHTTPHeaderField: "Authorization")
self.publisher = URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(
type: [ScheduleItem].self,
decoder: JSONDecoder()
)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("SINKERROR")
print(error)
case .finished:
print("SINKSUCCESS")
}
},
receiveValue: { repo in
print("DONE")
self.items.append(contentsOf: repo)
}
)
})
}
}
The output from the print statements is DONE SINKSUCCESS Both can only be found once in the debug output.
This is the view:
struct TreatmentCardView: View {
let startDate: String
let uid: Int
@ObservedObject var data = ItemViewModel()
@State private var lastTime: String = "2"
var body: some View {
ZStack {
GeometryReader { geometry in
VStack{
Text("Headline")
.font(.title)
.foregroundColor(Color.white)
.padding([.top, .leading, .bottom])
Print("ISARRAYEMPTY")
Print(String(data.items.isEmpty))
Text(String(data.items.isEmpty))
.font(.headline)
.fontWeight(.light)
.foregroundColor(Color.white)
.frame(width: 300, height: 25, alignment: .leading)
.multilineTextAlignment(.leading)
.padding(.top)
}
}
}
.background(Color("ColorPrimary"))
.cornerRadius(15)
.onAppear{
self.data.fetchData()
}
}
}
Print(String(data.items.isEmpty)) is first false, then true, indicating the view was re-rendered.
What is a bit weird to me is that I would have expected the view to render at least once before the data is pulled, but I don't see any indication of this happening.
I've been trying to make this word for two days now. Any help and advise is greatly appreciated!
Upvotes: 2
Views: 728
Reputation: 52555
My guess is that higher up in your view hierarchy, you have something that is causing ItemViewModel
to refresh. Since it looks like you're using Firebase, my suspicion is that it is something else in your Firebase stack (like perhaps user auth status?).
I took out your API request and replaced it with a simple Future
that is guaranteed to respond just to prove the rest of your code works (which it does).
Your possible solutions are:
@StateObject
, as suggested by another answererObservableObject
that is stored higher in the view hierarchy (for example, and EnvironmentObject
) that gets passed to TreatmentCardView
instead of creating it there each time.To see if you're just getting another side effect from the Firebase API call, here's my simplified version with no Firebase:
class ItemViewModel: ObservableObject {
@Published var items = [String]()
private var publisher: AnyCancellable?
func fetchData() {
let myFuture = Future<String, Error> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
promise(.success("TEST STRING"))
}
}
self.publisher = myFuture
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("SINKERROR")
print(error)
case .finished:
print("SINKSUCCESS")
}
},
receiveValue: { repo in
print("DONE")
self.items.append("String")
}
)
}
}
struct TreatmentCardView: View {
let startDate: String
let uid: Int
@ObservedObject var data = ItemViewModel()
@State private var lastTime: String = "2"
var body: some View {
ZStack {
GeometryReader { geometry in
VStack{
Text("Headline")
.font(.title)
.foregroundColor(Color.white)
.padding([.top, .leading, .bottom])
//
// Print("ISARRAYEMPTY")
// Print(String(data.items.isEmpty))
Text(String(data.items.isEmpty))
.font(.headline)
.fontWeight(.light)
.foregroundColor(Color.white)
.frame(width: 300, height: 25, alignment: .leading)
.multilineTextAlignment(.leading)
.padding(.top)
}
}
}
.background(Color("ColorPrimary"))
.cornerRadius(15)
.onAppear{
self.data.fetchData()
}
}
}
Upvotes: 2
Reputation: 8178
I don't know why the view redraws itself, but you may use @StateObject
instead of @ObservedObject
for your ItemViewModel
. Your pulled data should stay that way.
@StateObject var data = ItemViewModel()
The ObservedObject
will be recreated every time a view is discarded and redrawn, where StateObject
keeps it.
Upvotes: 1