-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
They will instead be removed as part of a minor version bump.
- Loading branch information
1 parent
a8cd8a8
commit 0c2a791
Showing
7 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import Foundation | ||
|
||
/// A thin wrapper around an array. NFlow provides some convenience methods for pushing | ||
/// and popping, and makes it harder to perform navigation operations that SwiftUI does | ||
/// not support. | ||
@available(*, deprecated, message: "Use [Route<Screen>] instead") | ||
public struct NFlow<Screen> { | ||
/// The underlying array of screens. | ||
public internal(set) var array: [Screen] | ||
|
||
/// Initializes the stack with an empty array of screens. | ||
public init() { | ||
self.array = [] | ||
} | ||
|
||
/// Initializes the stack with a single root screen. | ||
/// - Parameter root: The root screen. | ||
public init(root: Screen) { | ||
self.array = [root] | ||
} | ||
|
||
/// Pushes a new screen onto the stack. | ||
/// - Parameter screen: The screen to push. | ||
public mutating func push(_ screen: Screen) { | ||
array.append(screen) | ||
} | ||
|
||
/// Pops a given number of screens off the stack | ||
/// - Parameter count: The number of screens to pop. Defaults to 1. | ||
public mutating func pop(count: Int = 1) { | ||
array = array.dropLast(count) | ||
} | ||
|
||
/// Pops to a given index in the array of screens. The resulting screen count | ||
/// will be index + 1. | ||
/// - Parameter index: The index that should become top of the stack. | ||
public mutating func popTo(index: Int) { | ||
array = Array(array.prefix(index + 1)) | ||
} | ||
|
||
/// Pops to the root screen (index 0). The resulting screen count | ||
/// will be 1. | ||
public mutating func popToRoot() { | ||
popTo(index: 0) | ||
} | ||
|
||
/// Pops to the topmost (most recently pushed) screen in the stack | ||
/// that satisfies the given condition. If no screens satisfy the condition, | ||
/// the screens array will be unchanged. | ||
/// - Parameter condition: The predicate indicating which screen to pop to. | ||
/// - Returns: A `Bool` indicating whether a screen was found. | ||
@discardableResult | ||
public mutating func popTo(where condition: (Screen) -> Bool) -> Bool { | ||
guard let index = array.lastIndex(where: condition) else { | ||
return false | ||
} | ||
popTo(index: index) | ||
return true | ||
} | ||
|
||
/// Replaces the current screen array with a new array. The count of the new | ||
/// array should be no more than the previous stack's count plus one. | ||
/// - Parameter newArray: The new screens array. | ||
public mutating func replaceNFlow(with newArray: [Screen]) { | ||
assert( | ||
newArray.count <= array.count + 1, | ||
""" | ||
ERROR: SwiftUI does not support increasing the navigation stack | ||
by more than one in a single update. (FB9200490) | ||
OLD STACK: | ||
\(array) | ||
NEW STACK: | ||
\(newArray) | ||
""" | ||
) | ||
array = newArray | ||
} | ||
} | ||
|
||
public extension NFlow where Screen: Equatable { | ||
/// Pops to the topmost (most recently pushed) screen in the stack | ||
/// equal to the given screen. If no screens are found, | ||
/// the screens array will be unchanged. | ||
/// - Parameter screen: The predicate indicating which screen to pop to. | ||
/// - Returns: A `Bool` indicating whether a matching screen was found. | ||
@discardableResult | ||
mutating func popTo(_ screen: Screen) -> Bool { | ||
popTo(where: { $0 == screen }) | ||
} | ||
} | ||
|
||
public extension NFlow where Screen: Identifiable { | ||
/// Pops to the topmost (most recently pushed) identifiable screen in the stack | ||
/// with the given ID. If no screens are found, the screens array will be unchanged. | ||
/// - Parameter id: The id of the screen to pop to. | ||
/// - Returns: A `Bool` indicating whether a matching screen was found. | ||
@discardableResult | ||
mutating func popTo(id: Screen.ID) -> Bool { | ||
popTo(where: { $0.id == id }) | ||
} | ||
|
||
/// Pops to the topmost (most recently pushed) identifiable screen in the stack | ||
/// matching the given screen. If no screens are found, the screens array | ||
/// will be unchanged. | ||
/// - Parameter screen: The screen to pop to. | ||
/// - Returns: A `Bool` indicating whether a matching screen was found. | ||
@discardableResult | ||
mutating func popTo(_ screen: Screen) -> Bool { | ||
popTo(id: screen.id) | ||
} | ||
} | ||
|
||
/// Avoids an ambiguity for `popTo` when `Screen` is both `Identifiable` and `Equatable`. | ||
public extension NFlow where Screen: Identifiable & Equatable { | ||
/// Pops to the topmost (most recently pushed) identifiable screen in the stack | ||
/// matching the given screen. If no screens are found, the screens array | ||
/// will be unchanged. | ||
/// - Parameter screen: The screen to pop to. | ||
/// - Returns: A `Bool` indicating whether a matching screen was found. | ||
@discardableResult | ||
mutating func popTo(_ screen: Screen) -> Bool { | ||
popTo(id: screen.id) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import Foundation | ||
import SwiftUI | ||
|
||
/// NStack maintains a stack of pushed views for use within a `NavigationView`. | ||
@available(*, deprecated, message: "Use Router instead. It is capable of both navigation and presentation.") | ||
public struct NStack<Screen, ScreenView: View>: View { | ||
/// The array of screens that represents the navigation stack. | ||
@Binding var stack: [Screen] | ||
|
||
/// A closure that builds a `ScreenView` from a `Screen`and its index. | ||
@ViewBuilder var buildView: (Screen, Int) -> ScreenView | ||
|
||
/// Initializer for creating an NStack using a binding to an array of screens. | ||
/// - Parameters: | ||
/// - stack: A binding to an array of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen` and its index. | ||
public init(_ stack: Binding<[Screen]>, @ViewBuilder buildView: @escaping (Screen, Int) -> ScreenView) { | ||
self._stack = stack | ||
self.buildView = buildView | ||
} | ||
|
||
public var body: some View { | ||
stack | ||
.enumerated() | ||
.reversed() | ||
.reduce(NavigationNode<Screen, ScreenView>.end) { pushedNode, new in | ||
let (index, screen) = new | ||
return NavigationNode<Screen, ScreenView>.view( | ||
buildView(screen, index), | ||
pushing: pushedNode, | ||
stack: $stack, | ||
index: index | ||
) | ||
} | ||
} | ||
} | ||
|
||
public extension NStack { | ||
/// Convenience initializer for creating an NStack using a binding to a `NFlow` | ||
/// of screens. | ||
/// - Parameters: | ||
/// - stack: A binding to a stack of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen`. | ||
init(_ nFlow: Binding<NFlow<Screen>>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) { | ||
self._stack = Binding( | ||
get: { nFlow.wrappedValue.array }, | ||
set: { nFlow.wrappedValue.array = $0 } | ||
) | ||
self.buildView = { screen, _ in buildView(screen) } | ||
} | ||
|
||
/// Convenience initializer for creating an NStack using a binding to a `NFlow` | ||
/// of screens. | ||
/// - Parameters: | ||
/// - stack: A binding to a stack of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen` and its index. | ||
init(_ nFlow: Binding<NFlow<Screen>>, @ViewBuilder buildView: @escaping (Screen, Int) -> ScreenView) { | ||
self._stack = Binding( | ||
get: { nFlow.wrappedValue.array }, | ||
set: { nFlow.wrappedValue.array = $0 } | ||
) | ||
self.buildView = buildView | ||
} | ||
|
||
/// Convenience initializer for creating an NStack without using an index in the | ||
/// `buildView` closure. | ||
/// - Parameters: | ||
/// - stack: A binding to a stack of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen`. | ||
init(_ stack: Binding<[Screen]>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) { | ||
self._stack = stack | ||
self.buildView = { screen, _ in buildView(screen) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import Foundation | ||
import SwiftUI | ||
|
||
/// A view that represents a linked list of views, each pushing the next in | ||
/// a navigation stack. | ||
indirect enum NavigationNode<Screen, V: View>: View { | ||
case view(V, pushing: NavigationNode<Screen, V>, stack: Binding<[Screen]>, index: Int) | ||
case end | ||
|
||
private var isActiveBinding: Binding<Bool> { | ||
switch self { | ||
case .end, .view(_, .end, _, _): | ||
return .constant(false) | ||
case .view(_, .view, let stack, let index): | ||
return Binding( | ||
get: { | ||
stack.wrappedValue.count > index + 1 | ||
}, | ||
set: { isPushed in | ||
guard !isPushed else { return } | ||
guard stack.wrappedValue.count > index + 1 else { return } | ||
stack.wrappedValue = Array(stack.wrappedValue.prefix(index + 1)) | ||
} | ||
) | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private var pushingView: some View { | ||
switch self { | ||
case .end: | ||
EmptyView() | ||
case .view(let view, _, _, _): | ||
view | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private var pushedView: some View { | ||
switch self { | ||
case .end: | ||
EmptyView() | ||
case .view(_, let node, _, _): | ||
node | ||
} | ||
} | ||
|
||
var body: some View { | ||
pushingView | ||
.background( | ||
NavigationLink(destination: pushedView, isActive: isActiveBinding, label: EmptyView.init) | ||
// NOTE: If this is set to true, there are some unexpected | ||
// pops when pushing more than 3 screens. | ||
.isDetailLinkiOS() | ||
.hidden() | ||
) | ||
} | ||
} | ||
|
||
extension NavigationLink { | ||
func isDetailLinkiOS() -> some View { | ||
#if os(iOS) | ||
isDetailLink(false) | ||
#else | ||
self | ||
#endif | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import Foundation | ||
|
||
/// A thin wrapper around an array. PFlow provides some convenience methods for presenting and dismissing. | ||
@available(*, deprecated, message: "Use [Route<Screen>] instead") | ||
public struct PFlow<Screen> { | ||
/// The underlying array of screens. | ||
public internal(set) var array: [(Screen, PresentationOptions)] | ||
|
||
/// Initializes the stack with an empty array of screens. | ||
public init() { | ||
self.array = [] | ||
} | ||
|
||
/// Initializes the stack with a single root screen. | ||
/// - Parameter root: The root screen. | ||
public init(root: Screen) { | ||
self.array = [(root, .init(style: .default))] | ||
} | ||
|
||
/// Pushes a new screen onto the stack. | ||
/// - Parameter screen: The screen to present. | ||
/// - Parameter style: How to present the screen. | ||
/// - Parameter onDismiss: Called when the presented view is later | ||
/// dismissed. | ||
public mutating func present(_ screen: Screen, style: PresentationStyle = .default, onDismiss: (() -> Void)? = nil) { | ||
let options = PresentationOptions(style: style, onDismiss: onDismiss) | ||
array.append((screen, options)) | ||
} | ||
|
||
/// Dismisses the top screen off the stack. | ||
public mutating func dismiss() { | ||
array = array.dropLast() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import Foundation | ||
import SwiftUI | ||
|
||
/// PStack maintains a stack of presented views for use within a `PresentationView`. | ||
@available(*, deprecated, message: "Use Router instead. It is capable of both navigation and presentation.") | ||
public struct PStack<Screen, ScreenView: View>: View { | ||
/// The array of screens that represents the presentation stack. | ||
@Binding var stack: [(Screen, PresentationOptions)] | ||
|
||
/// A closure that builds a `ScreenView` from a `Screen`. | ||
@ViewBuilder var buildView: (Screen) -> ScreenView | ||
|
||
/// Initializer for creating an PStack using a binding to an array of screens. | ||
/// - Parameters: | ||
/// - stack: A binding to an array of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen`. | ||
public init(_ stack: Binding<[(Screen, PresentationOptions)]>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) { | ||
self._stack = stack | ||
self.buildView = buildView | ||
} | ||
|
||
public var body: some View { | ||
stack | ||
.enumerated() | ||
.reversed() | ||
.reduce(PresentationNode<Screen, ScreenView>.end) { presentedNode, new in | ||
let (index, (screen, options)) = new | ||
return PresentationNode<Screen, ScreenView>.view( | ||
buildView(screen), | ||
presenting: presentedNode, | ||
stack: $stack, | ||
index: index, | ||
options: options | ||
) | ||
} | ||
} | ||
} | ||
|
||
public extension PStack { | ||
/// Convenience initializer for creating an PStack using a binding to a `PFlow` | ||
/// of screens. | ||
/// - Parameters: | ||
/// - stack: A binding to a PFlow of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen`. | ||
init(_ stack: Binding<PFlow<Screen>>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) { | ||
self._stack = Binding( | ||
get: { stack.wrappedValue.array }, | ||
set: { stack.wrappedValue.array = $0 } | ||
) | ||
self.buildView = buildView | ||
} | ||
} | ||
|
||
public extension PStack { | ||
/// Convenience initializer for creating an PStack using a binding to an array | ||
/// of screens, using the default presentation style. | ||
/// - Parameters: | ||
/// - stack: A binding to an array of screens. | ||
/// - buildView: A closure that builds a `ScreenView` from a `Screen`. | ||
init(_ stack: Binding<[Screen]>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) { | ||
self._stack = Binding( | ||
get: { stack.wrappedValue.map { ($0, .init(style: .default, onDismiss: nil)) } }, | ||
set: { stack.wrappedValue = $0.map { $0.0 } } | ||
) | ||
self.buildView = buildView | ||
} | ||
} |
Oops, something went wrong.