Skip to content

Commit

Permalink
Adds PStack and updates name to FlowStacks
Browse files Browse the repository at this point in the history
  • Loading branch information
johnpatrickmorgan committed Jul 24, 2021
1 parent 6b5ab66 commit f0e2293
Show file tree
Hide file tree
Showing 21 changed files with 732 additions and 39 deletions.
31 changes: 31 additions & 0 deletions FlowStacks.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Pod::Spec.new do |s|

s.name = 'FlowStacks'
s.version = '0.0.7'
s.summary = 'Hoist navigation state into a coordinator in SwiftUI.'

s.description = <<-DESC
FlowStacks allows you to hoist SwiftUI navigation or presentation state into a
higher-level coordinator view. The coordinator pattern allows you to write isolated views
that have zero knowledge of their context within an app.
DESC

s.homepage = 'https://github.com/johnpatrickmorgan/FlowStacks'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'johnpatrickmorgan' => 'johnpatrickmorganuk@gmail.com' }
s.source = { :git => 'https://github.com/johnpatrickmorgan/FlowStacks.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/jpmmusic'


s.ios.deployment_target = '13.0'
s.osx.deployment_target = '11.0'
s.watchos.deployment_target = '7.0'
s.tvos.deployment_target = '13.0'

s.swift_version = '5.4'

s.source_files = 'Sources/**/*'

s.frameworks = 'Foundation', 'SwiftUI'

end
14 changes: 7 additions & 7 deletions Package.swift → FlowStacks/Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
// swift-tools-version:5.3
// swift-tools-version:5.5

import PackageDescription

let package = Package(
name: "NStack",
name: "FlowStacks",
platforms: [
.iOS(.v13), .watchOS(.v7), .macOS(.v11), .tvOS(.v13),
],
products: [
.library(
name: "NStack",
targets: ["NStack"]),
name: "FlowStacks",
targets: ["FlowStacks"]),
],
dependencies: [],
targets: [
.target(
name: "NStack",
name: "FlowStacks",
dependencies: []),
.testTarget(
name: "NStackTests",
dependencies: ["NStack"]),
name: "FlowStacksTests",
dependencies: ["FlowStacks"]),
]
)
50 changes: 30 additions & 20 deletions README.md → FlowStacks/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NStack
# FlowStacks

An NStack allows you to manage SwiftUI navigation state with a single stack property. This makes it easy to hoist that state into a high-level view, such as a coordinator. The coordinator pattern allows you to write isolated views that have zero knowledge of their context within the navigation flow of an app.
*FlowStacks* allow you to manage complex SwiftUI navigation and presentation state with a single piece of state. This makes it easy to hoist that state into a high-level coordinator view. The coordinator pattern allows you to write isolated views that have zero knowledge of their context within the navigation flow of an app.

## Usage

Expand All @@ -14,11 +14,11 @@ enum Screen {
}
```

You can then add a stack of these screens as a single property in a coordinator view. In the body of the coordinator view, return a `NavigationView` containing an `NStack`. The `NStack` should be initialized with a binding to the stack, and a `ViewBuilder` closure. The closure builds a view from a given screen, e.g.:
You can then add a flow representing a stack of these screens (`NFlow` for navigation, or `PFlow` for presentation) as a single property in a coordinator view. In the body of the coordinator view, initialize an `NStack` (or `PStack` for presentation) with a binding to the flow, and a `ViewBuilder` closure. The closure builds a view for a given screen, e.g.:

```swift
struct AppCoordinator: View {
@State var stack = Stack<Screen>(root: .home)
@State var flow = NFlow<Screen>(root: .home)

var body: some View {
NavigationView {
Expand All @@ -36,24 +36,24 @@ struct AppCoordinator: View {
}

private func showNumberList() {
stack.push(.numberList)
flow.push(.numberList)
}

private func showNumber(_ number: Int) {
stack.push(.number(number))
flow.push(.number(number))
}

private func pop() {
stack.pop()
flow.pop()
}

private func popToRoot() {
stack.popToRoot()
flow.popToRoot()
}
}
```

As you can see, pushing a new view is as easy as `stack.push(...)` and popping can be achieved with `stack.pop()`. There are convenience methods for popping to the root and popping to a specific screen in the stack.
As you can see, pushing a new view is as easy as `stack.push(...)` and popping can be achieved with `stack.pop()`. There are convenience methods for popping to the root or popping to a specific screen in the stack.

If the user taps the back button, the stack will be automatically updated to reflect its new state. Navigating back with an edge swipe gesture or long-press gesture on the back button will also update the stack.

Expand All @@ -77,35 +77,35 @@ enum Screen {
}

class AppCoordinatorViewModel: ObservableObject {
@Published var stack = Stack<Screen>()
@Published var flow = NFlow<Screen>()

init() {
stack.push(.home(.init(onGoTapped: showNumberList)))
flow.push(.home(.init(onGoTapped: showNumberList)))
}

func showNumberList() {
stack.push(.numberList(.init(onNumberSelected: showNumber, cancel: pop)))
flow.push(.numberList(.init(onNumberSelected: showNumber, cancel: pop)))
}

func showNumber(_ number: Int) {
stack.push(.numberDetail(.init(number: number, cancel: popToRoot)))
flow.push(.numberDetail(.init(number: number, cancel: popToRoot)))
}

func pop() {
stack.pop()
flow.pop()
}

func popToRoot() {
stack.popToRoot()
flow.popToRoot()
}
}

struct AppCoordinator: View {
@ObservedObject var viewModel = AppCoordinatorViewModel()
@ObservedObject var viewModel: AppCoordinatorViewModel

var body: some View {
NavigationView {
NStack($viewModel.stack) { screen in
NStack($viewModel.flow) { screen in
switch screen {
case .home(let viewModel):
HomeView(viewModel: viewModel)
Expand All @@ -120,10 +120,20 @@ struct AppCoordinator: View {
}
```

## Limitations
## Presentation

In order to use presentation instead of navigation for showing and unshowing screens, the examples above can be re-written using a `PStack` instead of an `NStack`, and a `PFlow` instead of an `NFlow`. The `push` methods become `present` and the `pop` methods become `dismiss`. The presnt method allows you to customize the presentation style and add a callback on dismissal:

Currently, SwiftUI does not support increasing the navigation stack by more than one in a single update. The `Stack` object will throw an assertion failure if you try to do so.
```swift
flow.present(detailView, style: .fullScreenCover) {
print("Detail dismissed")
}
```

## How does it work?

This [blog post](https://johnpatrickmorgan.github.io/2021/07/03/NStack/) outlines how NStack translates the stack of screens into a hierarchy of views and `NavigationLink`s.
This [blog post](https://johnpatrickmorgan.github.io/2021/07/03/NStack/) outlines how `NStack` translates the stack of screens into a hierarchy of views and `NavigationLink`s. `PStack` uses a similar approach.

## Limitations

SwiftUI does not allow more than one screen to be pushed, presented or dismissed in one update, though it is possible to pop any number of views in one update. `NFlow` and `PFlow` only expose methods to make updates that are supported in SwiftUI.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Foundation

/// A thin wrapper around an array. Stack provides some convenience methods for pushing
/// 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.
public struct Stack<Screen> {
public struct NFlow<Screen> {

/// The underlying array of screens.
public internal(set) var array: [Screen]
Expand Down Expand Up @@ -61,7 +61,7 @@ public struct Stack<Screen> {
/// 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 replaceStack(with newArray: [Screen]) {
public mutating func replaceNFlow(with newArray: [Screen]) {
assert(
newArray.count <= array.count + 1,
"""
Expand All @@ -77,7 +77,7 @@ public struct Stack<Screen> {
}
}

extension Stack where Screen: Equatable {
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,
Expand All @@ -90,7 +90,7 @@ extension Stack where Screen: Equatable {
}
}

extension Stack where Screen: Identifiable {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public extension NStack {
/// - Parameters:
/// - stack: A binding to a stack of screens.
/// - buildView: A closure that builds a `ScreenView` from a `Screen`.
init(_ stack: Binding<Stack<Screen>>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) {
init(_ stack: Binding<NFlow<Screen>>, @ViewBuilder buildView: @escaping (Screen) -> ScreenView) {
self._stack = Binding(
get: { stack.wrappedValue.array },
set: { stack.wrappedValue.array = $0 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ indirect enum NavigationNode<Screen, V: View>: View {
},
set: { isPushed in
guard !isPushed else { return }
guard stack.wrappedValue.count > index + 1 else { return }
stack.wrappedValue = Array(stack.wrappedValue.prefix(index + 1))
}
)
Expand Down
34 changes: 34 additions & 0 deletions FlowStacks/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.
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()
}
}
53 changes: 53 additions & 0 deletions FlowStacks/Sources/FlowStacks/Presentation/PStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation
import SwiftUI

/// PStack maintains a stack of presented views for use within a `PresentationView`.
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 `Stack`
/// of screens.
/// - Parameters:
/// - stack: A binding to a stack 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
}
}
Loading

0 comments on commit f0e2293

Please sign in to comment.