DrGonzoX
DrGonzoX

Reputation: 423

Data from ObservedObject not rendered in SwiftUI view

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

Answers (2)

jnpdx
jnpdx

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:

  1. Use @StateObject, as suggested by another answerer
  2. Move your API calls to an ObservableObject 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

Kai Zheng
Kai Zheng

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

Related Questions