WWDC Quick Look 💓 By SwiftGGTeam
Use SwiftUI with UIKit

Use SwiftUI with UIKit

元の動画を見る

ハイライト

Health アプリ チームの Sara Frederixon が、SwiftUI を既存の UIKit アプリに段階的に導入する方法を実際の経験から説明します。セッションでは、UIHostingController の更新、SwiftUI ビューと既存のデータ ソースの接続、SwiftUI を使用したコレクション ビューとテーブル ビューのセルの構築、混合時のデータ フロー管理の 4 つの側面を扱います。


主要内容

多くの iOS アプリは長年にわたって UIKit で書かれてきました。データモデル、ビューコントローラー、コレクションビュー、テーブルビューはすべて機能します。チームが SwiftUI を使用して新しいインターフェイスを作成したい場合、最大の懸念は、移行範囲が制御不能になることです。つまり、新しいセルの場合、ページ構造全体が影響を受けます。

ヘルスケアアプリもこの問題に悩まされています。多くの健康データの視覚化があり、UIKit コードはすでに既存のモデルとナビゲーションをホストしています。Sara Frederixon 氏は冒頭で、このセッションの目標は、アプリケーション構造を書き直すことなく、既存の UIKit アプリケーションに SwiftUI を組み込むことであると説明しました (00:18)。

Apple が提供するパスは 3 つのステップに分かれています。UIHostingControllerSwiftUI ビューをビュー コントローラーとして使用します。使用ObservableObjectUIKit 側の既存のデータを SwiftUI にブリッジします。 iOS 16 の新しいものを使用してくださいUIHostingConfigurationコレクション ビューとテーブル ビューのセル (00:50) に SwiftUI を直接記述します。

詳細

UIHostingController はまだ入り口です

UIHostingControllerSwiftUIを含むビュー階層です。UIViewController。モーダル、ポップオーバー、子ビュー コントローラーなど、ビュー コントローラーが必要な UIKit 内のどこにでも表示できます (01:33)。

// Presenting a UIHostingController

let heartRateView = HeartRateView() // a SwiftUI view
let hostingController = UIHostingController(rootView: heartRateView)

// Present the hosting controller modally
self.present(hostingController, animated: true)

キーポイント:

  • HeartRateView()は SwiftUI ビューであるため、UIKit は内部でどのようにレイアウトされているかを知る必要はありません。
  • UIHostingController(rootView:)SwiftUI ビューを UIKit ビュー コントローラーにラップします。
  • present(_:animated:)これは依然として標準の UIKit API であり、呼び出し方法は変更されていません。

ページをポップアップの代わりに埋め込む必要がある場合は、ホスティング コントローラーを子ビュー コントローラーに追加できます (02:31)。

// Embedding a UIHostingController

let heartRateView = HeartRateView() // a SwiftUI view
let hostingController = UIHostingController(rootView: heartRateView)

// Add the hosting controller as a child view controller
self.addChild(hostingController)
self.view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)

// Now position & size the hosting controller’s view as desired…

キーポイント:

  • addChild(_:)SwiftUI ホスティング コントローラーを UIKit のビュー コントローラー階層に接続します。
  • addSubview(_:)ホスティング ビューを現在のビューに配置することのみを担当します。
  • didMove(toParent:)子ビュー コントローラーのライフサイクル通知を完了します。
  • 最後の行は、開発者に UIKit のレイアウト メソッドを使用してこのビューを配置し、制限することを思い出させます。

iOS 16が与えるものUIHostingController追加したsizingOptions。SwiftUI コンテンツが変更されると、ホスティング コントローラーは自動的に更新されます。preferredContentSizeまたは、ビューの固有のコンテンツ サイズ。コンテンツ サイズに依存するポップオーバーなどのシナリオでは、直接的なメリットが得られます (02:43)。

// Presenting UIHostingController as a popover

let heartRateView = HeartRateView() // a SwiftUI view
let hostingController = UIHostingController(rootView: heartRateView)

// Enable automatic preferredContentSize updates on the hosting controller
hostingController.sizingOptions = .preferredContentSize

hostingController.modalPresentationStyle = .popover
self.present(hostingController, animated: true)

キーポイント:

  • sizingOptions = .preferredContentSizeホスティング コントローラーに、SwiftUI コンテンツに基づいて優先コンテンツ サイズを更新するように依頼します。
  • modalPresentationStyle = .popoverUIKit にポップオーバーとして表示させます。
  • SwiftUI コンテンツのサイズが変更された後も、ポップオーバーは引き続きコンテンツに合わせて表示されます。

外部データは最初に値によって渡され、次に ObservableObject を使用して自動的に更新されます。

UIKit アプリケーションのデータは通常、既存のモデル レイヤーによって保持されます。SwiftUI ビューは新しく追加されたインターフェイス レイヤーであるため、@Stateそして@StateObjectSwiftUI によって作成および所有されるこの種のデータは、ここには当てはまりません。Session は最初に最も直接的な方法を提供します。値を SwiftUI ビューに渡し、次に UIKit 側でホスティング コントローラーを手動で更新します。rootView05:06)。

// Passing data to SwiftUI with manual UIHostingController updates

struct HeartRateView: View {
    var beatsPerMinute: Int

    var body: some View {
        Text("\(beatsPerMinute) BPM")
    }
}

class HeartRateViewController: UIViewController {
    let hostingController: UIHostingController< HeartRateView >
    var beatsPerMinute: Int {
        didSet { update() }
    }

    func update() {
        hostingController.rootView = HeartRateView(beatsPerMinute: beatsPerMinute)
    }
}

キーポイント:

  • HeartRateView1つだけ受け取るInt、それ自体はデータ ソースを所有しません。
  • Text("\(beatsPerMinute) BPM")渡された値を使用してインターフェイスを生成します。
  • beatsPerMinuteUIKitビューコントローラーに保存します。
  • didSet { update() }UIKit 側のデータが変更された後、更新をアクティブにトリガーする必要があることを示します。
  • hostingController.rootView = ...SwiftUI ルート ビューを再作成し、最新の値をそこに送信します。

この方法は簡単ですが、データが変更されるたびに手動で更新する必要があります。その後、セッションは既存のモデルを次のように変更することを推奨します。ObservableObject、変更される属性を次のようにマークします。@Published、そして SwiftUI ビューに使用させます@ObservedObjectそれをお読みください (06:49)。

// Passing an ObservableObject to automatically update SwiftUI views

class HeartData: ObservableObject {
    @Published var beatsPerMinute: Int

    init(beatsPerMinute: Int) {
       self.beatsPerMinute = beatsPerMinute
    }
}

struct HeartRateView: View {
    @ObservedObject var data: HeartData

    var body: some View {
        Text("\(data.beatsPerMinute) BPM")
    }
}

キーポイント:

  • HeartData: ObservableObjectこの外部モデルを SwiftUI で監視できるようにします。
  • @Published var beatsPerMinuteリフレッシュ信号をトリガーする属性です。
  • @ObservedObject var data外部モデルを SwiftUI ビューに接続します。
  • Text("\(data.beatsPerMinute) BPM")モデルのプロパティを直接読み取ります。
  • 属性が変更された後、SwiftUI は関連するビューを自動的に更新するため、UIKit はそれを再度変更する必要はありません。rootView

UIKit ビュー コントローラーを初期化するときは、同じモデルを保存して SwiftUI ビューに渡すだけです (08:30)。

// Passing an ObservableObject to automatically update SwiftUI views

class HeartRateViewController: UIViewController {
    let data: HeartData
    let hostingController: UIHostingController<HeartRateView>

    init(data: HeartData) {
        self.data = data
        let heartRateView = HeartRateView(data: data)
        self.hostingController = UIHostingController(rootView: heartRateView)
    }
}

キーポイント:

  • dataUIKit側が保持しているものと同じですHeartData例。
  • HeartRateView(data: data)モデル参照をSwiftUIに与えます。
  • UIHostingController(rootView:)初期化中にのみ SwiftUI ビューをラップします。
  • 後続の心拍数サンプルが 78 から 94 に変化すると、@PublishedSwiftUI 更新インターフェイス (09:14) を駆動します。

UIHostingConfiguration SwiftUI をセルに配置します

iOS 16の新機能UIHostingConfiguration、セルの内容の構成です。開発者はそれを次のように割り当てます。UICollectionViewCellまたはUITableViewCellcontentConfiguration, セルに直接 SwiftUI を記述することができます。ここに追加のビューやビュー コントローラーを埋め込む必要はありません (09:52)。

cell.contentConfiguration = UIHostingConfiguration {
  // Start writing SwiftUI here!
}

キーポイント:

  • contentConfigurationこれは、UIKit セルの最新の構成エントリ ポイントです。
  • UIHostingConfiguration { ... }SwiftUIを受信するViewBuilder
  • SwiftUI ビューを中括弧内に直接記述します。

このセッションでは心拍数セルを使用して、移行の利点を示します。元の複雑なカスタム セル レイアウトは、いくつかの SwiftUI の小さなビューに分割できます (11:02)。

// Building a custom cell using SwiftUI with UIHostingConfiguration

cell.contentConfiguration = UIHostingConfiguration {
    HeartRateTitleView()
}

struct HeartRateTitleView: View {
    var body: some View {
        HStack {
            Label("Heart Rate", systemImage: "heart.fill")
                .foregroundStyle(.pink)
                .font(.system(.subheadline, weight: .bold))
            Spacer()
            Text(Date(), style: .time)
                .foregroundStyle(.secondary)
                .font(.footnote)
        }
    }
}

キーポイント:

  • HeartRateTitleView()入れられるUIHostingConfiguration, セルの内容になります。
  • Label("Heart Rate", systemImage:)タイトルとシステムアイコンを表示します。
  • .foregroundStyle(.pink)そして.font(...)標準の SwiftUI モディファイアを使用してスタイルを調整します。
  • Spacer()左にタイトルを押し、右に時間を押します。
  • Text(Date(), style: .time)SwiftUIの日付テキストを使用して現在時刻を表示します。

拡張し続ける場合は構成で組み合わせるだけVStackHStackおよびその他の SwiftUI ビュー。セッションには Swift チャートも配置されますChartセルに入れて使用しますLineMark心拍数曲線を表示します (13:25)。

// Building a custom cell using SwiftUI with UIHostingConfiguration

cell.contentConfiguration = UIHostingConfiguration {
    VStack(alignment: .leading) {
        HeartRateTitleView()
        Spacer()
        HStack(alignment: .bottom) {
            HeartRateBPMView()
            Spacer()
            Chart(heartRateSamples) { sample in
                LineMark(x: .value("Time", sample.time),
                         y: .value("BPM", sample.beatsPerMinute))
                   .symbol(Circle().strokeBorder(lineWidth: 2))
                   .foregroundStyle(.pink)
            }
        }
    }
}

キーポイント:

  • VStack(alignment: .leading)セルの垂直方向の内容を整理します。
  • HeartRateTitleView()以前に抽出したタイトル ビューを再利用します。
  • HeartRateBPMView()現在の心拍数の数値を表示します。
  • Chart(heartRateSamples)サンプルコレクションを使用してチャートを推進します。
  • LineMark各サンプルを時間と BPM の 2 つの軸にマッピングします。
  • .symbol(...)そして.foregroundStyle(.pink)ポリラインの点と色を調整します。

セルの余白、背景、スライド操作および選択状態

UIHostingConfigurationいくつかのタイプのセル シナリオで共通の機能をサポートします。デフォルトでは、ルート SwiftUI コンテンツは UIKit セルのレイアウト余白に応じて縮小されます。マージンを変更する必要がある場合は、設定後に追加できます。.margins14:37)。

cell.contentConfiguration = UIHostingConfiguration {
    HeartRateBPMView()
}
.margins(.horizontal, 16)

キーポイント:

  • HeartRateBPMView()セルの SwiftUI コンテンツです。
  • .margins(.horizontal, 16)デフォルトの水平マージンをオーバーライドします。
  • このモディファイアのオブジェクトはUIHostingConfiguration、個別の UIKit ビューではなく。

背景は、SwiftUI モディファイアを使用して直接設定することもできます。セッションは、背景がセルの背面でホストされ、背景がセルの端まで広がることを示しています。自動サイズ設定セルのサイズはコンテンツによってのみ決定され、背景はサイズの決定には関与しません (15:16)。

cell.contentConfiguration = UIHostingConfiguration {
   HeartTitleView()
}
.background(.pink)

キーポイント:

  • HeartTitleView()前景コンテンツです。
  • .background(.pink)構成の背景を設定します。
  • 背景は、セル コンテンツ ビューの SwiftUI コンテンツの背後にあります。

リスト内のスワイプ アクションは、SwiftUI ビューに直接記述することもできます。Apple は、ボタンがアクションを実行するときにアイテムの安定した識別子を使用し、インデックス パスを使用しないように特に注意を促しています。セルが表示されている間、インデックス パスも変更される可能性があります (16:32)。

cell.contentConfiguration = UIHostingConfiguration {
    MedicalConditionView()
        .swipeActions(edge: .trailing) {  }
}

キーポイント:

  • MedicalConditionView()病状セルの SwiftUI コンテンツです。
  • .swipeActions(edge: .trailing)リストの右にスワイプしたときにアクションを表示します。
  • { … }中央のボタンとビジネス アクション。
  • 他の行の誤操作を避けるために、ビジネス アクションは安定した項目識別子にバインドされる必要があります。

SwiftUI コンテンツが UIKit セルの選択または強調表示された状態に応答したい場合は、次のようにする必要があります。configurationUpdateHandler構成を再作成し、UIKit によって提供される構成を読み取ります。state17:25)。

// Incorporating UIKit cell states

cell.configurationUpdateHandler = { cell, state in
    cell.contentConfiguration = UIHostingConfiguration {
      HStack {
        HealthCategoryView()
            Spacer()
            if state.isSelected {
                Image(systemName: "checkmark")
            }
        }
    }
}

キーポイント:

  • configurationUpdateHandlerセルの状態が変化すると再度実行されます。
  • state選択や強調表示などの UIKit セルの状態が含まれます。
  • UIHostingConfigurationSwiftUI コンテンツが最新の状態を使用するように、ハンドラー内で再構築します。
  • if state.isSelected選択した場合にのみチェックマークを表示します。

diffable データ ソースはコレクションのみを変更します。ObservableObject はセル内の更新を担当します。

コレクション ビューまたはテーブル ビューのセルが SwiftUI で構築される場合、データ フローは 2 つのカテゴリで処理されます。最初のカテゴリは、挿入、削除、再配置などのコレクションの変更です。引き続き diffable データ ソース スナップショット処理を使用します。各スナップショットは次の場所に配置する必要があります。MedicalConditionこれにより、diffable データ ソースが ID を正確に計算できるようになります (18:41)。

2 番目のカテゴリは、単一モデルの属性変更です。従来の UIKit メソッドでは通常、スナップショット内の項目を再構成または再ロードする必要があります。SwiftUIセルを使用する場合、次のようにモデルを作成できます。ObservableObject、SwiftUIビューで使用させます@ObservedObject観察してください。@Published属性が変更された後、セル内の SwiftUI ビューは、最初に diffable データ ソースまたはコレクション ビューを経由せずに直接更新されます (20:35)。

iOS 16 ではサイズの問題も修正されています。UIHostingConfigurationUIKit の新しい自動サイズ変更セルを利用します。SwiftUI セルの内容が変更されると、それを含むセルが変更されます。UICollectionViewCellまたはUITableViewCellサイズは必要に応じて自動的に調整されます。この機能はデフォルトで有効になっています (21:44)。

最後に、SwiftUI はデータをモデルに書き戻すこともできます。セッション用MedicalCondition例: モデルはObservableObjecttextはい@Published。SwiftUI ビューは、この公開された属性の双方向バインディングを作成でき、ユーザーは編集後にそれをモデルに直接書き戻すことができます (22:48)。

// Creating a two-way binding to data in SwiftUI

class MedicalCondition: Identifiable, ObservableObject {
    let id: UUID

    @Published var text: String
}

struct MedicalConditionView: View {
    @ObservedObject var condition: MedicalCondition

    var body: some View {
        HStack {

            Spacer()
        }
    }
}

キーポイント:

  • MedicalCondition: Identifiablediff 可能なデータ ソースが使用できる安定した ID を提供します。
  • ObservableObjectこのモデルを SwiftUI で監視できるようにします。
  • @Published var textSwiftUI で読み取ってインターフェースを更新できるプロパティです。
  • @ObservedObject var conditionセルに対応するモデルをSwiftUIビューに接続します。
  • 逐語的な説明、読み取り専用にするTextに変更しますTextField、そして与えるcondition.text追加$プレフィックスを追加すると、モデルに書き戻すバインディングを作成できます (23:47)。

2 つのベアラー モード間の境界

セッションの終わりには、簡単にステップできる境界線が提供されます。UIHostingControllerビューコントローラーです。これを使用する場合、ビュー コントローラーとそのビューを UIKit 階層に追加する必要があります。ツールバー、キーボード ショートカット、使用法UIViewControllerRepresentableSwiftUI ビューはすべて、UIKit ビュー コントローラー階層 (24:33) に依存しています。

UIHostingConfigurationセルに使用されますが、作成されませんUIViewController。ほとんどの SwiftUI 機能をサポートしていますが、次のものに依存します。UIViewControllerRepresentableSwiftUI ビューはセルに配置できません (25:01)。

重要ポイント

  • 何をすべきか: 複雑な UIKit カスタム セルを次のように変更します。UIHostingConfiguration実行する価値がある理由: セルの内部レイアウトを SwiftUI の小さなビューに分割できるため、スタイル、グラフ、ステータス表示を簡単に組み合わせることができます。 開始方法: 既存のコレクション ビューまたはテーブル ビューを保持し、セルの置換のみを行います。contentConfiguration

  • 何をすべきか: 既存の UIKit 詳細ページの部分カードを SwiftUI に変更します。 実行する価値がある理由:UIHostingControllerページ全体を置き換える必要がなく、子ビュー コントローラーとして埋め込むことができます。 開始方法: 次を使用して SwiftUI カード ビューを作成します。UIHostingController(rootView:)運ぶ、渡すaddChild既存のビューコントローラーに接続します。

  • 内容: UIKit モデルへの変更により、SwiftUI の部分的なインターフェイスの更新が自動的に行われるようにします。 実行する価値がある理由: 手動変更rootView見逃しやすい、ObservableObjectそして@Published更新をデータ変更にバインドする機能。 開始方法: 既存のモデルをObservableObject、SwiftUI ビューで使用されます@ObservedObject保存してください。

  • 何をすべきか: SwiftUI のスワイプ アクションをリスト セルに追加します。 実行する価値がある理由: リストのインタラクションは引き続き UIKit によって管理され、インライン アクションは SwiftUI 修飾子を使用してコンテンツの近くに記述できます。 開始方法:UIHostingConfigurationルートの SwiftUI ビューに追加する.swipeActions、アクション内のアイテムの安定した ID を使用します。

  • 何をすべきか: 編集可能なセルの双方向データ フローを確立します。 実行する価値がある理由: SwiftUI のユーザーTextField入力後、引き続きUIKit側で使用するモデルオブジェクトを直接書き戻すことができます。 開始方法: モデルの編集可能なフィールドに使用させます@Published、SwiftUIで渡されます$condition.text入力コントロールに渡されます。


関連セッション

コメント

GitHub Issues · utterances