SwiftUI TabView Off-Screen Tabs Not Redrawing.

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?

In my main project, I have ended up needing to use the method in the demo project: calling .onAppear and checking to see if the value matches the viewModel's value and updating the value as needed. It's a bit frustrating simply because one of the advantages of SwiftUI's declarative nature is that I should not need to manage the state in this way and it feels though adding the update value functions to each form item's subview is simply a hacky way to keep the view's data up-to-date.

SwiftUI TabView Off-Screen Tabs Not Redrawing.
 
 
Q