Skip to content

Commit

Permalink
Adds docs and updates examples
Browse files Browse the repository at this point in the history
  • Loading branch information
johnpatrickmorgan committed Dec 6, 2022
1 parent b9c301b commit 3627422
Show file tree
Hide file tree
Showing 15 changed files with 81 additions and 36 deletions.
21 changes: 2 additions & 19 deletions NavigationBackportApp/Shared/ArrayBindingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,9 @@ private struct NumberView: View {

private struct EmojiView: View {
let visualisation: EmojiVisualisation
@State var isPushing = false

var body: some View {
VStack {
Text(visualisation.text)
Toggle(isOn: $isPushing, label: { Text("Push") }).labelsHidden()
}
.nbNavigationDestination(isPresented: $isPushing, destination: {
TestView()
})
.navigationTitle("Visualise \(visualisation.count)")
}
}

struct TestView: View {
@Environment(\.presentationMode) var presentationMode

var body: some View {
Button("Back") {
presentationMode.wrappedValue.dismiss()
}
Text(visualisation.text)
.navigationTitle("Visualise \(visualisation.count)")
}
}
3 changes: 3 additions & 0 deletions Sources/NavigationBackport/CodableRepresentation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

public extension NBNavigationPath {
/// A codable representation of a navigation path.
struct CodableRepresentation {
static let encoder = JSONEncoder()
static let decoder = JSONDecoder()
Expand Down Expand Up @@ -36,6 +37,8 @@ extension NBNavigationPath.CodableRepresentation: Encodable {
return try _openExistential(element, do: encodeOpened(_:))
}

/// Encodes the representation into the encoder's unkeyed container.
/// - Parameter encoder: The encoder to use.
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in elements.reversed() {
Expand Down
1 change: 1 addition & 0 deletions Sources/NavigationBackport/DestinationBuilderHolder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import SwiftUI

/// Keeps hold of the destination builder closures for a given type or local destination ID.
class DestinationBuilderHolder: ObservableObject {
static func identifier(for type: Any.Type) -> String {
String(reflecting: type)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import SwiftUI

/// Modifier for appending a new destination builder.
struct DestinationBuilderModifier<TypedData>: ViewModifier {
let typedDestinationBuilder: DestinationBuilder<TypedData>

Expand Down
1 change: 1 addition & 0 deletions Sources/NavigationBackport/DestinationBuilderView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import SwiftUI

/// Builds a view from the given Data, using the destination builder environment object.
struct DestinationBuilderView<Data>: View {
let data: Data

Expand Down
35 changes: 18 additions & 17 deletions Sources/NavigationBackport/LocalDestinationBuilderModifier.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Foundation
import SwiftUI

/// Uniquely identifies an instance of a local destination builder.
struct LocalDestinationID: RawRepresentable, Hashable {
let rawValue: UUID
}

/// Persistent object to hold the local destination ID and remove it when the destination builder is removed.
class LocalDestinationIDHolder: ObservableObject {
let id = LocalDestinationID(rawValue: UUID())
weak var destinationBuilder: DestinationBuilderHolder?
Expand All @@ -14,6 +16,7 @@ class LocalDestinationIDHolder: ObservableObject {
}
}

/// Modifier that appends a local destination builder and ensures the Bool binding is observed and updated.
struct LocalDestinationBuilderModifier: ViewModifier {
let isPresented: Binding<Bool>
let builder: () -> AnyView
Expand All @@ -26,26 +29,24 @@ struct LocalDestinationBuilderModifier: ViewModifier {
destinationBuilder.appendLocalBuilder(identifier: destinationID.id, builder)
destinationID.destinationBuilder = destinationBuilder

return Group {
content
.environmentObject(destinationBuilder)
.onChange(of: pathHolder.path) { _ in
if isPresented.wrappedValue {
if !pathHolder.path.contains(where: { ($0 as? LocalDestinationID) == destinationID.id }) {
isPresented.wrappedValue = false
}
return content
.environmentObject(destinationBuilder)
.onChange(of: pathHolder.path) { _ in
if isPresented.wrappedValue {
if !pathHolder.path.contains(where: { ($0 as? LocalDestinationID) == destinationID.id }) {
isPresented.wrappedValue = false
}
}
}
.onChange(of: isPresented.wrappedValue) { isPresented in
if isPresented {
pathHolder.path.append(destinationID.id)
} else {
let index = pathHolder.path.lastIndex(where: { ($0 as? LocalDestinationID) == destinationID.id })
if let index {
pathHolder.path.remove(at: index)
}
.onChange(of: isPresented.wrappedValue) { isPresented in
if isPresented {
pathHolder.path.append(destinationID.id)
} else {
let index = pathHolder.path.lastIndex(where: { ($0 as? LocalDestinationID) == destinationID.id })
if let index {
pathHolder.path.remove(at: index)
}
}
}
}
}
}
1 change: 1 addition & 0 deletions Sources/NavigationBackport/NBNavigationLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import SwiftUI

@available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
/// When value is non-nil, shows the destination associated with its type.
public struct NBNavigationLink<P: Hashable, Label: View>: View {
var value: P?
var label: Label
Expand Down
4 changes: 4 additions & 0 deletions Sources/NavigationBackport/NBNavigationPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import Foundation
import SwiftUI

@available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
/// A type-erased wrapper for an Array of any Hashable types, to be displayed in a `NBNavigationStack`.
public struct NBNavigationPath: Equatable {
var elements: [AnyHashable]

/// The number of screens in the path.
public var count: Int { elements.count }

/// WHether the path is empty.
public var isEmpty: Bool { elements.isEmpty }

public init(_ elements: [AnyHashable] = []) {
Expand Down
1 change: 1 addition & 0 deletions Sources/NavigationBackport/NBNavigationStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import SwiftUI

@available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
/// A replacement for SwiftUI's `NavigationStack` that's available on older OS versions.
public struct NBNavigationStack<Root: View, Data: Hashable>: View {
var unownedPath: Binding<[Data]>?
@StateObject var ownedPath = NavigationPathHolder()
Expand Down
5 changes: 5 additions & 0 deletions Sources/NavigationBackport/NavigationBackport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import SwiftUI
typealias DestinationBuilder<T> = (T) -> AnyView

enum NavigationBackport {
/// Calculates the minimal number of steps to update from one stack of screens to another, within the constraints of SwiftUI.
/// - Parameters:
/// - start: The initial state.
/// - end: The goal state.
/// - Returns: A series of state updates from the start to end.
public static func calculateSteps<Screen>(from start: [Screen], to end: [Screen]) -> [[Screen]] {
let replacableScreens = end.prefix(start.count)
let remainingScreens = start.count < end.count ? end.suffix(from: start.count) : []
Expand Down
1 change: 1 addition & 0 deletions Sources/NavigationBackport/NavigationPathHolder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import SwiftUI

/// An object that publishes changes to the path Array it holds.
class NavigationPathHolder: ObservableObject {
@Published var path: [AnyHashable] = []
}
1 change: 1 addition & 0 deletions Sources/NavigationBackport/Navigator.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

/// A navigator to use when the `NBNavigationStack` is initialized with a `NBNavigationPath` binding or no binding.`
public typealias PathNavigator = Navigator<AnyHashable>

/// An object available via the environment that gives access to the current path.
Expand Down
1 change: 1 addition & 0 deletions Sources/NavigationBackport/PathAppender.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import SwiftUI

/// An object that never publishes changes, but allows appending to an NBNavigationStack's path.
class PathAppender: ObservableObject {
var append: ((AnyHashable) -> Void)?
}
36 changes: 36 additions & 0 deletions Sources/NavigationBackport/View+nbNavigationDestination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ public extension View {

public extension View {
@available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
/// Associates a destination view with a binding that can be used to push
/// the view onto a ``NBNavigationStack``.
///
/// In general, favor binding a path to a navigation stack for programmatic
/// navigation. Add this view modifer to a view inside a ``NBNavigationStack``
/// to programmatically push a single view onto the stack. This is useful
/// for building components that can push an associated view. For example,
/// you can present a `ColorDetail` view for a particular color:
///
/// @State private var showDetails = false
/// var favoriteColor: Color
///
/// NBNavigationStack {
/// VStack {
/// Circle()
/// .fill(favoriteColor)
/// Button("Show details") {
/// showDetails = true
/// }
/// }
/// .nbNavigationDestination(isPresented: $showDetails) {
/// ColorDetail(color: favoriteColor)
/// }
/// .nbNavigationTitle("My Favorite Color")
/// }
///
/// Do not put a navigation destination modifier inside a "lazy" container,
/// like ``List`` or ``LazyVStack``. These containers create child views
/// only when needed to render on screen. Add the navigation destination
/// modifier outside these containers so that the navigation stack can
/// always see the destination.
///
/// - Parameters:
/// - isPresented: A binding to a Boolean value that indicates whether
/// `destination` is currently presented.
/// - destination: A view to present.
func nbNavigationDestination<V>(isPresented: Binding<Bool>, @ViewBuilder destination: () -> V) -> some View where V: View {
let builtDestination = AnyView(destination())
return modifier(
Expand Down
5 changes: 5 additions & 0 deletions Sources/NavigationBackport/apply.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/// Utilty for applying a transform to a value.
/// - Parameters:
/// - transform: The transform to apply.
/// - input: The value to be transformed.
/// - Returns: The transformed value.
func apply<T>(_ transform: (inout T) -> Void, to input: T) -> T {
var transformed = input
transform(&transformed)
Expand Down

0 comments on commit 3627422

Please sign in to comment.