From d7b6097c09e1d7f33981829db9ff2f9f144db481 Mon Sep 17 00:00:00 2001 From: ernest0n Date: Sat, 14 May 2022 15:27:23 +0300 Subject: [PATCH] Add documentation --- Diagram.drawio | 1 - Example/Example/ExampleApp.swift | 4 +- README.md | 268 ++++++++++++------ .../Internal/Helpers/AnySequence+first.swift | 4 +- .../Internal/Helpers/View+asAny.swift | 4 +- .../Internal/LinkedList.swift | 4 +- .../ModalStack/ModalStack.swift | 58 +++- .../ModalStack/PresentScreen.swift | 4 +- .../ModalStack/PresentationStyle.swift | 5 +- .../View+definesPresentationContext.swift | 5 +- .../ModalStack/View+fullScreenCover.swift | 4 +- .../ModalStack/View+sheet.swift | 2 +- .../NavigationStack/NavigationStack.swift | 49 +++- .../NavigationStack/NavigationStackView.swift | 17 +- .../NavigationStack/PushScreen.swift | 4 +- 15 files changed, 308 insertions(+), 125 deletions(-) delete mode 100644 Diagram.drawio diff --git a/Diagram.drawio b/Diagram.drawio deleted file mode 100644 index 0d72c5f..0000000 --- a/Diagram.drawio +++ /dev/null @@ -1 +0,0 @@ -5VvZkps4FP0aPzplsfPYWzJT1Zmk0lOTydOUDDKmGyOPkLf5+pGMxCYg2MHg7n5J0NXK1bnnLrgn+t1q/4nA9fIz9lE00Wb+fqLfTzTNngH2LxccUoFhWKkgIKGfikAueAr/Q0I4E9JN6KOkNJBiHNFwXRZ6OI6RR0sySAjelYctcFTedQ0DpAiePBip0u+hT5ep1NHsXP4bCoOl3BlYbtqzgnKweJNkCX28K4j0h4l+RzCm6dNqf4cirjupl3Tex4be7GAExbTLhHvr5vfHhy9g+3l5v7LBP1/dTy9TJ11lC6ONeOGHeBsSHK/Yql/mz1yl6enpQaoE+UxDookJXeIAxzB6yKW3BG9iH/F9Z6yVj3nEeM2EgAmfEaUHcd1wQzETLekqEr1oH9K/+fQPpmj9EIvx5/t9sXGQjZiSQ2ESb/6Q6/FGPu3YkvMWYRTd4QiT49vpC8dDnsfkCSX4BRV65o5pmHxGqhGuhsarEKIEb4iHWvQvIQ1JgGjLODsDDDM0hFeIvQGbR1AEabgtnwMKyAfZuBwV7EEA4wSQuApI1ptkOS4utCIwwFnAmJ0GDB8iZ1ELDMtz0HwxDjC0emAI+p3OPgAwE4x7KDNrZ+wcV78hBB4KA9Y4jGlS2PwrF+Q7A3dW3tWpsFN1vNE6nj2kJ6jMlsfBi0WCqIL0TCPng1+qq4h+ghJ+oaMagD28AZjI8Y06A3C0uW5Z4xiA8TMD0HXdLUFrOowFaOZpFqDPrtUCFAP4xoKWJ48gFP8Vop1iBznKOS53y5CipzU83vaOBYllRJ/LsltEKNq3w6wRFsCq6FoGabs8xstky0J8J++0dx8LVC2P72TfuI/VOlKMXo8lgR3tLDYZ0oA1BVpF45UZ2qsyYKdsv1lQM5r96g06ZjL99es3802j6ddox7DWr467Rjq/pGPNuDYQ2w1K/gNuwwBSpoQ+tdw50+7V1etjKxmoVDHRrIhyBeGjR8j1a/27wbJjmhwd9g0b4K73eR97Cvj/MdrzyWnUMJNLzonsZ0nLGrPLIrKLnT7dMO0fN9QYPp9HgJm5XQc/17J1yI3ch8kyA3fC4gR6w0uKTOBFMElCT4o/hpF8pefNai3et++MyOwYr4CGapGwCKY2zTVLRjHVnHOCmFNTIqOyrSwSNKVERjWFcjulRNKHVV6xYtAXTJhM1cLN25RK/4RBasPfMnM073ul1Q647oFWjS4ZlDEor1qNvJqsYdwPr2bMmS4pmTPn2jM36eWkdQxfPue7SiY7GEJHTi/5gT4Z3e7K6E69MVZimutNQUFTaJnTYN+J6DA8qLvXFsQD9Zuequp+c9JxVD1+UgrUL2OqqvtNTYdRtWleG6o1lRyG8F5neqIefYTU8s99hFl/pcN8I5bHfFO1A8O+ttqBWgY7tXbgtMW48sPm66odDP8p9EKhZbqa/J1R38Fm5/KB22oUNeWDYaoHpnVa9cACv1I9mI5WPtDqjLxqX7EvsRXjGJUto2xaJYRxoxUW5SiA084vX6loKXCk2UKR56YsMiasMnSVelOzELNawFJdCFTWSa1GWae3O68pGbXcec4njdfeetM1hUq2umgZkqYKbb5aieVc07Xq+U/0XDtylC9c5+EGgPZ1Lo0btejFryQOjgpa1PrlRzhHURk6MAqDmD1HaMHdOQ+aQg9GN0K8Cn0/ddqIBRJwflyNg0xQNlvavOV1TAV2TbhpDM7E73jFHpPs63oRNy0W1PZDINO2QC/QmZZ/UDetcsUFXYNaRhmYJkre5J1zxrm+psoZ1XUuzRlqgWiH4Mv7Y4z2z2RvhDHUEtXAjNExEXrnXGL3xCXVdS7MJTLcKcBrHvohYQYZ8mz+/ZFKe/J87aTCmvlfJKXD8z/r0h/+Bw== \ No newline at end of file diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift index cbe2640..5a610ad 100644 --- a/Example/Example/ExampleApp.swift +++ b/Example/Example/ExampleApp.swift @@ -25,7 +25,7 @@ struct ExampleApp: App { pushActions: [ PushAction(title: "push") { $0.push( - screenTag: "Screen 2", + tag: "Screen 2", ExampleScreenView( title: "Screen 2", pushActions: [ @@ -56,7 +56,7 @@ struct ExampleApp: App { ], presentActions: [ PresentAction(title: "present") { - $0.present(.fullScreenCover, screenTag: "Screen 2", ExampleScreenView( + $0.present(.fullScreenCover, tag: "Screen 2", ExampleScreenView( title: "Screen 2", presentActions: [ PresentAction(title: "Update global state \(globalState)") { _ in diff --git a/README.md b/README.md index 9e19fc6..2b51a4e 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,223 @@ # ScreenNavigatorKit -# API -`NavigationStack` -- push -- push(screenTag:) -- pop -- pop(screenTag:) -- popLast(_ k:) -- popToRoot +Framework that provide convenient environment for manage navigation in SwiftUI. -`ModalStack` -- present(_ presentationStyle:) -- present(_ presentationStyle:screenTag:) -- dismiss -- dismiss(screenTag:) -- dismissLast(_ k:) -- dismissAll -- `PresentationStyle` - - sheet - - fullScreenCover +## Pros: +- 🤢 No boolean flag such as `@State var isActive` +- 🤮 No `enum` flag such as `@State var route: RouteAction?` with big `switch-case` statement +- 🤡 No implicit `UIKit` hacks with `UIViewController` +- 💩 No singleton/shared/global presenter of application + +### 🔩 Requirements + +- iOS 14.0+ +- Xcode 12.0+ +- Swift 5.3+ + +# 🧐 How it work?! + +Framework has only two `state object`, each of which isolates "toggle-work" of `@State var isActive: Bool` and `@State var isPresent: Bool` flags. -# Usage +## 1. `NavigationStack` +Like `UINavigationController`, it **store stack state** and **provide stack transformation** using `push` and `pop` methods: ```swift -import ScreenNavigatorKit +let navigationStack = NavigationStack() -@main -struct ExampleApp: App { - @StateObject var navigationStack = NavigationStack() - - var body: some Group { - WindowGroup { - // under-the-hood NavigationView - NavigationStackView(navigationStack) { - RootScreenView() - } +// Standard usage + +navigationStack.push(Text("My View")) +navigationStack.pop() +navigationStack.popToRoot() + +// Advanced usage + +enum Screen: Hashable { + case detail + ... +} + +navigationStack.push(tag: Screen.detail, DetailView()) +navigationStack.pop(to: Screen.detail) +``` +Its companion is `NavigationStackView` – wrapper over `NavigationView` that bind `NavigationStack` with it: +```swift +struct ContentView: View { + @StateObject var navigationStack = NavigationStack() + + var body: some View { + NavigationStackView(navigationStack) { + RootView( + showDetails: { model in + navigationStack.push(DetailView(model: model)) + }, + showSettings: { + navigationStack.push(SettingsView()) + } + ) } } } -// RootScreenView.swift +// Another usage with automatic initialized NavigationStack + +struct ContentView: View { + var body: some View { + NavigationStackView { navigationStack in + RootView( + showDetails: { model in + navigationStack.push(DetailView(model: model)) + }, + showSettings: { + navigationStack.push(SettingsView()) + } + ) + } + } +} +``` -struct RootScreenView: View { +Any pushed view has access to `NavigationStack` of `NavigationStackView` through `EnvironmentObject`: +```swift +struct DetailView: View { + let model: Model @EnvironmentObject var navigationStack: NavigationStack - var body: View { + var body: some View { VStack { - Text("Root") - - Button("Push Screen 1") { - navigationStack.push(FirstScreenView()) - } - - Button("Present Screen 2") { - navigationStack.push( - screenTag: "Screen2", - SecondScreenView() - ) + Text(model.title) + Button("pop to root") { + navigationStack.popToRoot() } } } } +``` +**💫 EXTRA FEATURE:** You can tag any pushed view using any `Hashable` type. It allow refer to specific screen on pop: +```swift +navigationStack.push(tag: "Screen 1", Screen1())) +navigationStack.pop(to: "Screen 1") +``` -// FirstScreenView.swift +## 2. `ModalStack` -struct FirstScreenView: View { - @EnvironmentObject var navigationStack: NavigationStack - @StateObject var modalStack = ModalStack() +Like `NavigationStack`, the `ModalStack` **control modal stack hierarchy** and **provide stack transformation** using `present` and `dismiss` methods: +```swift +let modalStack = ModalStack() - var body: View { - VStack { - Text("First") - - Button("Pop") { - navigationStack.pop() - } +// Standard usage - Button("Pop To Root") { - navigationStack.popToRoot() - } +modalStack.present(.sheet, Text("My View")) +modalStack.present(.fullScreenCover, Text("Another View")) +modalStack.dismiss() +modalStack.dismissAll() - Button("Present Modal Screen") { - modalStack.present( - .sheet, - Text("Modal Screen Example") - ) - } - } - .definesPresentationContext(with: modalStack) - } +// Advanced usage + +enum Screen: Hashable { + case detail + ... } -// SecondScreenView.swift +modalStack.present(.sheet, tag: Screen.detail, DetailView()) +modalStack.dismiss(to: Screen.detail) +``` -struct SecondScreenView: View { - @EnvironmentObject var navigationStack: NavigationStack +**🚧 NOTE:** `SwiftUI` not allow dismiss multiple views at once! Therefore, methods such as `dismissAll()` or `dismiss(to:)`/`dismiss(from:)` will close all views **sequentially**. - var body: View { - VStack { - Text("Second") +To attach `ModalStack` to a `view`, you need to declare a **root view** on top of which all views will be presented using the method `definesPresentationContext(with:)`: +```swift +struct ExampleApp: App { + @StateObject var modalStack = ModalStack() - Button("Pop from taggeg screen") { - navigationStack.pop(from: "Screen 2") - } + var body: some Scene { + WindowGroup { + RootView() + .definesPresentationContext(with: modalStack) + // or just call .definesPresentationContext() + } + } +} +``` +Any presented view has access to `ModalStack` through `EnvironmentObject` too: +```swift +struct RootView: View { + @EnvironmentObject var modalStack: ModalStack - Button("Pop last 2 screen") { - screenNavigator.popLast(2) + var body: some View { + VStack { + Text("Home screen") + Button("FAQ") { + modalStack.present(.sheet, FAQView()) + } + Button("Auhorize") { + modalStack.present(.fullScreenCover, LoginView()) } } } } -``` \ No newline at end of file +``` +> 💫 Just like in `NavigationStack` you can tag presented views when present with `ModalStack` + +# API +`NavigationStack` +- push +- push(tag:) +- pop +- pop(tag:) +- popLast(_ k:) +- popToRoot + +`ModalStack` +- present(_ presentationStyle:) +- present(_ presentationStyle:tag:) +- dismiss +- dismiss(tag:) +- dismissLast(_ k:) +- dismissAll +- `PresentationStyle` + - sheet + - fullScreenCover + +# FAQ + +> Can i mix this framework with existing navigation approach in my project? + +**Yes, you can**. The framework does not affect navigation built in other ways, such as through the standard `@State var isActive: Bool` flags or through UIKit hacks.\ +`NavigationStack` and `ModalStack` create local state and and manage only their own state. + +> What about `Alert`? + +Unfortunately, the framework **does not support** such a mechanism for working with `Alert`, BUT **you can implement it yourself by analogy** with `ModalStack`.\ +Your project can have many different custom presentations (`popup`, `snackbar`, `toats`, `notifications`) and each of them require specific logic for handle hierarchy, depending on their implementation.\ +So adding new presentation methods to the framework **is not planned**. + +# 📦 Installation + +#### [Swift Package Manager](https://github.com/apple/swift-package-manager) + +Create a `Package.swift` file. + +```swift +// swift-tools-version:5.3 + +import PackageDescription + +let package = Package( + name: "YOUR_PROJECT_NAME", + dependencies: [ + .package(url: "https://github.com/Ernest0-Production/ScreenNavigatorKit.git", from: "0.0.3") + ], + targets: [ + .target(name: "YOUR_TARGET_NAME", dependencies: ["ScreenNavigatorKit"]) + ] +) +``` + +### Credits + +- [Telegram](https://t.me/Ernest0n) + +### License + +ScreenNavigatorKit is released under the MIT license. See [LICENSE](https://github.com/Ernest0-Production/ScreenNavigatorKit/blob/main/LICENSE.md) for details. diff --git a/Sources/ScreenNavigatorKit/Internal/Helpers/AnySequence+first.swift b/Sources/ScreenNavigatorKit/Internal/Helpers/AnySequence+first.swift index 3636856..8c1b7fb 100644 --- a/Sources/ScreenNavigatorKit/Internal/Helpers/AnySequence+first.swift +++ b/Sources/ScreenNavigatorKit/Internal/Helpers/AnySequence+first.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// AnySequence+first.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 09.05.2022. // diff --git a/Sources/ScreenNavigatorKit/Internal/Helpers/View+asAny.swift b/Sources/ScreenNavigatorKit/Internal/Helpers/View+asAny.swift index 25bb769..c33f5fe 100644 --- a/Sources/ScreenNavigatorKit/Internal/Helpers/View+asAny.swift +++ b/Sources/ScreenNavigatorKit/Internal/Helpers/View+asAny.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// View+asAny.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 03.05.2022. // diff --git a/Sources/ScreenNavigatorKit/Internal/LinkedList.swift b/Sources/ScreenNavigatorKit/Internal/LinkedList.swift index 29730b9..1b40db8 100644 --- a/Sources/ScreenNavigatorKit/Internal/LinkedList.swift +++ b/Sources/ScreenNavigatorKit/Internal/LinkedList.swift @@ -1,6 +1,6 @@ // -// WeakLinkedList.swift -// +// LinkedList.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 04.05.2022. // diff --git a/Sources/ScreenNavigatorKit/ModalStack/ModalStack.swift b/Sources/ScreenNavigatorKit/ModalStack/ModalStack.swift index 0c1c822..29e450d 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/ModalStack.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/ModalStack.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// ModalStack.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 03.05.2022. // @@ -8,14 +8,24 @@ import SwiftUI import Combine - +/// Managed object that control modal stack hierarchy when present or dismiss views. +/// +/// - Should be binded in specific View that become root view. public final class ModalStack: ObservableObject { + // MARK: - Initializer + public init() {} + // MARK: - Properties + private let screenList = LinkedList( head: PresentScreen() // Presenter screen ) + public var itemsCount: Int { + screenList.sequence.reduce(0) { sum, _ in sum + 1 } + } + // MARK: - Presenter View body func rootBody(content: Content) -> some View { @@ -27,24 +37,29 @@ public final class ModalStack: ObservableObject { // MARK: - Present - public func present( + /// Present view over last presented view. + public func present( _ presentationStyle: PresentationStyle, - screenTag: ScreenTag, _ destination: Destination ) { present( presentationStyle: presentationStyle, - hashTag: AnyHashable(screenTag), + hashTag: nil, destination: destination ) } - public func present( + /// Present view over last presented view tagging it with a tag. + /// + /// The tag will allow to dismiss(to:) or dismiss(from:) exactly to this view. + public func present( _ presentationStyle: PresentationStyle, - _ destination: Destination) { + tag: Tag, + _ destination: Destination + ) { present( presentationStyle: presentationStyle, - hashTag: nil, + hashTag: AnyHashable(tag), destination: destination ) } @@ -72,16 +87,25 @@ public final class ModalStack: ObservableObject { // MARK: - Dismiss + /// Dismiss top-most view in modal stack. + /// + /// If modal stack has only root view, it will stay on root view. public func dismiss() { dismissLast(1) } + /// Dismiss all presented view **consequentially**. + /// + /// If modal stack has only root view, it will stay on root view. public func dismissAll() { screenList.tail .previousNodesSequence .forEach({ $0.presentedView = nil }) } + /// Dismiss last top-most views **consequentially**. + /// + /// If modal stack has only root view, it will stay on root view. public func dismissLast(_ screensNumber: Int) { let currentScreen = screenList.tail .previousNodesSequence @@ -94,9 +118,13 @@ public final class ModalStack: ObservableObject { .forEach({ $0.presentedView = nil }) } - public func dismiss(from screenTag: Tag) { + /// Dismiss all top-most view starting with view specific tag. + /// + /// Search first view with passed screen tag and remove it and all next presented views **consequentially**. + /// If the view is not found, nothing happens. + public func dismiss(from tag: Tag) { let currentScreen = screenList.sequence - .first(where: { $0.element.tag == AnyHashable(screenTag) })? + .first(where: { $0.element.tag == AnyHashable(tag) })? .previousNode currentScreen? @@ -105,9 +133,13 @@ public final class ModalStack: ObservableObject { .forEach({ $0.presentedView = nil }) } - public func dismiss(to screenTag: Tag) { + /// Dismiss to view with view specific tag. + /// + /// Search first view with passed screen tag and remove it and all next presented views **consequentially**. + /// If the view is not found, nothing happens. + public func dismiss(to tag: Tag) { let currentScreen = screenList.sequence - .first(where: { $0.element.tag == AnyHashable(screenTag) }) + .first(where: { $0.element.tag == AnyHashable(tag) }) currentScreen? .nextNodesSequence diff --git a/Sources/ScreenNavigatorKit/ModalStack/PresentScreen.swift b/Sources/ScreenNavigatorKit/ModalStack/PresentScreen.swift index 9aad7ee..fc79693 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/PresentScreen.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/PresentScreen.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// PresentScreen.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 09.05.2022. // diff --git a/Sources/ScreenNavigatorKit/ModalStack/PresentationStyle.swift b/Sources/ScreenNavigatorKit/ModalStack/PresentationStyle.swift index cde94ce..7ef3bf7 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/PresentationStyle.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/PresentationStyle.swift @@ -1,11 +1,12 @@ // -// File.swift -// +// PresentationStyle.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 09.05.2022. // public enum PresentationStyle { case sheet + /// Presents a modal view that covers as much of the screen as possible. case fullScreenCover } diff --git a/Sources/ScreenNavigatorKit/ModalStack/View+definesPresentationContext.swift b/Sources/ScreenNavigatorKit/ModalStack/View+definesPresentationContext.swift index c00b7a0..98ba39c 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/View+definesPresentationContext.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/View+definesPresentationContext.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// View+definesPresentationContext.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 03.05.2022. // @@ -8,6 +8,7 @@ import SwiftUI public extension View { + /// Makes this view the **root view** on top of which all ModalStack views will be presented. func definesPresentationContext( with modalStack: @autoclosure @escaping () -> ModalStack = ModalStack() ) -> some View { diff --git a/Sources/ScreenNavigatorKit/ModalStack/View+fullScreenCover.swift b/Sources/ScreenNavigatorKit/ModalStack/View+fullScreenCover.swift index b93dd00..8af6faa 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/View+fullScreenCover.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/View+fullScreenCover.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// View+fullScreenCover.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 25.04.2022. // diff --git a/Sources/ScreenNavigatorKit/ModalStack/View+sheet.swift b/Sources/ScreenNavigatorKit/ModalStack/View+sheet.swift index 37b86bb..83dbf45 100644 --- a/Sources/ScreenNavigatorKit/ModalStack/View+sheet.swift +++ b/Sources/ScreenNavigatorKit/ModalStack/View+sheet.swift @@ -1,5 +1,5 @@ // -// View+present.swift +// View+sheet.swift // ScreenNavigatorKit // // Created by Ernest Babayan on 17.04.2022. diff --git a/Sources/ScreenNavigatorKit/NavigationStack/NavigationStack.swift b/Sources/ScreenNavigatorKit/NavigationStack/NavigationStack.swift index 0dae5a9..ba638a6 100644 --- a/Sources/ScreenNavigatorKit/NavigationStack/NavigationStack.swift +++ b/Sources/ScreenNavigatorKit/NavigationStack/NavigationStack.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// NavigationStack.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 03.05.2022. // @@ -8,13 +8,25 @@ import SwiftUI import Combine +/// Managed object that change navigation view stack hierarchy on push or pop operation. +/// +/// - Should be binded in specific NavigationStackView +/// - Can not be empty. It always has a root view even if it's a EmptyView public final class NavigationStack: ObservableObject { + // MARK: - Initializer + public init() {} + // MARK: - Properties + private let screenList = LinkedList( head: PushScreen() // Root screen ) + public var itemsCount: Int { + screenList.sequence.reduce(0) { sum, _ in sum + 1 } + } + // MARK: - Root View body func rootBody(content: Content) -> some View { @@ -26,6 +38,7 @@ public final class NavigationStack: ObservableObject { // MARK: - Push + /// Push view in navigation stack view. public func push(_ destination: Destination) { push( hashTag: .none, @@ -33,12 +46,15 @@ public final class NavigationStack: ObservableObject { ) } - public func push( - screenTag: ScreenTag, + /// Push view in navigation stack view tagging it with a tag. + /// + /// The tag will allow to pop(to:) or pop(from:) exactly to this view. + public func push( + tag: Tag, _ destination: Destination ) { push( - hashTag: AnyHashable(screenTag), + hashTag: AnyHashable(tag), destination: destination ) } @@ -64,30 +80,45 @@ public final class NavigationStack: ObservableObject { // MARK: - Pop + /// Pop top-most view in navigation stack view. + /// + /// If navigation stack has not any pushed views, it will stay on root view. public func pop() { popLast(1) } + /// Pop to root view in navigation stack view. public func popToRoot() { screenList.head.element.pushedView = nil } + /// Pop last top-most views in navigation stack view. + /// + /// If navigation stack has not any pushed views, it will stay on root view. public func popLast(_ screensNumber: Int) { let currentScreen = screenList.tail.previousNodesSequence.dropFirst(screensNumber).first currentScreen?.pushedView = nil } - public func pop(from screenTag: Tag) { + /// Pop all top-most views starting with view with specific tag in navigation stack view. + /// + /// Search first view with passed screen tag and remove it and all next views from navigation stack. + /// If the view is not found, nothing happens. + public func pop(from tag: Tag) { let currentScreen = screenList.sequence - .first(where: { $0.element.tag == AnyHashable(screenTag) })? + .first(where: { $0.element.tag == AnyHashable(tag) })? .previousNode currentScreen?.pushedView = nil } - public func pop(to screenTag: Tag) { + /// Pop to view with specific tag in navigation stack view. + /// + /// Search first view with passed screen tag and remove all next views from navigation stack until tagged view is at the top of the stack. + /// If the view is not found, nothing happens. + public func pop(to tag: Tag) { let currentScreen = screenList.sequence - .first(where: { $0.element.tag == AnyHashable(screenTag) }) + .first(where: { $0.element.tag == AnyHashable(tag) }) currentScreen?.pushedView = nil } diff --git a/Sources/ScreenNavigatorKit/NavigationStack/NavigationStackView.swift b/Sources/ScreenNavigatorKit/NavigationStack/NavigationStackView.swift index bce2c9c..555d5ae 100644 --- a/Sources/ScreenNavigatorKit/NavigationStack/NavigationStackView.swift +++ b/Sources/ScreenNavigatorKit/NavigationStack/NavigationStackView.swift @@ -1,27 +1,36 @@ // -// File.swift -// +// NavigationStackView.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 03.05.2022. // import SwiftUI +/// Under-the-hood it's a NavigationView binded to NavigationStack state object +/// that provides (as environment object) tools to change navigation stack (i.e. push/pop) in child views. public struct NavigationStackView: View { public init( _ navigationStack: @autoclosure @escaping () -> NavigationStack = NavigationStack(), @ViewBuilder content: @escaping () -> Content ) { self._navigationStack = StateObject(wrappedValue: navigationStack()) + self.content = { _ in content() } + } + + public init(@ViewBuilder content: @escaping (NavigationStack) -> Content) { + self._navigationStack = StateObject(wrappedValue: NavigationStack()) self.content = content } @StateObject var navigationStack: NavigationStack - @ViewBuilder let content: () -> Content + @ViewBuilder let content: (NavigationStack) -> Content public var body: some View { NavigationView { - navigationStack.rootBody(content: content()) + navigationStack.rootBody( + content: content(navigationStack) + ) } .environmentObject(navigationStack) } diff --git a/Sources/ScreenNavigatorKit/NavigationStack/PushScreen.swift b/Sources/ScreenNavigatorKit/NavigationStack/PushScreen.swift index bc3e29f..7bd9750 100644 --- a/Sources/ScreenNavigatorKit/NavigationStack/PushScreen.swift +++ b/Sources/ScreenNavigatorKit/NavigationStack/PushScreen.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// PushScreen.swift +// ScreenNavigatorKit // // Created by Ernest Babayan on 08.05.2022. //