タイトル03.004

SwiftUIとUIKitを組み合わせる

今回はSwiftUIのビューを従来のアプリの画面として利用したり、SwiftUIのビューの中でUIKitのビューを利用する方法です。
SwiftUIのボタン操作で地図の表示を切り替えるサンプルコードを詳しく確認しましょう。

毎月札幌でiOSアプリ作りをアシストするセミナーをやっています。1時間にわたるセミナーの全内容を、物理的に参加できない方のためにnote上で公開します。

お知らせ
電子書籍『Swift5初級ガイド』をAppleのブックストアから出しています。サンプルは無料です。MacでもiPadでもiPhoneでも読めます。
WWDC2020で発表されたSwift 5.3に対応した第6版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
ブックストアから一度購入すると今後のアップデートは無料で読めます。

6宣伝store

iOSアプリ作りをアシストするセミナーは今後も月一回のペースで続ける予定です。(2020年3月以降COVID-19感染拡大防止のため休止しています)
詳細は connpass.com の 札幌Swift でご確認ください。そして機会があればぜひ参加してください。
アプリ作りやプログラミング教育に関連する話題は 札幌Swift のfacebookページ で発信しています。

・画像クリックで拡大表示できます
・画像を拡大表示中は画像の左右をクリックで画像だけを順に表示できます
・ソースコード部分は左右にスクロールできます
Xcode は 11.3.1 を使っていています。
サンプルはすべてXcodeのiOS用プレイグラウンド書類用コードです。(iPadやMacのPlaygrounds 3.2でも直接入力し実行できます)
この記事の最後の有料部分にあるリンクから完全なサンプルをダウンロードできます。
サンプルはMacのPlaygroundsアプリでもiPadのPlaygroundsでも実行できます。


1 なぜ組み合わせるのか

そもそもSwiftUIと従来のフレームワークをなぜ使うのか、まず確認しましょう。

SwiftUIは様々な画面サイズに対応し、ダークモードやダイナミックタイプなどの機能に対応したコードを少ないコーディング行数で実現できます。
手軽なのに強力なのでUIKitなど従来のアプリでも使いたい場面があります。

一方SwiftUIは登場したばかりなので、現在OSが提供する機能のすべてには対応できていません。
あとで説明しますが、SwiftUIのチュートリアルでもMapKitのビューをSwiftUIの中で使っています。

この二つの理由からSwiftUIのビューをUIKitで使う、UIKitのビューをSwiftUIで使う双方向の組み合わせがそれぞれ用意されています。

1-2 SwiftUIとUIKitのビューの違い

UIKitのビューはmacOSのCocoaビューの改良版として登場しました。
Cocoaのビューはさらに遡るとNeXTSTEPが元になっています。
UIKitのビューはビューコントローラーと連携することを前提にしています。

SwiftUIのビューは外部のコントローラーは不要です。

またUIKitのUIViewはclassで実装されていますが、SwiftUIのViewはstructです。
まったく別のものなので、当然そのまま置き換えることはできません。
SwiftUIのViewとUIViewは機能はほぼ同じですが、役割も構造も違うのです。

1-3 SwiftUIは新しいフレームワーク

SwiftUIは2019年に登場した新しいフレームワークです。

開発は従来の環境で行われたはずなので、初期の段階から従来のビューの代わりにSwiftUIのビューを表示する仕組みを使っていたはずです。

その後SwiftUIを実用的にするために、UIKitなど従来のビューをSwiftUIのビューから利用する方法が用意されました。
その方法は後から作られたSwiftUIフレームワーク側にあります。


2 SwiftUIのビューを使う

SwiftUIのビューを従来の環境で使う方法はシンプルです。
UIKitではビューコントローラーとして利用する方法があります。

SwiftUIのドキュメントではFramework IntegrationにUIHostingControllerクラスがあります。

UIHostingControllerクラスはUIViewControllerを継承していて、ビューコントローラーとして利用すると、SwiftUIのビューの表示や操作が可能になるしくみです。
UIHostingControllerクラスのイニシャライザにSwiftUIのビューを渡すだけで利用可能です。

// UIHostingControllerの定義
class UIHostingController<Content> : UIViewController where Content : View

イニシャライザはいくつかありますが今回はSwiftUIのViewを渡すこちらを使います。

// UIHostingControllerのイニシャライザ
init(rootView: Content)


2-1 Playgroundsでは

XcodeのプレイグラウンドとiPadとMacのPlaygroundsアプリではライブビューに表示させるためのコードが必要です。
liveViewにはUIViewControllerのインスタンスでも代入できるため、次のように書くことができます。

// SwiftUIのSampleViewをライブビューに表示させる
PlaygroundPage.current.liveView = UIHostingController(rootView: SampleView())

Xcode11からは直接指定する方法も利用可能になっています。

// UIHostingController不要の方法
PlaygroundPage.current.setLiveView(SampleView())

こちらの方がliveViewプロパティに代入するよりも行が短くなります。

なおsetLiveView(_:)は引数が従来のPlaygroundLiveViewableプロトコルのものもあります。
(UIViewControllerまたはUIViewインスタンスを渡せます)

2-2 アプリでは

Xcode 11.3.1 でiOSのSingle View App 新規プロジェクトをUser Interface: をSwiftUIで保存すると func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {の中でwindowのrootViewControllerにUIHostingController(rootView: contentView)を代入しています。

SwiftUIで最初の画面を作成しUIViewControllerとして実行しています。

// SceneDelegate.swiftより抜粋
if let windowScene = scene as? UIWindowScene {
   let window = UIWindow(windowScene: windowScene)
   window.rootViewController = UIHostingController(rootView: contentView)
   self.window = window
   window.makeKeyAndVisible()
}


3 UIKitのビューを使う

UIKitのビューをSwiftUIで使う例はSwiftUI Tutorialsにありました。
国立公園アプリで地図を表示する部分です。

SwiftUIにはまだ地図を表示するためのビューはないため、MapKitのMKMapViewを使ったMapView型を作り使っています。

3-1 SwiftUI Tutorialsでの使用例

SwiftUI TutorialsのMapViewはシンプルです。

// SwiftUI Tutorialsより
struct MapView: UIViewRepresentable {
  var coordinate: CLLocationCoordinate2D

  func makeUIView(context: Context) -> MKMapView {
     MKMapView(frame: .zero)
  }

  func updateUIView(_ view: MKMapView, context: Context) {
     let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
     let region = MKCoordinateRegion(center: coordinate, span: span)
     view.setRegion(region, animated: true)
  }
}

UIViewRepresentableプロトコルに準拠するstruct MapViewを定義しています。
UIViewRepresentableプロトコルもSwiftUIのドキュメントFramework Integrationに載っています。

MapViewは独自のイニシャライザを持たずプロパティをひとつ持つので、Swiftが自動で用意するMemberwise initializerの MapView(coordinate:) がイニシャライザとして使えます。

Memberwise initializerはstruct型でイニシャライザをひとつも定義しない場合に利用できます。

3-2 UIViewRepresentableプロトコルを使う

UIViewRepresentableプロトコルはUIKitのUIViewをSwiftUIのビュー階層で使うためのラッパーです。

ドキュメントではわかりにくいのですが、UIViewRepresentableはViewプロトコルにも準拠しています。
UIViewRepresentableを採用するとViewプロトコルも採用したことになります。
このためstructでUIViewRepresentableを採用するとSwiftUIのビューになります

// Jump to Definition で確認した定義
protocol UIViewRepresentable : View where Self.Body == Never

アプリのカスタムインスタンスの1つでUIViewRepresentableプロトコルを採用し、そのメソッドを使用してUIViewインスタンスを作成、更新、および破棄します。

3-3 makeUIView(context:)

makeUIView(context:) はUIViewRepresentableプロトコルで必須のメソッドです。

// makeUIView(context:)の定義
func makeUIView(context: Self.Context) -> Self.UIViewType

引数のcontextはシステムの現在の状態に関する情報を含みます。
SwiftUI TutorialsのMapViewではこの引数は利用していません。

  func makeUIView(context: Context) -> MKMapView {
     MKMapView(frame: .zero)
  }

シンプルにMKMapViewのインスタンスを返しています。(return が省略されています)
frameは仮にCGRect.zeroを指定しています。

frameはSwiftUIのビューとして決められます。
SwiftUIのビューとして普通にレイアウトするだけでOKです。

3-4 updateUIView(_:context:)

updateUIView(_:context:) もUIViewRepresentableプロトコルで必須のメソッドです。

// updateUIView(_:context:)の定義
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

最初の引数はUIView型のインスタンスです。

二つ目の引数contextはシステムの現在の状態に関する情報を含みます。

2020年3月6日現在このメソッドのドキュメントに誤りがあります。
一つ目の引数の説明部分で引数が nsView となっています。(uiViewが正しい)
コピペして修正忘れと思われます。

SwiftUI TutorialsのMapViewではMapKit関連で地図に表示する中央の緯度経度情報と表示範囲を設定しています。

  func updateUIView(_ view: MKMapView, context: Context) {
     let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
     let region = MKCoordinateRegion(center: coordinate, span: span)
     view.setRegion(region, animated: true)
  }


3-5 プレイグラウンドで地図を表示するコード

SwiftUI TutorialsのMapViewをそのまま使ってプレイグラウンドで表示するサンプルコードです。
地図の中央は札幌テレビ塔の座標です。

// 03-50 SwiftUI TutorialsのMapView
import SwiftUI
import PlaygroundSupport
import MapKit

/// 札幌テレビ塔の緯度経度
let tvTowerCoordinate = CLLocationCoordinate2DMake(43.06109, 141.35643)

// SwiftUI Tutorialsより
struct MapView: UIViewRepresentable {
  var coordinate: CLLocationCoordinate2D

  func makeUIView(context: Context) -> MKMapView {
     MKMapView(frame: .zero)
  }

  func updateUIView(_ view: MKMapView, context: Context) {
     let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
     let region = MKCoordinateRegion(center: coordinate, span: span)
     view.setRegion(region, animated: true)
  }
}

// MapViewだけを表示するSwiftUIのビュー
struct Sample03View: View {

  var body: some View {
     MapView(coordinate:tvTowerCoordinate)
  }
}

// playgroundで実行する場合に必要なコード
PlaygroundPage.current.setLiveView(Sample03View())

注意:Xcodeでは最初の実行はLive Viewの表示まで時間がかかります。
また最初はLive View上にマウスポインタを移動しても矢印カーソルのままのため操作できません
一度終了し再度実行してください。
マウスポインタが「指」の場合は操作が可能です。

iPadのPlaygroundsで実行した画面です。

画像2

ここから先は

7,464字 / 3画像 / 1ファイル
この記事のみ ¥ 500

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。