1

I have a data model in my SwiftUI app that looks something like this:

struct Entry: Identifiable{
  var id = UUID().uuidString
  var name = ""

  var duration: Int{
    //An SQLite query that returns the total of the "duration" column
    let total = try! dbc.scalar(tableFlight.filter(db.entry == id).select(db.duration.total))
    return Int(total)
  }
}

struct Flight: Identifiable{
  var id = UUID().uuidString
  var duration = 0
  var entry: String?
}

I have an ObservableObject view model that produces the entries like this:

class EntryModel: ObservableObject{
  static let shared = EntryModel()
  @Published var entries = [Entry]()

  init(){
    get()
  }

  func get(){
    //Stuff to fetch the entries
    entries = //SQLite query that returns an array of Entry objects
  }
}

Then finally, in my View, I list all the entry names and their associated duration like this:

ForEach(modelEntry.entries){ entry in
  VStack{
    Text(entry.name) //<-- Updates fine
    Text(entry.duration) //<-- Gets set initially, but no updates
  }
}

The issue I'm having is that when I update a Flight for that Entry, the duration in my view doesn't update. I know that won't happen because only the entries will redraw when they are changed.

But even if I manually call the get() function in my EntryModel, the associated duration still doesn't update.

Is there a better way to do this? How do I get the parent's computed properties to recalculate when its child element is updated?

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128
  • How about calling `objectWillChange.send()` after `get()`? I'm not sure if this would work or not, but it's worth a try -- at least it would signal the View to try to update. – jnpdx Oct 16 '21 at 04:55
  • 1
    The thing that I don't get is that your duration is not even coming from anything in your entry model. So what exactly are you updating in your entry model?. Because dbc is not in your model. I can only assume (and fear) that it's some sort of global variable (or you used lowercase for a class to confuse the reader). – Jacob Oct 16 '21 at 05:07
  • As indicated by the comment under the `duration` property, it's an SQLite query that returns related data. `dbc` is a database singleton. It's not really relevant to the issue at hand, though. – Clifton Labrum Oct 16 '21 at 05:22
  • Sorry, but that's just anti-value-types coding. A value type should not have hidden dependencies like this, and especially be able to behave differently if used at different points in time. That one should better be a global function, or, even better, part of a class that exposes the `dbc` dependency. – Cristik Oct 16 '21 at 09:04
  • 1
    I see. Don’t do that. It’s going to hurt you. Put that query in the initializer. Your query will be ran every time the view is re-rendered not just for description but anything. – Jacob Oct 16 '21 at 12:21
  • @Cristik @Kubee I’m relatively new to `struct`s so I would love to learn more. So my `duration`’s query should be in the `init` of `Entry`? How does the duration get set and stay updated? Do you have any examples I could look at? – Clifton Labrum Oct 16 '21 at 15:25
  • You likely need a new architecture there, if you want to have the property return the real-time value from the database. A struct is simply not appropriate for this. A class is a better candidate, as a class instance has identity, so you can keep a reference to the database connection, and use it to query the value. – Cristik Oct 19 '21 at 09:00

1 Answers1

0

I figured it out. My actual code used a child View inside the ForEach where the VStack is. I was just passing an entry to it, so the values were only getting set initially and were thus not reactive.

By changing that entry to a Binding, it's working:

ForEach($modelEntry.entries){ $entry in
  ChildView(entry: $entry)
}

Note that the $ on the $modelEntry and the return $entry is an Xcode 13+ feature (but backward compatible to iOS 14 and macOS 11.0).

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128