mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
[PM-26060] Consolidate StackNavigator to BitwardenKit (#2095)
This commit is contained in:
parent
2e4b325edb
commit
cd937bc2a3
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - AuthModule
|
||||
|
||||
/// An object that builds coordinators for the auth flow.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
@ -1,294 +0,0 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - StackNavigator
|
||||
|
||||
/// An object used to navigate between views in a stack interface.
|
||||
///
|
||||
@MainActor
|
||||
public protocol StackNavigator: Navigator {
|
||||
/// Whether the stack of views in the navigator is empty.
|
||||
var isEmpty: Bool { get }
|
||||
|
||||
/// Dismisses the view that was presented modally by the navigator.
|
||||
///
|
||||
/// - Parameters animated: Whether the transition should be animated.
|
||||
///
|
||||
func dismiss(animated: Bool)
|
||||
|
||||
/// Dismisses the view that was presented modally by the navigator
|
||||
/// and executes a block of code when dismissing completes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether the transition should be animated.
|
||||
/// - completion: The block that is executed when dismissing completes.
|
||||
///
|
||||
func dismiss(animated: Bool, completion: (() -> Void)?)
|
||||
|
||||
/// Pushes a view onto the navigator's stack.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The view to push onto the stack.
|
||||
/// - animated: Whether the transition should be animated.
|
||||
/// - hidesBottomBar: Whether the bottom bar should be hidden when the view is pushed.
|
||||
///
|
||||
func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool)
|
||||
|
||||
/// Pushes a view controller onto the navigator's stack.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The view controller to push onto the stack.
|
||||
/// - animated: Whether the transition should be animated.
|
||||
///
|
||||
func push(_ viewController: UIViewController, animated: Bool)
|
||||
|
||||
/// Pops a view off the navigator's stack.
|
||||
///
|
||||
/// - Parameter animated: Whether the transition should be animated.
|
||||
/// - Returns: The `UIViewController` that was popped off the navigator's stack.
|
||||
///
|
||||
@discardableResult
|
||||
func pop(animated: Bool) -> UIViewController?
|
||||
|
||||
/// Pops all the view controllers on the stack except the root view controller.
|
||||
///
|
||||
/// - Parameter animated: Whether the transition should be animated.
|
||||
/// - Returns: An array of `UIViewController`s that were popped of the navigator's stack.
|
||||
///
|
||||
@discardableResult
|
||||
func popToRoot(animated: Bool) -> [UIViewController]
|
||||
|
||||
/// Presents a view modally.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The view to present.
|
||||
/// - animated: Whether the transition should be animated.
|
||||
/// - overFullscreen: Whether or not the presented modal should cover the full screen.
|
||||
/// - onCompletion: A closure to call on completion.
|
||||
///
|
||||
func present<Content: View>(
|
||||
_ view: Content,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
onCompletion: (() -> Void)?,
|
||||
)
|
||||
|
||||
/// Presents a view controller modally. Supports presenting on top of presented modals if necessary.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The view controller to present.
|
||||
/// - animated: Whether the transition should be animated.
|
||||
/// - overFullscreen: Whether or not the presented modal should cover the full screen.
|
||||
/// - onCompletion: A closure to call on completion.
|
||||
///
|
||||
func present(
|
||||
_ viewController: UIViewController,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
onCompletion: (() -> Void)?,
|
||||
)
|
||||
|
||||
/// Replaces the stack with the specified view.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The view that will replace the stack.
|
||||
/// - animated: Whether the transition should be animated.
|
||||
///
|
||||
func replace<Content: View>(_ view: Content, animated: Bool)
|
||||
}
|
||||
|
||||
extension StackNavigator {
|
||||
/// Dismisses the view that was presented modally by the navigator. Animation is controlled by
|
||||
/// `UI.animated`.
|
||||
///
|
||||
func dismiss() {
|
||||
dismiss(animated: UI.animated)
|
||||
}
|
||||
|
||||
/// Dismisses the view that was presented modally by the navigator. Animation is controlled by
|
||||
/// `UI.animated`. Executes a block of code when dismissing completes.
|
||||
///
|
||||
func dismiss(completion: (() -> Void)?) {
|
||||
dismiss(animated: UI.animated, completion: completion)
|
||||
}
|
||||
|
||||
/// Pushes a view onto the navigator's stack.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The view to push onto the stack.
|
||||
/// - animated: Whether the transition should be animated. Defaults to `UI.animated`.
|
||||
///
|
||||
func push<Content: View>(_ view: Content, animated: Bool = UI.animated) {
|
||||
push(view, animated: animated, hidesBottomBar: false)
|
||||
}
|
||||
|
||||
/// Pushes a view controller onto the navigator's stack.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The view controller to push onto the stack.
|
||||
/// - animated: Whether the transition should be animated. Defaults to `UI.animated`.
|
||||
/// - navigationTitle: The navigation title to pre-populate the navigation bar so that it doesn't flash.
|
||||
/// - searchController: If non-nil, pre-populate the navigation bar with a search bar backed by the
|
||||
/// supplied UISearchController.
|
||||
/// Normal SwiftUI search contorls will not work if this value is supplied. Tracking the searchController
|
||||
/// behavior must be done through a UISearchControllerDelegate or a UISearchResultsUpdating object.
|
||||
///
|
||||
func push(
|
||||
_ viewController: UIViewController,
|
||||
animated: Bool = UI.animated,
|
||||
navigationTitle: String? = nil,
|
||||
searchController: UISearchController? = nil,
|
||||
) {
|
||||
if let navigationTitle {
|
||||
// Preset some navigation item values so that the navigation bar does not flash oddly once
|
||||
// the view's push animation has completed. This happens because `UIHostingController` does
|
||||
// not resolve its `navigationItem` properties until the view has been displayed on screen.
|
||||
// In this case, that doesn't happen until the push animation has completed, which results
|
||||
// in both the title and the search bar flashing into view after the push animation
|
||||
// completes. This occurs on all iOS versions (tested on iOS 17).
|
||||
//
|
||||
// The values set here are temporary, and are overwritten once the hosting controller has
|
||||
// resolved its root view's navigation bar modifiers.
|
||||
viewController.navigationItem.largeTitleDisplayMode = .never
|
||||
viewController.navigationItem.title = navigationTitle
|
||||
if let searchController {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewController.navigationItem.preferredSearchBarPlacement = .stacked
|
||||
}
|
||||
viewController.navigationItem.searchController = searchController
|
||||
viewController.navigationItem.hidesSearchBarWhenScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
push(viewController, animated: animated)
|
||||
}
|
||||
|
||||
/// Pops a view off the navigator's stack. Animation is controlled by `UI.animated`.
|
||||
///
|
||||
/// - Returns: The `UIViewController` that was popped off the navigator's stack.
|
||||
///
|
||||
@discardableResult
|
||||
func pop() -> UIViewController? {
|
||||
pop(animated: UI.animated)
|
||||
}
|
||||
|
||||
/// Pops all the view controllers on the stack except the root view controller. Animation is controlled by
|
||||
/// `UI.animated`.
|
||||
///
|
||||
/// - Returns: An array of `UIViewController`s that were popped of the navigator's stack.
|
||||
///
|
||||
@discardableResult
|
||||
func popToRoot() -> [UIViewController] {
|
||||
popToRoot(animated: UI.animated)
|
||||
}
|
||||
|
||||
/// Presents a view modally.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The view to present.
|
||||
/// - animated: Whether the transition should be animated. Defaults to `UI.animated`.
|
||||
/// - overFullscreen: Whether or not the presented modal should cover the full screen.
|
||||
/// - onCompletion: The closure to call after presenting.
|
||||
///
|
||||
func present<Content: View>(
|
||||
_ view: Content,
|
||||
animated: Bool = UI.animated,
|
||||
overFullscreen: Bool = false,
|
||||
onCompletion _: (() -> Void)? = nil,
|
||||
) {
|
||||
present(
|
||||
view,
|
||||
animated: animated,
|
||||
overFullscreen: overFullscreen,
|
||||
onCompletion: nil,
|
||||
)
|
||||
}
|
||||
|
||||
/// Presents a view controller modally. Supports presenting on top of presented modals if necessary. Animation is
|
||||
/// controlled by `UI.animated`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The view controller to present.
|
||||
/// - overFullscreen: Whether or not the presented modal should cover the full screen.
|
||||
/// - onCompletion: The closure to call after presenting.
|
||||
///
|
||||
func present(
|
||||
_ viewController: UIViewController,
|
||||
overFullscreen: Bool = false,
|
||||
onCompletion: (() -> Void)? = nil,
|
||||
) {
|
||||
present(
|
||||
viewController,
|
||||
animated: UI.animated,
|
||||
overFullscreen: overFullscreen,
|
||||
onCompletion: onCompletion,
|
||||
)
|
||||
}
|
||||
|
||||
/// Replaces the stack with the specified view. Animation is controlled by `UI.animated`.
|
||||
///
|
||||
/// - Parameter view: The view that will replace the stack.
|
||||
///
|
||||
func replace<Content: View>(_ view: Content) {
|
||||
replace(view, animated: UI.animated)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UINavigationController
|
||||
|
||||
extension UINavigationController: StackNavigator {
|
||||
public var isEmpty: Bool {
|
||||
viewControllers.isEmpty
|
||||
}
|
||||
|
||||
public var rootViewController: UIViewController? {
|
||||
self
|
||||
}
|
||||
|
||||
public func dismiss(animated: Bool) {
|
||||
dismiss(animated: animated, completion: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func pop(animated: Bool) -> UIViewController? {
|
||||
popViewController(animated: animated)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func popToRoot(animated: Bool) -> [UIViewController] {
|
||||
popToRootViewController(animated: animated) ?? []
|
||||
}
|
||||
|
||||
public func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool) {
|
||||
let viewController = UIHostingController(rootView: view)
|
||||
viewController.hidesBottomBarWhenPushed = hidesBottomBar
|
||||
let animated = self.view.window != nil ? animated : false
|
||||
push(viewController, animated: animated)
|
||||
}
|
||||
|
||||
public func push(_ viewController: UIViewController, animated: Bool) {
|
||||
let animated = view.window != nil ? animated : false
|
||||
pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
public func present<Content: View>(
|
||||
_ view: Content,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
onCompletion: (() -> Void)? = nil,
|
||||
) {
|
||||
let controller = UIHostingController(rootView: view)
|
||||
controller.isModalInPresentation = true
|
||||
if overFullscreen {
|
||||
controller.modalPresentationStyle = .overFullScreen
|
||||
controller.view.backgroundColor = .clear
|
||||
}
|
||||
let animated = self.view.window != nil ? animated : false
|
||||
present(controller, animated: animated, onCompletion: onCompletion)
|
||||
}
|
||||
|
||||
public func replace<Content: View>(_ view: Content, animated: Bool) {
|
||||
let animated = self.view.window != nil ? animated : false
|
||||
setViewControllers([UIHostingController(rootView: view)], animated: animated)
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
@MainActor
|
||||
class StackNavigatorTests: BitwardenTestCase {
|
||||
var subject: UINavigationController!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
subject = UINavigationController()
|
||||
setKeyWindowRoot(viewController: subject)
|
||||
}
|
||||
|
||||
/// `present(_:animated:)` presents the hosted view.
|
||||
func testPresent() {
|
||||
subject.present(EmptyView(), animated: false)
|
||||
XCTAssertTrue(subject.presentedViewController is UIHostingController<EmptyView>)
|
||||
}
|
||||
|
||||
/// `present(_:animated:)` presents the hosted view on existing presented views.
|
||||
func testPresentOnPresentedView() {
|
||||
subject.present(EmptyView(), animated: false)
|
||||
subject.present(ScrollView<EmptyView> {}, animated: false)
|
||||
XCTAssertTrue(subject.presentedViewController is UIHostingController<EmptyView>)
|
||||
waitFor(subject.presentedViewController?.presentedViewController != nil)
|
||||
XCTAssertTrue(
|
||||
subject.presentedViewController?.presentedViewController
|
||||
is UIHostingController<ScrollView<EmptyView>>,
|
||||
)
|
||||
}
|
||||
|
||||
/// `dismiss(animated:)` dismisses the hosted view.
|
||||
func testDismiss() {
|
||||
subject.present(EmptyView(), animated: false)
|
||||
subject.dismiss(animated: false)
|
||||
waitFor(subject.presentedViewController == nil)
|
||||
}
|
||||
|
||||
/// `push(_:animated:)` pushes the hosted view.
|
||||
func testPush() {
|
||||
subject.push(EmptyView(), animated: false)
|
||||
XCTAssertTrue(subject.topViewController is UIHostingController<EmptyView>)
|
||||
}
|
||||
|
||||
/// `pop(animated:)` pops the hosted view.
|
||||
func testPop() {
|
||||
subject.push(EmptyView(), animated: false)
|
||||
subject.push(EmptyView(), animated: false)
|
||||
subject.pop(animated: false)
|
||||
XCTAssertEqual(subject.viewControllers.count, 1)
|
||||
XCTAssertTrue(subject.topViewController is UIHostingController<EmptyView>)
|
||||
}
|
||||
|
||||
/// `replace(_:animated:)` replaces the hosted view.
|
||||
func testReplace() {
|
||||
subject.push(EmptyView(), animated: false)
|
||||
subject.replace(Text("replaced"), animated: false)
|
||||
XCTAssertEqual(subject.viewControllers.count, 1)
|
||||
XCTAssertTrue(subject.topViewController is UIHostingController<Text>)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - FileSelectionModule
|
||||
|
||||
/// An object that builds coordinators for the file selection flow.
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - SettingsModule
|
||||
|
||||
/// An object that builds coordinators for the settings tab.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - TutorialModule
|
||||
|
||||
/// An object that builds tutorial coordinators
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - AuthenticatorItemModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - ItemListModule
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import BitwardenKit
|
||||
import BitwardenShared
|
||||
import SwiftUI
|
||||
|
||||
final class MockStackNavigator: StackNavigator {
|
||||
struct NavigationAction {
|
||||
var type: NavigationType
|
||||
var view: Any?
|
||||
var animated: Bool
|
||||
var embedInNavigationController: Bool?
|
||||
var hidesBottomBar: Bool?
|
||||
var isModalInPresentation: Bool?
|
||||
var overFullscreen: Bool?
|
||||
public final class MockStackNavigator: StackNavigator {
|
||||
public struct NavigationAction {
|
||||
public var type: NavigationType
|
||||
public var view: Any?
|
||||
public var animated: Bool
|
||||
public var embedInNavigationController: Bool?
|
||||
public var hidesBottomBar: Bool?
|
||||
public var isModalInPresentation: Bool?
|
||||
public var overFullscreen: Bool?
|
||||
}
|
||||
|
||||
enum NavigationType {
|
||||
public enum NavigationType {
|
||||
case dismissed
|
||||
case dismissedWithCompletionHandler
|
||||
case pushed
|
||||
@ -24,26 +23,28 @@ final class MockStackNavigator: StackNavigator {
|
||||
case replaced
|
||||
}
|
||||
|
||||
var actions: [NavigationAction] = []
|
||||
var alertOnDismissed: (() -> Void)?
|
||||
var alerts: [BitwardenKit.Alert] = []
|
||||
var isEmpty = true
|
||||
var isNavigationBarHidden = false
|
||||
var isPresenting = false
|
||||
var rootViewController: UIViewController?
|
||||
public var actions: [NavigationAction] = []
|
||||
public var alertOnDismissed: (() -> Void)?
|
||||
public var alerts: [BitwardenKit.Alert] = []
|
||||
public var isEmpty = true
|
||||
public var isNavigationBarHidden = false
|
||||
public var isPresenting = false
|
||||
public var rootViewController: UIViewController?
|
||||
|
||||
var viewControllersToPop: [UIViewController] = []
|
||||
public var viewControllersToPop: [UIViewController] = []
|
||||
|
||||
func dismiss(animated: Bool) {
|
||||
public init() {}
|
||||
|
||||
public func dismiss(animated: Bool) {
|
||||
actions.append(NavigationAction(type: .dismissed, animated: animated))
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||
public func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||
completion?()
|
||||
actions.append(NavigationAction(type: .dismissedWithCompletionHandler, animated: animated))
|
||||
}
|
||||
|
||||
func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool) {
|
||||
public func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool) {
|
||||
actions.append(NavigationAction(
|
||||
type: .pushed,
|
||||
view: view,
|
||||
@ -52,7 +53,7 @@ final class MockStackNavigator: StackNavigator {
|
||||
))
|
||||
}
|
||||
|
||||
func push(_ viewController: UIViewController, animated: Bool) {
|
||||
public func push(_ viewController: UIViewController, animated: Bool) {
|
||||
actions.append(NavigationAction(
|
||||
type: .pushed,
|
||||
view: viewController,
|
||||
@ -61,27 +62,27 @@ final class MockStackNavigator: StackNavigator {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pop(animated: Bool) -> UIViewController? {
|
||||
public func pop(animated: Bool) -> UIViewController? {
|
||||
actions.append(NavigationAction(type: .popped, animated: animated))
|
||||
return viewControllersToPop.last
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func popToRoot(animated: Bool) -> [UIViewController] {
|
||||
public func popToRoot(animated: Bool) -> [UIViewController] {
|
||||
actions.append(NavigationAction(type: .poppedToRoot, animated: animated))
|
||||
return viewControllersToPop
|
||||
}
|
||||
|
||||
func present(_ alert: BitwardenKit.Alert) {
|
||||
public func present(_ alert: BitwardenKit.Alert) {
|
||||
alerts.append(alert)
|
||||
}
|
||||
|
||||
func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||
public func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||
alerts.append(alert)
|
||||
alertOnDismissed = onDismissed
|
||||
}
|
||||
|
||||
func present<Content: View>( // swiftlint:disable:this function_parameter_count
|
||||
public func present<Content: View>( // swiftlint:disable:this function_parameter_count
|
||||
_ view: Content,
|
||||
animated: Bool,
|
||||
embedInNavigationController: Bool,
|
||||
@ -102,7 +103,7 @@ final class MockStackNavigator: StackNavigator {
|
||||
)
|
||||
}
|
||||
|
||||
func present(
|
||||
public func present(
|
||||
_ viewController: UIViewController,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
@ -119,11 +120,11 @@ final class MockStackNavigator: StackNavigator {
|
||||
)
|
||||
}
|
||||
|
||||
func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
|
||||
public func setNavigationBarHidden(_ hidden: Bool, animated: Bool) {
|
||||
isNavigationBarHidden = hidden
|
||||
}
|
||||
|
||||
func replace<Content: View>(_ view: Content, animated: Bool) {
|
||||
public func replace<Content: View>(_ view: Content, animated: Bool) {
|
||||
actions.append(NavigationAction(type: .replaced, view: view, animated: animated))
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - StackNavigator
|
||||
@ -223,28 +222,74 @@ public extension StackNavigator {
|
||||
// MARK: - UINavigationController
|
||||
|
||||
extension UINavigationController: StackNavigator {
|
||||
/// Returns whether the navigation controller's stack is empty.
|
||||
///
|
||||
/// - Returns: `true` if there are no view controllers in the stack, `false` otherwise.
|
||||
public var isEmpty: Bool {
|
||||
viewControllers.isEmpty
|
||||
}
|
||||
|
||||
/// Returns the root view controller of the navigation stack.
|
||||
///
|
||||
/// For UINavigationController, this returns the navigation controller itself
|
||||
/// as it serves as the root container for the navigation stack.
|
||||
///
|
||||
/// - Returns: The navigation controller instance.
|
||||
public var rootViewController: UIViewController? {
|
||||
self
|
||||
}
|
||||
|
||||
/// Dismisses the modally presented view controller without a completion handler.
|
||||
///
|
||||
/// This is a convenience method that calls the system's dismiss method
|
||||
/// with a nil completion handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether the dismissal should be animated.
|
||||
public func dismiss(animated: Bool) {
|
||||
dismiss(animated: animated, completion: nil)
|
||||
}
|
||||
|
||||
/// Pops the top view controller from the navigation stack.
|
||||
///
|
||||
/// Removes and returns the top view controller from the navigation stack.
|
||||
/// If the stack only contains the root view controller, this method does nothing
|
||||
/// and returns nil.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether the pop transition should be animated.
|
||||
/// - Returns: The view controller that was popped, or nil if no controller was popped.
|
||||
@discardableResult
|
||||
public func pop(animated: Bool) -> UIViewController? {
|
||||
popViewController(animated: animated)
|
||||
}
|
||||
|
||||
/// Pops all view controllers except the root view controller.
|
||||
///
|
||||
/// Removes all view controllers from the stack except the root view controller
|
||||
/// and returns an array of the popped controllers.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether the pop transition should be animated.
|
||||
/// - Returns: An array of view controllers that were popped from the stack.
|
||||
/// Returns an empty array if no controllers were popped.
|
||||
@discardableResult
|
||||
public func popToRoot(animated: Bool) -> [UIViewController] {
|
||||
popToRootViewController(animated: animated) ?? []
|
||||
}
|
||||
|
||||
/// Pushes a SwiftUI view onto the navigation stack.
|
||||
///
|
||||
/// Wraps the provided SwiftUI view in a UIHostingController and pushes it
|
||||
/// onto the navigation stack. Automatically disables animation if the
|
||||
/// navigation controller is not currently in a window to prevent animation
|
||||
/// issues during initial setup.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The SwiftUI view to push onto the stack.
|
||||
/// - animated: Whether the push transition should be animated.
|
||||
/// - hidesBottomBar: Whether the bottom bar (tab bar) should be hidden
|
||||
/// when this view controller is displayed.
|
||||
public func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool) {
|
||||
let viewController = UIHostingController(rootView: view)
|
||||
viewController.hidesBottomBarWhenPushed = hidesBottomBar
|
||||
@ -252,11 +297,38 @@ extension UINavigationController: StackNavigator {
|
||||
push(viewController, animated: animated)
|
||||
}
|
||||
|
||||
/// Pushes a view controller onto the navigation stack.
|
||||
///
|
||||
/// Adds the specified view controller to the top of the navigation stack.
|
||||
/// Automatically disables animation if the navigation controller is not
|
||||
/// currently in a window to prevent animation issues during initial setup.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The view controller to push onto the stack.
|
||||
/// - animated: Whether the push transition should be animated.
|
||||
public func push(_ viewController: UIViewController, animated: Bool) {
|
||||
let animated = view.window != nil ? animated : false
|
||||
pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
/// Presents a SwiftUI view modally.
|
||||
///
|
||||
/// Wraps the provided SwiftUI view in a UIHostingController and presents it modally.
|
||||
/// Optionally embeds the view in a new navigation controller and configures
|
||||
/// various presentation options. Automatically disables animation if the
|
||||
/// navigation controller is not currently in a window to prevent animation issues
|
||||
/// during initial setup.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The SwiftUI view to present modally.
|
||||
/// - animated: Whether the presentation should be animated.
|
||||
/// - embedInNavigationController: Whether to wrap the view in a new
|
||||
/// navigation controller.
|
||||
/// - isModalInPresentation: Whether the modal enforces modal behavior,
|
||||
/// preventing interactive dismissal.
|
||||
/// - overFullscreen: Whether the modal should use full-screen presentation
|
||||
/// with a clear background.
|
||||
/// - onCompletion: Optional closure called after presentation completes.
|
||||
public func present<Content: View>(
|
||||
_ view: Content,
|
||||
animated: Bool,
|
||||
@ -284,6 +356,16 @@ extension UINavigationController: StackNavigator {
|
||||
present(controller, animated: animated, onCompletion: onCompletion)
|
||||
}
|
||||
|
||||
/// Replaces the entire navigation stack with a single SwiftUI view.
|
||||
///
|
||||
/// Removes all existing view controllers from the navigation stack and
|
||||
/// replaces them with a single new view controller containing the specified
|
||||
/// SwiftUI view. Automatically disables animation if the navigation controller
|
||||
/// is not currently in a window to prevent animation issues during initial setup.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The SwiftUI view that will become the new root of the stack.
|
||||
/// - animated: Whether the replacement should be animated.
|
||||
public func replace<Content: View>(_ view: Content, animated: Bool) {
|
||||
let animated = self.view.window != nil ? animated : false
|
||||
setViewControllers([UIHostingController(rootView: view)], animated: animated)
|
||||
@ -1,8 +1,8 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - StackNavigatorTests
|
||||
|
||||
class StackNavigatorTests: BitwardenTestCase {
|
||||
@ -14,7 +14,7 @@ class StackNavigatorTests: BitwardenTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
subject = UINavigationController()
|
||||
subject = MockUINavigationController()
|
||||
setKeyWindowRoot(viewController: subject)
|
||||
}
|
||||
|
||||
@ -63,14 +63,7 @@ class StackNavigatorTests: BitwardenTestCase {
|
||||
/// `present(_:animated:)` presents the hosted view on existing presented views.
|
||||
@MainActor
|
||||
func test_present_onPresentedView() {
|
||||
subject.present(EmptyView(), animated: false, embedInNavigationController: false)
|
||||
subject.present(ScrollView<EmptyView> {}, animated: false, embedInNavigationController: false)
|
||||
XCTAssertTrue(subject.presentedViewController is UIHostingController<EmptyView>)
|
||||
waitFor(subject.presentedViewController?.presentedViewController != nil)
|
||||
XCTAssertTrue(
|
||||
subject.presentedViewController?.presentedViewController
|
||||
is UIHostingController<ScrollView<EmptyView>>,
|
||||
)
|
||||
// This test in in `BitwardenShared.StackNavigatorHostedTests` because it requires a host app.
|
||||
}
|
||||
|
||||
/// `present(_:animated:)` presents the hosted view without embedding it in a navigation controller.
|
||||
@ -0,0 +1,283 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
// MARK: - MockUINavigationController
|
||||
|
||||
public class MockUINavigationController: UINavigationController {
|
||||
// MARK: Static properties
|
||||
|
||||
/// A size for the `mockWindow` and `mockView` objects to have.
|
||||
/// This happens to be the size of the iPhone 5, 5C, 5S, and SE.
|
||||
private static var mockWindowSize = CGRect(x: 0, y: 0, width: 320, height: 568)
|
||||
|
||||
// MARK: Presentation Tracking
|
||||
|
||||
/// Indicates whether the `present` method has been called.
|
||||
public var presentCalled = false
|
||||
|
||||
/// The view controller that was presented, if any.
|
||||
public var presentedView: UIViewController?
|
||||
|
||||
/// Indicates whether the presentation was animated.
|
||||
public var presentAnimated = false
|
||||
|
||||
/// The completion handler passed to the `present` method.
|
||||
public var presentCompletion: (() -> Void)?
|
||||
|
||||
/// Returns the currently presented view controller.
|
||||
override public var presentedViewController: UIViewController? {
|
||||
presentedView
|
||||
}
|
||||
|
||||
// MARK: Dismissal Tracking
|
||||
|
||||
/// Indicates whether the `dismiss` method has been called.
|
||||
public var dismissCalled = false
|
||||
|
||||
/// Indicates whether the dismissal was animated.
|
||||
public var dismissAnimated = false
|
||||
|
||||
/// The completion handler passed to the `dismiss` method.
|
||||
public var dismissCompletion: (() -> Void)?
|
||||
|
||||
// MARK: Push/Pop Tracking
|
||||
|
||||
/// Indicates whether the `pushViewController` method has been called.
|
||||
public var pushViewControllerCalled = false
|
||||
|
||||
/// The view controller that was pushed, if any.
|
||||
public var pushedViewController: UIViewController?
|
||||
|
||||
/// Indicates whether the push operation was animated.
|
||||
public var pushAnimated = false
|
||||
|
||||
/// Indicates whether the `popViewController` method has been called.
|
||||
public var popViewControllerCalled = false
|
||||
|
||||
/// The view controller that was popped, if any.
|
||||
var poppedViewController: UIViewController?
|
||||
|
||||
/// Indicates whether the pop operation was animated.
|
||||
var popAnimated = false
|
||||
|
||||
// MARK: Navigation Controller Support
|
||||
|
||||
/// Internal storage for a navigation controller.
|
||||
private var _navigationController: UINavigationController?
|
||||
|
||||
/// Returns the internally stored navigation controller, bypassing the superclass one.
|
||||
override public var navigationController: UINavigationController? {
|
||||
get { _navigationController }
|
||||
set { _navigationController = newValue }
|
||||
}
|
||||
|
||||
// MARK: Mock Window and View Hierarchy
|
||||
|
||||
/// The mock window used for testing view hierarchy.
|
||||
private var mockWindow: UIWindow?
|
||||
|
||||
/// The mock view used as the main view.
|
||||
private var mockView: UIView?
|
||||
|
||||
/// Returns the mock view or the default view if no mock view is set.
|
||||
override public var view: UIView! {
|
||||
get {
|
||||
mockView ?? super.view
|
||||
}
|
||||
set {
|
||||
mockView = newValue
|
||||
super.view = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the mock view or default view is loaded.
|
||||
override public var isViewLoaded: Bool {
|
||||
mockView != nil || super.isViewLoaded
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes the mock navigation controller with the specified nib name and bundle.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - nibNameOrNil: The name of the nib file to load, or nil if no nib should be loaded.
|
||||
/// - nibBundleOrNil: The bundle containing the nib file, or nil for the main bundle.
|
||||
override public init(
|
||||
nibName nibNameOrNil: String?,
|
||||
bundle nibBundleOrNil: Bundle?,
|
||||
) {
|
||||
super.init(
|
||||
nibName: nibNameOrNil,
|
||||
bundle: nibBundleOrNil,
|
||||
)
|
||||
setUpMockHierarchy()
|
||||
}
|
||||
|
||||
/// Initializes the mock navigation controller with a nil nib name and bundle.
|
||||
public init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
/// Initializes the mock navigation controller with a root view controller.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - rootViewController: The view controller to use as the root of the navigation stack.
|
||||
override public init(rootViewController: UIViewController) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
// Set viewControllers array directly to avoid hierarchy issues
|
||||
viewControllers = [rootViewController]
|
||||
}
|
||||
|
||||
/// Initializes the mock view controller from a coder.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - coder: The coder to initialize from.
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpMockHierarchy()
|
||||
}
|
||||
|
||||
// MARK: View Life Cycle Methods
|
||||
|
||||
/// Called after the view controller's view is loaded into memory.
|
||||
/// Ensures that a mock view exists even if `loadView` wasn't called.
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Ensure we have a view even if loadView wasn't called
|
||||
if view == nil {
|
||||
view = UIView(frame: MockUINavigationController.mockWindowSize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the view controller's view programmatically.
|
||||
/// Sets up a mock view with the predefined mock window size.
|
||||
override public func loadView() {
|
||||
if mockView == nil {
|
||||
mockView = UIView(frame: MockUINavigationController.mockWindowSize)
|
||||
}
|
||||
view = mockView
|
||||
}
|
||||
|
||||
// MARK: UIViewController Overrides
|
||||
|
||||
/// Presents a view controller modally and tracks the presentation details for testing.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewControllerToPresent: The view controller to present.
|
||||
/// - animated: Whether to animate the presentation.
|
||||
/// - completion: A completion handler to call after the presentation finishes.
|
||||
override public func present(
|
||||
_ viewControllerToPresent: UIViewController,
|
||||
animated: Bool,
|
||||
completion: (() -> Void)? = nil,
|
||||
) {
|
||||
presentCalled = true
|
||||
presentedView = viewControllerToPresent
|
||||
presentAnimated = animated
|
||||
presentCompletion = completion
|
||||
|
||||
// Set up the presented view controller's hierarchy
|
||||
viewControllerToPresent.beginAppearanceTransition(true, animated: animated)
|
||||
viewControllerToPresent.endAppearanceTransition()
|
||||
|
||||
// Call completion if provided
|
||||
completion?()
|
||||
}
|
||||
|
||||
/// Dismisses the currently presented view controller and tracks the dismissal details for testing.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether to animate the dismissal.
|
||||
/// - completion: A completion handler to call after the dismissal finishes.
|
||||
override public func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
dismissCalled = true
|
||||
dismissAnimated = animated
|
||||
dismissCompletion = completion
|
||||
|
||||
if let presentedView {
|
||||
presentedView.beginAppearanceTransition(false, animated: animated)
|
||||
presentedView.endAppearanceTransition()
|
||||
}
|
||||
|
||||
// Clear the presented view controller
|
||||
presentedView = nil
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
override public func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
pushViewControllerCalled = true
|
||||
pushedViewController = viewController
|
||||
pushAnimated = animated
|
||||
|
||||
// Add to view controllers array
|
||||
var controllers = viewControllers
|
||||
controllers.append(viewController)
|
||||
viewControllers = controllers
|
||||
|
||||
// Simulate appearance transitions safely
|
||||
DispatchQueue.main.async {
|
||||
viewController.beginAppearanceTransition(true, animated: animated)
|
||||
viewController.endAppearanceTransition()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override public func popViewController(animated: Bool) -> UIViewController? {
|
||||
popViewControllerCalled = true
|
||||
popAnimated = animated
|
||||
|
||||
guard viewControllers.count > 1 else { return nil }
|
||||
|
||||
var controllers = viewControllers
|
||||
let poppedVC = controllers.removeLast()
|
||||
poppedViewController = poppedVC
|
||||
viewControllers = controllers
|
||||
|
||||
// Simulate appearance transitions safely
|
||||
DispatchQueue.main.async {
|
||||
poppedVC.beginAppearanceTransition(false, animated: animated)
|
||||
poppedVC.endAppearanceTransition()
|
||||
}
|
||||
|
||||
return poppedVC
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Resets and clears all local variables, to prepare the mock for reuse.
|
||||
public func reset() {
|
||||
presentCalled = false
|
||||
presentedView = nil
|
||||
presentAnimated = false
|
||||
presentCompletion = nil
|
||||
|
||||
dismissCalled = false
|
||||
dismissAnimated = false
|
||||
dismissCompletion = nil
|
||||
|
||||
pushViewControllerCalled = false
|
||||
pushedViewController = nil
|
||||
pushAnimated = false
|
||||
|
||||
popViewControllerCalled = false
|
||||
poppedViewController = nil
|
||||
popAnimated = false
|
||||
|
||||
_navigationController = nil
|
||||
}
|
||||
|
||||
// MARK: Mock Hierarchy
|
||||
|
||||
/// Sets up a `UIWindow` and `UIView` to use as mocks in the view hierarchy.
|
||||
private func setUpMockHierarchy() {
|
||||
// Create a mock window to avoid issues with view hierarchy
|
||||
mockWindow = UIWindow(frame: MockUINavigationController.mockWindowSize)
|
||||
mockWindow?.rootViewController = self
|
||||
|
||||
// Create a mock view
|
||||
mockView = UIView(frame: mockWindow?.frame ?? .zero)
|
||||
view = mockView
|
||||
}
|
||||
}
|
||||
@ -43,17 +43,6 @@ public class MockUIViewController: UIViewController {
|
||||
/// The completion handler passed to the `dismiss` method.
|
||||
public var dismissCompletion: (() -> Void)?
|
||||
|
||||
// MARK: Navigation Tracking
|
||||
|
||||
/// Indicates whether the `pushViewController` method has been called.
|
||||
public var pushViewControllerCalled = false
|
||||
|
||||
/// The view controller that was pushed, if any.
|
||||
public var pushedViewController: UIViewController?
|
||||
|
||||
/// Indicates whether the `popViewController` method has been called.
|
||||
public var popViewControllerCalled = false
|
||||
|
||||
// MARK: Navigation Controller Support
|
||||
|
||||
/// Internal storage for a navigation controller.
|
||||
@ -107,6 +96,11 @@ public class MockUIViewController: UIViewController {
|
||||
setUpMockHierarchy()
|
||||
}
|
||||
|
||||
/// Initializes the mock navigation controller with a nil nib name and bundle.
|
||||
public init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
/// Initializes the mock view controller from a coder.
|
||||
///
|
||||
/// - Parameters:
|
||||
@ -164,7 +158,7 @@ public class MockUIViewController: UIViewController {
|
||||
}
|
||||
|
||||
/// Dismisses the currently presented view controller and tracks the dismissal details for testing.
|
||||
///
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - animated: Whether to animate the dismissal.
|
||||
/// - completion: A completion handler to call after the dismissal finishes.
|
||||
@ -197,9 +191,7 @@ public class MockUIViewController: UIViewController {
|
||||
dismissAnimated = false
|
||||
dismissCompletion = nil
|
||||
|
||||
pushViewControllerCalled = false
|
||||
pushedViewController = nil
|
||||
popViewControllerCalled = false
|
||||
_navigationController = nil
|
||||
}
|
||||
|
||||
// MARK: Mock Hierarchy
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - AuthModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - ProfileSwitcherModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
// MARK: - StackNavigatorTests
|
||||
|
||||
class StackNavigatorHostedTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var subject: UINavigationController!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
subject = UINavigationController()
|
||||
setKeyWindowRoot(viewController: subject)
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `present(_:animated:)` presents the hosted view on existing presented views.
|
||||
/// This is in `BitwardenSharedTests` instead of `BitwardenKitTests` because it requires a host app,
|
||||
/// due to the fact that the implementation of `StackNavigator` creates a `UIHostingController`,
|
||||
/// so we cannot mock it without significantly more rigamarole, which seems excessive for one test.
|
||||
@MainActor
|
||||
func test_present_onPresentedView() {
|
||||
subject.present(EmptyView(), animated: false, embedInNavigationController: false)
|
||||
subject.present(ScrollView<EmptyView> {}, animated: false, embedInNavigationController: false)
|
||||
XCTAssertTrue(subject.presentedViewController is UIHostingController<EmptyView>)
|
||||
waitFor(subject.presentedViewController?.presentedViewController != nil)
|
||||
XCTAssertTrue(
|
||||
subject.presentedViewController?.presentedViewController
|
||||
is UIHostingController<ScrollView<EmptyView>>,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - ExtensionSetupModule
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - FileSelectionModule
|
||||
|
||||
/// An object that builds coordinators for the file selection flow.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - LoginRequestModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - PasswordAutoFillModule
|
||||
|
||||
/// An object that builds coordinators for the password autofill flow.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import BitwardenSdk
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - AddEditFolderModule
|
||||
|
||||
/// An object that builds coordinators for the add and edit folder view.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - SettingsModule
|
||||
|
||||
/// An object that builds coordinators for the settings tab.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - ExportCXFModule
|
||||
|
||||
/// An object that builds coordinators for the Credential Exchange export flow.
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - Generator Module
|
||||
|
||||
/// An object that builds coordinators for the generator tab.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - ImportCXFModule
|
||||
|
||||
/// An object that builds coordinators for the Credential Exchange import flow.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - PasswordHistoryModule
|
||||
|
||||
/// An object that builds coordinators for the password history view.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - SendModule
|
||||
|
||||
/// An object that builds coordinators for the send tab.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - SendItemModule
|
||||
|
||||
/// An object that builds coordinators for the send item flow.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - ImportLoginsModule
|
||||
|
||||
/// An object that builds coordinators for the import logins views.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - VaultModule
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - VaultModule
|
||||
|
||||
@ -55,7 +55,7 @@ class MockAppModule:
|
||||
}
|
||||
|
||||
func makeItemListCoordinator(
|
||||
stackNavigator _: AuthenticatorShared.StackNavigator,
|
||||
stackNavigator _: StackNavigator,
|
||||
) -> AnyCoordinator<ItemListRoute, ItemListEvent> {
|
||||
itemListCoordinator.asAnyCoordinator()
|
||||
}
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import AuthenticatorShared
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
|
||||
final class MockStackNavigator: StackNavigator {
|
||||
struct NavigationAction {
|
||||
var type: NavigationType
|
||||
var view: Any?
|
||||
var animated: Bool
|
||||
var hidesBottomBar: Bool?
|
||||
var overFullscreen: Bool?
|
||||
}
|
||||
|
||||
enum NavigationType {
|
||||
case dismissed
|
||||
case dismissedWithCompletionHandler
|
||||
case pushed
|
||||
case popped
|
||||
case poppedToRoot
|
||||
case presented
|
||||
case presentedInSheet
|
||||
case replaced
|
||||
}
|
||||
|
||||
var actions: [NavigationAction] = []
|
||||
var alerts: [BitwardenKit.Alert] = []
|
||||
var isEmpty = true
|
||||
var isPresenting: Bool { actions.last?.type == .presented }
|
||||
var rootViewController: UIViewController?
|
||||
|
||||
var viewControllersToPop: [UIViewController] = []
|
||||
|
||||
func dismiss(animated: Bool) {
|
||||
actions.append(NavigationAction(type: .dismissed, animated: animated))
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||
completion?()
|
||||
actions.append(NavigationAction(type: .dismissedWithCompletionHandler, animated: animated))
|
||||
}
|
||||
|
||||
func push<Content: View>(_ view: Content, animated: Bool, hidesBottomBar: Bool) {
|
||||
actions.append(NavigationAction(
|
||||
type: .pushed,
|
||||
view: view,
|
||||
animated: animated,
|
||||
hidesBottomBar: hidesBottomBar,
|
||||
))
|
||||
}
|
||||
|
||||
func push(_ viewController: UIViewController, animated: Bool) {
|
||||
actions.append(NavigationAction(
|
||||
type: .pushed,
|
||||
view: viewController,
|
||||
animated: animated,
|
||||
))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pop(animated: Bool) -> UIViewController? {
|
||||
actions.append(NavigationAction(type: .popped, animated: animated))
|
||||
return viewControllersToPop.last
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func popToRoot(animated: Bool) -> [UIViewController] {
|
||||
actions.append(NavigationAction(type: .poppedToRoot, animated: animated))
|
||||
return viewControllersToPop
|
||||
}
|
||||
|
||||
func present(_ alert: BitwardenKit.Alert) {
|
||||
alerts.append(alert)
|
||||
}
|
||||
|
||||
func present<Content: View>(
|
||||
_ view: Content,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
onCompletion: (() -> Void)?,
|
||||
) {
|
||||
onCompletion?()
|
||||
actions.append(
|
||||
NavigationAction(
|
||||
type: .presented,
|
||||
view: view,
|
||||
animated: animated,
|
||||
overFullscreen: overFullscreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func present(
|
||||
_ viewController: UIViewController,
|
||||
animated: Bool,
|
||||
overFullscreen: Bool,
|
||||
onCompletion: (() -> Void)?,
|
||||
) {
|
||||
onCompletion?()
|
||||
actions.append(
|
||||
NavigationAction(
|
||||
type: .presented,
|
||||
view: viewController,
|
||||
animated: animated,
|
||||
overFullscreen: overFullscreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func replace<Content: View>(_ view: Content, animated: Bool) {
|
||||
actions.append(NavigationAction(type: .replaced, view: view, animated: animated))
|
||||
}
|
||||
}
|
||||
@ -194,7 +194,7 @@ class MockAppModule:
|
||||
|
||||
func makeVaultCoordinator(
|
||||
delegate _: BitwardenShared.VaultCoordinatorDelegate,
|
||||
stackNavigator _: BitwardenShared.StackNavigator,
|
||||
stackNavigator _: StackNavigator,
|
||||
) -> BitwardenShared.AnyCoordinator<BitwardenShared.VaultRoute, AuthAction> {
|
||||
vaultCoordinator.asAnyCoordinator()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user