Custom segue animation (navigation animation) in S

2020-05-23 08:07发布

问题:

In SwiftUI we use NavigationView and NavigationLink views to perform navigations (what we used to call segue in UIKit). The standard segue in UIKit is the show segue. In SwiftUI we can simply do:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Destination")) {
                    Text("Navigate!")
                }
            }
        }
    }
}

to have the exact same effect (even if the word segue has disappeared).

Sometimes (actually rather often) we need to customise the segue animation.

  • We can decide not to animate the segue at all, indeed in the storyboard we can find the attribute Animates (true/false) in the attribute inspector by clicking on a segue. This way the destination view controller appears immediately in place of the source view controller.
  • Or we can decide to perform a custom animation. Usually this is done by implementing an object that conforms to the UIViewControllerAnimatedTransitioning protocol. All the magic happens in the animateTransition method that gives us access to the source view controller and the destination view controller.

For example, a simple cross-fade segue animation could be something like:

-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    UIView* containerView = [transitionContext containerView];
    UIViewController* fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController* toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    toVC.view.alpha = 0;
    [containerView addSubview:toVC.view];

    [UIView animateWithDuration:1 animations:^{
        toVC.view.alpha = 1;
        fromVC.view.alpha = 0;
    } completion:^(BOOL finished) {
        [fromVC.view removeFromSuperview];
        [transitionContext completeTransition:YES];
    }];
}

Now the question: how can I get the same in SwiftUI? Is it possible not to animate a navigation or to customise the navigation animation? I expected to be able to do:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Destination")) {
                    Text("Navigate!")
                }
            }
        }
        .animation(nil)
    }
}

or something similar to prevent the animation (or to add a custom animation), but nothing changes.

回答1:

Unfortunately it's still not possible to customise the NavigationView transition animations in SwiftUI (xCode 11.3.1).

Looking for a way to do that I ended up creating this open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, a view that mimics the navigation behaviours of the standard NavigationView adding some useful features. For example you can turn off the transition animations or you can customise them with the transition you prefer.

Months ago I asked here above how to turn off transition animations. To do that you can use the NavigationStackView this way:

struct ContentView : View {
    var body: some View {
        NavigationStackView(transitionType: .none) {
            ZStack {
                Color.yellow.edgesIgnoringSafeArea(.all)

                PushView(destination: View2()) {
                    Text("PUSH")
                }
            }
        }
    }
}

struct View2: View {
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            PopView {
                Text("POP")
            }
        }
    }
}

If you specify .none as transitionType you can turn off the animations. PushView and PopView are two views that allow you push and pop views (similar to the SwiftUI NavigationLink).

The result is:

If you, instead, want to customise the transition animation (as I asked in the question here above with the cross-fade example) you can do like this:

struct ContentView : View {
    let navigationTransition = AnyTransition.opacity.animation(.easeOut(duration: 2))

    var body: some View {
        NavigationStackView(transitionType: .custom(navigationTransition)) {
            ZStack {
                Color.yellow.edgesIgnoringSafeArea(.all)

                PushView(destination: View2()) {
                    Text("PUSH")
                }
            }
        }
    }
}

struct View2: View {
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)

            PopView {
                Text("POP")
            }
        }
    }
}

For a cross-fase transition I specified:

let navigationTransition = AnyTransition.opacity.animation(.easeOut(duration: 2))

a cross fade transition that lasts 2 seconds. Then I passed this transition to the NavigationStackView:

NavigationStackView(transitionType: .custom(navigationTransition))