Skip to content

Latest commit

 

History

History
70 lines (55 loc) · 3.19 KB

AnimatingRoutes.md

File metadata and controls

70 lines (55 loc) · 3.19 KB

Animating routes

Simulating screen transitions à la iOS.

Introduction

On a platform like iOS, users may expect animated screen transitions when navigating through the app. (Less so the case with macOS) Apps get these transitions for free with NavigationView. But with SwiftUI Router, however, this is not the case. Ideally, you want a transition that differs as the user goes forward (deeper) in the app and when they go back (higher).

SwiftUI Router exposes the Navigator environment object. An object that allows for navigation done programmatically. It also contains the property .lastAction, which is of type NavigationAction?. This object contains read-only information about the last navigation that occurred. Information like the previous path, the current path, whether the app navigated forward or back. But also the direction of the navigation, which is what we're interested in right now.

The direction of a navigation action implies whether the app navigated deeper, higher or sideways in the routing hierarchy. Consider an app user currently being on the news screen (/news) and they press an article. The user navigates from /news to /news/some-article. This is labeled as deeper. The user scrolls to the bottom and presses a related news article (/news/related-article). This is labeled as sideways. Finally, when the user goes back to the news screen (/news), the direction is higher.

We can use this information to decide how our views are animated as the user navigates through your app.

Below an example of a very primitive ViewModifier that animates routes similar to views in a NavigationView:

struct NavigationTransition: ViewModifier {
	@EnvironmentObject private var navigator: Navigator
	
	func body(content: Content) -> some View {
		content
			.animation(.easeInOut, value: navigator.path)
			.transition(
				navigator.lastAction?.direction == .deeper || navigator.lastAction?.direction == .sideways
					? AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
					: AnyTransition.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
			)
	}
}

When the user navigates either deeper or sideways in the routing hierarchy, the new view enters the screen from the right, whereas the previous view leaves the screen on the left. However, when the user navigates higher in the routing hierarchy (e.g.: pressing the back button), the transitions are reversed.

To make the view modifier more accessible and user-friendlier, consider wrapping it in a View method like any other modifier:

extension View {
	func navigationTransition() -> some View {
		modifier(NavigationTransition())
	}
}

The modifier can be applied to Route views:

Route("news") {
	NewsScreen()
}
.navigationTransition()

The modifier can also be applied to a SwitchRoutes. This will apply the animated transition to all Route views inside the SwitchRoutes.

SwitchRoutes {
	Route("news/:id", validator: newsIdValidator) { uuid in
		NewsItemScreen(uuid: uuid)
	}
	Route("news") {
		NewsScreen()
	}
	Route {
		HomeScreen()
	}
}
.navigationTransition()

Tada~

Preview