Slide 1

Slide 1 text

UIKit ベースの Custom UIContentCon fi guration API を ⽤いた複雑なカスタムセルの作り⽅ Go Takagi 20 23 / 09 / 01 iOSDC Japan 2 023 day 0 #Track B ( 1 9 : 20 - )

Slide 2

Slide 2 text

Keynote ‣ UIKit に⼊ってきた Con fi guration という概念を学ぶ • UI 更新を安全に⾏う仕組み ‣ UICollectionViewでUIContentCon fi gurationを使いこなそう! • Custom 実装例が少ない • 仕組みを理解してどうやって作ればいいかを知る ‣ ちなみに UIHostingCon fi guration は? • 特徴を整理して使い分けを考える 2

Slide 3

Slide 3 text

Me ( Go Takagi ) ‣ID • shimastripe / shimastriper • bento.me/shimastripe ‣Work at • 株式会社 ⽇本経済新聞社 ‣ iOS エンジニア ‣ iOSDC NOC チーム ‣Like • Swift / ⾃動化 / 柴⽝ 3 電⼦版広報⽝デンシバ 去年は PencilKit の話!

Slide 4

Slide 4 text

Con fi guration ?

Slide 5

Slide 5 text

UI における Con fi guration ‣ データ構成をベースに、view のコンテンツとスタイルを更新する • 構成(Data)に基づいて UI を決めておく宣⾔的なAPI • レンダリングに影響するコードを直接呼ばない (管理しない) • 基本 Struct ベース ‣ 新しい Con fi guration を適⽤すると状態がリセットされて更新 • view を使いまわしても過去の view の状態に引っ張られない • リセット忘れといったバグを⽣まずに安全に再利⽤できる 5 https://developer.apple.com/documentation/uikit/appearance_customization/con fi gurations

Slide 6

Slide 6 text

例: UIButton.Con fi guration (iOS 15 +) ‣ UIButton を⾃由にスタイリング可能に • コンテンツや外観を⾃由にいじれる • 上書きしたら新しい Con fi guration で UI 描画される ‣ 他にもバリエーションは増えている • UIPasteControl.Con fi guration (iOS 16 +) • UIContentUnavailableCon fi guration (iOS 17 +) • TVMediaItemContentCon fi guration (tvOS 1 5 +) 6 https://developer.apple.com/wwdc 23 / 10 05 5

Slide 7

Slide 7 text

Con fi guration が反映、前の状態は引き継がない 7 var config = UIButton.Configuration.bordered() config.title = "iOSDC 2023" config.baseForegroundColor = .systemRed button.configuration = config

Slide 8

Slide 8 text

Con fi guration が反映、前の状態は引き継がない 8 var config = UIButton.Configuration.bordered() config.title = "iOSDC 2023" config.baseForegroundColor = .systemRed button.configuration = config var newConfig = UIButton.Configuration.filled() newConfig.title = "iOSDC 2023" button.configuration = newConfig 新たな Con fi g を Set

Slide 9

Slide 9 text

ContentCon fi guration

Slide 10

Slide 10 text

iOS 1 4 : UICollectionView List ‣ UITableView ライクで Section 表⽰できるように 1 0 https://developer.apple.com/videos/play/wwdc 20 20 / 10 097 / 横スクロール insetGrouped List Sidebar List (折りたたみ)

Slide 11

Slide 11 text

Cell lifecycle : 作った View を再利⽤する 1 1 https://developer.apple.com/wwdc 21 / 10 25 2 Preparation Display Con fi gure Cell Display Cell Dequeue Cell Pool から Cell を取り出す 取り出した Cell を加⼯する 表⽰、画⾯外に出たら Pool に戻す → Enqueue Cell

Slide 12

Slide 12 text

ReusableView: View を再利⽤する lifecycle 1 2 Reuse Pool 表⽰領域

Slide 13

Slide 13 text

1 3 ReusableView: View を再利⽤する lifecycle 表⽰領域 Reuse Pool

Slide 14

Slide 14 text

1 4 ReusableView: View を再利⽤する lifecycle 表⽰領域 Reuse Pool

Slide 15

Slide 15 text

1 5 ReusableView: View を再利⽤する lifecycle 表⽰領域 Reuse Pool

Slide 16

Slide 16 text

前の状態を把握しきれていないと 1 6 • 知らないタイミングで別の場所で画像は追加されていた • そのままリセットをしそびれる...... 😔 直前の画像が残っている Reuse Pool

Slide 17

Slide 17 text

Cell へのデータ反映も Con fi guration だと嬉しい 1 7 別の Con fi guration を Set ! 😄 安全に更新 Reuse Pool

Slide 18

Slide 18 text

ListContentCon fi guration ‣ List Cell コンテンツ (ContentView) が作れる • CollectionViewCell に contentCon fi guration をセット • Text / SecondaryText / Image などのデータを設定 1 8 https://developer.apple.com/videos/play/wwdc 20 20 / 10 097 / var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.image = UIImage(systemName: "hammer") contentConfiguration.text = "Ready. Set. Code." cell.contentConfiguration = contentConfiguration

Slide 19

Slide 19 text

ContentCon fi guration ‣ 今までの Cell • サブクラスを作って、ContentView に addSubView() して中⾝を実装 ‣ ContentCon fi guration • CollectionViewCell (⼊れ物) + ContentCon fi guration (中⾝) に分離 • 別種類のセルで ContentCon fi guration を共有可能、Composable な構成に • UICollectionViewCell や UITableViewCell で共有したり 1 9

Slide 20

Slide 20 text

Reminder の CollectionViewListCell の活⽤ 2 0

Slide 21

Slide 21 text

Reminder の CollectionViewListCell の活⽤ 2 1 同じ ContentCon fi guration を適⽤ Root Cell は disclosure ∨ を付加 ほとんど同じ HeaderView‧BodyCell を⽤意しなくて済む 😄

Slide 22

Slide 22 text

実際はカスタムCell が必要なことが多い ‣ 標準 Cell 以上のことをやりたいとき 2 2 セル内にボタン 動画表⽰モードと切り替え ⼤きいサムネイルの縦⻑セル

Slide 23

Slide 23 text

UIContentView: カスタマイズする View ‣ Con fi guration をセットできる view (UIView) を作る • ここに Label や Button などコンテンツとなる View を実装する 2 3 @MainActor public protocol UIContentView : NSObjectProtocol { @MainActor var configuration: UIContentConfiguration { get set } }

Slide 24

Slide 24 text

UIContentCon fi guration: データ構成 ‣ Con fi guration (データ構成) • ContentView を⽣成することができる 2 4 public protocol UIContentConfiguration { @MainActor func makeContentView() -> UIView & UIContentView }

Slide 25

Slide 25 text

2 通りの動きをサポートする ‣ Reuse されたことがある Cell に Con fi guration をセットした場合 • ContentView に Con fi guration をセットして Update する ‣ Reuse されたことがない Cell に Con fi guration をセットした場合 • Con fi guration から ContentView を⽣成する 2 5

Slide 26

Slide 26 text

それぞれの役⽬を整理する 2 6 Reuse Pool CollectionViewCell CollectionViewCell Empty(ContentView) 表⽰領域

Slide 27

Slide 27 text

Reuse されたことがある Cell の場合 2 7 CollectionViewCell CollectionViewCell Empty(ContentView) 表⽰領域 Update Set Con fi guration Display Reuse Pool

Slide 28

Slide 28 text

Reuse されたことがない Cell の場合 2 8 CollectionViewCell CollectionViewCell Empty(ContentView) 表⽰領域 Display Set Con fi guration Make View Reuse Pool

Slide 29

Slide 29 text

Point: Reuse Cell は ContentView まで Cache ‣ Reuse されたことがある Cell に Con fi guration をセットした場合 • ContentView に Con fi guration をセットして Update する ‣ Reuse されたことがない Cell に Con fi guration をセットした場合 • Con fi guration から ContentView を⽣成する 2 9

Slide 30

Slide 30 text

final class CustomContentView: UIView, UIContentView { init(configuration: CustomConfiguration) { super.init(frame: .zero) setupInternalViews() apply(configuration: configuration) } private func setupInternalViews() { // View ͷॳظԽॲཧΛߦ͏ɺ addSubView() ΍ AutoLayout } private var appliedConfiguration: CustomConfiguration! var configuration: UIContentConfiguration { get { appliedConfiguration } set { apply(configuration: newValue as! CustomConfiguration) } } private func apply(configuration: CustomConfiguration) { guard appliedConfiguration != configuration else { return } appliedConfiguration = configuration // ඞཁʹԠͯ͡஋ͷ Reset Λߦ͏ titleLabel.text = configuration.title }

Slide 31

Slide 31 text

final class CustomContentView: UIView, UIContentView { init(configuration: CustomConfiguration) { super.init(frame: .zero) setupInternalViews() apply(configuration: configuration) } private func setupInternalViews() { // View ͷॳظԽॲཧΛߦ͏ɺ addSubView() ΍ AutoLayout } private var appliedConfiguration: CustomConfiguration! var configuration: UIContentConfiguration { get { appliedConfiguration } set { apply(configuration: newValue as! CustomConfiguration) } } private func apply(configuration: CustomConfiguration) { guard appliedConfiguration != configuration else { return } appliedConfiguration = configuration // ඞཁʹԠͯ͡஋ͷ Reset Λߦ͏ titleLabel.text = configuration.title } ContentView ⽣成時に呼ばれる 
 内部で使う SubView は init()で追加する 1 . Make

Slide 32

Slide 32 text

final class CustomContentView: UIView, UIContentView { init(configuration: CustomConfiguration) { super.init(frame: .zero) setupInternalViews() apply(configuration: configuration) } private func setupInternalViews() { // View ͷॳظԽॲཧΛߦ͏ɺ addSubView() ΍ AutoLayout } private var appliedConfiguration: CustomConfiguration! var configuration: UIContentConfiguration { get { appliedConfiguration } set { apply(configuration: newValue as! CustomConfiguration) } } private func apply(configuration: CustomConfiguration) { guard appliedConfiguration != configuration else { return } appliedConfiguration = configuration // ඞཁʹԠͯ͡஋ͷ Reset Λߦ͏ titleLabel.text = configuration.title } Con fi guration がセットされる度に呼ばれる 
 View へ値のリセットと反映‧Hiddenの切り替え‧タップイベントの登録 2 . Update

Slide 33

Slide 33 text

Cell × ContentView の種類だけ Pool にいる 3 3 Reuse Pool CollectionViewCell 表⽰領域 CollectionViewCell

Slide 34

Slide 34 text

ContentView が異なる Con fi guration をセット!? 3 4 CollectionViewCell Reuse された Cell Custom Con fi guration Warning: You are setting a new content configuration to a cell that has an existing content configuration, but the existing content view does not support the new configuration. This means the existing content view must be replaced with a new content view created from the new configuration, instead of updating the existing content view directly, which is expensive. Use separate registrations or reuse identifiers for different types of cells to avoid this. WARNINGS

Slide 35

Slide 35 text

Cell Registration で Reuse Identi fi er も不要 ‣ データ構造 と Reuse Cell 単位が簡潔に結びつく 3 5 let cellRegistration = CellRegistration { (cell, indexPath, item) in cell.contentConfiguration = CustomConfiguration(item: item) } let otherRegistration = CellRegistration { (cell, indexPath, item) in cell.contentConfiguration = OtherConfiguration(item: item) } Reuse 単位

Slide 36

Slide 36 text

Cell Registration で Reuse Identi fi er も不要 ‣ Closure 内で切り替えると Reuse 効率が悪くなる可能性がある 3 6 let cellRegistration = CellRegistration { (cell, indexPath, item) in if item.category == .dog { cell.contentConfiguration = DogConfiguration(item: item) } else { cell.contentConfiguration = CatConfiguration(item: item) } } ❌ Reuse 単位 対応する ContentView が異なるとダメ

Slide 37

Slide 37 text

UIHostingCon fi guration

Slide 38

Slide 38 text

UIHostingCon fi guration ‣ Cell の contentCon fi guration を SwiftUI で作れる • BackgroundCon fi guration もサポート • iOS 16 + ‣ 画期的!これからはこれで All OK? 3 8 cell.contentConfiguration = UIHostingConfiguration { HeartRateBPMView() } https://developer.apple.com/wwdc 22 / 10 07 2 ヘルスケアアプリで利⽤ SwiftUI View

Slide 39

Slide 39 text

UIViewRepresentable が使えない ‣ あくまでSwiftUI Only の View を作らないとダメ • UIKit のコンポーネントを SwiftUI 化したものは利⽤できない • 以下は SwiftUI にないため HostingCon fi guration 上で使⽤不可 • WKWebView • MPVolumeView (⾳量コントロール) • etc ... • 動画プレイヤーを AVVideoPlayer で代替する⾏為はできない • SwiftUI バグ回避のための UIKit 利⽤みたいな Workaround もできない 3 9 (20230906 追記 間違っているため次ページ参照)

Slide 40

Slide 40 text

UIViewControllerRepresentable が使えない ‣ embedding view controllers inside of Cell は⼀般的に⾮推奨で、Hostingでは使えない • 検証した結果、UIViewRepresentable は問題なく動作しました ‣ あくまでSwiftUI Only の View を作らないとダメ • UIKit のコンポーネントを SwiftUI 化したものは利⽤できない • 以下は SwiftUI にないため HostingCon fi guration 上で使⽤不可 • WKWebView • MPVolumeView (⾳量コントロール) • etc ... • 動画プレイヤーを AVVideoPlayer で代替する⾏為はできない • SwiftUI バグ回避のための UIKit 利⽤みたいな Workaround もできない 4 0 (20230906 追記)

Slide 41

Slide 41 text

UIHostingController (iOS 13 +) ‣ SwiftUI View を持つ ViewController • 今まではこれしかなかった ‣ SwiftUI → UIKit ブリッジ ができる • Cell をこれで作ろうとした⼈も多いハズ 4 1 https://developer.apple.com/wwdc 22 / 10 07 2 UIViewController view SwiftUI Content

Slide 42

Slide 42 text

UIHostingController の Cell は可能だけど⾮推奨 ‣ できるけど、不可解な挙動に出くわしやすい • 実装を徹底的にテストして、⾃分の責任でやってとのこと 4 2 https://developer.apple.com/forums/thread/ 719 44 5

Slide 43

Slide 43 text

パフォーマンスが悪い、特に List のスクロール ‣ セルの要素が多いと描画がカクつきながら配置 • あまり凝ってない Cell でも体感でカクつく、、 ‣ アニメーションをつけると Reuse 前の情報が出てしまう • Cell の Self-sizing とも少しズレてて違和感 4 3 contentConfiguration = UIHostingConfiguration { let patA = Bool.random() Text(patA ? "iOSDC" : "2023") .padding(patA ? 20 : 0) .background(patA ? Color.yellow : .mint) .animation(.easeInOut) }

Slide 44

Slide 44 text

HostingCon fi guration の Reuse 周りが気になる ‣ List はスクロールして Cell を⾼速に使い回す • パフォーマンスが重要 • ContentView は Reuse される? • SwiftUIContent の View の lifetime 管理は影響ありそうか? • いわゆる View Identity ※ 実装がわからないため動作ベースの検証に留まります 4 4

Slide 45

Slide 45 text

HostingContentView (Private Class) ‣ HostingCon fi guration の ContentView • 内部的には UIHostingContentView • View の body の型 (Structual Identity) 単位で Reuse されていた 4 5 contentConfiguration = UIHostingConfiguration { if Bool.random() { Text("iOSDC") } else { Image(systemName: "star") } } .background(.background) UIHostingContentView<_ConditionalContent, _UIHostingConfigurationBackgroundView>

Slide 46

Slide 46 text

ContentView の Cache はされていそう 4 6 contentConfiguration = UIHostingConfiguration { Text("iOSDC") } // UIHostingContentView contentConfiguration = UIHostingConfiguration { Image(systemName: "star") } // UIHostingContentView // warning: the existing content view does not // support the new configuration!! body (Structural) の型が違うと 
 HostingCon fi guration でも Warning (ちなみに AnyView でくくると怒られない)

Slide 47

Slide 47 text

View Identity が影響しないか ‣ View の同値性を意識して lifetime を管理する • ◎ だと更新する、△ だと完全に作り直されてしまう • Xcode 1 5 で細かく覗けそう......? 4 7 contentConfiguration = UIHostingConfiguration { let patA = Bool.random() Text(patA ? "iOSDC" : "2023") if patA { Text("iOSDC") } else { Text("2023") } } ◎ △

Slide 48

Slide 48 text

Con fi guration 上書きでは作り直しになってそう? 4 8 Text(patA ? "iOSDC" : "2023") if patA { Text("iOSDC") } else { Text("2023") } ◎ △ Con fi gurationを再セット SwiftUI 上で更新

Slide 49

Slide 49 text

個⼈的 HostingCon fi guration 使い分け ‣ Performance を意識する画⾯ (Cell) なら UIContentCon fi guration • List のような Reuse 頻度が⾼い場合 • ニュースの⾒出しの⼀覧はサクサク表⽰したい......!! • 要素の表⽰が動的に変わる場合 • 現状の調査だと View が再⽣成されてしまいそう ‣ 利⽤頻度が低い Cell なら HostingCon fi guration • Reuse 頻度が低ければ問題にならない • メリハリをつけるためのサイズが⼤きい Cell • Reuse 単位を変更して、View内の条件分岐を避けてみる? • ただし、SwiftUI で完結が必須、UIViewControllerRepresntableは使えない、iOS 16 + も 4 9 (20230906 追記)

Slide 50

Slide 50 text

その他 Tips

Slide 51

Slide 51 text

UIContentView の暗黙的な AutoLayout ‣ contentCon fi guration に設定すると 44px の⾼さ制約が付加 • 例えば中の ImageView に⾼さをつけておくと衝突する ‣ ⾼さ制約の Priority をずらして衝突を回避する 5 1 let const = imageView.heightAnchor.constraint(equalToConstant: 200) const.priority = .defaultHigh NSLayoutConstraint.activate([const])

Slide 52

Slide 52 text

CompositionalLayout.list は Scroll Performance ◎ ‣ CollectionViewCompositionalLayout • ⾃前実装と⽐較してみる • スクロールの Hitch が少なく、滑らか • 縦スクロールコンテンツは基本 List がオススメ 5 2 List estimated 再現 NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44) ) === ൺֱ === UICollectionViewCompositionalLayout.list(using: .plain)

Slide 53

Slide 53 text

(Collectionview) invalidateLayout() を避ける ‣ 画⾯回転時に呼んでいる事例が多い • しかし、道中の Cell のサイズ計算全てがリセットされてしまう • スクロールの途中から戻ると表⽰時に伸びる奇っ怪な動きになったり • 以下を先に検討してみるといい ‣ selfSizingInvalidation (iOS 16 +) • CollectionView.selfSizingInvalidation = .enabledIncludingConstraints ‣ recon fi gureSnapshot() (iOS 15 +) • 表⽰している Cell をターゲットに Di ff ableDataSource に apply() 5 3

Slide 54

Slide 54 text

Cell 側の状態変化で反映: UICon fi gurationState ‣ 複数の⼿段があるので受け取りたい箇所で実装する • Cell のサブクラス: iOS 1 4 + • updateCon fi guration() • Cell に Closure を登録: iOS 1 5 + • cellUpdateHandler • ContentCon fi guration: iOS 1 4 + • updated(for: ) ‣ isSelected / isFocused などの Cell の状態を受信 • それに応じた contentCon fi guration を作ってセットする (選択中は⾊を変えるなど) 5 4

Slide 55

Slide 55 text

まとめ ‣ Con fi guration の概念を理解 • UIKit でも Data-driven に安全に UI を構築する仕組みが⼊ってきている ‣ ContentCon fi guration の仕組みとカスタム実装を紹介 • Cell をサブクラス化する必要がない時代へ • 複数の Cell で ContentCon fi guration を再利⽤できる Composable な API ‣ List レイアウト における UIHostingCon fi guration との使いわけ • HostingCon fi guration は SwiftUI だけで完結しないといけない UIViewRepresentableの範囲まで • Cell においては ContentView の Cache は⾏われていそう • 特に List 系の再利⽤時パフォーマンスに注意、取捨選択 ‣ 移⾏していけば⾃然と安全に利⽤を実現する仕組み!使っていきましょう! 5 5 (20230906 追記)