Skip to content

Commit

Permalink
4.7.0
Browse files Browse the repository at this point in the history
dankinsoid committed Mar 14, 2024

Unverified

This user has not yet uploaded their public signing key.
1 parent 243bae5 commit 3145bb5
Showing 5 changed files with 293 additions and 28 deletions.
Binary file modified .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -205,7 +205,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/VDFlow.git", from: "4.6.0")
.package(url: "https://github.com/dankinsoid/VDFlow.git", from: "4.7.0")
],
targets: [
.target(name: "SomeProject", dependencies: ["VDFlow"])
289 changes: 289 additions & 0 deletions Sources/VDFlow/NavigationSteps.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import SwiftUI

public struct NavigationSteps<Selection: Hashable, Content: View>: View {

let content: Content
@StateOrBinding var selection: Selection
@State private var ids: [Selection] = []

public init(selection: Binding<Selection>, @ViewBuilder content: () -> Content) {
self.content = content()
self._selection = .binding(selection)
}

public var body: some View {
_VariadicView.Tree(Root(base: self)) {
content
}
}

private struct Root: _VariadicView.UnaryViewRoot {

let base: NavigationSteps

func body(children: _VariadicView.Children) -> some View {
MyNavigationStack(selection: base.$selection, children: children)
}
}
}

public extension NavigationSteps where Selection == Int {

init(@ViewBuilder content: () -> Content) {
self._selection = StateOrBinding(wrappedValue: 0)
self.content = content()
}
}

private struct StepTag: _ViewTraitKey {

static var defaultValue: AnyHashable = Optional<Int>.none
}

public extension _VariadicView.Children.Element {

var stepTag: AnyHashable {
self[StepTag.self]
}
}

public extension View {

func stepTag<Value: Hashable>(_ value: Value) -> some View {
_trait(StepTag.self, value)
}
}

private final class UINavigationStackDelegate: NSObject, UINavigationControllerDelegate {

var notUpdate = false
var didShow: (UIViewController) -> Void = { _ in }

func navigationController(
_ navigationController: UINavigationController,
didShow viewController: UIViewController,
animated: Bool
) {
didShow(viewController)
}
}

private struct MyNavigationStack<Selection: Hashable>: UIViewControllerRepresentable {

@Binding var selection: Selection
let children: _VariadicView.Children
@State private var delegate = UINavigationStackDelegate()

func makeUIViewController(context: Context) -> UINavigationController {
let controller = UINavigationController()
controller.delegate = delegate
return controller
}

func updateUIViewController(
_ stack: UINavigationController,
context: Context
) {
guard !delegate.notUpdate else { return }
delegate.didShow = { [weak delegate, $selection] controller in
if let tag = controller.stackTag?.id.base as? Selection, tag != $selection.wrappedValue {
delegate?.notUpdate = true
$selection.wrappedValue = tag
delegate?.notUpdate = false
}
}
guard let selectedIndex else { return }
if stack.view.window != nil {
EnvironmentValues.NavigationPopKey._defaultValue = pop
}
stack.setViewControllers(
controllers(for: stack, selectedIndex: selectedIndex),
animated: stack.view.window != nil
)
}

func controllers(for stack: UINavigationController, selectedIndex: Int) -> [UIViewController] {
Array(0...selectedIndex).map { i in
controller(for: i, stack: stack)
}
}

func controller(for i: Int, stack: UINavigationController) -> UIViewController {
let view = children[i]
if let controller = stack.viewControllers.first(where: { $0.stackID?.id == view.id }) as? Host {
controller.rootView = Child(content: view, pop: pop)
return controller
}
let controller = Host(rootView: Child(content: view, pop: pop))
controller.stackID = IDWrapper(children[i].id)
if let tag = tag(of: view, i) {
controller.stackTag = IDWrapper(tag)
}
return controller
}

func tag(of child: _VariadicView.Children.Element, _ i: Int) -> Selection? {
(child.stepTag.base as? Selection) ?? (i as? Selection)
}

func pop(offset: Int) {
guard let selectedIndex else { return }
let newIndex = max(0, min(selectedIndex - offset, children.count - 1))
guard let tag = tag(of: children[newIndex], newIndex) else { return }
selection = tag
}

var selectedIndex: Int? {
let tags = children.enumerated().map {
(tag(of: $0.element, $0.offset), $0.offset)
}
guard let i = tags.first(where: { $0.0 == selection })?.1 else {
return nil
}
return i
}

typealias Host = UIHostingController<Child>

struct Child: View {

let content: _VariadicView.Children.Element
let pop: (Int) -> Void

var body: some View {
content
.environment(\.pop, pop)
}
}
}

extension EnvironmentValues {

enum NavigationPopKey: EnvironmentKey {
static var _defaultValue: (Int) -> Void = { _ in printPreview("hmm") }
static let defaultValue: (Int) -> Void = { Self._defaultValue($0) }
}

public var pop: (Int) -> Void {
get { self[NavigationPopKey.self] }
set { self[NavigationPopKey.self] = newValue }
}

public var popToRoot: () -> Void {
{ pop(.max) }
}

public var push: (Int) -> Void {
{ pop(-$0) }
}
}

private extension UIViewController {

var stackID: IDWrapper? {
get { objc_getAssociatedObject(self, &stackIDKey) as? IDWrapper }
set { objc_setAssociatedObject(self, &stackIDKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
var stackTag: IDWrapper? {
get { objc_getAssociatedObject(self, &stackTagKey) as? IDWrapper }
set { objc_setAssociatedObject(self, &stackTagKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
}

private var stackIDKey = 0
private var stackTagKey = 0

private final class IDWrapper {
var id: AnyHashable

init(_ id: AnyHashable) {
self.id = id
}
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
enum NavStackPreview: PreviewProvider {

static var previews: Previews {
Previews()
}

struct Previews: View {

@State var selection = 0

var body: some View {
NavigationSteps(selection: $selection) {
ForEach(0..<10) { i in
Page(i: i, selection: $selection)
}
}
.previewOverlay()
}
}

struct Page: View {
@Environment(\.pop) var pop
let i: Int
@Binding var selection: Int

var body: some View {
HStack {
if selection > 0 {
Button("Pop") {
pop(1)
}
}
Spacer()
Button("\(i) == \(selection)") {
selection = .random(in: 0..<10)
}
Spacer()
if selection < 9 {
Button("Push") {
pop(-1)
}
}
}
.padding()
}
}
}

final class PreviewPrintObject: ObservableObject {

static let shared = PreviewPrintObject()

@Published var views: [AnyView] = []
}

func printPreview(@ViewBuilder _ view: () -> some View) {
PreviewPrintObject.shared.views.append(AnyView(view()))
PreviewPrintObject.shared.views = Array(PreviewPrintObject.shared.views.suffix(5))
}

func printPreview(_ items: Any?...) {
printPreview { Text("\(items)" as String) }
}

extension View {
func previewOverlay() -> some View {
modifier(PreviewPrintModifier())
}
}

struct PreviewPrintModifier: ViewModifier {

@ObservedObject private var object = PreviewPrintObject.shared

func body(content: Content) -> some View {
content.overlay(
VStack(spacing: 0) {
ForEach(Array(object.views.enumerated()), id: \.offset) { offset, view in
view
}
},
alignment: .bottom
)
}
}
29 changes: 2 additions & 27 deletions Sources/VDFlow/NavigationView+Ext.swift
Original file line number Diff line number Diff line change
@@ -17,8 +17,8 @@ public extension NavigationLink {

public struct NavigationStepDestination<Content: View, Value>: View {

public let content: Content
public let stepBinding: Binding<Value>
let content: Content
let stepBinding: Binding<Value>

public var body: some View {
content
@@ -50,28 +50,3 @@ public extension View {
}
}
}

@available(iOS 16.0, macOS 13.0, watchOS 9.0, *)
public extension StepsCollection {

var navigationPath: NavigationPath {
get {
let allValues = Steps.allCases
guard let selected, let index = allValues.firstIndex(of: selected) else {
return NavigationPath()
}
return NavigationPath(allValues.prefix(through: index))
}
set {
let allValues = Steps.allCases
guard
!newValue.isEmpty,
!allValues.isEmpty
else {
selected = nil
return
}
selected = allValues[allValues.index(allValues.startIndex, offsetBy: newValue.count - 1)]
}
}
}
1 change: 1 addition & 0 deletions Sources/VDFlow/StateStep.swift
Original file line number Diff line number Diff line change
@@ -102,6 +102,7 @@ public extension View {
$0.insert({ binding.wrappedValue.selected = nil }, at: 0)
}
.tag(binding.wrappedValue[keyPath: keyPath].id)
.stepTag(binding.wrappedValue[keyPath: keyPath].id)
}

func stepEnvironment<Value>(_ binding: Binding<Value>) -> some View {

0 comments on commit 3145bb5

Please sign in to comment.