user482594
user482594

Reputation: 17486

In SwiftUI, how can I avoid redrawing a parent view while updating shared state data from a child view to avoid Navigation issues?

Since Xcode 12.5, I am seeing a lot of "Unable to present. Please file a bug." console logs in the Xcode console which I remember not seeing prior to 12.5.

This message is shown when I am using NavigationLink from a parent view to navigate to a child view, and if logic in the child view updates one of the states that parent view depends on.

Below is a sample pseudo code where a list of messages are shown in the parent view, message detail is shown in the child view from a list, and lastly, independent settings child view.

struct MessageListView: View {
    ...
    @StateObject var messageList = MessageList()

    var body: some View {
        debugPrint("!!MessageListView has been redrawn!!")
        return VStack {
            NavigationLink(destination:SettingsView()){
                Text("Go to settings")
            }
            ForEach(messageList.data.sorted(by: {$0.key < $1.key}), id:\.key) { k, m in
                NavigationLink(destination:MessageView(message:...){
                    Text(m.text)
                }
            }
        }
    }
}

struct MessageView: View {
    ...
    @ObservedObject var messageList : MessageList
    @ObservedObject var message : Message
    ...
    var body: some View {
        Text(...)
        .onAppear {
            messageList.readAndIncrement(message.id) //<- This updates both this view & parent view.
        }
    }
}

class MessageList : ObservableObject {
    @Published var data : [String:Message] = [:]

    func readAndIncrement(id: String){
        //Modify one of the message in dictionary.
    }
}

So when user clicks on message and traverses the navigation like below,

MessageListView -> MessageView

As soon as MessageView appears on screen, it will increment message's "read count" due to at the logic in onAppear, which will update data in MessageListView at the same time.

As soon as that happens, it appears that the parent view, MessageListView which is observing MessageList objects gets updated, the two following things happen.

  1. debugPrint will print message !!MessageListView has been redrawn!! (from source code above) on console, which proves that the parent view has been updated while Navigation is currently showing child view.
  2. 'Unable to present. Please file a bug.' console log gets shown on screen, probably because the update in the parent view "cannot" be presented on screen?

So SwiftUI seems to be throwing "Unable to present" error log when I am updating parent view's observed data while viewing & interacting in the child view, but I am unsure how I can properly fix to get rid of this error.

The reason that I am thinking this is not a bug, but my error is because of the following.

When user traverses into a completely different view, something like

MessageListView -> SettingsView

and if the following two conditions are met,

  1. The child view (SettingsView) does not use or rely on any of the parent view's state objects.
  2. Parent view's data model is updated by other means, such as from network updates/sync, periodic refresh, etc.

then the parent view (MessageListView) gets redrawn for its dependent model's every single update and the child view starts stacking itself like below.

enter image description here

At least, in the above case, Unable to present. Please file a bug. doesn't get shown in the console.

In the iOS 14.5 & Xcode 12.5 patch notes, the only comments related to NavigationLink is as follows, but that does not seem to be relevant to my case.

The destination of NavigationLink that only differs by local state now resets that state when switching between links as expected. (72117345)

So my question is... how can I properly manage states (or decouple them) in this case that won't cause funkiness in SwiftUI Navigation?

Is it against SwiftUI's paradigm to update parent view's state data while presenting child view on screen or is this simply a NavigationLink bug in Xcode 12.5?

Upvotes: 6

Views: 2224

Answers (1)

user482594
user482594

Reputation: 17486

I am answering my own question about the workaround that I found. The solution that worked for me is to use Class instead of Struct and update properties of the Class. This prevented the View, which is watching the @Published array/list, from getting updated.

Previously, I was using Struct for the Message type. What ended up happening is that any change to one of the properties of the struct/Message will trigger UI updates (which is expected), even if View is not directly watching the struct itself, but watching the list of structs (MessageList class's data property in my case.).

I converted Message from Struct to Class that implements ObservableObject and updates its properties. That prevented parent View, which dependent on the list of Messages from getting redrawn, when one of the list elements had to be updated.

In summary, when you assign @Published annotation to an array of Struct, list item addition/removal as well as any change to one of the properties of the struct item in the list will trigger dependent View updates in SwiftUI.

On the other hand, if you assign @Published annotation to an array of Class, only list item addition/removal will trigger SwiftUI update. Any change to one of the properties of the Class won't trigger dependent View updates in SwiftUI.

Upvotes: 2

Related Questions