0

I have a List that is updated with a Fetch class, an ObservableObject. It has an init function. This is that Fetch class.


@Published private(set) var items: [ItemsResult] = []
    
    init() {
        let url = URL(string: "[redacted]")!
        
        URLSession.shared.dataTask(with: url) {(data, response, error) in
            do {
                if let itemsData = data {
                    let decodedData = try JSONDecoder().decode([ItemsResult].self, from: itemsData)
                    DispatchQueue.global(qos: .utility).async {
                        DispatchQueue.main.async {
                            print("running task")
                            self.items = decodedData
                        }
                    }
                    
                    print(self.items)
                } else {
                    print("No data")
                }
            } catch {
                print("Error: \(error)")
            }
        }.resume()
    }

When the app is build, it correctly displays the data returned by the API and it matches the database. However when I tap / click on one in order to delete it, or use the textarea I've added to add a new item it doesn't update.

struct TickrApp: View {
    @EnvironmentObject var fetch: Fetch

    var body: some View {
        NavigationView {
            Form {
                Section {
                    VStack(alignment: .center) {
                        Text("Welcome to Tickr")
                    }
                }
                Section {
                    List(fetch.items) { item in
                        CheckView(checked: item.done, title: item.content.replacingOccurrences(of:"_", with: " "))
                    }
                }
                AddItemView()
            }.navigationBarTitle(Text("Tickr"))
        }
    }
}

The database is being updated as shown when I log the decodedData they respond, however in each I just call Fetch(). Requests are made the same in all three cases.

One of the calls, for text input.

  func toggle() {
    checked = !checked
        let url = URL(string: "")!
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        
        let task = URLSession.shared.dataTask(with: req) { data, response, error in
            guard let _ = data,
                let response = response as? HTTPURLResponse,
                error == nil else {
                print("error", error ?? "Unknown error")
                return
            }
    
            guard (200 ... 299) ~= response.statusCode else {
                print("statusCode should be 2xx, but is \(response.statusCode)")
                print("response = \(response)")
                return
            }
        }
        task.resume()
        Fetch()

In order to update the list visually I need to completely quit the app / rerun it in order to have the new and/or deleted items show correctly. No errors show about background publishing changes or anything.

Noah64
  • 89
  • 6
  • I think you're missing some important code here. You said that the API call works (at least initially), which is the entirety of the code here. Then, you reference tapping one of the items or using a text area, but none of that code is shown. It's also not clear from the included code how your `List` would call the `ObservableObject`'s `init` again. Can you update with a [mre]? – jnpdx Jul 28 '21 at 19:44
  • @jnpdx yeah sorry about that, rushed it a bit. How's this edit? – Noah64 Jul 28 '21 at 22:22

1 Answers1

1

It appears that you're trying to call Fetch() to refresh your data. There are two things that are going to be a problem with this:

  1. You're calling it outside of the dataTask completion handler. That means that it may get called before the write finishes

  2. Calling Fetch() just creates a new instance of Fetch, when what you really want to do is update the results on your existing instance.

I'm assuming that your first code snipped is from Fetch. I'd change it to look more like this:

class Fetch: ObservableObject {
  @Published private(set) var items: [ItemsResult] = []

  init() {
    performFetch()
  }

  func performFetch() {
    //your existing fetch code that was in `init`
  }
}

Then, in your AddItemView and CheckView, make sure you have this line:

@EnvironmentObject var fetch: Fetch

This will ensure you're using the same instance of Fetch so your list will reflect the same collection of results.

Once you're done with an operation like toggle(), call self.fetch.performFetch() to update your results. So, your last code snippet would turn into something like this:

let task = URLSession.shared.dataTask(with: req) { data, response, error in 
  //guard statements to check for errors
  self.fetch.performFetch() //perform your refresh on the *existing* `Fetch` instance
}

A bigger refactor would involve moving your async code (like toggle) to a view model, instead of doing any of it in a View code. Also, look into using the URLSession Publishers using Combine, since you're using SwiftUI: https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine

jnpdx
  • 45,847
  • 6
  • 64
  • 94