Open ln-12 opened 3 years ago
Hi @ln-12, the problem here is due to the if-else
statement in the SecondView
. The transition between the ProgressView
and the Text
(there's always a transition in SwiftUI when you write an if-else
like that) conflicts somehow with the screen transition. You can try to fix it by removing the else
branch:
struct SecondView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
Text(viewModel.someText)
Spacer()
}
.background(Color.red)
}
}
Yeah, I already noticed that. Unfortunately, my real UI is much complexer (I have multiple else branches), so that this won't work.
So you think this is a SwiftUI problem and not specific to your library, right?
Well... basically yes. It depends on how SwiftUI manages the if-else
transition in those cases. In your example you have two if-else
transitions "overlapping" (the one involving the screens and the one involving the content of the SecondView
). The second transition is not performed correctly. But it doesn't strictly depends on the navigation stack. Take a look at this simple example (it's basically your example without the navigation stack):
@main
struct MyApp: App {
@StateObject private var viewModel = ViewModel()
@State private var push = false
private let transition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading))
private let transitionAnimation = Animation.easeOut(duration: 3)
var body: some Scene {
WindowGroup {
if !push {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
Button(action: {
push.toggle()
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
DispatchQueue.main.sync {
self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
}
}
}){
Text("Go")
}
Spacer()
}
.background(Color.green)
.transition(transition)
.animation(transitionAnimation)
} else {
SecondView()
.transition(transition)
.animation(transitionAnimation)
.environmentObject(viewModel)
}
}
}
}
final class ViewModel: NSObject, ObservableObject {
@Published var someText: String = ""
}
struct SecondView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
Text(viewModel.someText)
}
Spacer()
}.background(Color.red)
}
}
As you can see the bug still occurs:
We should try to fix the issue in this example (without the navigation stack) and then see if we can improve the navigation stack by integrating the fix directly into it.
I couldn't figure it out after some hours of debugging so I posted a question on StackOverflow. Hopefully, someone can figure it out as it would make your library perfect for my use case :)
I got the following working by using the TabView
element. The big downside is that animations can't be customized, so only the default tab view page switching animation can be used. I am not sure why the transition is ignored and I can't get a working implementation of a custom TabViewStyle
. It would be so much easier if Apple would open source stuff like Google does for Android (or simply provide a working native navigation).
@main
struct TransitionTestApp: App {
@StateObject private var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewModel)
}
}
}
struct ContentView: View {
@EnvironmentObject var viewModel: ViewModel
private let transition = AnyTransition.asymmetric(insertion: .move(edge: .top),
removal: .move(edge: .bottom))
private let transitionAnimation = Animation.easeOut(duration: 3)
var body: some View {
VStack {
TabView(selection: $viewModel.selectedTab) {
FirstView()
.tag(0)
.transition(transition)
.animation(transitionAnimation)
SecondView()
.tag(1)
.transition(transition)
.animation(transitionAnimation)
}
.onAppear(perform: {
UIScrollView.appearance().bounces = false
})
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.transition(transition)
.animation(transitionAnimation)
}
}
}
struct FirstView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
Button(action: {
withAnimation {
viewModel.selectedTab += 1
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
DispatchQueue.main.sync {
self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
}
}
}){
Text("Go")
}
Spacer()
}
.background(Color.green)
}
}
final class ViewModel: NSObject, ObservableObject {
@Published var someText: String = ""
@Published var selectedTab = 0
}
struct SecondView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
Text(viewModel.someText)
}
Spacer()
}.background(Color.red)
}
}
Using the SwiftUIPager library, it works as intended! I don't know why the content is looking correct in this library, but not in yours. Maybe you can have a look what they are doing different.
import SwiftUI
import SwiftUIPager
@main
struct TransitionTestApp: App {
@StateObject private var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewModel)
}
}
}
struct ContentView: View {
@EnvironmentObject var viewModel: ViewModel
var items = Array(0..<2)
var body: some View {
Pager(
page: viewModel.page,
data: items,
id: \.self
) { index in
if(index == 0) {
FirstView()
} else if(index == 1) {
SecondView()
}
}
.bounces(false)
.draggingAnimation(.standard)
}
}
struct FirstView: View {
@EnvironmentObject var viewModel: ViewModel
private let transitionAnimation = Animation.easeOut(duration: 3)
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
Button(action: {
withAnimation(transitionAnimation) {
viewModel.page.update(.next)
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
DispatchQueue.main.sync {
self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
}
}
}){
Text("Go")
}
Spacer()
}
.background(Color.green)
}
}
final class ViewModel: NSObject, ObservableObject {
@Published var someText: String = ""
@Published var page: Page = .first()
}
struct SecondView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
Text(viewModel.someText)
}
Spacer()
}.background(Color.red)
}
}
With this approach you also get the "swipe to go back" behavior discussed in issue #8
I did a quick implementation with your library here: https://github.com/ln-12/swiftui-navigation-stack/tree/animation-fix
With this approach, you have to set your rootView
like this:
NavigationStackView(navigationStack: self.navigationStack) {}
.onAppear(perform: {
self.navigationStack.push(RootView())
})
With that, I get the right push animations in all cases and also have the "swipe to go back" gesture. It's far from perfect, other animations might not work, but it is exactly want I was looking for. Maybe you find a way to properly integrate it or it might just help someone else who is looking for this functionality. You can also tell me how you would integrate it so that I can create a pull request :)
First of all: thanks for that awesome library! It's so much better that what is currently available in SwiftUI!
Now to describe the actual problem: when I am navigating between static views, everything works just fine. But as soon as a view is changing, it is not animated anymore. This does not happen when the text of a
Text()
view is changing for example. It only occurs when a view is replaced during the transition, like when showing aProgressView()
and switching to theText()
once the content is available. The error looks like this during the animation:The
Text()
should be fully contained within the red area, but as you can see, it is located where it should be after the transition. I setup a minimal working example below. The transition is slowed down to visualize the error. TheasyncAfter
operation stands for some network request.Is it possible to fix this? I can only load the content, after the user taps the button which starts the transition. If not, the only idea which comes to my mind is that any view updates could be delayed until the transition is finished.