Running into a weird issue with TabViews not rerendering the view when objectWillChange.send() is called (either manually or with @Published). For context, in my real project I have a tab with form data and the adjacent tab is a summary tab which renders a few elements from the form data. The summary tab is not getting updated when the form data changes. I have created a simple demo project that demonstrates the issue. The project can be found here.
The content view is just a tab view with four tabs, all of which point to the same core data object.
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@State private var selectedIndex: Int = 0
var tabTitles: Array<String> = ["Tab 1", "Tab 2", "Tab 3", "Tab 4"]
var body: some View {
// Create a page style tab view from the tab titles.
TabView(selection: $selectedIndex) {
ForEach(tabTitles.indices, id: \.self) { index in
TextView(viewModel: TextViewModel(
title: tabTitles[index],
context: viewContext))
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
The text view just contains the title and a text field for updating the core data object in the view model.
struct TextView: View {
@ObservedObject private var viewModel: TextViewModel
@State private var text: String
private var relay = PassthroughSubject<String, Never>()
private var debouncedPublisher: AnyPublisher<String, Never>
init(viewModel: TextViewModel) {
self.viewModel = viewModel
self._text = State(initialValue: viewModel.textValue)
self.debouncedPublisher = relay
.debounce(for: 1, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
var body: some View {
LazyVStack {
Text(viewModel.title)
.font(.title)
TextField("write something", text: $text)
.onChange(of: text) {
relay.send($0)
}
}
.padding()
.onReceive(debouncedPublisher) {
viewModel.textValue = $0
}
/// Without this the view does not update, with it the view updates properly...
.onAppear {
self.text = viewModel.textValue
}
}
}
And the view model is pretty simple. I've tried making item @Published, I've tried making the text value computed (with @Published item), etc. This example uses a combine publisher on item's value attribute which is a lot like what I'm doing in the main project.
class TextViewModel: ObservableObject {
@Published var textValue: String {
didSet {
// Check that the new value is not the same as the current item.value or else an infinte loop is created.
if item.value != textValue {
item.value = textValue
try! context.save()
}
}
}
private(set) var item: Item
private(set) var title: String
private var subscriber: AnyCancellable?
private var context: NSManagedObjectContext
init(title: String, context: NSManagedObjectContext) {
self.title = title
self.context = context
let request = NSFetchRequest<Item>(entityName: "Item")
request.predicate = NSPredicate(format: "%K == %@", #keyPath(Item.key), "key")
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Item.value), ascending: true)]
let fetched = try! context.fetch(request)
let fetchedItem = fetched.first!
self.textValue = fetchedItem.value!
self.item = fetchedItem
// Create a publisher to update the text value whenever the value is updated.
self.subscriber = fetchedItem.publisher(for: \.value)
.sink(receiveValue: {
if let newValue = $0 {
self.textValue = newValue
}
})
}
}
Item is just a simple core data property with a key: String and value: String. I know I can directly bind the view to to the text value using $viewModel.textValue. It doesn't update the view when the value changes either and I don't want that behavior in my real app for a variety of reasons. Is there something that I am missing here? Do I really need to call onAppear for all of my views within the TabView to check and see if the value is up-to-date and update it if needed? It seems a bit silly to me. I haven't really found much info out there on this. I've also tried forcing a redraw using the (super yucky) use of @State var redraw: Bool and toggling it in onAppear. That does not trigger a redraw either.
The other thing I've tried that works is setting an @State isSelected: Bool on the TextView and in the ForEach setting it to index == selectedIndex. This works and may be the least revolting solution I have found. Thoughts?