Search code examples
swiftuiios-navigationview

How do I add Animations to Transitons between custom NavigationItems made from AnyView?


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
}


Solution

  • 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))
                }
            }
    
        }
    }