【SwiftUI】UIHostingControllerのdismissのアニメーションを無効化にする

2022.10.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

UIHositingControllerでラップしたSwiftUIのViewを画面破棄する時のアニメーションを無効化にする方法を調べたので記載します。

環境

  • Xcode 14

はじめに

今回はUIViewControllerViewControllerからSwiftUIのViewSwiftUIViewUIHostingControllerでラップしたものを表示して試しました。

最初の画面(ViewController)

Storyboard

コード

import UIKit
import SwiftUI

class ViewController: UIViewController { 

    // ボタンを押すと、UIHostingControllerでラップしたSwiftUIViewに遷移します
    @IBAction private func presentNextView() {
        let swiftUIViewController = UIHostingController(rootView: SwiftUIView())
        swiftUIViewController.modalPresentationStyle = .overFullScreen

        present(swiftUIViewController, animated: false)
    }
}

次の画面(SwiftUIView)

プレビュー

コード

import SwiftUI

struct SwiftUIView: View {

    @Environment(\.dismiss) var dismiss

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.black)
                .ignoresSafeArea()

            Button {
                var transaction = Transaction()
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    dismiss()
                }
            } label: {
                Text("dismiss")
            }
        }
    }
}

トランザクションを使用して画面破棄のアニメーションを無効化にする(失敗)

SwiftUIのViewから.sheet等で表示させたViewを画面破棄する際にアニメーションを無効化させるには、withTransactionメソッドにTransactionを使用してアニメーションを無効化にしたカスタムアニメーションを渡すことで画面破棄のアニメーションを無効化に出きます。

var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
    dismiss()
}

しかし、この方法だとUIHositingControllerでラップしたSwiftUIのViewを画面破棄する時のアニメーションを無効化に出来ませんでした。

デモ

しっかり画面破棄のアニメーションが表示されています。

推測

前回の背景色を透明にする時でもそうだったのですが、UIHositingControllerを使用する場合は、そのラップをしているUIHositingController側のデフォルト値が変更出来ておらず、変更が反映できていないのではないかと推測しました。

【SwiftUI】sheetの背景を透過させる方法

解決策

親側のViewControllerで画面破棄の処理を行う

SwiftUIView

画面破棄を行いたい箇所をdismiss()から、dismissHandler()に変更しています。

struct SwiftUIView: View {

    let dismissHandler: () -> Void

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.black)
                .ignoresSafeArea()

            Button {
                dismissHandler()
            } label: {
                Text("dismiss")
            }
        }
    }
}

ViewController

UIHostingControllerのrootViewとしてSwiftUIViewを渡す際にdismissHandler内でアニメーションを実行しないdismissを実行するようにしました。

import UIKit
import SwiftUI

class ViewController: UIViewController {  

    @IBAction private func presentNextView() {
        let swiftUIViewController = UIHostingController(rootView: SwiftUIView(dismissHandler: {
            self.dismiss(animated: false)
        }))
        swiftUIViewController.modalPresentationStyle = .overFullScreen

        present(swiftUIViewController, animated: false)
    }
}

デモ

無事にアニメーションを無効化に出来ました。

UIViewControllerTransitioningDelegateでアニメーションをカスタマイズする

上記の方法でdismissのアニメーションの無効化に成功したのですが、別の方法も試しました。

UIViewControllerTransitioningDelegateに準拠させることで遷移アニメーションをカスタマイズ出来ます。また、UIHostingControllerUIViewControllerを継承しているので、UIViewControllerTransitioningDelegateに準拠させることが出来ます。

今回はSwiftUIViewをラップしたSwiftUIViewControllerUIViewControllerTransitioningDelegateを準拠させました。

SwiftUIViewController

import UIKit
import SwiftUI

class SwiftUIViewController: UIHostingController<SwiftUIView> {

    init() {
        super.init(rootView: SwiftUIView())
        self.transitioningDelegate = self
    }

    @MainActor required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - UIViewControllerTransitionDeleate
extension SwiftUIViewController: UIViewControllerTransitioningDelegate {

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AnimatedTransition()
    }
}

class AnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        transitionContext.completeTransition(true)
    }
}

まず、init()実行時にtransitioningDelegateselfを渡します。

init() {
    super.init(rootView: SwiftUIView())
    self.transitioningDelegate = self
}

UIViewControllerTransitionDeleateanimationController(forDismissed:)メソッドを呼び出して、画面破棄時のUIViewControllerAnimatedTransitioningに準拠したアニメーターオブジェクトを返すように要求します。

// MARK: - UIViewControllerTransitionDeleate
extension SwiftUIViewController: UIViewControllerTransitioningDelegate {

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AnimatedTransition()
    }
}

アニメータオブジェクトの、transitionDuration(using:)で画面遷移の時間間隔を設定します。今回はアニメーションを無しにしたい為、0にしています。

animateTransition(using:)では、実行したカスタムアニメーションを記述するのですが、今回は特にアニメーションは必要ない為、completeTransition(true)でアニメーションが終了したことのみを伝えるようにしています。

class AnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        transitionContext.completeTransition(true)
    }
}

デモ

この方法でも無事に画面破棄時のアニメーションを無効化にすることが出来ました!

おわりに

SwiftUIのViewUIViewControllerと合わせて使用することもあると思いますが、UIHostingControllerでラップしたViewが意図した動きをしない時はUIHostingController側の設定が変わっていない可能性もある為、UIHostingControllerの設定変更を試してみるのも良いかもしれませんね。

他に何か良い方法がありましたら教えていただければと思います。

参考