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.

mohamed ahmed
4 min readFeb 5, 2024

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 the navigationDestination function is being called with a generic type Route. It suggests that this function responsible for determining the navigation destination based on the provided Route type.
  • { $0 }: This is a closure parameter passed to the navigationDestination 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 input Route itself.
  • self.modifier(NavigationModifier())This applies the NavigationModifier 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 the NavigationStack. The $navigator.mainRoutes is a Binding to an array of Route objects representing the current navigation stack.

the Next Article Will Show How To handle Push Notifications Using Navigator 🙂

👉link

--

--

mohamed ahmed

Senior iOS Developer with 9 years if experience in building & Architecting Large scale mobile Applications