I'm currently refactoring my iPad app from UIKit to SwiftUI and need help replicating a specific view presentation behavior.
Current Implementation:
I have a SwiftUI view (AccountDetailsView
) that is presented from a UIKit view controller (HomeViewController
) using UIHostingController. The AccountDetailsView
takes up 2/3 of the view controller's width from the right side, and the remaining 1/3 of the screen is transparent. When the transparent area is tapped, the view dismisses. This is achieved by setting the transitioningDelegate
and using a custom transition.
Refactoring:
I'm now refactoring HomeViewController
to HomeView
using SwiftUI and want to replicate the same behavior that AccountDetailsView
should act like custom modal view.
let controller = UIHostingController(rootView: AccountDetailsView())
let size = CGSize(width: (view.bounds.width * 2 / 3).rounded(), height: UIScreen.main.bounds.height)
controller.preferredContentSize = size
controller.view.backgroundColor = .clear
controller.transitioningDelegate = SlideTransitioningController(direction: .right)
controller.modalPresentationStyle = .custom
present(controller, animated: true)
Here's a SlideTransitioningController
implementation if it helps:
import UIKit
public final class SlideTransitioningController: NSObject, UIViewControllerTransitioningDelegate {
var direction: PresentationDirection
var backgroundType: BackgroundType
var dismissOnTapOutside: Bool
public init(
direction: PresentationDirection = .left,
background: BackgroundType = .blacked,
dismissOnTapOutside: Bool = true
) {
self.direction = direction
self.backgroundType = background
self.dismissOnTapOutside = dismissOnTapOutside
}
// MARK: - UIViewControllerTransitioningDelegate
public func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
SlideInPresentationController(
presentedViewController: presented,
presenting: presenting,
direction: direction,
backgroundType: backgroundType,
dismissOnTapOutside: dismissOnTapOutside
)
}
public func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
SlideInPresentationAnimator(direction: direction, isPresentation: true)
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
SlideInPresentationAnimator(direction: direction, isPresentation: false)
}
}
// MARK: - Types
public extension SlideTransitioningController {
enum PresentationDirection: Equatable {
case left, top, right, bottom, center
}
enum BackgroundType {
case blurred, blacked
}
}
One approach is to embed your SwiftUI view within a UIKit UIHostingController
, where you can add UIKit-specific features. After that, you can convert the UIHostingController
back to a SwiftUI view using UIViewControllerRepresentable
.
Here’s an implementation example for reference:
class SlideTransitionWrapper<Content: View>: UIViewController, UIPopoverPresentationControllerDelegate {
private let content: () -> Content
var onDismiss: (() -> Void)?
var hostVC: UIHostingController<Content>?
private lazy var slideTransitioningController = SlideTransitioningController(direction: .right, onDismiss: onDismiss)
required init?(coder: NSCoder) { fatalError("") }
init(content: @escaping () -> Content) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
func show() {
guard hostVC == nil else { return }
let controller = UIHostingController(rootView: content())
let size = CGSize(width: (UIScreen.main.bounds.width * 2 / 3).rounded(),
height: UIScreen.main.bounds.height)
controller.preferredContentSize = size
controller.view.backgroundColor = .clear
controller.transitioningDelegate = slideTransitioningController
controller.modalPresentationStyle = .custom
hostVC = controller
controller.presentationController?.delegate = self
present(controller, animated: true)
}
func hide() {
guard let vc = hostVC, !vc.isBeingDismissed else { return }
dismiss(animated: true, completion: nil)
hostVC = nil
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
hostVC = nil
onDismiss?()
}
}
struct SlideTransitionRepresentable<Content: View>: UIViewControllerRepresentable {
@Binding var show: Bool
let content: () -> Content
func makeUIViewController(context: UIViewControllerRepresentableContext<SlideTransitionRepresentable<Content>>)
-> SlideTransitionWrapper<Content> {
let vc = SlideTransitionWrapper(content: content)
vc.onDismiss = {
vc.hostVC = nil
show = false
}
return vc
}
func updateUIViewController(
_ uiViewController: SlideTransitionWrapper<Content>,
context: UIViewControllerRepresentableContext<SlideTransitionRepresentable<Content>>
) {
show ? uiViewController.show() : uiViewController.hide()
}
}
extension View {
public func slideTransition<Content: View>(
isPresented: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) -> some View {
background(SlideTransitionRepresentable(show: isPresented, content: content))
}
}
And usage of it be will be like below:
.slideTransition(isPresented: $showSlideTransitioningView) {
YourView()
}