Runtime warning: "Modifying state during view update, this will cause undefined behavior."

I wrapped the UILabel into swiftui.

struct AttributedLabel: UIViewRepresentable {
        
        // ...
       
        func updateUIView(_ uiView: UILabel, context: Context) {
            uiView.frame.size.width = availableWidth
            uiView.attributedText = nsAttributedText
            uiView.sizeToFit()
            // Modifying state during view update, this will cause undefined behavior.
            self.dynamicHeight = uiView.frame.size.height
        }
    }

the line of self.dynamicHeight = uiView.frame.size.height I may get a running warning:
Modifying state during view update, this will cause undefined behavior.

I know I can wrap it into:

// Or using, Task { @MainActor in }
DispatchQueue.main.async {
    self.dynamicHeight = uiView.frame.size.height
}

But for me, it doesn't make sense, as dynamicHeight is type of @Binding @MainActor var dynamicHeight: CGFloat and uiView is UILabel which is @MainActor. Also, @MainActor func updateUIView(_ uiView:, context:) is @MainActor as well.
Is it a bug?

This has nothing to do with actors -- you just aren't allowed to assign to @Binding (or @State) properties during a view update.

However, I believe SwiftUI questions are generally off-topic for this forum, so I would encourage further questions to go somewhere else like StackOverflow.

it is related to the actor. because to fix this issue, i need to either use DispatchQueue.main.async or Task { @MainActor in }.
This means i need to transfer it to main thread to get rid of this warning.
But according to swift strict concurreny, the code supposes to run on main thread without calling DispatchQueue.main.async or Task { @MainActor in }. this can be langugage issue.

No, updateUIView is already running on the main actor. Both of the things you wrote schedule work to run on the main thread at some point in the future, so the callback doesn't run until the view update has finished.

1 Like

I know updateUIView is running on main thread. but DispatchQueue.main.async or Task { @MainActor in } are also running on the main thread. as main thread is serial. it should not require DispatchQueue.main.async or Task { @MainActor in }.

If you can use iOS 16 as deployment target use sizeThatFits for dynamic height calculation:

1 Like

The warning you see isn't a concurrency warning from the compiler, it's an internal warning of the SwiftUI framework that notifies you that it doesn't support modifying parent views during a view update pass.

3 Likes

If it isn’t concurrency, why does it need to wrap inside DispatchQueue.main.async or Task { @MainActor in } to fix the issue?

Because the framework cannot detect the potential layout loop anymore. The layout is probably still undefined. Before sizeThatFits I also used the DispatchQueue hack sometimes, but there was often some visible layout shifting going on, and sometimes the last text lines were missing.

Is this simplified sample code? Because if it is not – as of iOS 15 SwiftUI's Text view can display AttributedString (which you can initialize from NSAttributedString) just fine.

Yes, I know. our app also supports ios 14.