Reputation: 600
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:
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:
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
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:
Inject the external state to the SwiftUI view, using a @Environment
, @EnvironmentObject
, or @ObservedObject
, depending on your specific use case.
Create a @State
property inside the view to mirror the specific state that triggers the transition.
Add an onChange
modifier to your view and use it to update the view's internalState
property every time the external state changes.
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
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