diff --git a/ReMVVMExt/Sources/Navigation.swift b/ReMVVMExt/Sources/Navigation.swift index 02297dc..362200c 100644 --- a/ReMVVMExt/Sources/Navigation.swift +++ b/ReMVVMExt/Sources/Navigation.swift @@ -9,7 +9,6 @@ import ReMVVMCore public protocol NavigationState: StoreState { - var navigation: Navigation { get } } @@ -29,20 +28,16 @@ public struct NavigationRoot { self.stacks = stacks.map { (AnyNavigationItem($0),$1)} } - public enum Main: NavigationItem { - - public var action: StoreAction { FakeAction() } + private struct FakeAction: StoreAction {} case single - - private struct FakeAction: StoreAction {} + public var action: StoreAction { FakeAction() } } } public struct Navigation { - public let modals: [Modal] public let root: NavigationRoot @@ -52,7 +47,7 @@ public struct Navigation { } public var factory: ViewModelFactory { - return modals.last?.factory ?? root.currentStack.last ?? CompositeViewModelFactory() + modals.last?.factory ?? root.currentStack.last ?? CompositeViewModelFactory() } public enum Modal { @@ -83,7 +78,6 @@ public struct Navigation { } public enum NavigationReducer: Reducer { - static let reducer = ShowOnRootReducer .compose(with: ShowReducer.self) .compose(with: SynchronizeStateReducer.self) @@ -93,6 +87,6 @@ public enum NavigationReducer: Reducer { .compose(with: DismissModalReducer.self) public static func reduce(state: Navigation, with action: StoreAction) -> Navigation { - return reducer.reduce(state: state, with: action) + reducer.reduce(state: state, with: action) } } diff --git a/ReMVVMExt/Sources/NavigationDispatcher.swift b/ReMVVMExt/Sources/NavigationDispatcher.swift new file mode 100644 index 0000000..64fdd55 --- /dev/null +++ b/ReMVVMExt/Sources/NavigationDispatcher.swift @@ -0,0 +1,38 @@ +// +// File.swift +// +// +// Created by Paweł Zgoda-Ferchmin on 30/05/2023. +// + +import Foundation + +final class NavigationDispatcher { + static let main = NavigationDispatcher() + + let routingQueue = DispatchQueue(label: "RoutingQueue") + let semaphore = DispatchSemaphore(value: 0) + + let queueingEnabled: Bool = true + + private init() { } + + func async(function: @escaping (@escaping ()-> Void) -> Void) { + if queueingEnabled { + routingQueue.async { [weak self] in + DispatchQueue.main.async { [weak self] in + function { + self?.semaphore.signal() + } + } + let waitUntil = DispatchTime.now() + Double(Int64(20 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) + let result = self?.semaphore.wait(timeout: waitUntil) + if case .timedOut = result { + print("Navigation stuck on routing") + } + } + } else { + function { } + } + } +} diff --git a/ReMVVMExt/Sources/ReMVVMExtension.swift b/ReMVVMExt/Sources/ReMVVMExtension.swift index 0b41d21..b1adf4c 100644 --- a/ReMVVMExt/Sources/ReMVVMExtension.swift +++ b/ReMVVMExt/Sources/ReMVVMExtension.swift @@ -53,7 +53,7 @@ public enum ReMVVMExtension { stateMappers: [StateMapper] = [], reducer: R.Type, middleware: [AnyMiddleware], - logger: Logger = .noLogger) -> AnyStore where R: Reducer, R.State == ApplicationState, R.Action == StoreAction { + logger: Logger = .defaultLogger) -> AnyStore where R: Reducer, R.State == ApplicationState, R.Action == StoreAction { let appMapper = StateMapper>(for: \.appState) let stateMappers = [appMapper] + stateMappers.map { $0.map(with: \.appState) } diff --git a/ReMVVMExt/Sources/Reducers/DismissModalReducer.swift b/ReMVVMExt/Sources/Reducers/DismissModalReducer.swift index 0ae405f..578dd3f 100644 --- a/ReMVVMExt/Sources/Reducers/DismissModalReducer.swift +++ b/ReMVVMExt/Sources/Reducers/DismissModalReducer.swift @@ -9,14 +9,10 @@ import ReMVVMCore public struct DismissModalReducer: Reducer { - - public typealias Action = DismissModal - public static func reduce(state: Navigation, with action: DismissModal) -> Navigation { + guard !state.modals.isEmpty else { return state } var modals = state.modals - - guard !modals.isEmpty else { return Navigation(root: state.root, modals: modals) } if action.dismissAllViews { modals.removeAll() } else { @@ -24,11 +20,9 @@ public struct DismissModalReducer: Reducer { } return Navigation(root: state.root, modals: modals) } - } public struct DismissModalMiddleware: Middleware { - public let uiState: UIState public init(uiState: UIState) { @@ -42,18 +36,16 @@ public struct DismissModalMiddleware: Middleware { let uiState = self.uiState - guard !uiState.modalControllers.isEmpty else { return } - interceptor.next { _ in // side effect - - //dismiss not needed modals - if action.dismissAllViews { - uiState.dismissAll(animated: action.animated) - } else { - uiState.dismiss(animated: action.animated) + NavigationDispatcher.main.async { completion in + //dismiss not needed modals + if action.dismissAllViews { + uiState.dismissAll(animated: action.animated, completion: completion) + } else { + uiState.dismiss(animated: action.animated, completion: completion) + } } } - } } diff --git a/ReMVVMExt/Sources/Reducers/PopReducer.swift b/ReMVVMExt/Sources/Reducers/PopReducer.swift index ef39eb4..15f8d65 100644 --- a/ReMVVMExt/Sources/Reducers/PopReducer.swift +++ b/ReMVVMExt/Sources/Reducers/PopReducer.swift @@ -9,11 +9,8 @@ import ReMVVMCore public struct PopReducer: Reducer { - - public typealias Action = Pop - public static func reduce(state: Navigation, with action: Pop) -> Navigation { - return updateStateTree(state, for: action.mode) + updateStateTree(state, for: action.mode) } private static func updateStateTree(_ stateTree: Navigation, for mode: PopMode) -> Navigation { @@ -51,7 +48,6 @@ public struct PopReducer: Reducer { } public struct PopMiddleware: Middleware { - public let uiState: UIState public init(uiState: UIState) { @@ -62,28 +58,31 @@ public struct PopMiddleware: Middleware { action: Pop, interceptor: Interceptor, dispatcher: Dispatcher) { - guard state.navigation.topStack.count > 1 else { return } interceptor.next { _ in // side effect - switch action.mode { case .popToRoot, .resetStack: - self.uiState.navigationController?.popToRootViewController(animated: action.animated) + NavigationDispatcher.main.async { completion in + self.uiState.navigationController?.popToRootViewController(animated: action.animated, + completion: completion) + } case .pop(let count): if count > 1 { let viewControllers = self.uiState.navigationController?.viewControllers ?? [] let dropCount = min(count, viewControllers.count - 1) - 1 let newViewControllers = Array(viewControllers.dropLast(dropCount)) - self.uiState.navigationController?.setViewControllers(newViewControllers, animated: false) + NavigationDispatcher.main.async { completion in + self.uiState.navigationController?.setViewControllers(newViewControllers, + animated: false, + completion: completion) + } + } + NavigationDispatcher.main.async { completion in + self.uiState.navigationController?.popViewController(animated: action.animated, completion: completion) } - - self.uiState.navigationController?.popViewController(animated: action.animated) } - } - } - } diff --git a/ReMVVMExt/Sources/Reducers/PushReducer.swift b/ReMVVMExt/Sources/Reducers/PushReducer.swift index c75095a..7c1275b 100644 --- a/ReMVVMExt/Sources/Reducers/PushReducer.swift +++ b/ReMVVMExt/Sources/Reducers/PushReducer.swift @@ -9,11 +9,7 @@ import ReMVVMCore public struct PushReducer: Reducer { - - public typealias Action = Push - public static func reduce(state: Navigation, with action: Push) -> Navigation { - let root: NavigationRoot // dismiss all modals without navigation var modals: [Navigation.Modal] = state.modals.reversed().drop { !$0.hasNavigation }.reversed() @@ -54,7 +50,6 @@ public struct PushReducer: Reducer { } public struct PushMiddleware: Middleware { - public let uiState: UIState public init(uiState: UIState) { @@ -65,15 +60,16 @@ public struct PushMiddleware: Middleware { action: Push, interceptor: Interceptor, dispatcher: Dispatcher) { - let uiState = self.uiState interceptor.next { state in // side effect - - //dismiss not needed modals - uiState.dismiss(animated: action.controllerInfo.animated, - number: uiState.modalControllers.count - state.navigation.modals.count) + NavigationDispatcher.main.async { completion in + //dismiss not needed modals + uiState.dismiss(animated: action.controllerInfo.animated, + number: uiState.modalControllers.count - state.navigation.modals.count, + completion: completion) + } guard let navigationController = uiState.navigationController else { assertionFailure("PushMiddleware: No navigation Controller") @@ -95,12 +91,16 @@ public struct PushMiddleware: Middleware { viewControllers = viewControllers.dropLast(dropCount) } - navigationController.setViewControllers(viewControllers + [controller], - animated: action.controllerInfo.animated) + NavigationDispatcher.main.async { completion in + navigationController.setViewControllers(viewControllers + [controller], + animated: action.controllerInfo.animated, + completion: completion) + } } else { - navigationController.pushViewController(controller, animated: action.controllerInfo.animated) + NavigationDispatcher.main.async { completion in + navigationController.pushViewController(controller, animated: action.controllerInfo.animated, completion: completion) + } } - } } } diff --git a/ReMVVMExt/Sources/Reducers/ShowModalReducer.swift b/ReMVVMExt/Sources/Reducers/ShowModalReducer.swift index 484e874..681c09b 100644 --- a/ReMVVMExt/Sources/Reducers/ShowModalReducer.swift +++ b/ReMVVMExt/Sources/Reducers/ShowModalReducer.swift @@ -10,11 +10,7 @@ import ReMVVMCore import UIKit public struct ShowModalReducer: Reducer { - - public typealias Action = ShowModal - public static func reduce(state: Navigation, with action: ShowModal) -> Navigation { - let factory = action.controllerInfo.factory ?? state.factory let modal: Navigation.Modal = action.withNavigationController ? .navigation([factory]) : .single(factory) // dismiss all modals without navigation @@ -25,8 +21,8 @@ public struct ShowModalReducer: Reducer { } public struct ShowModalMiddleware: Middleware { - public let uiState: UIState + public init(uiState: UIState) { self.uiState = uiState } @@ -35,7 +31,6 @@ public struct ShowModalMiddleware: Middleware { action: ShowModal, interceptor: Interceptor, dispatcher: Dispatcher) { - let uiState = self.uiState var controller: UIViewController? @@ -57,9 +52,13 @@ public struct ShowModalMiddleware: Middleware { interceptor.next { state in // side effect - //dismiss not needed modals - uiState.dismiss(animated: action.controllerInfo.animated, - number: uiState.modalControllers.count - state.navigation.modals.count + 1) + NavigationDispatcher.main.async { completion in + //dismiss not needed modals + uiState.dismiss(animated: action.controllerInfo.animated, + number: uiState.modalControllers.count - state.navigation.modals.count + 1, + completion: completion) + + } let newModal: UIViewController if action.withNavigationController { @@ -78,13 +77,15 @@ public struct ShowModalMiddleware: Middleware { newModal.modalPresentationStyle = action.presentationStyle if #available(iOS 15.0, *) { - - if newModal.modalPresentationStyle == .pageSheet || newModal.modalPresentationStyle == .formSheet, let cornerRadius = action.preferredCornerRadius { + if newModal.modalPresentationStyle == .pageSheet || newModal.modalPresentationStyle == .formSheet, + let cornerRadius = action.preferredCornerRadius { newModal.sheetPresentationController?.preferredCornerRadius = cornerRadius } } - uiState.present(newModal, animated: action.controllerInfo.animated) + NavigationDispatcher.main.async { completion in + uiState.present(newModal, animated: action.controllerInfo.animated, completion: completion) + } } } } diff --git a/ReMVVMExt/Sources/Reducers/ShowOnRootReducer.swift b/ReMVVMExt/Sources/Reducers/ShowOnRootReducer.swift index 3d868ba..5c72be2 100644 --- a/ReMVVMExt/Sources/Reducers/ShowOnRootReducer.swift +++ b/ReMVVMExt/Sources/Reducers/ShowOnRootReducer.swift @@ -9,9 +9,7 @@ import ReMVVMCore struct ShowOnRootReducer: Reducer { - public static func reduce(state: Navigation, with action: ShowOnRoot) -> Navigation { - let current = NavigationRoot.Main.single let factory = action.controllerInfo.factory ?? state.factory let stacks = [(current, [factory])] @@ -21,7 +19,6 @@ struct ShowOnRootReducer: Reducer { } public struct ShowOnRootMiddleware: Middleware { - public let uiState: UIState public init(uiState: UIState) { @@ -32,17 +29,21 @@ public struct ShowOnRootMiddleware: Middleware { action: ShowOnRoot, interceptor: Interceptor, dispatcher: Dispatcher) { - let uiState = self.uiState interceptor.next { _ in // newState - state variable is used below // side effect - uiState.setRoot(controller: action.controllerInfo.loader.load(), - animated: action.controllerInfo.animated, - navigationBarHidden: action.navigationBarHidden) - // dismiss modals - uiState.rootViewController.dismiss(animated: true, completion: nil) + NavigationDispatcher.main.async { completion in + uiState.rootViewController.dismiss(animated: true, completion: completion) + } + + NavigationDispatcher.main.async { completion in + uiState.setRoot(controller: action.controllerInfo.loader.load(), + animated: action.controllerInfo.animated, + navigationBarHidden: action.navigationBarHidden, + completion: completion) + } } } } diff --git a/ReMVVMExt/Sources/Reducers/ShowTabReducer.swift b/ReMVVMExt/Sources/Reducers/ShowTabReducer.swift index 1d36a2c..6485265 100644 --- a/ReMVVMExt/Sources/Reducers/ShowTabReducer.swift +++ b/ReMVVMExt/Sources/Reducers/ShowTabReducer.swift @@ -10,7 +10,8 @@ import Loaders import ReMVVMCore import UIKit -typealias NavigationType = [AnyNavigationItem] +public typealias NavigationType = [AnyNavigationItem] + extension NavigationRoot { var navigationType: NavigationType { stacks.map { $0.0 } } } @@ -20,9 +21,7 @@ extension NavigationItem where Self: CaseIterable { } struct ShowReducer: Reducer { - public static func reduce(state: Navigation, with action: Show) -> Navigation { - let current = action.item var stacks: [(AnyNavigationItem, [ViewModelFactory])] let factory = action.controllerInfo.factory ?? state.factory @@ -47,7 +46,6 @@ struct ShowReducer: Reducer { } public struct ShowMiddleware: Middleware { - public let uiState: UIState public init(uiState: UIState) { @@ -55,17 +53,14 @@ public struct ShowMiddleware: Middleware { } public func onNext(for state: State, action: Show, interceptor: Interceptor, dispatcher: Dispatcher) { - guard state.navigation.root.currentItem != action.item || action.resetStack else { - dispatcher.dispatch(action: Pop(mode: .popToRoot, animated: true)) return } interceptor.next(action: action) { [uiState] _ in - let wasTabOnTop = state.navigation.root.navigationType == action.navigationType - && uiState.rootViewController is NavigationContainerController + && uiState.rootViewController is NavigationContainerController let containerController: NavigationContainerController if wasTabOnTop { @@ -82,22 +77,35 @@ public struct ShowMiddleware: Middleware { } } + //set up current if empty (or reset) - if let top = containerController.currentNavigationController, - top.viewControllers.isEmpty - || action.resetStack { - top.setViewControllers([action.controllerInfo.loader.load()], - animated: false) + let topNavigationController = containerController + .containers? + .first { action.item == ($0.tabBarItem as? TabItem)?.navigationTab }? + .currentNavigationController + + if let topNavigationController, + topNavigationController.viewControllers.isEmpty || action.resetStack { + NavigationDispatcher.main.async { completion in + topNavigationController.setViewControllers([action.controllerInfo.loader.load()], + animated: false, + completion: completion) + } } if !wasTabOnTop { - uiState.setRoot(controller: containerController, - animated: action.controllerInfo.animated, - navigationBarHidden: action.navigationBarHidden) + NavigationDispatcher.main.async { completion in + uiState.setRoot(controller: containerController, + animated: action.controllerInfo.animated, + navigationBarHidden: action.navigationBarHidden, + completion: completion) + } } - // dismiss modals - uiState.rootViewController.dismiss(animated: true, completion: nil) + NavigationDispatcher.main.async { completion in + // dismiss modals + uiState.rootViewController.dismiss(animated: true, completion: completion) + } } } } diff --git a/ReMVVMExt/Sources/Reducers/SynchronizeStateReducer.swift b/ReMVVMExt/Sources/Reducers/SynchronizeStateReducer.swift index 86d38cd..7a356f7 100644 --- a/ReMVVMExt/Sources/Reducers/SynchronizeStateReducer.swift +++ b/ReMVVMExt/Sources/Reducers/SynchronizeStateReducer.swift @@ -12,9 +12,6 @@ import UIKit // needed to synchronize the state when user use back button or swipe gesture struct SynchronizeStateReducer: Reducer { - - public typealias Action = SynchronizeState - public static func reduce(state: Navigation, with action: SynchronizeState) -> Navigation { if action.type == .navigation { return PopReducer.reduce(state: state, with: Pop()) @@ -39,7 +36,6 @@ public final class SynchronizeStateMiddleware: Middlewar dispatcher: Dispatcher) { if let action = action as? SynchronizeState { - if action.type == .navigation, let navigationCount = uiState.navigationController?.viewControllers.count, state.navigation.topStack.count > navigationCount { @@ -58,7 +54,6 @@ public final class SynchronizeStateMiddleware: Middlewar //swizzle viewDidDissapear private extension UIViewController { - private struct AssociatedKeys { static var didDisapearClosureKey = "com.db.didDisapear" } @@ -86,7 +81,6 @@ private extension UIViewController { } private static func swizzleViewDidDisappear(_ class_: AnyClass, to block: @escaping ViewDidDisappearBlock) -> IMP? { - let selector = #selector(UIViewController.viewDidDisappear(_:)) let method: Method? = class_getInstanceMethod(class_, selector) let newImplementation: IMP = imp_implementationWithBlock(unsafeBitCast(block, to: AnyObject.self)) @@ -140,11 +134,10 @@ open class ReMVVMNavigationController: UINavigationController { } override func responds(to aSelector: Selector!) -> Bool { - return super.responds(to: aSelector) || delegate?.responds(to: aSelector) ?? false + super.responds(to: aSelector) || delegate?.responds(to: aSelector) ?? false } func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { - delegate?.navigationController?(navigationController, didShow: viewController, animated: animated) dispatcher.dispatch(action: SynchronizeState(.navigation)) } diff --git a/ReMVVMExt/Sources/StoreActions.swift b/ReMVVMExt/Sources/StoreActions.swift index 53ee7e9..5aaadcd 100644 --- a/ReMVVMExt/Sources/StoreActions.swift +++ b/ReMVVMExt/Sources/StoreActions.swift @@ -14,19 +14,19 @@ import UIKit public protocol NavigationAction: StoreAction { } public struct SynchronizeState: NavigationAction { - + public enum SynchronizeType { + case navigation + case modal + } + public let type: SynchronizeType + public init(_ type: SynchronizeType) { self.type = type } - - public enum SynchronizeType { - case navigation, modal - } } public struct ShowOnRoot: NavigationAction { - public let controllerInfo: LoaderWithFactory public let navigationBarHidden: Bool @@ -34,7 +34,6 @@ public struct ShowOnRoot: NavigationAction { factory: ViewModelFactory? = nil, animated: Bool = true, navigationBarHidden: Bool = true) { - self.controllerInfo = LoaderWithFactory(loader: loader, factory: factory, animated: animated) @@ -46,7 +45,6 @@ public struct ShowOnRoot: NavigationAction { factory: ViewModelFactory? = nil, animated: Bool = true, navigationBarHidden: Bool = true) where V: View { - self.controllerInfo = LoaderWithFactory(view: view, factory: factory, animated: animated) @@ -60,7 +58,7 @@ public struct Show: NavigationAction { public let navigationBarHidden: Bool public let item: AnyNavigationItem public let resetStack: Bool - let navigationType: NavigationType + public let navigationType: NavigationType public init(on item: Item, loader: Loader, @@ -68,7 +66,6 @@ public struct Show: NavigationAction { animated: Bool = true, navigationBarHidden: Bool = true, resetStack: Bool = false) { - self.controllerInfo = LoaderWithFactory(loader: loader, factory: factory, animated: animated) @@ -85,7 +82,6 @@ public struct Show: NavigationAction { animated: Bool = true, navigationBarHidden: Bool = true, resetStack: Bool = false) where V: View { - self.controllerInfo = LoaderWithFactory(view: view, factory: factory, animated: animated) @@ -97,7 +93,6 @@ public struct Show: NavigationAction { } public struct Push: NavigationAction { - public let controllerInfo: LoaderWithFactory public let pop: PopMode? @@ -123,7 +118,6 @@ public struct Push: NavigationAction { animated: animated, clearBackground: clearBackground) } - } public enum PopMode { @@ -141,7 +135,6 @@ public struct Pop: NavigationAction { } public struct ShowModal: NavigationAction { - public let controllerInfo: LoaderWithFactory public let withNavigationController: Bool public let showOverSplash: Bool @@ -157,7 +150,6 @@ public struct ShowModal: NavigationAction { showOverSelfType: Bool = true, presentationStyle: UIModalPresentationStyle = .fullScreen, preferredCornerRadius: CGFloat? = nil) { - self.controllerInfo = LoaderWithFactory(loader: loader, factory: factory, animated: animated) @@ -191,7 +183,6 @@ public struct ShowModal: NavigationAction { } public struct DismissModal: NavigationAction { - public let dismissAllViews: Bool public let animated: Bool @@ -202,7 +193,6 @@ public struct DismissModal: NavigationAction { } public struct LoaderWithFactory { - public let loader: Loader public let factory: ViewModelFactory? public let animated: Bool @@ -220,14 +210,12 @@ public struct LoaderWithFactory { factory: ViewModelFactory?, animated: Bool = true, clearBackground: Bool = false) where V: View { - let hostLoader: Loader = Loader { let loader = Loader(view).load() if clearBackground { loader.view.backgroundColor = .clear } return loader - } self.init(loader: hostLoader, factory: factory, animated: animated) diff --git a/ReMVVMExt/Sources/TabBar/NavigationViewModel.swift b/ReMVVMExt/Sources/TabBar/NavigationViewModel.swift index 1d9cfe9..2e8aabf 100644 --- a/ReMVVMExt/Sources/TabBar/NavigationViewModel.swift +++ b/ReMVVMExt/Sources/TabBar/NavigationViewModel.swift @@ -32,11 +32,15 @@ open class NavigationViewModel: Initializable, StateObserv @ObservableValue public var items: [Item] = [] @ObservableValue public var selected: Item? + private var tabType: Any.Type? + required public init() { } public func didReduce(state: NavigationState, oldState: NavigationState?) { if Item.self == AnyNavigationItem.self { - let tabType = type(of: state.navigation.root.currentItem.base) + if tabType == nil { + tabType = type(of: state.navigation.root.currentItem.base) + } let items = state.navigation.root.stacks.map { $0.0 } .filter { type(of: $0.base) == tabType } .compactMap { $0 as? Item } diff --git a/ReMVVMExt/Sources/TabBar/TabBarViewController.swift b/ReMVVMExt/Sources/TabBar/TabBarViewController.swift index 7d82241..1f4aeb1 100644 --- a/ReMVVMExt/Sources/TabBar/TabBarViewController.swift +++ b/ReMVVMExt/Sources/TabBar/TabBarViewController.swift @@ -11,19 +11,16 @@ import ReMVVMCore import UIKit public struct NavigationConfig { - public typealias TabBarItems = (_ tabBar: UITabBar, _ items: [TabBarItem]) -> TabBarItemsResult where T: NavigationItem - public typealias CustomControls = (_ tabBar: UITabBar, _ items: [T]) -> CustomControlsResult where T: NavigationItem - public typealias Custom = (_ items: [T]) -> NavigationContainerController where T: NavigationItem public enum ConfigError: Error { - case toManyElements + case tooManyElements } public init(_ creator: @escaping TabBarItems, for type: T.Type = T.self) throws where T: CaseIterableNavigationItem { - guard T.allCases.count <= 5 else { throw ConfigError.toManyElements } + guard T.allCases.count <= 5 else { throw ConfigError.tooManyElements } navigationType = T.navigationType config = .uiTabBar { tabBar, items in @@ -35,7 +32,6 @@ public struct NavigationConfig { } public init(_ creator: @escaping CustomControls, for type: T.Type = T.self) where T: CaseIterableNavigationItem { - navigationType = T.navigationType config = .customTabBar { tabBar, items in creator(tabBar, items.compactMap { $0.base as? T }) @@ -43,7 +39,6 @@ public struct NavigationConfig { } public init(_ creator: @escaping Custom, for type: T.Type = T.self) where T: CaseIterableNavigationItem { - navigationType = T.navigationType config = .custom { items in return creator(items.compactMap {$0.base as? T}) @@ -53,7 +48,6 @@ public struct NavigationConfig { let navigationType: NavigationType let config: Config enum Config where T: NavigationItem { - case uiTabBar(TabBarItems) case customTabBar(CustomControls) case custom(Custom) @@ -128,7 +122,6 @@ private class TabBar: UITabBar { } override func setItems(_ items: [UITabBarItem]?, animated: Bool) { - super.setItems(items, animated: animated) guard controlItems != nil else { return } subviews @@ -171,7 +164,6 @@ private class TabBar: UITabBar { } class TabItem: UITabBarItem { - let navigationTab: AnyNavigationItem let controlItem: UIControl? @@ -186,7 +178,7 @@ class TabItem: UITabBarItem { } } -class ContainerViewController: UIViewController { +public class ContainerViewController: UIViewController { let currentNavigationController: UINavigationController init(navigationController: UINavigationController) { @@ -205,7 +197,7 @@ class ContainerViewController: UIViewController { } class TabBarViewController: UITabBarController, NavigationContainerController { - + @ReMVVM.ViewModel private var viewModel: NavigationViewModel? @ReMVVM.Dispatcher private var dispatcher init(config: NavigationConfig?, navigationControllerFactory: @escaping () -> UINavigationController) { @@ -218,7 +210,7 @@ class TabBarViewController: UITabBarController, NavigationContainerController { fatalError("init(coder:) has not been implemented") } - private var containers: [ContainerViewController]? { + var containers: [ContainerViewController]? { viewControllers?.compactMap { $0 as? ContainerViewController } } @@ -230,8 +222,6 @@ class TabBarViewController: UITabBarController, NavigationContainerController { private var config: NavigationConfig? private var navigationControllerFactory: () -> UINavigationController - @ReMVVM.ViewModel private var viewModel: NavigationViewModel? - override open var childForStatusBarStyle: UIViewController? { return currentNavigationController?.topViewController } @@ -248,19 +238,23 @@ class TabBarViewController: UITabBarController, NavigationContainerController { setup(current: viewModel.selected) viewModel.$items = { [weak self] items in - self?.setup(items: items) + NavigationDispatcher.main.async { [weak self] completion in + self?.setup(items: items) + completion() + } } viewModel.$selected = { [weak self] item in - self?.setup(current: item) + NavigationDispatcher.main.async { [weak self] completion in + self?.setup(current: item) + completion() + } } } private func setup(items: [AnyNavigationItem]) { - let tabItems: [UITabBarItem] if case let .customTabBar(configurator) = config?.config { - let result = configurator(customTabBar, items) customTabBar.height = result.height let customView = result.overelay @@ -279,7 +273,6 @@ class TabBarViewController: UITabBarController, NavigationContainerController { customTabBar.controlItems = controlItems moreNavigationController.navigationBar.isHidden = true - } else { let tabBarItems: [TabItem] = items.map { TabItem(navigationTab: $0, controlItem: nil) } @@ -311,11 +304,10 @@ class TabBarViewController: UITabBarController, NavigationContainerController { guard index >= 0 && index < viewControllers?.count ?? 0, let viewController = self.viewControllers?[index] else { return } - self.sendAction(for: viewController) + sendAction(for: viewController) } private func setup(current: AnyNavigationItem?) { - let selected = viewControllers?.first { guard let tab = $0.tabBarItem as? TabItem else { return false } return current == tab.navigationTab @@ -328,17 +320,14 @@ class TabBarViewController: UITabBarController, NavigationContainerController { private func sendAction(for viewController: UIViewController) { guard let tab = viewController.tabBarItem as? TabItem else { return } - dispatcher.dispatch(action: tab.navigationTab.action) } - } extension TabBarViewController: UITabBarControllerDelegate { - public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { - DispatchQueue.main.async { - self.sendAction(for: viewController) + DispatchQueue.main.async { [weak self] in + self?.sendAction(for: viewController) } return false } diff --git a/ReMVVMExt/Sources/UINavigationController+Navigation.swift b/ReMVVMExt/Sources/UINavigationController+Navigation.swift new file mode 100644 index 0000000..17ee822 --- /dev/null +++ b/ReMVVMExt/Sources/UINavigationController+Navigation.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by Paweł Zgoda-Ferchmin on 30/05/2023. +// + +import UIKit + +extension UINavigationController { + public func pushViewController(_ viewController: UIViewController, + animated: Bool, completion: @escaping () -> Void) { + CATransaction.begin() + CATransaction.setCompletionBlock(completion) + pushViewController(viewController, animated: animated) + CATransaction.commit() + } + + public func popViewController(animated: Bool, completion: @escaping () -> Void) { + CATransaction.begin() + CATransaction.setCompletionBlock(completion) + popViewController(animated: animated) + CATransaction.commit() + } + + public func setViewControllers(_ viewControllers: [UIViewController], animated: Bool, completion: @escaping () -> Void) { + CATransaction.begin() + CATransaction.setCompletionBlock(completion) + setViewControllers(viewControllers, animated: animated) + CATransaction.commit() + } + + public func popToRootViewController(animated: Bool, completion: @escaping () -> Void) { + CATransaction.begin() + CATransaction.setCompletionBlock(completion) + popToRootViewController(animated: animated) + CATransaction.commit() + } +} diff --git a/ReMVVMExt/Sources/UIState.swift b/ReMVVMExt/Sources/UIState.swift index 4d2fcd6..726471e 100644 --- a/ReMVVMExt/Sources/UIState.swift +++ b/ReMVVMExt/Sources/UIState.swift @@ -18,9 +18,7 @@ public struct UIStateConfig { public init(initialController: @escaping @autoclosure () -> UIViewController, navigationController: (() -> ReMVVMNavigationController)? = nil, navigationConfigs: [NavigationConfig] = [], - navigationBarHidden: Bool = true - ) { - + navigationBarHidden: Bool = true) { self.initialController = initialController self.navigationController = navigationController ?? { ReMVVMNavigationController() } self.navigationConfigs = navigationConfigs @@ -28,13 +26,12 @@ public struct UIStateConfig { } } - public protocol NavigationContainerController where Self: UIViewController { var currentNavigationController: UINavigationController? { get } + var containers: [ContainerViewController]? { get } } public final class UIState { - private let window: UIWindow private let uiStateMainController: UINavigationController @@ -52,45 +49,60 @@ public final class UIState { window.rootViewController = uiStateMainController - setRoot(controller: config.initialController(), animated: false, navigationBarHidden: config.navigationBarHidden) + setRoot(controller: config.initialController(), + animated: false, + navigationBarHidden: config.navigationBarHidden) { } } - public func setRoot(controller: UIViewController, animated: Bool, navigationBarHidden: Bool) { + public func setRoot(controller: UIViewController, + animated: Bool, + navigationBarHidden: Bool, + completion: @escaping () -> Void) { if uiStateMainController.isNavigationBarHidden != navigationBarHidden { uiStateMainController.setNavigationBarHidden(navigationBarHidden, animated: animated) } - uiStateMainController.setViewControllers([controller], animated: animated) + uiStateMainController.setViewControllers([controller], + animated: animated, + completion: completion) } public var rootViewController: UIViewController { - return uiStateMainController.viewControllers[0] + uiStateMainController.viewControllers[0] } public var navigationController: UINavigationController? { - return modalControllers.compactMap { $0 as? UINavigationController }.last ?? (rootViewController as? NavigationContainerController)?.currentNavigationController ?? uiStateMainController + modalControllers + .compactMap { $0 as? UINavigationController } + .last ?? (rootViewController as? NavigationContainerController)? + .currentNavigationController ?? uiStateMainController } private var topPresenter: UIViewController { - return modalControllers.last ?? rootViewController + modalControllers.last ?? rootViewController } - public func present(_ viewController: UIViewController, animated: Bool) { - topPresenter.present(viewController, animated: animated, completion: { [topPresenter] in + public func present(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { + topPresenter.present(viewController, animated: animated) { [topPresenter] in topPresenter.setNeedsStatusBarAppearanceUpdate() - }) + completion() + } modalControllers.append(viewController) } - public func dismissAll(animated: Bool) { - dismiss(animated: animated, number: Int.max) + public func dismissAll(animated: Bool, completion: @escaping () -> Void) { + dismiss(animated: animated, number: Int.max, completion: completion) } - public func dismiss(animated: Bool, number: Int = 1) { + public func dismiss(animated: Bool, number: Int = 1, completion: @escaping () -> Void) { let number = modalControllers.count >= number ? number : modalControllers.count - guard number > 0 else { return } + guard number > 0 else { + completion() + return + } modalControllers.removeLast(number) - topPresenter.dismiss(animated: animated, completion: { [topPresenter] in + topPresenter.dismiss(animated: animated) { [topPresenter] in topPresenter.setNeedsStatusBarAppearanceUpdate() - }) + completion() + } } }