2

Is there a way to add animations to transitions between custom NavigationItems made from AnyView as described in this article?

I like everything about this NavigationStack system except for not being able to add any animated transitions.

I understand the problem has to do with type-erasing some Views with AnyView, as described in this answer, and that Group seems to be a better choice for animating custom view navigations.

Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.

I've run out of ideas and I need some help.

I'll add my code for context.

SceneDelegate:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
    let contentView = ContentView().environmentObject(UserData())
...
}

ContentView:

struct ContentView: View {
    @EnvironmentObject var userData: UserData   
    var body: some View {
        NavigationHost()
            .environmentObject(NavigationStack(
                NavigationItem(view: AnyView(
                    (userData.isSignedIn ? AnyView(HomeView()) : AnyView(RegisterView1(profile: Profile.default)) )
                    .transition(.move(edge: .trailing))
                    .animation(Animation.linear(duration: 1))))))
                    //this animation works for some reason, but only this one.
            .environmentObject(UserData())
    }
}

NavigationHost:

struct NavigationHost: View{
    @EnvironmentObject var navigation: NavigationStack
    @EnvironmentObject var userData: UserData

    var body: some View {
            ZStack {
                    self.navigation.currentView.view
                        .environmentObject(self.userData)
                        .environmentObject(self.navigation)
                    ...
                }
            }
    }
}

NavigationStack:

final class NavigationStack: ObservableObject {
    @Published var viewStack: [NavigationItem] = []
    @Published var currentView: NavigationItem

    init(_ currentView: NavigationItem ){
        print("Navigation Stack Initialized")
        self.currentView = currentView
    }

    func back() {
        if viewStack.count == 0 {
            return
        }
        let last = viewStack.count - 1
        currentView = viewStack[last]
        viewStack.remove(at: last)
    }

    navigation.advance(NavigationItem(AnyView))
    func advance(_ view: NavigationItem) {
        viewStack.append( currentView )
        currentView = view
    }

    func home() {
       currentView = NavigationItem( view: AnyView(HomeView()) )
       viewStack.removeAll()
    }

}

struct NavigationItem{
    var view: AnyView
}

Miss Swiss
  • 89
  • 1
  • 9
  • As far as I can conclude from the article it is hardly possible transition/animation in the provided approach, because .transition and .animation modifiers take effect whenever there is some view hierarchy change in root view, ie. one view is replaced with another view, but in described approach ContentView always has the only one NavigationHost view. It is updated, yes, but it is the one, only, there is no alternate to transition in. – Asperi Nov 18 '19 at 07:32
  • Oh, I never thought the problem resided within scene delegate. Thank you for elaborating the problem for me. – Miss Swiss Nov 18 '19 at 15:10
  • @Asperi Is there a way to evade the problem? For instance, 1: deliberately reload the rootView every time ‘NavigationStack’ is modified, and pass a ‘.environmentObject(navigationStack)’ to the new rootView, or 2: create custom animations? Edit: Sorry if I seem to be confused with how swiftUI works. – Miss Swiss Nov 18 '19 at 15:20

1 Answers1

5

Here is updated entities from that article to adopt simple animated transitions between screens. All that is scratchy and only for demo, but hope it could be helpful somehow.

Note: transitions do not work in Preview (at least at my Xcode 11.2/iOS 13.2), so tested in Simulator or real device.

Here is how it looks: SwiftUI animated custom navigation item

Here is code:

final class NavigationStack: ObservableObject {
    enum Direction {
        case root
        case forward
        case backward
    }

    @Published var viewStack: [NavigationItem] = []
    @Published var currentView: NavigationItem
    @Published var navigate: Direction = .root

    init(_ currentView: NavigationItem ){
        self.currentView = currentView
    }
    func unwind(){
        if viewStack.count == 0{
            return
        }
        let last = viewStack.count - 1
        currentView = viewStack[last]
        withAnimation {
            self.navigate = .backward
        }
        viewStack.remove(at: last)
    }
    func advance(_ view:NavigationItem){
        viewStack.append( currentView)
        currentView = view
        withAnimation {
            self.navigate = .forward
        }
    }
    func home( ){
        currentView = NavigationItem(view: AnyView(HomeView()))
        viewStack.removeAll()
        withAnimation {
            self.navigate = .root
        }
    }
}

struct NavigationHost: View{
    @EnvironmentObject var navigation: NavigationStack

    var body: some View {
        ZStack(alignment: .topLeading) {
            if navigation.navigate == .root {
                self.navigation.currentView.view
            }
            else if navigation.navigate == .backward {
                self.navigation.currentView.view
                    .transition(.move(edge: .leading))
            }
            if navigation.navigate == .forward {
                self.navigation.currentView.view
                    .transition(.move(edge: .trailing))
            }
        }

    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • So interestingly, the animation happens between the last rendering of body and the new rendering of view right? As in, both versions of the view are not rendered in the last call to body (only the last view is presented), is that correct? – Zorayr May 08 '20 at 18:25
  • Small nitpick comment, maybe worth using push/pop function names to mimic `UINavigationController`'s API? Good old days – Zorayr May 08 '20 at 18:29
  • Also, what's the code for `NavigationItem`? What is it needed for? – Zorayr May 08 '20 at 18:43
  • ...hm, this code actually doesn't work. Has something changed since then? With this code, what you see is Page 1, Page 2, animated slide in of Page 2 – Zorayr May 08 '20 at 19:35
  • 6
    That’s a terrible suggestion. This basically breaks any accessibility support built into UINavigationController, as well all the built-in tools provided. Just like it’s terrible when web “apps” do this, it’s equally terrible with SwiftUI. – Léo Natan Aug 21 '20 at 22:16