muvaaa
muvaaa

Reputation: 600

Why SwiftUI-transition does not work as expected when I use it in UIHostingController?

I'm trying to get a nice transition for a view that needs to display date. I give an ID to the view so that SwiftUI knows that it's a new label and animates it with transition. Here's the condensed version without formatters and styling and with long duration for better visualisation:

struct ContentView: View {
    @State var date = Date()

    var body: some View {
        VStack {
            Text("\(date.description)")
                .id("DateLabel" + date.description)
                .transition(.slide)
                .animation(.easeInOut(duration: 5))

            Button(action: { date.addTimeInterval(24*60*60) }) {
                Text("Click")
            }
        }
    }
}

Result, it's working as expected, the old label is animating out and new one is animating in:

Result1

But as soon as I wrap it inside UIHostingController:

struct ContentView: View {
    @State var date = Date()

    var body: some View {
        AnyHostingView {
            VStack {
                Text("\(date.description)")
                    .id("DateLabel" + date.description)
                    .transition(.slide)
                    .animation(.easeInOut(duration: 5))

                Button(action: { date.addTimeInterval(24*60*60) }) {
                    Text("Click")
                }
            }
        }
    }
}

struct AnyHostingView<Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIHostingController<Content>
    let content: Content

    init(content: () -> Content) {
        self.content = content()
    }

    func makeUIViewController(context: Context) -> UIHostingController<Content> {
        let vc = UIHostingController(rootView: content)
        return vc
    }

    func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
        uiViewController.rootView = content
    }
}

Result, the new label is not animated in, rather it's just inserted into it's final position, while the old label is animating out:

enter image description here

I have more complex hosting controller but this demonstrates the issue. Am I doing something wrong with the way I update the hosting controller view, or is this a bug in SwiftUI, or something else?

Upvotes: 4

Views: 1680

Answers (2)

Mischa
Mischa

Reputation: 17269

I encountered a similar problem: The transition worked properly when used inside a SwiftUI view, but didn't work at all when wrapped inside a UIKit view with a UIHostingController.

I also have the problem that the state that triggers the transition is held outside of the view and needs to stay there (source of truth). However, based on Asperi's recommendation to move the state inside the SwiftUI view, the following recipe has proven to work for me:

  1. Inject the external state to the SwiftUI view, using a @Environment, @EnvironmentObject, or @ObservedObject, depending on your specific use case.

  2. Create a @State property inside the view to mirror the specific state that triggers the transition.

  3. Add an onChange modifier to your view and use it to update the view's internalState property every time the external state changes.


Example

struct MySwiftUIView: View {
    
    @EnvironmentObject var externalState: MySourceOfTruth
    @State var internalState: Date = .now
    
    var body: some View {
        Text("\(internalState)")
            .transition(.slide)
            .animation(.default, value: internalState)
            .onChange(of: externalState, initial: false) { _, newValue in
                internalState = externalState.date.description
            }
    }
} 

Upvotes: 0

Asperi
Asperi

Reputation: 258247

State do not functioning well between different hosting controllers (it is not clear if this is limitation or bug, just empirical observation).

The solution is embed dependent state inside hosting view. Tested with Xcode 12.1 / iOS 14.1.

struct ContentView: View {
    var body: some View {
        AnyHostingView {
            InternalView()
        }
    }
}

struct InternalView: View {
    @State private var date = Date()   // keep relative state inside
    var body: some View {
        VStack {
             Text("\(date.description)")
                  .id("DateLabel" + date.description)
                  .transition(.slide)
                  .animation(.easeInOut(duration: 5))

             Button(action: { date.addTimeInterval(24*60*60) }) {
                  Text("Click")
             }
        }
    }
}

Note: you can also experiment with ObservableObject/ObservedObject based view model - that pattern has different life cycle.

Upvotes: 3

Related Questions