diff --git a/NavigationBackportApp/Shared/ArrayBindingView.swift b/NavigationBackportApp/Shared/ArrayBindingView.swift index 6c0a69e..63a97cd 100644 --- a/NavigationBackportApp/Shared/ArrayBindingView.swift +++ b/NavigationBackportApp/Shared/ArrayBindingView.swift @@ -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)") } } diff --git a/Sources/NavigationBackport/CodableRepresentation.swift b/Sources/NavigationBackport/CodableRepresentation.swift index 3651b6c..56712e6 100644 --- a/Sources/NavigationBackport/CodableRepresentation.swift +++ b/Sources/NavigationBackport/CodableRepresentation.swift @@ -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() @@ -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() { diff --git a/Sources/NavigationBackport/DestinationBuilderHolder.swift b/Sources/NavigationBackport/DestinationBuilderHolder.swift index 5a195fc..01d3535 100644 --- a/Sources/NavigationBackport/DestinationBuilderHolder.swift +++ b/Sources/NavigationBackport/DestinationBuilderHolder.swift @@ -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) diff --git a/Sources/NavigationBackport/DestinationBuilderModifier.swift b/Sources/NavigationBackport/DestinationBuilderModifier.swift index 665cd60..982bff0 100644 --- a/Sources/NavigationBackport/DestinationBuilderModifier.swift +++ b/Sources/NavigationBackport/DestinationBuilderModifier.swift @@ -1,6 +1,7 @@ import Foundation import SwiftUI +/// Modifier for appending a new destination builder. struct DestinationBuilderModifier: ViewModifier { let typedDestinationBuilder: DestinationBuilder diff --git a/Sources/NavigationBackport/DestinationBuilderView.swift b/Sources/NavigationBackport/DestinationBuilderView.swift index 8432dbe..3595278 100644 --- a/Sources/NavigationBackport/DestinationBuilderView.swift +++ b/Sources/NavigationBackport/DestinationBuilderView.swift @@ -1,6 +1,7 @@ import Foundation import SwiftUI +/// Builds a view from the given Data, using the destination builder environment object. struct DestinationBuilderView: View { let data: Data diff --git a/Sources/NavigationBackport/LocalDestinationBuilderModifier.swift b/Sources/NavigationBackport/LocalDestinationBuilderModifier.swift index 69b55ed..937b746 100644 --- a/Sources/NavigationBackport/LocalDestinationBuilderModifier.swift +++ b/Sources/NavigationBackport/LocalDestinationBuilderModifier.swift @@ -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? @@ -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 let builder: () -> AnyView @@ -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) + } } } - } } } diff --git a/Sources/NavigationBackport/NBNavigationLink.swift b/Sources/NavigationBackport/NBNavigationLink.swift index d8b575b..c5db736 100644 --- a/Sources/NavigationBackport/NBNavigationLink.swift +++ b/Sources/NavigationBackport/NBNavigationLink.swift @@ -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: View { var value: P? var label: Label diff --git a/Sources/NavigationBackport/NBNavigationPath.swift b/Sources/NavigationBackport/NBNavigationPath.swift index c0a91af..b3e2ba7 100644 --- a/Sources/NavigationBackport/NBNavigationPath.swift +++ b/Sources/NavigationBackport/NBNavigationPath.swift @@ -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] = []) { diff --git a/Sources/NavigationBackport/NBNavigationStack.swift b/Sources/NavigationBackport/NBNavigationStack.swift index 3b7bbdb..df41b3d 100644 --- a/Sources/NavigationBackport/NBNavigationStack.swift +++ b/Sources/NavigationBackport/NBNavigationStack.swift @@ -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: View { var unownedPath: Binding<[Data]>? @StateObject var ownedPath = NavigationPathHolder() diff --git a/Sources/NavigationBackport/NavigationBackport.swift b/Sources/NavigationBackport/NavigationBackport.swift index dafcd00..ef576df 100644 --- a/Sources/NavigationBackport/NavigationBackport.swift +++ b/Sources/NavigationBackport/NavigationBackport.swift @@ -4,6 +4,11 @@ import SwiftUI typealias DestinationBuilder = (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(from start: [Screen], to end: [Screen]) -> [[Screen]] { let replacableScreens = end.prefix(start.count) let remainingScreens = start.count < end.count ? end.suffix(from: start.count) : [] diff --git a/Sources/NavigationBackport/NavigationPathHolder.swift b/Sources/NavigationBackport/NavigationPathHolder.swift index 822fa05..1ea08e8 100644 --- a/Sources/NavigationBackport/NavigationPathHolder.swift +++ b/Sources/NavigationBackport/NavigationPathHolder.swift @@ -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] = [] } diff --git a/Sources/NavigationBackport/Navigator.swift b/Sources/NavigationBackport/Navigator.swift index 85480f2..b82d0aa 100644 --- a/Sources/NavigationBackport/Navigator.swift +++ b/Sources/NavigationBackport/Navigator.swift @@ -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 /// An object available via the environment that gives access to the current path. diff --git a/Sources/NavigationBackport/PathAppender.swift b/Sources/NavigationBackport/PathAppender.swift index 9be7ed9..aa14417 100644 --- a/Sources/NavigationBackport/PathAppender.swift +++ b/Sources/NavigationBackport/PathAppender.swift @@ -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)? } diff --git a/Sources/NavigationBackport/View+nbNavigationDestination.swift b/Sources/NavigationBackport/View+nbNavigationDestination.swift index cb3628c..2fa1de2 100644 --- a/Sources/NavigationBackport/View+nbNavigationDestination.swift +++ b/Sources/NavigationBackport/View+nbNavigationDestination.swift @@ -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(isPresented: Binding, @ViewBuilder destination: () -> V) -> some View where V: View { let builtDestination = AnyView(destination()) return modifier( diff --git a/Sources/NavigationBackport/apply.swift b/Sources/NavigationBackport/apply.swift index 2d29739..0e4f71d 100644 --- a/Sources/NavigationBackport/apply.swift +++ b/Sources/NavigationBackport/apply.swift @@ -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(_ transform: (inout T) -> Void, to input: T) -> T { var transformed = input transform(&transformed)