From bf4e9fda8b31db99c4af7b9966204456b4562f6a Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Fri, 17 Feb 2023 17:16:36 -0500 Subject: [PATCH] add macos --- Package.swift | 1 + .../CodableRepresentation.swift | 4 +- .../NavigationStackBackport/Destination.swift | 2 +- .../KSNavigationController.swift | 309 ++++++++++++++++++ .../NavigationAuthority.swift | 11 +- .../NavigationLink.swift | 12 +- .../NavigationPath.swift | 12 +- .../NavigationPathBox.swift | 2 +- .../NavigationStack.swift | 8 +- .../NavigationUpdate.swift | 22 +- .../Presentation.swift | 2 +- .../Type Aliases.swift | 15 + .../UIKitNavigation.swift | 47 ++- 13 files changed, 404 insertions(+), 43 deletions(-) create mode 100644 Sources/NavigationStackBackport/KSNavigationController.swift create mode 100644 Sources/NavigationStackBackport/Type Aliases.swift diff --git a/Package.swift b/Package.swift index 3467982..acdce1c 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ let package = Package( name: "navigation-stack-backport", platforms: [ .iOS(.v14), + .macOS(.v12), ], products: [ .library(name: "NavigationStackBackport", targets: ["NavigationStackBackport"]), diff --git a/Sources/NavigationStackBackport/CodableRepresentation.swift b/Sources/NavigationStackBackport/CodableRepresentation.swift index ebb762a..ee00ed0 100644 --- a/Sources/NavigationStackBackport/CodableRepresentation.swift +++ b/Sources/NavigationStackBackport/CodableRepresentation.swift @@ -8,7 +8,7 @@ public extension NavigationPath { extension NavigationPath.CodableRepresentation: Codable { public init(from decoder: Decoder) throws { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { storage = try SwiftUI.NavigationPath.CodableRepresentation(from: decoder) return } @@ -26,7 +26,7 @@ extension NavigationPath.CodableRepresentation: Codable { } public func encode(to encoder: Encoder) throws { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { try (storage as! SwiftUI.NavigationPath.CodableRepresentation).encode(to: encoder) return } diff --git a/Sources/NavigationStackBackport/Destination.swift b/Sources/NavigationStackBackport/Destination.swift index 5e24584..ecaa1ee 100644 --- a/Sources/NavigationStackBackport/Destination.swift +++ b/Sources/NavigationStackBackport/Destination.swift @@ -2,7 +2,7 @@ import SwiftUI public extension Backport { @ViewBuilder func navigationDestination(for data: D.Type, @ViewBuilder destination: @escaping (D) -> C) -> some View { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { content.navigationDestination(for: D.self, destination: destination) } else { content.modifier(DestinationModifier(destination: destination)) diff --git a/Sources/NavigationStackBackport/KSNavigationController.swift b/Sources/NavigationStackBackport/KSNavigationController.swift new file mode 100644 index 0000000..613aab0 --- /dev/null +++ b/Sources/NavigationStackBackport/KSNavigationController.swift @@ -0,0 +1,309 @@ +// Forked from https://github.com/coffellas-cto/KSNavigationController + +// +// KSNavigationController.m +// +// Copyright © 2016 Alex Gordiyenko. All rights reserved. +// + +/* + The MIT License (MIT) + + Copyright (c) 2016 A. Gordiyenko + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +#if os(macOS) +import Cocoa + +// MARK: Stack + +class _KSStackItem : NSObject { + var value: T + var next: _KSStackItem? + init(_ value: T) { + self.value = value + } +} + +class _KSStack: NSObject { + fileprivate var _head: _KSStackItem? + fileprivate var _count: UInt = 0 + var headValue: T? { + get { + return self._head?.value + } + } + var count: UInt { + get { + return self._count + } + } + + func push(_ object: T) -> Void { + let item = _KSStackItem(object) + item.next = self._head + self._head = item + self._count += 1 + } + + func pop() -> T? { + guard self._head != nil else { + NSException(name: NSExceptionName.internalInconsistencyException, reason: "Popped an empty stack", userInfo: nil).raise() + return nil + } + + let retVal = self._head?.value + self._head = self._head?.next + self._count -= 1 + return retVal + } + + func iterate(_ block: (T) -> (Void)) -> Void { + var item = self._head + while true { + if let item = item { + block(item.value) + } else { + break + } + + item = item?.next + } + } + +} + +protocol KSNavigationControllerDelegate: NSObject { + func navigationController(_ navigationController: KSNavigationController, didShow viewController: NSViewController, animated: Bool) +} + +// MARK: KSNavigationControllerCompatible + +/** + Protocol your `NSViewController` subclass must conform to. + + Conform to this protocol if you want your `NSViewController` subclass to work with `KSNavigationController`. + */ +protocol KSNavigationControllerCompatible { + /** + Navigation controller object which holds your `NSViewController` subclass. + + Warning: Do not set this properly by yourself. + */ + var navigationController: KSNavigationController? {get set} +} + +// MARK: KSNavigationController + +/** + This class mimics UIKit's `UINavigationController` behavior. + + Navigation bar is not implemented. All methods must be called from main thread. + */ +class KSNavigationController: NSViewController { + + private lazy var __addRootViewOnce: () = { + if let rootViewController = rootViewController { + self._activeView = rootViewController.view + } + self.addActiveViewAnimated(false, subtype: nil) + }() + + // MARK: Properties + + weak var delegate: KSNavigationControllerDelegate? + + /** The root view controller on the bottom of the stack. */ + fileprivate(set) var rootViewController: NSViewController? + + /** The current view controller stack. */ + var viewControllers: [NSViewController] { + get { + var retVal = [NSViewController]() + self._stack.iterate { (object: NSViewController) -> (Void) in + retVal.append(object) + } + + if let rootViewController = rootViewController { + retVal.append(rootViewController) + } + return retVal + } + } + + /** Number of view controllers currently in stack. */ + var viewControllersCount: UInt { + get { + return self._stack.count + 1 + } + } + + /** The top view controller on the stack. */ + var topViewController: NSViewController? { + get { + if self._stack.count > 0 { + return self._stack.headValue; + } + + return self.rootViewController; + } + } + + fileprivate var _activeView: NSView? + fileprivate var _addRootViewOnceToken: Int = 0 + fileprivate var _stack: _KSStack = _KSStack() + fileprivate var _transition: CATransition { + get { + let transition = CATransition() + transition.type = CATransitionType.push + self.view.animations = ["subviews": transition] + return transition + } + } + + // MARK: Life Cycle + + /** + Initializes and returns a newly created navigation controller. + This method throws exception if `rootViewController` doesn't conform to `KSNavigationControllerCompatible` protocol. + - parameter rootViewController: The view controller that resides at the bottom of the navigation stack. + - returns: The initialized navigation controller object or nil if there was a problem initializing the object. + */ + init?(rootViewController: NSViewController) { + self.rootViewController = rootViewController + super.init(nibName: nil, bundle: nil) + if var rootViewController = rootViewController as? KSNavigationControllerCompatible { + rootViewController.navigationController = self + } else { + NSException(name: NSExceptionName.internalInconsistencyException, reason: "`rootViewController` doesn't conform to `KSNavigationControllerCompatible`", userInfo: nil).raise() + return nil + } + } + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + self.rootViewController = NSViewController() + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.wantsLayer = true + } + + override func viewWillAppear() { + super.viewWillAppear() + _ = self.__addRootViewOnce + } + + override func loadView() { + self.view = NSView() + } + + // MARK: Public Methods + + func setViewControllers(_ viewControllers: [NSViewController], animated: Bool) { + // TODO: Animate only once (not per layer) depending on whether the resulting root VC was behind or ahead in the previous viewControllers, like native iOS + while(popViewControllerAnimated(true) != nil) { } + for (index, viewController) in viewControllers.enumerated() { + pushViewController(viewController, animated: index == (viewControllers.count - 1)) + } + } + + /** + Pushes a view controller onto the receiver’s stack and updates the display. Uses a horizontal slide transition. + - parameter viewController: The view controller to push onto the stack. + - parameter animated: Set this value to YES to animate the transition, NO otherwise. + */ + func pushViewController(_ viewController: NSViewController, animated: Bool) { + self._activeView?.removeFromSuperview() + self._stack.push(viewController) + if var viewControllerWithNav = viewController as? KSNavigationControllerCompatible { + viewControllerWithNav.navigationController = self + } + + self._activeView = viewController.view + self.addActiveViewAnimated(animated, subtype: NSApp.userInterfaceLayoutDirection == .leftToRight ? CATransitionSubtype.fromRight.rawValue : CATransitionSubtype.fromLeft.rawValue) + } + + /** + Pops the top view controller from the navigation stack and updates the display. + - parameter animated: Set this value to YES to animate the transition, NO otherwise. + - returns: The popped view controller. + */ + func popViewControllerAnimated(_ animated: Bool) -> NSViewController? { + if self._stack.count == 0 { + return nil + } + + self._activeView?.removeFromSuperview() + let retVal = self._stack.pop() + self._activeView = self._stack.headValue?.view + if self._activeView == nil, let rootViewController = rootViewController { + self._activeView = rootViewController.view + } + + self.addActiveViewAnimated(animated, subtype: NSApp.userInterfaceLayoutDirection == .leftToRight ? CATransitionSubtype.fromLeft.rawValue : CATransitionSubtype.fromRight.rawValue) + return retVal + } + + /** + Pops until there's only a single view controller left on the stack. Returns the popped view controllers. + - parameter animated: Set this value to YES to animate the transitions if any, NO otherwise. + - returns: The popped view controllers. + */ + func popToRootViewControllerAnimated(_ animated: Bool) -> [NSViewController]? { + if self._stack.count == 0 { + return nil; + } + + var retVal = [NSViewController]() + for _ in 1...self._stack.count { + if let vc = self.popViewControllerAnimated(animated) { + retVal.append(vc) + } + } + + return retVal + } + + // MARK: Private Methods + + fileprivate func addActiveViewAnimated(_ animated: Bool, subtype: String?) { + if animated { + if let subtype = subtype { + self._transition.subtype = CATransitionSubtype(rawValue: subtype) + } + self.view.animator().addSubview(self._activeView!) + } else { + self.view.addSubview(self._activeView!) + } + + if let viewController = viewControllers.first { + // TODO: Ensure it's after any animation finishes? + delegate?.navigationController(self, didShow: viewController, animated: animated) + } + } +} +#endif diff --git a/Sources/NavigationStackBackport/NavigationAuthority.swift b/Sources/NavigationStackBackport/NavigationAuthority.swift index bc1ddec..875ebd0 100644 --- a/Sources/NavigationStackBackport/NavigationAuthority.swift +++ b/Sources/NavigationStackBackport/NavigationAuthority.swift @@ -2,7 +2,8 @@ import Combine import SwiftUI class NavigationAuthority: NSObject, ObservableObject { - weak var navigationController: UINavigationController? { + + weak var navigationController: NavigationController? { didSet { navigationController?.delegate = self } } @@ -38,7 +39,7 @@ extension NavigationAuthority { path.items.enumerated().forEach { index, item in guard viewControllers.indices.contains(index + 1), let view = destination.view(item, index + 1) else { return } - (viewControllers[index + 1] as? UIHostingController)?.rootView = view + (viewControllers[index + 1] as? HostingController)?.rootView = view } } @@ -84,8 +85,8 @@ extension NavigationAuthority { } } -extension NavigationAuthority: UINavigationControllerDelegate { - func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { +extension NavigationAuthority: NavigationControllerDelegate { + func navigationController(_ navigationController: NavigationController, didShow viewController: ViewController, animated: Bool) { let count = navigationController.viewControllers.count defer { viewControllersCount = count } @@ -129,7 +130,7 @@ private extension NavigationAuthority { viewControllers.indices.contains(index + 1) else { return } - (viewControllers[index + 1] as? UIHostingController)?.rootView = view(for: item, index: index + 1) + (viewControllers[index + 1] as? HostingController)?.rootView = view(for: item, index: index + 1) } } diff --git a/Sources/NavigationStackBackport/NavigationLink.swift b/Sources/NavigationStackBackport/NavigationLink.swift index 752f59f..38b4322 100644 --- a/Sources/NavigationStackBackport/NavigationLink.swift +++ b/Sources/NavigationStackBackport/NavigationLink.swift @@ -4,7 +4,7 @@ public struct NavigationLink: View { public let body: AnyView public init(value: P?, @ViewBuilder label: () -> Label) { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(value: value, label: label)) } else { body = AnyView(Backport(label: label(), item: value.map { .init(value: $0) })) @@ -12,7 +12,7 @@ public struct NavigationLink: View { } public init(value: P?, @ViewBuilder label: () -> Label) where P: Codable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(value: value, label: label)) } else { body = AnyView(Backport(label: label(), item: value.map { .init(value: $0) })) @@ -20,7 +20,7 @@ public struct NavigationLink: View { } public init(_ titleKey: LocalizedStringKey, value: P?) where Label == Text { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(titleKey, value: value)) } else { body = AnyView(Backport(label: Text(titleKey), item: value.map { .init(value: $0) })) @@ -28,7 +28,7 @@ public struct NavigationLink: View { } public init(_ titleKey: LocalizedStringKey, value: P?) where Label == Text, P: Codable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(titleKey, value: value)) } else { body = AnyView(Backport(label: Text(titleKey), item: value.map { .init(value: $0) })) @@ -36,7 +36,7 @@ public struct NavigationLink: View { } public init(_ title: S, value: P?) where Label == Text, S: StringProtocol { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(title, value: value)) } else { body = AnyView(Backport(label: Text(title), item: value.map { .init(value: $0) })) @@ -44,7 +44,7 @@ public struct NavigationLink: View { } public init(_ title: S, value: P?) where Label == Text, S: StringProtocol, P: Codable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationLink(title, value: value)) } else { body = AnyView(Backport(label: Text(title), item: value.map { .init(value: $0) })) diff --git a/Sources/NavigationStackBackport/NavigationPath.swift b/Sources/NavigationStackBackport/NavigationPath.swift index 7c63100..35ab510 100644 --- a/Sources/NavigationStackBackport/NavigationPath.swift +++ b/Sources/NavigationStackBackport/NavigationPath.swift @@ -7,7 +7,7 @@ public struct NavigationPath { private var box: any NavigationPathBox - @available(iOS 16.0, *) + @available(iOS 16, macOS 13, *) var swiftUIPath: SwiftUI.NavigationPath { get { box as! SwiftUI.NavigationPath } set { box = newValue } @@ -19,7 +19,7 @@ public struct NavigationPath { } public init() { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { box = SwiftUI.NavigationPath() } else { box = NavigationPathBackport(items: []) @@ -27,7 +27,7 @@ public struct NavigationPath { } public init(_ elements: S) where S.Element: Hashable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { box = SwiftUI.NavigationPath(elements) } else { box = NavigationPathBackport(items: elements.map { .init(value: $0) }) @@ -35,7 +35,7 @@ public struct NavigationPath { } public init(_ elements: S) where S.Element: Hashable, S.Element: Codable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { box = SwiftUI.NavigationPath(elements) } else { box = NavigationPathBackport(items: elements.map { .init(value: $0) }) @@ -43,7 +43,7 @@ public struct NavigationPath { } public init(_ codable: CodableRepresentation) { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { box = SwiftUI.NavigationPath(codable.storage as! SwiftUI.NavigationPath.CodableRepresentation) } else { box = NavigationPathBackport(items: codable.storage as! [NavigationPathItem]) @@ -67,7 +67,7 @@ public extension NavigationPath { extension NavigationPath: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { return lhs.box as? SwiftUI.NavigationPath == rhs.box as? SwiftUI.NavigationPath } else { return lhs.box as? NavigationPathBackport == rhs.box as? NavigationPathBackport diff --git a/Sources/NavigationStackBackport/NavigationPathBox.swift b/Sources/NavigationStackBackport/NavigationPathBox.swift index d80bd2e..c6c1006 100644 --- a/Sources/NavigationStackBackport/NavigationPathBox.swift +++ b/Sources/NavigationStackBackport/NavigationPathBox.swift @@ -10,7 +10,7 @@ protocol NavigationPathBox { mutating func removeLast(_ k: Int) } -@available(iOS 16.0, *) +@available(iOS 16, macOS 13, *) extension SwiftUI.NavigationPath: NavigationPathBox { var backportedCodable: NavigationPath.CodableRepresentation? { codable.map(NavigationPath.CodableRepresentation.init(storage:)) diff --git a/Sources/NavigationStackBackport/NavigationStack.swift b/Sources/NavigationStackBackport/NavigationStack.swift index fb5af18..feb07b3 100644 --- a/Sources/NavigationStackBackport/NavigationStack.swift +++ b/Sources/NavigationStackBackport/NavigationStack.swift @@ -4,7 +4,7 @@ public struct NavigationStack: View { public let body: AnyView public init(@ViewBuilder root: () -> Root) where Data == NavigationPath { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationStack(root: root)) } else { body = AnyView(ImplicitStateView(root: root())) @@ -12,7 +12,7 @@ public struct NavigationStack: View { } public init(path: Binding, @ViewBuilder root: () -> Root) where Data == NavigationPath { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationStack(path: path.swiftUIPath, root: root)) } else { body = AnyView(AuthorityView(path: path.storage, root: root())) @@ -20,7 +20,7 @@ public struct NavigationStack: View { } public init(path: Binding, @ViewBuilder root: () -> Root) where Data: MutableCollection, Data: RandomAccessCollection, Data: RangeReplaceableCollection, Data.Element: Hashable { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { body = AnyView(SwiftUI.NavigationStack(path: path, root: root)) } else { // TODO: implement special homogeneous NavigationPathBox? @@ -50,7 +50,7 @@ private extension NavigationStack { @StateObject private var authority = NavigationAuthority() var body: some View { - UIKitNavigation(root: root.environment(\.navigationContextId, 0), path: path) + NativeNavigation(root: root.environment(\.navigationContextId, 0), path: path) .ignoresSafeArea() .environment(\.navigationAuthority, authority) .onPreferenceChange(DestinationIDsKey.self) { ids in diff --git a/Sources/NavigationStackBackport/NavigationUpdate.swift b/Sources/NavigationStackBackport/NavigationUpdate.swift index fd1671d..8077fb0 100644 --- a/Sources/NavigationStackBackport/NavigationUpdate.swift +++ b/Sources/NavigationStackBackport/NavigationUpdate.swift @@ -1,15 +1,15 @@ import SwiftUI @MainActor struct NavigationUpdate { - var viewControllers: [UIViewController] { + var viewControllers: [ViewController] { didSet { changed = true } } - private let navigationController: UINavigationController - private var addedViewControllers: [UIViewController] = [] + private let navigationController: NavigationController + private var addedViewControllers: [ViewController] = [] private var changed = false - init(navigationController: UINavigationController) { + init(navigationController: NavigationController) { self.navigationController = navigationController viewControllers = navigationController.viewControllers } @@ -18,17 +18,23 @@ import SwiftUI changed = true if navigationController.viewControllers.indices.contains(index) { - (navigationController.viewControllers[index] as? UIHostingController)?.rootView = view + (navigationController.viewControllers[index] as? HostingController)?.rootView = view return } - let hostingController = UIHostingController(rootView: view) + let hostingController = HostingController(rootView: view) viewControllers.append(hostingController) addedViewControllers.append(hostingController) +#if os(iOS) navigationController.view.insertSubview(hostingController.view, at: 0) +#else + navigationController.view.addSubview(hostingController.view, positioned: .below, relativeTo: nil) +#endif navigationController.addChild(hostingController) +#if os(iOS) hostingController.didMove(toParent: navigationController) +#endif } func commit() { @@ -36,7 +42,9 @@ import SwiftUI Task { addedViewControllers.forEach { - $0.willMove(toParent: nil) +#if os(iOS) + $0.willMove(toParent: nil) +#endif $0.view.removeFromSuperview() $0.removeFromParent() } diff --git a/Sources/NavigationStackBackport/Presentation.swift b/Sources/NavigationStackBackport/Presentation.swift index 9cc3a79..e50fa0f 100644 --- a/Sources/NavigationStackBackport/Presentation.swift +++ b/Sources/NavigationStackBackport/Presentation.swift @@ -2,7 +2,7 @@ import SwiftUI public extension Backport { @ViewBuilder func navigationDestination(isPresented: Binding, @ViewBuilder destination: @escaping () -> C) -> some View { - if #available(iOS 16.0, *) { + if #available(iOS 16, macOS 13, *) { content.navigationDestination(isPresented: isPresented, destination: destination) } else { content.modifier(PresentationModifier(isPresented: isPresented, destination: destination)) diff --git a/Sources/NavigationStackBackport/Type Aliases.swift b/Sources/NavigationStackBackport/Type Aliases.swift new file mode 100644 index 0000000..855d0f0 --- /dev/null +++ b/Sources/NavigationStackBackport/Type Aliases.swift @@ -0,0 +1,15 @@ +import SwiftUI + +#if os(iOS) +typealias NavigationController = UINavigationController +typealias NavigationControllerDelegate = UINavigationControllerDelegate +typealias ViewController = UIViewController +typealias HostingController = UIHostingController +typealias ViewControllerRepresentable = UIViewControllerRepresentable +#else +typealias NavigationController = KSNavigationController +typealias NavigationControllerDelegate = KSNavigationControllerDelegate +typealias ViewController = NSViewController +typealias HostingController = NSHostingController +typealias ViewControllerRepresentable = NSViewControllerRepresentable +#endif diff --git a/Sources/NavigationStackBackport/UIKitNavigation.swift b/Sources/NavigationStackBackport/UIKitNavigation.swift index e922006..84e46c7 100644 --- a/Sources/NavigationStackBackport/UIKitNavigation.swift +++ b/Sources/NavigationStackBackport/UIKitNavigation.swift @@ -1,12 +1,13 @@ import SwiftUI -struct UIKitNavigation: UIViewControllerRepresentable { +struct NativeNavigation: ViewControllerRepresentable { let root: Root let path: NavigationPathBackport @Environment(\.navigationAuthority) private var authority - func makeUIViewController(context: Context) -> UINavigationController { - let navigationController = UINavigationController() +#if os(iOS) + func makeUIViewController(context: Context) -> NavigationController { + let navigationController = NavigationController() navigationController.navigationBar.prefersLargeTitles = true navigationController.navigationBar.barStyle = .default navigationController.navigationBar.isTranslucent = true @@ -14,26 +15,52 @@ struct UIKitNavigation: UIViewControllerRepresentable { return navigationController } - func updateUIViewController(_ navigationController: UINavigationController, context: Context) { - if !navigationController.viewControllers.isEmpty, let hostingController = navigationController.viewControllers[0] as? UIHostingController { + func updateUIViewController(_ navigationController: NavigationController, context: Context) { + if !navigationController.viewControllers.isEmpty, let hostingController = navigationController.viewControllers[0] as? HostingController { hostingController.rootView = root } else { - let rootViewController = UIHostingController(rootView: root) + let rootViewController = HostingController(rootView: root) navigationController.viewControllers = [rootViewController] prelayout(rootViewController: rootViewController, navigationController: navigationController) } authority.update(path: path) } +#else + func makeNSViewController(context: Context) -> NavigationController { + let navigationController = NavigationController() + authority.navigationController = navigationController + return navigationController + } + + func updateNSViewController(_ navigationController: NavigationController, context: Context) { + if !navigationController.viewControllers.isEmpty, let hostingController = navigationController.viewControllers[0] as? HostingController { + hostingController.rootView = root + } else { + let rootViewController = HostingController(rootView: root) + navigationController.setViewControllers([rootViewController], animated: false) + prelayout(rootViewController: rootViewController, navigationController: navigationController) + } + + authority.update(path: path) + } +#endif } -private extension UIKitNavigation { - func prelayout(rootViewController: UIHostingController, navigationController: UINavigationController) { - navigationController.view.insertSubview(rootViewController.view, at: 0) +private extension NativeNavigation { + func prelayout(rootViewController: HostingController, navigationController: NavigationController) { +#if os(iOS) + navigationController.view.insertSubview(rootViewController.view, at: 0) +#else + navigationController.view.addSubview(rootViewController.view, positioned: .below, relativeTo: nil) +#endif + navigationController.addChild(rootViewController) + +#if os(iOS) rootViewController.didMove(toParent: navigationController) - navigationController.view.setNeedsLayout() navigationController.view.layoutIfNeeded() +#endif } }