UIKitでSwiftUIを使用する

ども、インディです。

今回はUIkitベースのアプリで部分的にSwiftUIを導入する方法です。

これからあらゆるアプリがUIkitベースからSwiftUIベースに置き換えられていく中で、この知識は結構重要だと思い書くことにしました。

かくいう僕も、最近仕事でUIKitからSwiftUIを部分的に記述するようになって、最近になって学ぶようになりました。

内容としては深くはないですが、ぜひ一読していただければと思います。

SwiftUIをUIViewControllerに変換する

UIkitからSwiftUIの画面を呼び出すにはどうすればいいでしょうか。

全く別物だと捉えがちですが、実はSwiftUIをUIViewControllerに変換する方法があります。

それが、UIHostingControllerクラスです。

<定義>

class UIHostingController<Content> where Content : View

それでは、サンプルを見ていきましょう。

CircleView(SwiftUIの子ビュー)

struct CircleView : View {
    @ObservedObject var model: CircleModel

    var body: some View {
        ZStack {
            Circle()
                .fill(Color.blue)
            Text(model.text)
                .foregroundColor(Color.white)
        }
    }
}

CircleModel

import Combine

class CircleModel: ObservableObject {
    @Published var text: String

    init(text: String) {
        self.text = text
    }
}

ViewController(UIkitの親ビュー)

import UIKit
import SwiftUI

class ViewController: UIViewController {
    private weak var timer: Timer?
    private var model = CircleModel(text: "")

    override func viewDidLoad() {
        super.viewDidLoad()

        addCircleView()
        startTimer()
    }

    deinit {
        timer?.invalidate()
    }
}

private extension ViewController {
    func addCircleView() {
        let circleView = CircleView(model: model)
        let controller = UIHostingController(rootView: circleView)
        addChild(controller)
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        controller.didMove(toParent: self)

        NSLayoutConstraint.activate([
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    func startTimer() {
        var index = 0
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            index += 1
            self?.model.text = "Tick \(index)"
        }
    }
}

注目して欲しいのはこちら

    func addCircleView() {
        let circleView = CircleView(model: model)
        let controller = UIHostingController(rootView: circleView)
        addChild(controller)
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        controller.didMove(toParent: self)

        NSLayoutConstraint.activate([
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

ViewControllerのviewDidLoadでこちらが呼ばれています。

まず

        let circleView = CircleView(model: model)
        let controller = UIHostingController(rootView: circleView)

でCircleViewをViewControllerに変換しています。

続いて、子ビューを上に載せる処理をしていきます。

カスタムのContainerView(子ビュー)を載せるには以下のような手順で載せていきます。

addChild(someViewController) //ViewControllerの親子関係を作る
view.addSubview(someViewController.view!) //親自身のviewに子自身のviewを載せる
someViewController.didMove(toParent: self) //自身を引数にして呼び出す

これでviewの親子関係を作り呼び出すこともできそうです。

ただ、このままだと子ビューのフレームサイズが決定づけられていません。

そのため、コード上で直接constraintを調整する必要があります。

Constraintを調整する

残りのこちらのコードの解説です。

        controller.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])

一行目のtranslatesAutoresizingMaskIntoConstraints ですが、このプロパティはAuto Layout以前に使われていた、Autosizingのレイアウトの仕組みをAuto Layoutに変換するかどうかを設定するフラグです。

こちらをオフにします。

そして、NSLayoutConstraint で親と子のviewレイアウトの調整を書いていきます。ちょっと難しいのでこちらの詳細は割愛します。

レイアウトの調整をすることで無事にSwiftUIを呼び出すことができました。

まとめ

今回はUIkitベースのアプリで部分的にSwiftUIを導入する方法を以下の手順で実装しました。

  • UIHostingControllerを使用し、SwiftUIViewをUIViewControllerに変換
  • View同士に親子関係を持たせる
  • Constraintを調整する
  • 子viewを表示する

今度は逆にSwiftUIからUIkitを使用する方法についても解説できたらと思います。

それでは!

参考

https://newbedev.com/in-swiftui-how-to-use-uihostingcontroller-inside-an-uiview-or-as-an-uiview
自前コンテナでaddChildViewController:するときdidMoveToParentViewController:はやはり使うべき - Qiita
はじめに iOS5からUIViewControllerを自前のクラスにaddChildViewController:メソッドで追加できるようになった。 最近では、こういった自前コンテナは主にFacebookやPath2.0のス...

コメント

タイトルとURLをコピーしました