Skip to content

Commit

Permalink
Enabling full programmatic navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
kamaal111 committed Feb 27, 2024
1 parent 9cdd108 commit 4d55344
Show file tree
Hide file tree
Showing 7 changed files with 36 additions and 122 deletions.
3 changes: 0 additions & 3 deletions Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ struct ContentView: View {
subView: { screen in MainView(screen: screen, displayMode: .inline) },
sidebar: { Sidebar() }
)
.onScreenChange { screen in
print("screen", screen)
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions Example/Example/Views/Screens/HomeScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ private let logger = KamaalLogger(from: HomeScreen.self)
struct HomeScreen: View {
@Environment(\.colorScheme) private var colorScheme

@EnvironmentObject private var navigator: Navigator<Screens>

@StateObject private var popUpManager = KPopUpManager()

@State private var showPopUp = false
Expand Down Expand Up @@ -49,6 +51,9 @@ struct HomeScreen: View {
}) {
Text("Hud popup")
}
Button(action: { navigator.navigate(to: .coreData) }) {
Text("Brogrammatic core data screen nav")
}
StackNavigationLink(destination: Screens.coreData) {
Text("Go to core data screen")
}
Expand Down
15 changes: 0 additions & 15 deletions Example/Example/Views/SupportingViews/Sidebar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,7 @@ struct Sidebar: View {
}
}
}
#if os(macOS)
.toolbar(content: {
Button(action: self.toggleSidebar) {
Label("Toggle sidebar", systemImage: "sidebar.left")
.foregroundColor(.accentColor)
}
})
#endif
}

#if os(macOS)
private func toggleSidebar() {
guard let firstResponder = NSApp.keyWindow?.firstResponder else { return }
firstResponder.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
#endif
}

struct Sidebar_Previews: PreviewProvider {
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ let package = Package(
),
.target(
name: "KamaalNavigation",
dependencies: ["KamaalStructures", "KamaalUI"]
dependencies: ["KamaalUI"]
),
.target(
name: "KamaalBrowser",
Expand Down
107 changes: 20 additions & 87 deletions Sources/KamaalNavigation/Models/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,131 +6,64 @@
//

import SwiftUI
import KamaalStructures

public final class Navigator<StackValue: NavigatorStackValue>: ObservableObject {
@Published private var stacks: [StackValue: Stack<StackValue>] {
didSet { self.stacksDidSet() }
}
@Published private var stacks: [StackValue: [StackValue]]

@Published var currentStack: StackValue

private let notifications: [Notification.Name] = [
.navigate,
]

init(stack: [StackValue], initialStack: StackValue = .root) {
let stack = Stack.fromArray(stack)
self.stacks = [initialStack: stack]
self.currentStack = initialStack
self.setupNotifications()
}

deinit {
removeNotifications()
}

public enum NavigatorNotifications {
case navigate(destination: StackValue)

var name: Notification.Name {
switch self {
case .navigate:
return .navigate
}
}
}

var currentScreen: StackValue? {
self.stacks[self.currentStack]?.peek()
self.stacks[self.currentStack]?.last
}

var screens: [StackValue] {
Array(StackValue.allCases)
}

/// Changes stacks.
/// - Parameter stack: the stack to change to
@MainActor
func changeStack(to stack: StackValue) {
public func changeStack(to stack: StackValue) {
guard self.currentStack != stack else { return }

if self.stacks[stack] == nil {
self.stacks[stack] = Stack()
self.stacks[stack] = []
}
self.currentStack = stack
}

/// Navigates to the given destination.
///
/// WARNING: This method only works on macOS.
/// - Parameter destination: Where to navigate to.
@MainActor
public func navigate(to destination: StackValue) {
#if !os(macOS)
assertionFailure("This method is only supported on macOS")
#else
withAnimation { self.stacks[self.currentStack]?.push(destination) }
#endif
withAnimation { self.stacks[self.currentStack]?.append(destination) }
}

/// Navigates back.
///
/// WARNING: This method only works on macOS.
@MainActor
public func goBack() {
#if !os(macOS)
assertionFailure("This method is only supported on macOS")
#else
withAnimation { _ = self.stacks[self.currentStack]?.pop() }
#endif
withAnimation { _ = self.stacks[self.currentStack]?.popLast() }
}

public static func notify(_ event: NavigatorNotifications) {
NotificationCenter.default.post(name: event.name, object: event)
}

private func setupNotifications() {
for notification in self.notifications {
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleNotification),
name: notification,
object: nil
)
}
func getBindingPath(forStack stack: StackValue) -> Binding<[StackValue]> {
Binding(
get: { [weak self] in
guard let self else { return [] }
return stacks[stack] ?? []
},
set: { [weak self] newValue in
guard let self else { return }
stacks[stack] = newValue
})
}

private func removeNotifications() {
for notification in self.notifications {
NotificationCenter.default.removeObserver(self, name: notification, object: nil)
}
}

@objc
private func handleNotification(_ notification: Notification) {
switch notification.name {
case .navigate:
guard let event = notification.object as? NavigatorNotifications,
case let .navigate(destination: destination) = event else {
assertionFailure("Incorrect event sent")
return
}

Task { await self.navigate(to: destination) }
default:
assertionFailure("Unhandled notification")
}
}

private func stacksDidSet() {
NotificationCenter.default.post(name: .hasChangedScreens, object: self.currentScreen)
}
}

extension Notification.Name {
public static let navigate = makeNotificationName(withKey: "navigate")
public static let hasChangedScreens = makeNotificationName(withKey: "has_changed_screens")

private static func makeNotificationName(withKey key: String) -> Notification.Name {
Notification.Name("io.kamaal.BetterNavigation.notifications.\(key)")
func getBindingPath() -> Binding<[StackValue]> {
getBindingPath(forStack: currentStack)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public struct NavigationStackView<Root: View, SubView: View, Screen: NavigatorSt
NavigationSplitView(
sidebar: { AnyView(self.sidebar()) },
detail: {
NavigationStack {
NavigationStack(path: navigator.getBindingPath()) {
self.macView
.navigationDestination(for: Screen.self) { screen in
screen.view(true)
Expand All @@ -104,7 +104,7 @@ public struct NavigationStackView<Root: View, SubView: View, Screen: NavigatorSt
NavigationSplitView(
sidebar: { AnyView(self.sidebar()) },
detail: {
NavigationStack {
NavigationStack(path: navigator.getBindingPath()) {
self.root(self.navigator.currentStack)
.navigationDestination(for: Screen.self) { screen in
screen.view(true)
Expand All @@ -115,32 +115,26 @@ public struct NavigationStackView<Root: View, SubView: View, Screen: NavigatorSt
.environmentObject(self.navigator)
} else {
TabView(selection: self.$navigator.currentStack) {
ForEach(self.navigator.screens.filter(\.isTabItem), id: \.self) { screen in
NavigationStack {
self.root(screen)
ForEach(self.navigator.screens.filter(\.isTabItem), id: \.self) { stack in
NavigationStack(path: navigator.getBindingPath(forStack: stack)) {
self.root(stack)
.navigationDestination(for: Screen.self) { screen in
screen.view(true)
}
}
.navigationViewStyle(.stack)
.tabItem {
Image(systemName: screen.imageSystemName)
Text(screen.title)
Image(systemName: stack.imageSystemName)
Text(stack.title)
}
.tag(screen)
.tag(stack)
}
}
.environmentObject(self.navigator)
}
#endif
}
}

public func onScreenChange(_ perform: @escaping (Screen) -> Void) -> some View {
onReceive(NotificationCenter.default.publisher(for: .hasChangedScreens)) { output in
perform(output.object as? Screen ?? .root)
}
}
}

#if DEBUG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public struct StackNavigationLink<Content: View, Destination: NavigatorStackValu
self.content()
}
#else
NavigationLink(value: destination) {
content()
NavigationLink(value: self.destination) {
self.content()
}
#endif
}
Expand Down

0 comments on commit 4d55344

Please sign in to comment.