Upgrade to Pro — share decks privately, control downloads, hide ads and more …

UIKit ベースの Custom UIContentConfiguration API を 用いた複雑なカスタムセルの作り方

Go Takagi
September 01, 2023

UIKit ベースの Custom UIContentConfiguration API を 用いた複雑なカスタムセルの作り方

iOSDC2023 Day 0 Track B 19:20〜
UIKit ベースの Custom UIContentConfiguration API を 用いた複雑なカスタムセルの作り方

iOS 14 からUICollectionViewのセルのカスタマイズ方法にConfigurationというAPIが追加されました。

これまでカスタムセルはサブクラスを作成して実装していました。Configurationにより、セルレイアウトと外観を分離し再利用しやすいComposableなAPIの管理ができます。

また縦方向のリスト形式の例では削除や並び替えといった機能と滑らかな表示体験が提供でき、安全にOSの機能を考慮した複雑なカスタムセルが実装できます。

本セッションでは日本経済新聞社の紙面ビューアーアプリで実際に導入したUIKitベースのカスタム例を紹介します。
UIKitベースのパターンは実装例が比較的少ない一方、依然として重要な選択肢です。SwiftUIベースでは困難な制約やリスト表示のパフォーマンス観点などを踏まえ、仕組みからUIKitとSwiftUIの理解を深めます。

Go Takagi

September 01, 2023
Tweet

More Decks by Go Takagi

Other Decks in Technology

Transcript

  1. UIKit ベースの Custom
    UIContentCon
    fi
    guration API を


    ⽤いた複雑なカスタムセルの作り⽅


    Go Takagi
    20
    23
    /
    09
    /
    01
    iOSDC Japan
    2 023
    day
    0
    #Track B (
    1
    9
    :
    20
    - )

    View Slide

  2. Keynote
    ‣ UIKit に⼊ってきた Con
    fi
    guration という概念を学ぶ


    • UI 更新を安全に⾏う仕組み


    ‣ UICollectionViewでUIContentCon
    fi
    gurationを使いこなそう!


    • Custom 実装例が少ない


    • 仕組みを理解してどうやって作ればいいかを知る


    ‣ ちなみに UIHostingCon
    fi
    guration は?


    • 特徴を整理して使い分けを考える
    2

    View Slide

  3. Me ( Go Takagi )
    ‣ID


    • shimastripe / shimastriper


    • bento.me/shimastripe


    ‣Work at


    • 株式会社 ⽇本経済新聞社


    ‣ iOS エンジニア


    ‣ iOSDC NOC チーム


    ‣Like


    • Swift / ⾃動化 / 柴⽝
    3
    電⼦版広報⽝デンシバ
    去年は PencilKit の話!

    View Slide

  4. Con
    fi
    guration ?

    View Slide

  5. 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

    View Slide

  6. 例: 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

    View Slide

  7. Con
    fi
    guration が反映、前の状態は引き継がない
    7
    var config = UIButton.Configuration.bordered()


    config.title = "iOSDC 2023"


    config.baseForegroundColor = .systemRed


    button.configuration = config

    View Slide

  8. 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

    View Slide

  9. ContentCon
    fi
    guration

    View Slide

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

    View Slide

  11. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 前の状態を把握しきれていないと
    1
    6
    • 知らないタイミングで別の場所で画像は追加されていた


    • そのままリセットをしそびれる......
    😔
    直前の画像が残っている
    Reuse Pool

    View Slide

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

    View Slide

  18. 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


    View Slide

  19. ContentCon
    fi
    guration
    ‣ 今までの Cell


    • サブクラスを作って、ContentView に addSubView() して中⾝を実装


    ‣ ContentCon
    fi
    guration


    • CollectionViewCell (⼊れ物) + ContentCon
    fi
    guration (中⾝) に分離


    • 別種類のセルで ContentCon
    fi
    guration を共有可能、Composable な構成に


    • UICollectionViewCell や UITableViewCell で共有したり
    1
    9

    View Slide

  20. Reminder の CollectionViewListCell の活⽤
    2
    0

    View Slide

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

    View Slide

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

    View Slide

  23. UIContentView: カスタマイズする View
    ‣ Con
    fi
    guration をセットできる view (UIView) を作る


    • ここに Label や Button などコンテンツとなる View を実装する
    2
    3
    @MainActor


    public protocol UIContentView : NSObjectProtocol {


    @MainActor


    var configuration: UIContentConfiguration { get set }


    }


    View Slide

  24. UIContentCon
    fi
    guration: データ構成
    ‣ Con
    fi
    guration (データ構成)


    • ContentView を⽣成することができる
    2
    4
    public protocol UIContentConfiguration {


    @MainActor


    func makeContentView() -> UIView & UIContentView


    }


    View Slide

  25. 2 通りの動きをサポートする
    ‣ Reuse されたことがある Cell に Con
    fi
    guration をセットした場合


    • ContentView に Con
    fi
    guration をセットして Update する


    ‣ Reuse されたことがない Cell に Con
    fi
    guration をセットした場合


    • Con
    fi
    guration から ContentView を⽣成する
    2
    5

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. 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

    View Slide

  30. 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


    }


    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

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

    View Slide

  34. 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

    View Slide

  35. 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 単位

    View Slide

  36. 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 が異なるとダメ

    View Slide

  37. UIHostingCon
    fi
    guration

    View Slide

  38. 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

    View Slide

  39. UIViewRepresentable が使えない
    ‣ あくまでSwiftUI Only の View を作らないとダメ


    • UIKit のコンポーネントを SwiftUI 化したものは利⽤できない


    • 以下は SwiftUI にないため HostingCon
    fi
    guration 上で使⽤不可


    • WKWebView


    • MPVolumeView (⾳量コントロール)


    • etc ...


    • 動画プレイヤーを AVVideoPlayer で代替する⾏為はできない


    • SwiftUI バグ回避のための UIKit 利⽤みたいな Workaround もできない
    3
    9
    (20230906 追記 間違っているため次ページ参照)

    View Slide

  40. 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 追記)

    View Slide

  41. UIHostingController (iOS
    13
    +)
    ‣ SwiftUI View を持つ ViewController


    • 今まではこれしかなかった


    ‣ SwiftUI → UIKit ブリッジ ができる


    • Cell をこれで作ろうとした⼈も多いハズ
    4
    1
    https://developer.apple.com/wwdc
    22
    /
    10
    07 2
    UIViewController
    view
    SwiftUI Content

    View Slide

  42. UIHostingController の Cell は可能だけど⾮推奨
    ‣ できるけど、不可解な挙動に出くわしやすい


    • 実装を徹底的にテストして、⾃分の責任でやってとのこと
    4
    2
    https://developer.apple.com/forums/thread/
    719
    44
    5

    View Slide

  43. パフォーマンスが悪い、特に 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)


    }


    View Slide

  44. HostingCon
    fi
    guration の Reuse 周りが気になる
    ‣ List はスクロールして Cell を⾼速に使い回す


    • パフォーマンスが重要


    • ContentView は Reuse される?


    • SwiftUIContent の View の lifetime 管理は影響ありそうか?


    • いわゆる View Identity


    ※ 実装がわからないため動作ベースの検証に留まります
    4
    4

    View Slide

  45. 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>

    View Slide

  46. 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 でくくると怒られない)

    View Slide

  47. 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")


    }


    }




    View Slide

  48. Con
    fi
    guration 上書きでは作り直しになってそう?
    4
    8
    Text(patA ? "iOSDC" : "2023")


    if patA {


    Text("iOSDC")


    } else {


    Text("2023")


    }




    Con
    fi
    gurationを再セット SwiftUI 上で更新

    View Slide

  49. 個⼈的 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 追記)

    View Slide

  50. その他 Tips

    View Slide

  51. UIContentView の暗黙的な AutoLayout
    ‣ contentCon
    fi
    guration に設定すると 44px の⾼さ制約が付加


    • 例えば中の ImageView に⾼さをつけておくと衝突する


    ‣ ⾼さ制約の Priority をずらして衝突を回避する
    5
    1


    CustomContentView:0x00000000.height == 44 (active)>


    let const = imageView.heightAnchor.constraint(equalToConstant: 200)




    const.priority = .defaultHigh


    NSLayoutConstraint.activate([const])

    View Slide

  52. CompositionalLayout.list は Scroll Performance ◎
    ‣ CollectionViewCompositionalLayout


    • ⾃前実装と⽐較してみる


    • スクロールの Hitch が少なく、滑らか


    • 縦スクロールコンテンツは基本 List がオススメ
    5
    2
    List
    estimated 再現
    NSCollectionLayoutSize(


    widthDimension: .fractionalWidth(1.0),


    heightDimension: .estimated(44)


    )


    === ൺֱ ===


    UICollectionViewCompositionalLayout.list(using: .plain)


    View Slide

  53. (Collectionview) invalidateLayout() を避ける
    ‣ 画⾯回転時に呼んでいる事例が多い


    • しかし、道中の Cell のサイズ計算全てがリセットされてしまう


    • スクロールの途中から戻ると表⽰時に伸びる奇っ怪な動きになったり


    • 以下を先に検討してみるといい


    ‣ selfSizingInvalidation (iOS
    16
    +)


    • CollectionView.selfSizingInvalidation = .enabledIncludingConstraints


    ‣ recon
    fi
    gureSnapshot() (iOS
    15
    +)


    • 表⽰している Cell をターゲットに Di
    ff
    ableDataSource に apply()
    5
    3

    View Slide

  54. 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

    View Slide

  55. まとめ
    ‣ 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 追記)

    View Slide