How to animate `UIHostingController.view` frame when my View's size changes?

I have a UIHostingController on which I have set:

hostingController.sizingOptions = [.intrinsicContentSize]

The size of my SwiftUI content changes with animation (I update a @Published property on an ObservableObject inside a withAnimation block). However, I notice that my hostingController.view just jumps to the new frame without animating the change.

Question: how can I animate the frame changes in UIHostingController that are caused by sizingOptions = [.intrinsicContentSize]

Hello @isaacweisberg,

UIKit has its own .animate you can use on observed changes of .preferredContentSize, which reflects the view's current size.

For more information, ⌘+Click on a .UIHostingController in Xcode to read through the DocC comments that provides much context.

 Travis Trotto - DTS Engineer

Alright, I have tried to follow your advice and I don't see how this can help.

I have this setup:

struct AnimatedView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        let width = viewModel.isExpanded ? 120.0 : 50.0
        let height = viewModel.isExpanded ? 400.0 : 100.0
        let cornerRadius = viewModel.isExpanded ? 42.0 : 8.0
        
        Color(uiColor: .red)
            .frame(width: width, height: height)
            .cornerRadius(cornerRadius)
    }
}

The hosting controller is mounted to the top of its parent via constraints.

Then I do this in my tap handler for a totally separate button:

@objc func tap() {
    withAnimation(Animation.easeInOut(duration: 0.5), {
        viewModel.isExpanded.toggle()
    })
}

And what I observe here is that my hosting controller's view immediately teleports to the new frame - its center is immediately in the position where it should be only at the end of the animation. Meanwhile, the SwiftUI content is animating correctly, except it has already teleported.

I have tried to use your answer @DTS Engineer

I have assigned hostingController.sizingOptions = [.intrinsicContentSize, .preferredContentSize] and added a KVO observation to the \.preferredContentSize.

observation = hostingController.observe(
    \.preferredContentSize,
     options: [.new]
) { [weak self] controller, change in
    guard let self else { return }
    
    UIView.animate(
        withDuration: 0.5,
        delay: 0,
        options: [.curveEaseInOut],
        animations: {

        self.view.layoutIfNeeded()
    })
}

Because my UIHostingController is laid out via constraints, I don't see what else I can do to animate the content other than to ask for a relayout on the parent.

This however, doesn't work. Also, in the KVO handler of the preferredContentSize, I have found that when the closure is called, the center value has already been set to the final value! So even if I launch UIView.animate, there will be no frame updates in the animation transaction, and nothing will be animated.

You can see the problem I am describing in this video:

https://github.com/user-attachments/assets/bb7c9d7c-ef6a-47d9-85fd-27fcc6f12b13

So I don't think that your answer answers my question. And I would like to inquire once again - what is the correct canonical way to animate the UIHostingController changing its position along with the SwiftUI animation?

Here is slide show of the behavior that I am observing. On the right you can see the same UI done in plain UIKit.

@DTS Engineer Can you please take a look?

Hello @isaacweisberg,

I see the UIKit example on the right is animating correctly to me. It looks like you followed the steps correctly:

Line 75 of ViewController.swift the UIHostingController observes .preferredContentSize.

Nice work on implementing that! 😎👍

Can you clarify on what you mean? Are you trying to achieve the same effect in SwiftUI or is the UIKit example on the right not what you were looking for?

Let me know,

 Travis Trotto - DTS Engineer

@DTS Engineer I am expecting the part on the left, in SwiftUI to animate exactly like the counterpart in UIKit on the right.

But they do not;

  • UIKit animation affects both UIView.center and UIView.bounds, and they travel using the same timing curve over the same duration, which makes it seem that the top of the UIKit view is attached to the top of the screen.

  • However, this illusion is not preserved for SwiftUI view because UIHostingController.view.center is not animated; it's teleported to the end position. And I also happen to know that really bounds are not animated either, the UIView actually just fully teleports to the new state, while only the SwiftUI content is animated.

And my question is - what is the way to make UIHostingController animate along its SwiftUI content? Can you maybe confirm that there isn't a built-in support for that?

How to animate `UIHostingController.view` frame when my View's size changes?
 
 
Q