UIHostingController自定义转场布局异常:SwiftUI视图未随动画缩放
问题:SwiftUI视图在UIKit转场动画中无法随视图缩放,直接显示完整尺寸
import UIKit import SwiftUI class ViewController: UIViewController { @IBOutlet weak var testButton: UIButton! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBOutlet weak var swiftuiButton: UIButton! @IBAction func didTap(_ sender: Any) { let storyboard = UIStoryboard(name: "DetailsVC", bundle: .main) let vc = storyboard.instantiateInitialViewController() as! DetailsVC vc.modalPresentationStyle = .custom vc.transitioningDelegate = self present(vc, animated: true) } @IBAction func swiftUITest(_ sender: Any) { let swiftUIView = SwiftUIView() { self.dismiss(animated: true) } let hostingController = UIHostingController.init(rootView: swiftUIView) hostingController.modalPresentationStyle = .custom hostingController.transitioningDelegate = self present(hostingController, animated: true) } } struct SwiftUIView: View { var didTapClose:()->() var body: some View { GeometryReader { geo in ZStack { Color.yellow.ignoresSafeArea() Text("SWIFTUI VIEW") .foregroundColor(.black) } .transaction { transaction in transaction.animation = nil } .frame(width: geo.size.width, height: geo.size.height) .border(.red) .onTapGesture { didTapClose() } } } } extension ViewController:UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return ToTileDetailsAnimator(fromFrame: testButton.frame, duration: 5) } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return nil } } final class ToTileDetailsAnimator:NSObject, UIViewControllerAnimatedTransitioning { fileprivate(set) var fromFrame:CGRect fileprivate(set) var duration:Double init(fromFrame:CGRect, duration:Double) { self.fromFrame = fromFrame self.duration = duration } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) else { return } let finalFrame = fromVC.view.frame.insetBy(dx: 20, dy: 20) guard let toVC = transitionContext.viewController(forKey: .to) else { return } let containerView = transitionContext.containerView containerView.addSubview(toVC.view) toVC.view.frame = fromFrame toVC.view.clipsToBounds = true toVC.view.layer.cornerRadius = 6 toVC.view.layoutIfNeeded() fromVC.beginAppearanceTransition(false, animated: true) toVC.beginAppearanceTransition(true, animated: true) toVC.view.alpha = 0 UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: { toVC.view.frame = finalFrame toVC.view.alpha = 1 toVC.view.layoutIfNeeded() }) { (completed) in toVC.view.frame = finalFrame toVC.view.layoutIfNeeded() toVC.view.alpha = 1 fromVC.endAppearanceTransition() toVC.endAppearanceTransition() let transitionSuccess = !transitionContext.transitionWasCancelled transitionContext.completeTransition(transitionSuccess) } } }
展示UIViewController时转场动画表现正常,但展示UIHostingController时,SwiftUIView并未像UIKit视图那样随动画缩放,而是直接显示为完整尺寸。转场动画效果如下:
原因及解决方案
核心原因
- SwiftUI布局系统特性:SwiftUI是声明式布局,会直接根据父容器的最终尺寸计算自身布局,不会实时响应UIKit中逐帧修改的frame动画。UIHostingController的view在动画过程中frame变化时,内部SwiftUI视图不会跟随调整,直接使用最终的容器尺寸渲染。
- GeometryReader的影响:你在SwiftUIView中使用了
GeometryReader,它会直接获取宿主view的最终可用尺寸,而非动画过程中的临时frame,导致内容一开始就铺满整个视图。
解决方案
方案1:移除GeometryReader,让SwiftUI自适应宿主frame
修改SwiftUIView,去掉GeometryReader的强制尺寸约束,让视图自适应宿主view的frame变化:
struct SwiftUIView: View { var didTapClose:()->() var body: some View { ZStack { Color.yellow.ignoresSafeArea() Text("SWIFTUI VIEW") .foregroundColor(.black) } .border(.red) .onTapGesture { didTapClose() } } }
同时在创建HostingController时,设置初始的preferredContentSize匹配按钮尺寸:
@IBAction func swiftUITest(_ sender: Any) { let swiftUIView = SwiftUIView() { self.dismiss(animated: true) } let hostingController = UIHostingController(rootView: swiftUIView) hostingController.modalPresentationStyle = .custom hostingController.transitioningDelegate = self // 初始尺寸匹配按钮frame hostingController.preferredContentSize = testButton.frame.size present(hostingController, animated: true) }
方案2:改用缩放变换实现动画
修改转场动画逻辑,通过缩放变换替代直接修改frame,让整个HostingController视图(包括内部SwiftUI内容)跟随动画缩放:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) else { return } let finalFrame = fromVC.view.frame.insetBy(dx: 20, dy: 20) guard let toVC = transitionContext.viewController(forKey: .to) else { return } let containerView = transitionContext.containerView containerView.addSubview(toVC.view) // 初始状态:设置位置和缩放为1 toVC.view.frame = fromFrame toVC.view.transform = .identity toVC.view.clipsToBounds = true toVC.view.layer.cornerRadius = 6 toVC.view.layoutIfNeeded() fromVC.beginAppearanceTransition(false, animated: true) toVC.beginAppearanceTransition(true, animated: true) toVC.view.alpha = 0 // 计算缩放比例 let scaleX = finalFrame.width / fromFrame.width let scaleY = finalFrame.height / fromFrame.height UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: { // 应用缩放变换并调整中心位置 toVC.view.transform = CGAffineTransform(scaleX: scaleX, scaleY: scaleY) toVC.view.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY) toVC.view.alpha = 1 toVC.view.layoutIfNeeded() }) { (completed) in // 动画结束后恢复变换,设置最终frame toVC.view.transform = .identity toVC.view.frame = finalFrame toVC.view.layoutIfNeeded() toVC.view.alpha = 1 fromVC.endAppearanceTransition() toVC.endAppearanceTransition() let transitionSuccess = !transitionContext.transitionWasCancelled transitionContext.completeTransition(transitionSuccess) } }
内容的提问来源于stack exchange,提问作者zumzum
相关产品推荐
相关产品推荐

