Navigation Stack and Dependancies (Part 1)
In the dynamic landscape of mobile application development, the Navigation Stack serves as the backbone, orchestrating the flow of user interactions with precision and fluidity. Yet, its effectiveness is greatly amplified by two key dependencies: Push Notifications and Deep Linking. These elements, often overlooked in their complexity, play a pivotal role in enhancing user engagement and streamlining user journeys within the application ecosystem.
Push Notifications :-
with their ability to deliver timely and personalized messages, have redefined user engagement strategies. They serve as a direct conduit between applications and users, offering valuable updates, promotions, and reminders. However, their seamless integration within the Navigation Stack requires careful orchestration to ensure that they augment, rather than disrupt, the user experience.
Deep Linking :-
empowers users to navigate directly to specific content or features within an application, bypassing traditional entry points. This functionality not only enhances user convenience but also facilitates a more seamless transition between external stimuli and internal application states. Nevertheless, integrating Deep Linking within the Navigation Stack demands meticulous attention to routing logic and error handling to ensure a cohesive user journey.
1- let’s Start by creating Enum for The App Views
public enum AppViews {
//MARK: - Authentication Views
case firstScreen
case secondScreen(info: String)
}
case firstScreen
: Defines a case named firstScreen
. This represents a view related to onboarding in your application.
case secondScreen
: Defines a case named secondScreen
. This represents a view where users can enter their info.
2- MainDependancyContainer
public protocol MainDependancyContainer {
associatedtype T: View
@ViewBuilder
func dependancyCreator(view: AppViews) -> T
}
this protocol defines a blueprint for a dependency container that can create views based on the AppViews
enumeration. It allows for flexibility in specifying the type of view to be created while ensuring that the returned view conforms to the SwiftUI View
protocol.
extension MainDependancyContainer {
@MainActor
public func dependancyCreator(view: AppViews) -> some View {
Group {
switch view {
case .firstScreen:
FirstView(router: FirstScreenRouter())
case let .secondScreen(info):
SecondView(router: SecondScreenRouter(), info: info)
}
}
}
}
This extension provides a default implementation of the dependancyCreator
function for types conforming to the MainDependancyContainer
protocol. It creates SwiftUI views based on the provided AppViews
enumeration cases, utilizing appropriate routers and view models as needed for each view.
3- Router :-
so now let’s Jump to the Router part at the init of the First & Second View
this image shows the folders structure for each flow
The router component includes the RouterProtocol, encompassing all the navigation actions necessary for managing the flow within this context.
public protocol FirstScreenRouterProtocol: AnyObject {
func toSecondScreen(info: String)
}
public protocol SecondScreenRouterProtocol: AnyObject {
func goBack()
}
Let’s showcase the code representation of FirstView
and SecondView
.
public struct FirstView: View {
@State private var router: FirstScreenRouterProtocol
public init(router: FirstScreenRouterProtocol) {
_router = State(initialValue: router)
}
public var body: some View {
Button {
router.toSecondScreen(info: "Hello")
} label: {
Text("to Second Screen")
}
}
}
public struct SecondView: View {
@State private var router: SecondScreenRouterProtocol
public init(router: SecondScreenRouterProtocol) {
_router = State(initialValue: router)
}
public var body: some View {
Button {
router.goBack()
} label: {
Text("back")
}
}
}
Now, let’s craft FirstScreenRouter()
and SecondScreenRouter()
that conform to the navigation protocols, encapsulating the navigation actions within their respective contexts.
class FirstScreenRouter: FirstScreenRouterProtocol {
func toSecondScreen(info: String)() {
Navigator.shared.goTo(.secondScreen(info: info))
}
}
class FirstScreenRouter: SecondScreenRouterProtocol {
func goBack()() {
Navigator.shared.goBack()
}
}
4- NavigatorProtocols & Route :-
The NavigatorProtocols
will include the function responsible for navigation.
public protocol NavigatorProtocols {
func goTo(_ route:Route)
func goBack()
// Custom navigation functions can be added here.
}
public enum Route: Hashable{
case firstScreen
case secondScreen(info: String)
}
extension Route:View, MainDependancyContainer {
public var body: some View {
switch self {
case .firstScreen:
dependancyCreator(view: .firstScreen)
case let .secondScreen(info):
dependancyCreator(view: .secondScreen(info: info))
}
}
}
4- Navigator Class :-
The Navigator
class will handle the navigation logic by conforming to the NavigatorProtocols
.
public final class Navigator: NavigatorProtocols , ObservableObject {
@Published public var mainRoutes: [Route] = []
public static let shared = Navigator()
func goTo(_ route:Route) {
mainRoutes.append(route)
}
func goBack(){
_ = mainRoutes.popLast()
}
}
5- Navigation Modifier :-
public struct NavigationModifier: ViewModifier {
public func body(content: Content) -> some View {
content
.navigationDestination(for: Route.self) { $0 }
}
}
public extension View {
func navigationsConfig() -> some View {
self.modifier(NavigationModifier())
}
}
.navigationDestination(for: Route.self)
: This indicates that thenavigationDestination
function is being called with a generic typeRoute
. It suggests that this function responsible for determining the navigation destination based on the providedRoute
type.{ $0 }
: This is a closure parameter passed to thenavigationDestination
function. The$0
inside the closure refers to the input parameter passed to the closure. In this context, it indicates that the navigation destination is determined by the inputRoute
itself.self.modifier(NavigationModifier())
This applies theNavigationModifier
modifier to the view (self
).NavigationModifier
is likely a custom modifier that encapsulates navigation-related configuration or behavior.
6- App Entry Point :-
@main
struct NavigatorApp: App, MainDependancyContainer {
@StateObject private var navigator = Navigator.shared
var body: some Scene {
WindowGroup {
NavigationStack(path: $navigator.mainRoutes) {
dependancyCreator(view: .firstScreen)
.navigationsConfig()
}
}
}
}
NavigationStack
: This represents a custom SwiftUI view or component that manages navigation within the app.path: $navigator.mainRoutes
: This parameter define the navigation path or route within theNavigationStack
. The$navigator.mainRoutes
is aBinding
to an array ofRoute
objects representing the current navigation stack.
the Next Article Will Show How To handle Push Notifications Using Navigator 🙂
👉link