0

I've created a wrapper around navigation stack API with replace root & push and remove until features.

There is one bug that I couldn't overcome for a while. There is one issue that mismatches pop and pushes animations after certain navigation operations.

For it to be easier to understand let me explain some necessary infos.

This is the main entry point of package that exports navigationStack with all setted up

public struct AppBuilder: View {

   @ObservedObject var navigationHandler : NavigationHandler

   public init(initial: PageRouteInfo) {
     navigationHandler = NavigationHandler(initial: initial)
   }

 public var body: some View {
    NavigationStack(path:$navigationHandler.stack) {
      EmptyView()
       .navigationDestination(for: PageRouteInfo.self) { routeInfo in
          AnyView(routeInfo.view.transition(.asymmetric(insertion: .move(edge: .leading), removal: .identity)))
             .navigationBarBackButtonHidden(routeInfo.isInitial )

       }
    }
    .environmentObject(navigationHandler)
   }
}


This is the navigation model that conforms hashable protocol to fill stack with it


public struct PageRouteInfo: Hashable {

   let view: any View
   let isInitial: Bool
   let id : String = UUID().uuidString
   

   public init( view: any View) {
      self.view = view
      self.isInitial = false
   }

   public init(view: any View, isInitial : Bool) {
      self.view = view
      self.isInitial = isInitial
   }


   public static func == (lhs: PageRouteInfo, rhs: PageRouteInfo) -> Bool {
      lhs.hashValue == rhs.hashValue
   }

   public func hash(into hasher: inout Hasher) {
      hasher.combine(id)
   }

   public mutating func makeFirst() -> PageRouteInfo {
      return PageRouteInfo(view: view, isInitial: true)
   }

}

And this the class that manages all navigation stuff & holds @Published stack


import Foundation
import SwiftUI

@available(iOS 16.0, *)
@available(macOS 13.0, *)
public class NavigationHandler: Navigator {
   @Published public var stack: [PageRouteInfo] = [] {
      didSet {
         print(stack.map { "Name : \($0.view)" })
      }
   }

   public init(initial: PageRouteInfo) {
      stack.append(initial)
   }

   public func push(destionation: any DeepRoutes) {
      stack.append(destionation.toItem())
   }

   public func pop() {
      if isNotLast {
         stack.removeLast()
      }
   }

   public func popToRoot() {
      stack.removeLast((1 ... stack.count - 1).count)
   }

   public func pushAndRemoveUntil(destionation: any DeepRoutes) {
      var route = destionation.toItem()
      stack.append(route.makeFirst())
      if stack.last != route {
         withDelay {
            self.stack.removeFirst((0 ..< (self.stack.count - 1)).count)
         }
      }
   }

   public func replaceRoot(with: any DeepRoutes) {
      var route = with.toItem()
      if isNotLast {
         stack.insert(route.makeFirst(), at: 0)
         stack.remove(at: 1)
      } else {
         stack.insert(route.makeFirst(), at: 1)
         withDelay { self.stack.remove(at: 0) }
      }
   }
}

private extension NavigationHandler {
   var isNotLast: Bool {
      return stack.count > 1
   }
}

private extension NavigationHandler {
   func withDelay(_ callback: @escaping () -> ()) {
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
         callback()
      }
   }
}

With that all said, push, pop & popToRoot works like charm. But when it came to pushAndRemoveUntil & replaceRoot things go south.

For example, if I use pushAndRemoveUntil it works as intended beside animation. It pushes a new view to stack with no back button and removes the rest. But if you use anything after the bug starts in case of push it goes instantly or if you use pushAndRemoveUntil again it goes with pop animation. Same with the replaceRoot function.

Some visuals of desribed issue is below
Some visuals of described issue below

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160

0 Answers0