Files
2023-05-26 18:35:21 +05:30

359 lines
9.7 KiB
Swift

//
// Copyright (c) Vatsal Manot
//
import Merge
import Foundation
import Swallow
import SwiftUIX
public struct ViewTransition: ViewTransitionContext {
public enum Error: Swift.Error {
case cannotPopRoot
case isRoot
case nothingToDismiss
case navigationControllerMissing
case cannotSetRoot
}
private var payload: Payload
var animated: Bool = true
var payloadViewName: AnyHashable?
var payloadViewType: Any.Type?
var environmentInsertions: EnvironmentInsertions
init(
payload: (AnyPresentationView) -> ViewTransition.Payload,
view: AnyPresentationView
) {
self.payload = payload(view)
self.payloadViewType = type(of: view)
self.environmentInsertions = .init()
}
init<V: View>(
payload: (AnyPresentationView) -> ViewTransition.Payload,
view: V
) {
self.init(payload: payload, view: .init(view))
}
init(payload: ViewTransition.Payload) {
self.payload = payload
self.payloadViewName = nil
self.payloadViewType = nil
self.environmentInsertions = .init()
}
func finalize() -> Payload {
var result = payload
result.mutateViewInPlace {
$0 = $0.environment(environmentInsertions)
}
return result
}
}
extension ViewTransition {
public var revert: ViewTransition? {
switch payload {
case .present:
return .dismiss
case .replace:
return nil
case .dismiss:
return nil
case .dismissView:
return nil
case .push:
return .pop
case .pushOrPresent:
return .popOrDismiss
case .pop:
return nil
case .popToRoot:
return nil
case .popOrDismiss:
return nil
case .popToRootOrDismiss:
return nil
case .set:
return nil
case .setRoot:
return nil
case .linear:
return nil
case .custom:
return nil
case .none:
return ViewTransition.none
}
}
}
// MARK: - Conformances
extension ViewTransition: CustomStringConvertible {
public var description: String {
switch payload {
case .present:
return "Present"
case .replace:
return "Replace"
case .dismiss:
return "Dismiss"
case .dismissView(let name):
return "Dismiss \(name)"
case .push:
return "Push"
case .pushOrPresent:
return "Push or present"
case .pop:
return "Pop"
case .popToRoot:
return "Pop to root"
case .popOrDismiss:
return "Pop or dismiss"
case .popToRootOrDismiss:
return "Pop to root or dismiss"
case .set:
return "Set"
case .setRoot:
return "Set root"
case .linear:
return "Linear"
case .custom:
return "Custom"
case .none:
return "None"
}
}
}
// MARK: - API
extension ViewTransition {
public static func present<V: View>(_ view: V) -> Self {
.init(payload: ViewTransition.Payload.present, view: view)
}
public static func present(_ view: AnyPresentationView) -> Self {
.init(payload: ViewTransition.Payload.present, view: view)
}
public static func replace<V: View>(with view: V) -> Self {
.init(payload: ViewTransition.Payload.replace, view: view)
}
public static func replace(with view: AnyPresentationView) -> Self {
.init(payload: ViewTransition.Payload.replace, view: view)
}
public static var dismiss: Self {
.init(payload: .dismiss)
}
public static func dismissView<H: Hashable>(named name: H) -> Self {
.init(payload: .dismissView(named: .init(name)))
}
public static func push<V: View>(_ view: V) -> Self {
.init(payload: ViewTransition.Payload.push, view: view)
}
public static func push(_ view: AnyPresentationView) -> Self {
.init(payload: ViewTransition.Payload.push, view: view)
}
public static func pushOrPresent<V: View>(_ view: V) -> Self {
.init(payload: ViewTransition.Payload.pushOrPresent, view: view)
}
public static func pushOrPresent(_ view: AnyPresentationView) -> Self {
.init(payload: ViewTransition.Payload.pushOrPresent, view: view)
}
public static var pop: Self {
.init(payload: .pop)
}
public static var popToRoot: Self {
.init(payload: .popToRoot)
}
public static var popOrDismiss: Self {
.init(payload: .popOrDismiss)
}
public static var popToRootOrDismiss: Self {
.init(payload: .popToRootOrDismiss)
}
public static func set(
_ view: AnyPresentationView,
transition: _WindowSetTransition? = nil
) -> Self {
.init(payload: ViewTransition.Payload.set(view, transition: transition))
}
@_transparent
@inline(__always)
public static func set<Content: View>(
_ view: Content,
transition: _WindowSetTransition? = nil
) -> Self {
set(AnyPresentationView(view), transition: transition)
}
@_transparent
@inline(__always)
public static func set<V: View>(
transition: _WindowSetTransition? = nil,
@ViewBuilder _ view: () -> V
) -> Self {
set(view(), transition: transition)
}
@_transparent
@inline(__always)
public static func set<V: View>(
animation: AppKitOrUIKitView.AnimationOptions? = nil,
duration: Double = 0.3,
@ViewBuilder _ view: () -> V
) -> Self {
let transition = animation.map {
_WindowSetTransition._appKitOrUIKitBlockAnimation($0, duration: duration)
}
do {
try transition?._validate()
} catch {
runtimeIssue(error)
}
return set(
view(),
transition: transition
)
}
public static func setRoot<V: View>(_ view: V) -> Self {
.init(payload: ViewTransition.Payload.setRoot, view: view)
}
public static func setRoot(_ view: AnyPresentationView) -> Self {
.init(payload: ViewTransition.Payload.setRoot, view: view)
}
public static func linear(_ transitions: [ViewTransition]) -> Self {
.init(payload: .linear(transitions))
}
public static func linear(_ transitions: ViewTransition...) -> Self {
linear(transitions)
}
internal static func custom(
_ body: @escaping () -> AnyPublisher<ViewTransitionContext, Swift.Error>
) -> ViewTransition {
.init(payload: .custom(body))
}
@available(*, deprecated, renamed: "custom")
internal static func dynamic(
_ body: @escaping () -> Void
) -> ViewTransition {
.custom(body)
}
public static func custom(
@_implicitSelfCapture _ body: @escaping () -> Void
) -> ViewTransition {
// FIXME: Set a correct view transition context.
struct CustomViewTransitionContext: ViewTransitionContext {
}
return .custom { () -> AnyPublisher<ViewTransitionContext, Swift.Error> in
Deferred {
Future<ViewTransitionContext, Swift.Error> { attemptToFulfill in
body()
attemptToFulfill(.success(CustomViewTransitionContext()))
}
}
.eraseToAnyPublisher()
}
}
public static var none: Self {
.init(payload: .none)
}
}
extension ViewTransition {
public func environment(_ builder: EnvironmentInsertions) -> Self {
var result = self
result.environmentInsertions.merge(builder)
return result
}
public func merge<Coordinator: ViewCoordinator>(
coordinator: Coordinator
) -> Self {
self.environment(.object(coordinator))
.environment(.object(AnyViewCoordinator(coordinator)))
}
}
// MARK: - Helpers
extension ViewTransition.Payload {
mutating func mutateViewInPlace(_ body: (inout AnyPresentationView) -> Void) {
switch self {
case .linear(let transitions):
self = .linear(transitions.map({
var transition = $0
transition.mutateViewInPlace(body)
return transition
}))
default: do {
if var view = self.view {
body(&view)
self.view = view
}
}
}
}
}
extension ViewTransition {
mutating func mutateViewInPlace(_ body: (inout AnyPresentationView) -> Void) {
switch payload {
case .linear(let transitions):
payload = .linear(transitions.map({
var transition = $0
transition.mutateViewInPlace(body)
return transition
}))
default: do {
if var view = payload.view {
body(&view)
payload.view = view
}
}
}
}
}