From 7fe951eddb9d2b4af79a0c66b20b8a4842d71ef9 Mon Sep 17 00:00:00 2001 From: Den Andreychuk Date: Fri, 2 Jan 2026 17:09:25 +0200 Subject: [PATCH 1/4] Implement support to disable animations --- Package.resolved | 78 +++++++++++++++++++ .../UIViewController+ViewTransition.swift | 4 +- .../UIWindow+ViewTransition.swift | 2 +- ...OrUIKitViewControllerCoordinatorType.swift | 9 ++- .../AppKitOrUIKitWindowCoordinatorType.swift | 13 +++- .../_AppKitOrUIKitViewCoordinatorBase.swift | 6 +- .../Core/AnyViewCoordinator.swift | 12 +-- .../Intramodular/Core/ViewCoordinator.swift | 4 +- .../Transition/ViewTransition.Payload.swift | 2 +- .../Transition/ViewTransition.swift | 10 +-- 10 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..47f3aa0 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,78 @@ +{ + "originHash" : "896673328595425ab5a6021a854492efa4b4361d3d04341f9b0a9cdf08f6bac8", + "pins" : [ + { + "identity" : "merge", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vmanot/Merge.git", + "state" : { + "branch" : "master", + "revision" : "2f04b1322cf695fb5e464bcf75fc0c1cfaef0ef3" + } + }, + { + "identity" : "swallow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vmanot/Swallow.git", + "state" : { + "branch" : "master", + "revision" : "444e02e89e0336f66a7d4e36abd93f231436f55c" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-subprocess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/preternatural-fork/swift-subprocess.git", + "state" : { + "branch" : "release/0.2.1", + "revision" : "5f6ae03e819a255de6315aa1c89d28fddd0c7ffe" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-precompiled/swift-syntax", + "state" : { + "branch" : "release/6.1", + "revision" : "fc197a24fb2e77609fbe3d94624e36f84d758099" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system", + "state" : { + "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", + "version" : "1.6.3" + } + }, + { + "identity" : "swiftuix", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftUIX/SwiftUIX.git", + "state" : { + "branch" : "master", + "revision" : "9e6dc79e584dd36624254eea4c624bfce98bad4b" + } + } + ], + "version" : 3 +} diff --git a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift index 2690195..6d2e76a 100644 --- a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift +++ b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift @@ -218,13 +218,13 @@ extension ViewTransition { @_transparent func triggerPublisher( in controller: UIViewController, - animated: Bool, coordinator: VC ) -> AnyPublisher { let transition = merge(coordinator: coordinator) + let animated = transition.animated if case .custom(let trigger) = transition.finalize() { - return trigger() + return trigger(animated) } return Future { attemptToFulfill in diff --git a/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift b/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift index c4a3341..47b3d5e 100644 --- a/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift +++ b/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift @@ -16,7 +16,7 @@ extension ViewTransition { let animated = transition.animated if case .custom(let trigger) = transition.finalize() { - return trigger() + return trigger(animated) } return Future { attemptToFulfill in diff --git a/Sources/Intramodular/Bridging/AppKitOrUIKitViewControllerCoordinatorType.swift b/Sources/Intramodular/Bridging/AppKitOrUIKitViewControllerCoordinatorType.swift index 9deab64..3317883 100644 --- a/Sources/Intramodular/Bridging/AppKitOrUIKitViewControllerCoordinatorType.swift +++ b/Sources/Intramodular/Bridging/AppKitOrUIKitViewControllerCoordinatorType.swift @@ -53,16 +53,19 @@ open class UIViewControllerCoordinator: _AppKitOrUIKitViewCoordinatorBase fatalError() } - public override func triggerPublisher(for route: Route) -> AnyPublisher { + public override func triggerPublisher(for route: Route, animated: Bool = true) -> AnyPublisher { guard let rootViewController = rootViewController else { runtimeIssue("Could not resolve a root view controller.") return .failure(TriggerError.rootViewControllerMissing) } - return transition(for: route) + var transition = transition(for: route) + transition.animated = animated + + return transition .environment(environmentInsertions) - .triggerPublisher(in: rootViewController, animated: true, coordinator: self) + .triggerPublisher(in: rootViewController, coordinator: self) .handleOutput { [weak self] _ in self?.updateAllChildren() } diff --git a/Sources/Intramodular/Bridging/AppKitOrUIKitWindowCoordinatorType.swift b/Sources/Intramodular/Bridging/AppKitOrUIKitWindowCoordinatorType.swift index 4494ae1..f0275fa 100644 --- a/Sources/Intramodular/Bridging/AppKitOrUIKitWindowCoordinatorType.swift +++ b/Sources/Intramodular/Bridging/AppKitOrUIKitWindowCoordinatorType.swift @@ -56,12 +56,16 @@ open class AppKitOrUIKitWindowCoordinator: _AppKitOrUIKitViewCoordinatorB @discardableResult override public func triggerPublisher( - for route: Route + for route: Route, + animated: Bool = true ) -> AnyPublisher { do { let window = try self.window.unwrap() - return transition(for: route) + var transition = transition(for: route) + transition.animated = animated + + return transition .environment(environmentInsertions) .triggerPublisher(in: window, coordinator: self) .handleOutput { [weak self] _ in @@ -80,9 +84,10 @@ open class AppKitOrUIKitWindowCoordinator: _AppKitOrUIKitViewCoordinatorB @discardableResult override public func trigger( - _ route: Route + _ route: Route, + animated: Bool = true ) -> AnyPublisher { - super.trigger(route) + super.trigger(route, animated: animated) } } diff --git a/Sources/Intramodular/Bridging/_AppKitOrUIKitViewCoordinatorBase.swift b/Sources/Intramodular/Bridging/_AppKitOrUIKitViewCoordinatorBase.swift index 5bf694d..5ecc4aa 100644 --- a/Sources/Intramodular/Bridging/_AppKitOrUIKitViewCoordinatorBase.swift +++ b/Sources/Intramodular/Bridging/_AppKitOrUIKitViewCoordinatorBase.swift @@ -114,13 +114,13 @@ open class _AppKitOrUIKitViewCoordinatorBase: _opaque_AppKitOrUIKitViewCo fatalError() } - public func triggerPublisher(for route: Route) -> AnyPublisher { + public func triggerPublisher(for route: Route, animated: Bool) -> AnyPublisher { Empty().eraseToAnyPublisher() } @discardableResult - public func trigger(_ route: Route) -> AnyPublisher { - let publisher = triggerPublisher(for: route) + public func trigger(_ route: Route, animated: Bool = true) -> AnyPublisher { + let publisher = triggerPublisher(for: route, animated: animated) let result = PassthroughSubject() publisher.subscribe(result, in: cancellables) diff --git a/Sources/Intramodular/Core/AnyViewCoordinator.swift b/Sources/Intramodular/Core/AnyViewCoordinator.swift index ef1db75..47caf10 100644 --- a/Sources/Intramodular/Core/AnyViewCoordinator.swift +++ b/Sources/Intramodular/Core/AnyViewCoordinator.swift @@ -23,8 +23,8 @@ public final class AnyViewCoordinator: _opaque_AnyViewCoordinator, ViewCo } private let transitionImpl: (Route) -> ViewTransition - private let triggerPublisherImpl: (Route) -> AnyPublisher - private let triggerImpl: @MainActor (Route) -> AnyPublisher + private let triggerPublisherImpl: (Route, Bool) -> AnyPublisher + private let triggerImpl: @MainActor (Route, Bool) -> AnyPublisher public init( _ coordinator: VC @@ -41,14 +41,14 @@ public final class AnyViewCoordinator: _opaque_AnyViewCoordinator, ViewCo } @discardableResult - public func triggerPublisher(for route: Route) -> AnyPublisher { - triggerPublisherImpl(route) + public func triggerPublisher(for route: Route, animated: Bool = true) -> AnyPublisher { + triggerPublisherImpl(route, animated) } @discardableResult @MainActor - public func trigger(_ route: Route) -> AnyPublisher { - triggerImpl(route) + public func trigger(_ route: Route, animated: Bool = true) -> AnyPublisher { + triggerImpl(route, animated) } #if os(iOS) || os(macOS) || os(tvOS) diff --git a/Sources/Intramodular/Core/ViewCoordinator.swift b/Sources/Intramodular/Core/ViewCoordinator.swift index a7af8e0..12619e3 100644 --- a/Sources/Intramodular/Core/ViewCoordinator.swift +++ b/Sources/Intramodular/Core/ViewCoordinator.swift @@ -11,11 +11,11 @@ public protocol ViewCoordinator: EnvironmentPropagator, ObservableObject { typealias Transition = ViewTransition - func triggerPublisher(for _: Route) -> AnyPublisher + func triggerPublisher(for _: Route, animated: Bool) -> AnyPublisher @discardableResult @MainActor - func trigger(_: Route) -> AnyPublisher + func trigger(_: Route, animated: Bool) -> AnyPublisher func transition(for: Route) -> Transition } diff --git a/Sources/Intramodular/Transition/ViewTransition.Payload.swift b/Sources/Intramodular/Transition/ViewTransition.Payload.swift index 522c986..88ef9cf 100644 --- a/Sources/Intramodular/Transition/ViewTransition.Payload.swift +++ b/Sources/Intramodular/Transition/ViewTransition.Payload.swift @@ -55,7 +55,7 @@ extension ViewTransition { case linear([ViewTransition]) - case custom(() -> AnyPublisher) + case custom((_ animated: Bool) -> AnyPublisher) case none } diff --git a/Sources/Intramodular/Transition/ViewTransition.swift b/Sources/Intramodular/Transition/ViewTransition.swift index d18a990..a82033a 100644 --- a/Sources/Intramodular/Transition/ViewTransition.swift +++ b/Sources/Intramodular/Transition/ViewTransition.swift @@ -257,30 +257,30 @@ extension ViewTransition { } internal static func custom( - _ body: @escaping () -> AnyPublisher + _ body: @escaping (Bool) -> AnyPublisher ) -> ViewTransition { .init(payload: .custom(body)) } @available(*, deprecated, renamed: "custom") internal static func dynamic( - _ body: @escaping () -> Void + _ body: @escaping (Bool) -> Void ) -> ViewTransition { .custom(body) } public static func custom( - @_implicitSelfCapture _ body: @escaping () -> Void + @_implicitSelfCapture _ body: @escaping (Bool) -> Void ) -> ViewTransition { // FIXME: Set a correct view transition context. struct CustomViewTransitionContext: ViewTransitionContext { } - return .custom { () -> AnyPublisher in + return .custom { (animated: Bool) -> AnyPublisher in Deferred { Future { attemptToFulfill in - body() + body(animated) attemptToFulfill(.success(CustomViewTransitionContext())) } From 8695c647481f2b98dafb350b24279a20ab0d4118 Mon Sep 17 00:00:00 2001 From: Den Andreychuk Date: Mon, 26 Jan 2026 15:43:51 +0200 Subject: [PATCH 2/4] Implement ability to control animation in linear transition --- .../UIViewController+ViewTransition.swift | 45 +++++++++---------- .../UIWindow+ViewTransition.swift | 3 +- .../Transition/ViewTransition.swift | 2 +- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift index 6d2e76a..d168d29 100644 --- a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift +++ b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift @@ -11,29 +11,28 @@ import SwiftUIX extension UIViewController { public func trigger( _ transition: ViewTransition, - animated: Bool, completion: @escaping () -> () ) throws { switch transition.finalize() { case .present(let view): do { - presentOnTop(view, named: transition.payloadViewName, animated: animated) { + presentOnTop(view, named: transition.payloadViewName, animated: transition.animated) { completion() } } case .replace(let view): do { if let viewController = topmostPresentedViewController?.presentingViewController { - viewController.dismiss(animated: animated) { + viewController.dismiss(animated: transition.animated) { viewController.presentOnTop( view, named: transition.payloadViewName, - animated: animated + animated: transition.animated ) { completion() } } } else { - presentOnTop(view, named: transition.payloadViewName, animated: animated) { + presentOnTop(view, named: transition.payloadViewName, animated: transition.animated) { completion() } } @@ -44,7 +43,7 @@ extension UIViewController { throw ViewTransition.Error.nothingToDismiss } - dismiss(animated: animated) { + dismiss(animated: transition.animated) { completion() } } @@ -62,7 +61,7 @@ extension UIViewController { navigationController.pushViewController( view._toAppKitOrUIKitViewController(), - animated: animated + animated: transition.animated ) { completion() } @@ -72,12 +71,12 @@ extension UIViewController { if let navigationController = nearestNavigationController { navigationController.pushViewController( view._toAppKitOrUIKitViewController(), - animated: animated + animated: transition.animated ) { completion() } } else { - presentOnTop(view, named: transition.payloadViewName, animated: animated) { + presentOnTop(view, named: transition.payloadViewName, animated: transition.animated) { completion() } } @@ -88,7 +87,7 @@ extension UIViewController { throw ViewTransition.Error.navigationControllerMissing } - viewController.popViewController(animated: animated) { + viewController.popViewController(animated: transition.animated) { completion() } } @@ -98,14 +97,14 @@ extension UIViewController { throw ViewTransition.Error.navigationControllerMissing } - viewController.popToRootViewController(animated: animated) { + viewController.popToRootViewController(animated: transition.animated) { completion() } } case .popOrDismiss: do { if let navigationController = nearestNavigationController, navigationController.viewControllers.count > 1 { - navigationController.popViewController(animated: animated) { + navigationController.popViewController(animated: transition.animated) { completion() } } else { @@ -113,7 +112,7 @@ extension UIViewController { throw ViewTransition.Error.nothingToDismiss } - dismiss(animated: animated) { + dismiss(animated: transition.animated) { completion() } } @@ -121,7 +120,7 @@ extension UIViewController { case .popToRootOrDismiss: do { if let navigationController = nearestNavigationController, navigationController.viewControllers.count > 1 { - navigationController.popToRootViewController(animated: animated) { + navigationController.popToRootViewController(animated: transition.animated) { completion() } } else { @@ -129,7 +128,7 @@ extension UIViewController { throw ViewTransition.Error.nothingToDismiss } - dismiss(animated: animated) { + dismiss(animated: transition.animated) { completion() } } @@ -151,7 +150,7 @@ extension UIViewController { case .set(let view, _): do { if let viewController = nearestNavigationController { - viewController.setViewControllers([view._toAppKitOrUIKitViewController()], animated: animated) + viewController.setViewControllers([view._toAppKitOrUIKitViewController()], animated: transition.animated) completion() } else if let window = self.view.window, window.rootViewController === self { @@ -163,8 +162,8 @@ extension UIViewController { completion() } else if topmostPresentedViewController != nil { - dismiss(animated: animated) { - self.presentOnTop(view, named: transition.payloadViewName, animated: animated) { + dismiss(animated: transition.animated) { + self.presentOnTop(view, named: transition.payloadViewName, animated: transition.animated) { completion() } } @@ -178,11 +177,12 @@ extension UIViewController { var _error: Error? - let firstTransition = transitions.removeFirst() + var firstTransition = transitions.removeFirst() + firstTransition.animated = transition.animated && firstTransition.animated - try trigger(firstTransition, animated: animated) { + try trigger(firstTransition) { do { - try self.trigger(.linear(transitions), animated: animated) { + try self.trigger(.linear(transitions)) { completion() } } catch { @@ -221,7 +221,6 @@ extension ViewTransition { coordinator: VC ) -> AnyPublisher { let transition = merge(coordinator: coordinator) - let animated = transition.animated if case .custom(let trigger) = transition.finalize() { return trigger(animated) @@ -229,7 +228,7 @@ extension ViewTransition { return Future { attemptToFulfill in do { - try controller.trigger(transition, animated: animated) { + try controller.trigger(transition) { attemptToFulfill(.success(transition)) } } catch { diff --git a/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift b/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift index 47b3d5e..17e2d88 100644 --- a/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift +++ b/Sources/Intermodular/Helpers/AppKit or UIKit/UIWindow+ViewTransition.swift @@ -13,7 +13,6 @@ extension ViewTransition { coordinator: Coordinator ) -> AnyPublisher { let transition = merge(coordinator: coordinator) - let animated = transition.animated if case .custom(let trigger) = transition.finalize() { return trigger(animated) @@ -54,7 +53,7 @@ extension ViewTransition { } default: do { do { - try window.rootViewController.unwrap().trigger(transition, animated: animated) { + try window.rootViewController.unwrap().trigger(transition) { attemptToFulfill(.success(self)) } } catch { diff --git a/Sources/Intramodular/Transition/ViewTransition.swift b/Sources/Intramodular/Transition/ViewTransition.swift index a82033a..a3a3cc1 100644 --- a/Sources/Intramodular/Transition/ViewTransition.swift +++ b/Sources/Intramodular/Transition/ViewTransition.swift @@ -18,7 +18,7 @@ public struct ViewTransition: ViewTransitionContext { private var payload: Payload - var animated: Bool = true + public var animated: Bool = true var payloadViewName: AnyHashable? var payloadViewType: Any.Type? var environmentInsertions: EnvironmentInsertions From 031ac71e6cf973922414215f570279beec260b5f Mon Sep 17 00:00:00 2001 From: Den Andreychuk Date: Mon, 26 Jan 2026 15:51:26 +0200 Subject: [PATCH 3/4] Implement ability to set multiple view controllers --- .../Extensions/UIKit/UINavigationController++.swift | 13 +++++++++++++ .../UIViewController+ViewTransition.swift | 13 +++++++++++++ .../Transition/ViewTransition.Payload.swift | 5 +++++ .../Intramodular/Transition/ViewTransition.swift | 4 ++++ 4 files changed, 35 insertions(+) diff --git a/Sources/Intermodular/Extensions/UIKit/UINavigationController++.swift b/Sources/Intermodular/Extensions/UIKit/UINavigationController++.swift index 34f51a3..228901d 100644 --- a/Sources/Intermodular/Extensions/UIKit/UINavigationController++.swift +++ b/Sources/Intermodular/Extensions/UIKit/UINavigationController++.swift @@ -20,6 +20,19 @@ extension UINavigationController { CATransaction.commit() } + func setViewControllers( + _ viewControllers: [UIViewController], + animated: Bool, + completion: (() -> Void)? + ) { + CATransaction.begin() + CATransaction.setCompletionBlock(completion) + + setViewControllers(viewControllers, animated: animated) + + CATransaction.commit() + } + func popViewController( animated: Bool, completion: (() -> Void)? diff --git a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift index d168d29..26cc92f 100644 --- a/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift +++ b/Sources/Intermodular/Helpers/AppKit or UIKit/UIViewController+ViewTransition.swift @@ -169,6 +169,19 @@ extension UIViewController { } } } + + case .setMany(let views): do { + guard let navigationController = nearestNavigationController else { + throw ViewTransition.Error.navigationControllerMissing + } + + navigationController.setViewControllers( + views.map { $0._toAppKitOrUIKitViewController() }, + animated: transition.animated + ) { + completion() + } + } case .linear(var transitions): do { guard !transitions.isEmpty else { diff --git a/Sources/Intramodular/Transition/ViewTransition.Payload.swift b/Sources/Intramodular/Transition/ViewTransition.Payload.swift index 88ef9cf..60ebd3b 100644 --- a/Sources/Intramodular/Transition/ViewTransition.Payload.swift +++ b/Sources/Intramodular/Transition/ViewTransition.Payload.swift @@ -51,6 +51,7 @@ extension ViewTransition { case popToRootOrDismiss case set(AnyPresentationView, transition: _WindowSetTransition?) + case setMany([AnyPresentationView]) case setRoot(AnyPresentationView) case linear([ViewTransition]) @@ -88,6 +89,8 @@ extension ViewTransition.Payload { return nil case .set(let view, _): return view + case .setMany: + return nil case .setRoot(let view): return view case .linear: @@ -125,6 +128,8 @@ extension ViewTransition.Payload { break case .set(_, let transition): self = .set(newValue, transition: transition) + case .setMany: + break case .setRoot: self = .setRoot(newValue) case .linear: diff --git a/Sources/Intramodular/Transition/ViewTransition.swift b/Sources/Intramodular/Transition/ViewTransition.swift index a3a3cc1..56fecf3 100644 --- a/Sources/Intramodular/Transition/ViewTransition.swift +++ b/Sources/Intramodular/Transition/ViewTransition.swift @@ -82,6 +82,8 @@ extension ViewTransition { return nil case .set: return nil + case .setMany: + return nil case .setRoot: return nil case .linear: @@ -121,6 +123,8 @@ extension ViewTransition: CustomStringConvertible { return "Pop to root or dismiss" case .set: return "Set" + case .setMany: + return "Set Many" case .setRoot: return "Set root" case .linear: From 30fe7adf0f1bc7eee7f00237becf3fc85e727d40 Mon Sep 17 00:00:00 2001 From: Den Andreychuk Date: Mon, 26 Jan 2026 16:03:25 +0200 Subject: [PATCH 4/4] Add `setMany` factory method for `ViewTransition` --- Sources/Intramodular/Transition/ViewTransition.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Intramodular/Transition/ViewTransition.swift b/Sources/Intramodular/Transition/ViewTransition.swift index 56fecf3..7ddfc4f 100644 --- a/Sources/Intramodular/Transition/ViewTransition.swift +++ b/Sources/Intramodular/Transition/ViewTransition.swift @@ -252,6 +252,10 @@ extension ViewTransition { .init(payload: ViewTransition.Payload.setRoot, view: view) } + public static func setMany(_ views: [AnyPresentationView]) -> Self { + .init(payload: ViewTransition.Payload.setMany(views)) + } + public static func linear(_ transitions: [ViewTransition]) -> Self { .init(payload: .linear(transitions)) }