Skip to content

Commit

Permalink
Reinstates NStack and PStack (#20)
Browse files Browse the repository at this point in the history
They will instead be removed as part of a minor version bump.
  • Loading branch information
johnpatrickmorgan committed Apr 14, 2022
1 parent a8cd8a8 commit 0c2a791
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 0 deletions.
124 changes: 124 additions & 0 deletions Sources/FlowStacks/Navigation/NFlow.swift
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)
}
}
74 changes: 74 additions & 0 deletions Sources/FlowStacks/Navigation/NStack.swift
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) }
}
}
68 changes: 68 additions & 0 deletions Sources/FlowStacks/Navigation/NavigationNode.swift
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
}
}
34 changes: 34 additions & 0 deletions Sources/FlowStacks/Presentation/PFlow.swift
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()
}
}
67 changes: 67 additions & 0 deletions Sources/FlowStacks/Presentation/PStack.swift
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
}
}

0 comments on commit 0c2a791

Please sign in to comment.