diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48f34b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,102 @@
+<<<<<<< HEAD
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+=======
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
+DerivedData/
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
+>>>>>>> bfd2701 (Initial Commit)
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b0da13c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Mijick
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..4775843
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,17 @@
+// swift-tools-version: 5.8
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "Navigattie",
+ platforms: [
+ .iOS(.v15)
+ ],
+ products: [
+ .library(name: "Navigattie", targets: ["Navigattie"]),
+ ],
+ targets: [
+ .target(name: "Navigattie", dependencies: [])
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b4d0e6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Navigattie
+
+A description of this package.
diff --git a/Sources/Navigattie/Configurables/TransitionAnimation.swift b/Sources/Navigattie/Configurables/TransitionAnimation.swift
new file mode 100644
index 0000000..113574e
--- /dev/null
+++ b/Sources/Navigattie/Configurables/TransitionAnimation.swift
@@ -0,0 +1,17 @@
+//
+// TransitionAnimation.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import Foundation
+
+public enum TransitionAnimation {
+ case no
+ case dissolve
+ case horizontalSlide, verticalSlide
+}
diff --git a/Sources/Navigattie/Extensions/Foundation/Array++.swift b/Sources/Navigattie/Extensions/Foundation/Array++.swift
new file mode 100644
index 0000000..0fb4c4c
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Foundation/Array++.swift
@@ -0,0 +1,29 @@
+//
+// Array++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import Foundation
+
+extension Array {
+ @inlinable mutating func append(_ newElement: Element, if prerequisite: Bool) { if prerequisite {
+ append(newElement)
+ }}
+ @inlinable mutating func removeLastExceptFirst() { if count > 1 {
+ removeLast()
+ }}
+ @inlinable mutating func removeAllExceptFirst() { if count > 1 {
+ removeLast(count - 1)
+ }}
+ @inlinable mutating func removeLastTo(elementWhere predicate: (Element) throws -> Bool) rethrows { if let index = try lastIndex(where: predicate) {
+ removeLast(count - index - 1)
+ }}
+}
+extension Array {
+ var isNextToLast: Element? { count >= 2 ? self[count - 2] : nil }
+}
diff --git a/Sources/Navigattie/Extensions/Foundation/Equatable++.swift b/Sources/Navigattie/Extensions/Foundation/Equatable++.swift
new file mode 100644
index 0000000..06e272c
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Foundation/Equatable++.swift
@@ -0,0 +1,15 @@
+//
+// Equatable++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import Foundation
+
+extension Equatable {
+ func isOne(of other: Self?...) -> Bool { other.contains(self) }
+}
diff --git a/Sources/Navigattie/Extensions/Foundation/Error++.swift b/Sources/Navigattie/Extensions/Foundation/Error++.swift
new file mode 100644
index 0000000..911cf61
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Foundation/Error++.swift
@@ -0,0 +1,13 @@
+//
+// Error++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import Foundation
+
+extension String: Error {}
diff --git a/Sources/Navigattie/Extensions/Views/NavigatableView++.swift b/Sources/Navigattie/Extensions/Views/NavigatableView++.swift
new file mode 100644
index 0000000..250dca5
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Views/NavigatableView++.swift
@@ -0,0 +1,15 @@
+//
+// NavigatableView++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public extension NavigatableView {
+ func implementNavigationView() -> some View { NavigationView(rootView: self) }
+}
diff --git a/Sources/Navigattie/Extensions/Views/UIApplication++.swift b/Sources/Navigattie/Extensions/Views/UIApplication++.swift
new file mode 100644
index 0000000..8bb9707
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Views/UIApplication++.swift
@@ -0,0 +1,17 @@
+//
+// UIApplication++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension UIApplication {
+ func hideKeyboard() {
+ sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+ }
+}
diff --git a/Sources/Navigattie/Extensions/Views/UIScreen++.swift b/Sources/Navigattie/Extensions/Views/UIScreen++.swift
new file mode 100644
index 0000000..6ae5a9b
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Views/UIScreen++.swift
@@ -0,0 +1,26 @@
+//
+// UIScreen++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension UIScreen {
+ static let width: CGFloat = main.bounds.size.width
+ static let height: CGFloat = main.bounds.size.height
+ static let safeArea: UIEdgeInsets = {
+ UIApplication.shared.connectedScenes
+ .filter({$0.activationState == .foregroundActive})
+ .map({$0 as? UIWindowScene})
+ .compactMap({$0})
+ .first?.windows
+ .filter({$0.isKeyWindow})
+ .first?
+ .safeAreaInsets ?? .zero
+ }()
+}
diff --git a/Sources/Navigattie/Extensions/Views/View++.swift b/Sources/Navigattie/Extensions/Views/View++.swift
new file mode 100644
index 0000000..17e9c30
--- /dev/null
+++ b/Sources/Navigattie/Extensions/Views/View++.swift
@@ -0,0 +1,22 @@
+//
+// View++.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public extension View {
+ @ViewBuilder func matchedGeometryEffect(id: String, properties: MatchedGeometryProperties = .frame, anchor: UnitPoint = .center, isSource: Bool = true) -> some View {
+ if let namespace = NavigationManager.shared.namespace { matchedGeometryEffect(id: id, in: namespace, properties: properties, anchor: anchor, isSource: isSource) }
+ else { self }
+ }
+}
+
+extension View {
+ func modify(_ builder: (Self) -> V) -> some View { builder(self) }
+}
diff --git a/Sources/Navigattie/Managers/NavigationManager.swift b/Sources/Navigattie/Managers/NavigationManager.swift
new file mode 100644
index 0000000..8fafa62
--- /dev/null
+++ b/Sources/Navigattie/Managers/NavigationManager.swift
@@ -0,0 +1,93 @@
+//
+// NavigationManager.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public extension NavigationManager {
+ /// Returns to a previous view on the stack
+ static func pop() { performOperation(.removeLast) }
+
+ /// Returns to view with provided type
+ static func pop(to view: N) { performOperation(.removeAll(toID: view.id)) }
+
+ /// Returns to a root view
+ static func popToRoot() { performOperation(.removeAllExceptFirst) }
+}
+
+
+// MARK: - Internal
+public class NavigationManager: ObservableObject {
+ @Published private(set) var views: [AnyNavigatableView] = [] { willSet { onViewsWillUpdate(newValue) } }
+ private(set) var transitionType: TransitionType = .push
+ private(set) var transitionAnimation: TransitionAnimation = .no
+ private(set) var transitionsBlocked: Bool = false
+ private(set) var namespace: Namespace.ID?
+
+ static let shared: NavigationManager = .init()
+ private init() {}
+}
+
+// MARK: - Operations Handler
+extension NavigationManager {
+ static func push(_ view: some NavigatableView, _ animation: TransitionAnimation) { performOperation(.insert(view, animation)) }
+}
+private extension NavigationManager {
+ static func performOperation(_ operation: Operation) { if !NavigationManager.shared.transitionsBlocked {
+ DispatchQueue.main.async { withAnimation(nil) {
+ shared.views.perform(operation)
+ }}
+ }}
+}
+
+// MARK: - Setters
+extension NavigationManager {
+ static func setRoot(_ rootView: some NavigatableView) { shared.views = [.init(rootView, .no)] }
+ static func setNamespace(_ value: Namespace.ID) { if shared.namespace == nil { shared.namespace = value } }
+ static func blockTransitions(_ value: Bool) { shared.transitionsBlocked = value }
+}
+
+// MARK: - On Views Will Update
+private extension NavigationManager {
+ func onViewsWillUpdate(_ newValue: [AnyNavigatableView]) {
+ transitionType = newValue.count > views.count ? .push : .pop
+ transitionAnimation = (transitionType == .push ? newValue.last?.animation : views.last?.animation) ?? .no
+ }
+}
+
+// MARK: - Transition Type
+enum TransitionType { case pop, push }
+
+// MARK: - Array Operations
+fileprivate enum Operation {
+ case insert(any NavigatableView, TransitionAnimation)
+ case removeLast, removeAll(toID: String), removeAllExceptFirst
+}
+fileprivate extension [AnyNavigatableView] {
+ mutating func perform(_ operation: Operation) { if !NavigationManager.shared.transitionsBlocked {
+ hideKeyboard()
+ performOperation(operation)
+ }}
+}
+private extension [AnyNavigatableView] {
+ func hideKeyboard() {
+ UIApplication.shared.hideKeyboard()
+ }
+ mutating func performOperation(_ operation: Operation) {
+ switch operation {
+ case .insert(let view, let animation): append(.init(view, animation), if: canBeInserted(view))
+ case .removeLast: removeLastExceptFirst()
+ case .removeAll(let id): removeLastTo(elementWhere: { $0.id == id })
+ case .removeAllExceptFirst: removeAllExceptFirst()
+ }
+ }
+}
+private extension [AnyNavigatableView] {
+ func canBeInserted(_ view: any NavigatableView) -> Bool { !contains(where: { $0.id == view.id }) }
+}
diff --git a/Sources/Navigattie/Protocols/NavigatableView.swift b/Sources/Navigattie/Protocols/NavigatableView.swift
new file mode 100644
index 0000000..1e2c2e4
--- /dev/null
+++ b/Sources/Navigattie/Protocols/NavigatableView.swift
@@ -0,0 +1,34 @@
+//
+// NavigatableView.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public protocol NavigatableView: View {}
+
+// MARK: - Pushing and Removing From Stack
+public extension NavigatableView {
+ /// Pushes a new view. Stacks previous one
+ func push(with animation: TransitionAnimation) { NavigationManager.push(self, animation) }
+}
+public extension NavigatableView {
+ /// Removes the current view from the stack
+ func pop() { NavigationManager.pop() }
+
+ /// Removes all views up to the selected view in the stack. The view from the argument will be the new active view
+ func pop(to view: N) { NavigationManager.pop(to: view) }
+
+ /// Removes all views from the stack. Root view will be the new active view
+ func popToRoot() { NavigationManager.popToRoot() }
+}
+
+// MARK: - Others
+extension NavigatableView {
+ var id: String { .init(describing: Self.self) }
+}
diff --git a/Sources/Navigattie/Type Erasers/AnyNavigatableView.swift b/Sources/Navigattie/Type Erasers/AnyNavigatableView.swift
new file mode 100644
index 0000000..b22a42c
--- /dev/null
+++ b/Sources/Navigattie/Type Erasers/AnyNavigatableView.swift
@@ -0,0 +1,30 @@
+//
+// AnyNavigatableView.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+struct AnyNavigatableView: NavigatableView, Equatable {
+ let id: String
+ let animation: TransitionAnimation
+ private let _body: AnyView
+
+
+ init(_ view: some NavigatableView, _ animation: TransitionAnimation) {
+ self.id = view.id
+ self.animation = animation
+ self._body = AnyView(view)
+ }
+}
+extension AnyNavigatableView {
+ static func == (lhs: AnyNavigatableView, rhs: AnyNavigatableView) -> Bool { lhs.id == rhs.id }
+}
+extension AnyNavigatableView {
+ var body: some View { _body }
+}
diff --git a/Sources/Navigattie/View Modifiers/AnimationCompletionModifier.swift b/Sources/Navigattie/View Modifiers/AnimationCompletionModifier.swift
new file mode 100644
index 0000000..8da8557
--- /dev/null
+++ b/Sources/Navigattie/View Modifiers/AnimationCompletionModifier.swift
@@ -0,0 +1,37 @@
+//
+// AnimationCompletionModifier.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension View {
+ func onAnimationCompleted(for value: V, perform action: @escaping () -> ()) -> some View { modifier(Modifier(observedValue: value, completion: action)) }
+}
+
+
+// MARK: - Implementation
+fileprivate struct Modifier: AnimatableModifier {
+ var animatableData: V { didSet { notifyCompletionIfFinished() }}
+ private var targetValue: V
+ private var completion: () -> ()
+
+
+ init(observedValue: V, completion: @escaping () -> ()) {
+ self.animatableData = observedValue
+ self.targetValue = observedValue
+ self.completion = completion
+ }
+ func body(content: Content) -> some View { content }
+}
+
+private extension Modifier {
+ func notifyCompletionIfFinished() { if animatableData == targetValue {
+ DispatchQueue.main.async { self.completion() }
+ }}
+}
diff --git a/Sources/Navigattie/Views/NavigationStackView.swift b/Sources/Navigattie/Views/NavigationStackView.swift
new file mode 100644
index 0000000..cc0c7ea
--- /dev/null
+++ b/Sources/Navigattie/Views/NavigationStackView.swift
@@ -0,0 +1,148 @@
+//
+// NavigationStackView.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+struct NavigationStackView: View {
+ @ObservedObject private var stack: NavigationManager = .shared
+ @State private var temporaryViews: [AnyNavigatableView] = []
+ @State private var animatableOffset: CGFloat = 0
+ @State private var animatableOpacity: CGFloat = 1
+
+
+ init(namespace: Namespace.ID) { self._temporaryViews = .init(initialValue: NavigationManager.shared.views); NavigationManager.setNamespace(namespace) }
+ var body: some View {
+ ZStack(content: createStack)
+ .onChange(of: stack.views, perform: onViewsChanged)
+ .onAnimationCompleted(for: animatableOpacity, perform: onAnimationCompleted)
+ }
+}
+
+private extension NavigationStackView {
+ func createStack() -> some View {
+ ForEach(temporaryViews, id: \.id, content: createItem)
+ }
+}
+
+private extension NavigationStackView {
+ func createItem(_ item: AnyNavigatableView) -> some View {
+ item
+ .transition(.identity)
+ .opacity(getOpacity(item))
+ .offset(getOffset(item))
+ .compositingGroup()
+ }
+}
+
+// MARK: - Calculating Opacity
+private extension NavigationStackView {
+ func getOpacity(_ view: AnyNavigatableView) -> CGFloat {
+ do {
+ try checkOpacityPrerequisites(view)
+
+ let isLastView = isLastView(view)
+ let opacity = calculateOpacityValue(isLastView)
+ return opacity
+ } catch { return 0 }
+ }
+}
+private extension NavigationStackView {
+ func checkOpacityPrerequisites(_ view: AnyNavigatableView) throws {
+ if !view.isOne(of: stack.views.last, temporaryViews.last, temporaryViews.isNextToLast) { throw "Opacity can concern the last or next to last element of the stack" }
+ }
+ func isLastView(_ view: AnyNavigatableView) -> Bool {
+ let lastView = stack.transitionType == .push ? temporaryViews.last : stack.views.last
+ return view == lastView
+ }
+ func calculateOpacityValue(_ isLastView: Bool) -> CGFloat {
+ isLastView ? animatableOpacity : 1 - animatableOpacity
+ }
+}
+
+// MARK: - Calculating Offset
+private extension NavigationStackView {
+ func getOffset(_ view: AnyNavigatableView) -> CGSize {
+ do {
+ try checkOffsetPrerequisites(view)
+
+ let offsetSlideValue = calculateSlideOffsetValue(view)
+ let offset = animatableOffset + offsetSlideValue
+ let offsetX = calculateXOffsetValue(offset), offsetY = calculateYOffsetValue(offset)
+ return .init(width: offsetX, height: offsetY)
+ } catch { return .zero }
+ }
+}
+private extension NavigationStackView {
+ func checkOffsetPrerequisites(_ view: AnyNavigatableView) throws {
+ if !stack.transitionAnimation.isOne(of: .horizontalSlide, .verticalSlide) { throw "Offset cannot be set for a non-slide type" }
+ if !view.isOne(of: stack.views.last, temporaryViews.last, temporaryViews.isNextToLast) { throw "Offset can concern the last or next to last element of the stack" }
+ }
+ func calculateSlideOffsetValue(_ view: AnyNavigatableView) -> CGFloat {
+ switch view == temporaryViews.last {
+ case true: return stack.transitionType == .push ? 0 : maxOffsetValue
+ case false: return stack.transitionType == .push ? -maxOffsetValue : 0
+ }
+ }
+ func calculateXOffsetValue(_ offset: CGFloat) -> CGFloat { stack.transitionAnimation == .horizontalSlide ? offset : 0 }
+ func calculateYOffsetValue(_ offset: CGFloat) -> CGFloat { stack.transitionAnimation == .verticalSlide ? offset : 0 }
+}
+
+// MARK: - On Transition Begin
+private extension NavigationStackView {
+ func onViewsChanged(_ views: [AnyNavigatableView]) {
+ blockTransitions()
+ updateTemporaryViews(views)
+ resetOffsetAndOpacity()
+ animateOffsetAndOpacityChange()
+ }
+}
+private extension NavigationStackView {
+ func blockTransitions() {
+ NavigationManager.blockTransitions(true)
+ }
+ func updateTemporaryViews(_ views: [AnyNavigatableView]) {
+ if stack.transitionType == .push { temporaryViews = views }
+ }
+ func resetOffsetAndOpacity() {
+ let animatableOffsetFactor = stack.transitionType == .push ? 1.0 : -1.0
+
+ animatableOffset = maxOffsetValue * animatableOffsetFactor
+ animatableOpacity = 0
+ }
+ func animateOffsetAndOpacityChange() { withAnimation(animation) {
+ animatableOffset = 0
+ animatableOpacity = 1
+ }}
+}
+
+// MARK: - On Transition End
+private extension NavigationStackView {
+ func onAnimationCompleted() {
+ resetViewOnAnimationCompleted()
+ unblockTransitions()
+ }
+}
+private extension NavigationStackView {
+ func unblockTransitions() {
+ NavigationManager.blockTransitions(false)
+ }
+ func resetViewOnAnimationCompleted() {
+ if stack.transitionType == .pop {
+ temporaryViews = stack.views
+ animatableOffset = -maxOffsetValue
+ }
+ }
+}
+
+// MARK: - Configurables
+private extension NavigationStackView {
+ var maxOffsetValue: CGFloat { [.horizontalSlide: UIScreen.width, .verticalSlide: UIScreen.height][stack.transitionAnimation] ?? 0 }
+ var animation: Animation { stack.transitionAnimation == .no ? .easeInOut(duration: 0) : .spring(response: 0.44, dampingFraction: 1, blendDuration: 0.4) }
+}
diff --git a/Sources/Navigattie/Views/NavigationView.swift b/Sources/Navigattie/Views/NavigationView.swift
new file mode 100644
index 0000000..f0be1af
--- /dev/null
+++ b/Sources/Navigattie/Views/NavigationView.swift
@@ -0,0 +1,18 @@
+//
+// NavigationView.swift of Navigattie
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+struct NavigationView: View {
+ @Namespace var namespace
+
+ init(rootView: some NavigatableView) { NavigationManager.setRoot(rootView) }
+ var body: some View { NavigationStackView(namespace: namespace) }
+}