iOSDC Japan 2020 Day 1 Track B 10:50

Ae276805027a01983503c3edafbdb6b2?s=47 Taiki Suzuki
September 20, 2020

iOSDC Japan 2020 Day 1 Track B 10:50

2020/09/20 10:50〜 Track B レギュラートーク(20分)
4年間運用されて表示速度が低下した詳細画面を改善する過程で得た知見
https://fortee.jp/iosdc-japan-2020/proposal/1b41bd44-d2c8-488c-b053-1eab56e7da6c

Ae276805027a01983503c3edafbdb6b2?s=128

Taiki Suzuki

September 20, 2020
Tweet

Transcript

  1. ೥ؒӡ༻͞Εͯ දࣔ଎౓͕௿Լͨ͠ৄࡉը໘Λ վળ͢ΔաఔͰಘͨ஌ݟ by marty-suzuki / @marty_suzuki iOSDC Japan 2020

  2. marty_suzuki CyberAgent, Inc. marty-suzuki Taiki Suzuki

  3. ͲΜͳৄࡉը໘ͳͷ͔Λ ؆қతʹ঺հ

  4. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ίϯςϯπͷαϜωΠϧ

  5. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ λΠτϧ΍ࢹௌظݶͳͲ

  6. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ֤छΞΫγϣϯϘλϯ

  7. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ίϯςϯπʹඥͮ͘ΤϐιʔυҰཡ

  8. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ Ϩίϝϯυ͞ΕΔίϯςϯπҰཡ

  9. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ λοϓ͢Δ

  10. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ৄࡉ৘ใ Ωϟετ ελοϑ ޿͕ͬͯৄࡉ৘ใදࣔ

  11. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ

    ը໘ճస͢Δ
  12. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ

  13. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ

  14.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ ϚΠϏσΦ μ΢ϯϩʔυ

    γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϑϧεΫϦʔϯ    ίϯςϯπ͕ࢹௌՄೳͳ৔߹
  15.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ ্ʹҾ্͖͛Δ

  16.    Y ΤϐιʔυҰཡ J Ϩίϝϯυදࣔ ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ

  17.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ λοϓ͢Δ

  18.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ

    ίϯςϯπʹඥͮ͘ΤϐιʔυҰཡ
  19.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ γʔΫ͢Δ

  20.    Y ΤϐιʔυҰཡ J ΦεεϝͷΤϐιʔυ ΋ͬͱΈΔʼ γʔϯαϜωΠϧΛදࣔ 

  21. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ    View͕ݟ͍͑ͯΔ෦෼Ҏ্ʹଟ͍ը໘

  22. ը໘දࣔͷվળ

  23. AppStore൛ iPhone6s iOS11 AppStore൛ iPhone8 iOS12

  24. දࣔ଎౓ վળલ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ

    1360ms 2100ms ճ໨Ҏ߱ 760ms 1180ms
  25. ը໘දࣔ଎౓Λ ͲͷΑ͏ʹܭଌ͔ͨ͠

  26. class VideoFullscreenViewController: UIViewController { init() { super.init(nibName: nil, bundle: nil)

    } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } } init͔ΒviewDidAppear·ͰΛը໘͕දࣔ͞Ε·Ͱͱఆٛ
  27. import os.signpost class VideoFullscreenViewController: UIViewController { let osLog: OSLog init()

    { self.osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) os_signpost(.begin, log: osLog, name: "viewDidAppear Measurement") super.init(nibName: nil, bundle: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_signpost(.end, log: osLog, name: "viewDidAppear Measurement") } }
  28. import os.signpost class VideoFullscreenViewController: UIViewController { let osLog: OSLog init()

    { self.osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) os_signpost(.begin, log: osLog, name: "viewDidAppear Measurement") super.init(nibName: nil, bundle: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_signpost(.end, log: osLog, name: "viewDidAppear Measurement") } }
  29. import os.signpost class VideoFullscreenViewController: UIViewController { let osLog: OSLog init()

    { self.osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) os_signpost(.begin, log: osLog, name: "viewDidAppear Measurement") super.init(nibName: nil, bundle: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_signpost(.end, log: osLog, name: "viewDidAppear Measurement") } }
  30. import os.signpost class VideoFullscreenViewController: UIViewController { let osLog: OSLog init()

    { self.osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) os_signpost(.begin, log: osLog, name: "viewDidAppear Measurement") super.init(nibName: nil, bundle: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_signpost(.end, log: osLog, name: "viewDidAppear Measurement") } }
  31. None
  32. init͔ΒviewDidAppear·Ͱͷ଎౓ΛՄࢹԽ

  33. Ͳ͏͍͏ը໘ߏ੒͔ͩͬͨ

  34.    VideoPlayerViewController ɾಈըͷ࠶ੜશൠ

  35. DetailViewController ɾίϯςϯπͷৄࡉ৘ใදࣔ

  36. VideoFullscreenViewController ɾ%FUBJMͱ7JEFP1MBZFSͷ఻ୡ ɾUSBOTJUJPOͷॲཧ

  37. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() }
  38. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() }
  39. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() }
  40. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() }
  41. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() }
  42. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController init() { self.detailViewController = DetailViewController() self.videoPlayerViewController = detailViewController.videoPlayerViewController super.init(nibName: nil, bundle: nil) setViewControllers([detailViewController], animated: false) } override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class DetailViewController: UIViewController { private lazy var videoPlayerViewController = VideoPlayerViewController(...) private lazy var viewModel = DetailViewModel() override func viewDidLoad() { super.viewDidLoad() addChild(videoPlayerViewController) view.addSubview(videoPlayerViewController.view) videoPlayerViewController.didMove(toParent: self) } } class VideoPlayerViewController: UIViewController { private(set) lazy var videoPlayerView = VideoPlayerView() private(set) lazy var videoPlayerViewModel = VideoPlayerViewModel() } wfunc setup(completion:)ͰvideoPlayerView΍ videoPlayerViewModelΛॳظԽͯ͠ϨΠΞ΢τ ॲཧΛ࣮ߦ͢Δ
  43.    ॳظ͸DetailViewControllerͷΈ͕ͩͬͨ ػೳ͕૿͑ΔʹͭΕͯViewController΋૿͑ͨ

  44. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  45. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  46. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  47. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  48. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ৄࡉ৘ใ Ωϟετ ελοϑ VideoPlayerViewController.view VerticalStackView

    { ScrollView { VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) }
 } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  49. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  50. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  51. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  52. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  53. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  54. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } } wUIScrollView಺ͷUIStackViewʹaddArrangedSubview͞Ε͍ͯΔͨΊ DPMMFDUJPO7JFX಺ͷDFMMΛ͢΂ͯඳը͍ͯ͠Δ wॳظ࣮૷౰ॳ͸݅·Ͱ͔͠දࣔ͠ͳ͍࢓༷ͩͬͨͷͰڐ༰Ͱ͖͕ͨ ͕࣌ܦͬͯमਖ਼લͷஈ֊Ͱ݅·ͰϨίϝϯυ͕දࣔ͞ΕΔΑ͏ʹͳ͍ͬͯͨ
  55. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  56. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  57. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  58. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view HorizontalStackView { ThumbnailView ScrollView

    { VerticalStackView { SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  59. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view HorizontalStackView { ThumbnailView ScrollView

    { VerticalStackView { SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  60. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view HorizontalStackView { ThumbnailView ScrollView

    { VerticalStackView { SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  61. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view
 VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }   
  62. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view
 VerticalStackView { ScrollView {

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }   
  63. දࣔΛ஗͍ͯͨ͘͠ ओͳཁҼ͸ʁ

  64. දࣔ଎౓ վળલ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ

    1360ms 2100ms ճ໨Ҏ߱ 760ms 1180ms ॳճͱճ໨Ҏ߱Ͱදࣔ଎౓ͷ͕ࠩ͋Δ
  65. https://developer.apple.com/documentation/uikit/uinib A UINib object caches the contents of a nib

    file in memory, ready for unarchiving and instantiation. When your application needs to instantiate the contents of the nib file it can do so without having to load the data from the nib file first, improving performance. The UINib object can automatically release this cached nib data to free up memory for your application under low-memory conditions, reloading that data the next time your application instantiates the nib.
  66. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoFullscreenViewController.VideoPlayerView (xib)
 VerticalStackView { ScrollView

    { VerticalStackView { ThumbnailView (xib) SummaryView (xib) { VerticalStackView { Label ContentInfoView (xib) } } OtherView (xib) { VerticalStackView { ActionButtonsView EpisodeListView (xib) } } RecommendView (xib) { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }   
  67. protocol Nibable: AnyObject { associatedtype Instance static func makeFromNib() ->

    Instance static var nib: UINib { get } static var nibName: String { get } } extension Nibable { static func makeFromNib() -> Instance { let osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) defer { os_signpost(.end, log: osLog, name: "makeFromNib Measurement", "%@", nibName) } os_signpost(.begin, log: osLog, name: "makeFromNib Measurement", "%@", nibName) return nib.instantiate(withOwner: nil, options: nil).first as! Instance } }
  68. protocol Nibable: AnyObject { associatedtype Instance static func makeFromNib() ->

    Instance static var nib: UINib { get } static var nibName: String { get } } extension Nibable { static func makeFromNib() -> Instance { let osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) defer { os_signpost(.end, log: osLog, name: "makeFromNib Measurement", "%@", nibName) } os_signpost(.begin, log: osLog, name: "makeFromNib Measurement", "%@", nibName) return nib.instantiate(withOwner: nil, options: nil).first as! Instance } }
  69. protocol Nibable: AnyObject { associatedtype Instance static func makeFromNib() ->

    Instance static var nib: UINib { get } static var nibName: String { get } } extension Nibable { static func makeFromNib() -> Instance { let osLog = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) defer { os_signpost(.end, log: osLog, name: "makeFromNib Measurement", "%@", nibName) } os_signpost(.begin, log: osLog, name: "makeFromNib Measurement", "%@", nibName) return nib.instantiate(withOwner: nil, options: nil).first as! Instance } }
  70. ॳظԽ࣌ؒ (iPhone8 iOS12) ॳճ 2ճ໨Ҏ߱ 7JEFP1MBZFS7JFX 394.11ms 124.37ms 5IVNCOBJM7JFX 5.97ms

    1.27ms 4VNNBSZ7JFX 27.53ms 0.24ms $POUFOU*OGP7JFX 15.66ms 7.22ms 0UIFS7JFX 8.02ms 2.34ms &QJTPEF-JTU7JFX 14.25ms 6.29ms 3FDPNNFOE7JFX 13.90ms 1.39ms
  71. Α͋͘Δxibͷϩʔυ͕஗͘ͳΔཁҼ wxib্Ͱద༻͍ͯ͠ΔΧελϜϑΥϯτ͕ ಡΈࠐΊ͍ͯͳ͍ wSystem Font͕ద༻͞Ε͍ͯͨͷͰ ಛʹ໰୊͸ͳͦ͞͏ w୯७ʹViewͷੜ੒ʹ͕͔͔͍࣌ؒͬͯΔ!ʁ

  72. ͲͷΑ͏ʹViewΛੜ੒͢Δ͔

  73. վળ͢ΔͨΊͷΞϓϩʔν wxibΛར༻ͤͣίʔυϨΠΞ΢τʹม͑Δ w଎͘͸ͳΔͱࢥ͏͕ɺେ෯ʹ࡟ݮͰ͖Δ͔ ͸࣮૷ͯ͠Έͳ͍ͱΘ͔Βͳ͍ wViewΛੜ੒Λ஗Ԇͤ͞Δ wओཁͳViewͷੜ੒Λ͠ͳ͍৔߹ʹද͕ࣔ ଎͘ͳΔͷͰվળͷՄೳੑେ

  74. վળલͷ࣮૷

  75. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } }
  76. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } }
  77. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } }
  78. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } w_episodeDetail͕nilͰͳ͔ͬͨͱ͖ʹ஋Λྲྀ͢
  79. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wॳظԽ͞ΕͨλΠϛϯάͰৄࡉ৘ใऔಘΛ࣮ߦ
  80. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } }
  81. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } } wepisodeDetailͷऔಘ׬ྃޙʹϨΠΞ΢τॲཧ
  82. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } }
  83. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wଈ࣌ʹϨίϝϯυͷ஋͕ྲྀΕΔ
  84. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wϨίϝϯυ͕ۭ͔Ͳ͏͔͔Βม׵ͨ͠ ஋͕ଈ࣌ʹྲྀΕΔ
  85. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wॳظԽ͞ΕͨλΠϛϯάͰϨίϝϯυऔಘΛ࣮ߦ
  86. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } }
  87. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } } wisHidden͕trueͰଈ࣌ʹྲྀΕͯϨΠΞ΢τॲཧ wAPIͷऔಘ͕׬ྃ͢ΔͱrecommendListͷϨΠΞ΢τॲཧ wͦͷޙʹisHidden͕falseͰϨΠΞ΢τॲཧ
  88. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } }
  89. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } w˝·ͨ͸ContentInfoView͕λοϓ͞ΕΔͱൃՐ
  90. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wଈ࣌ʹ_isContentInfoHiddenͷ஋͕ྲྀΕΔ
  91. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> init(id: String, episodeAction: EpisodeAction, toggleContentInfo: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) episodeAction.getRecommendList() .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wΠϕϯτ͕ྲྀΕΔͱ_isContentInfoHiddenͷ ஋Λ൓సͤ͞Δ
  92. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } }
  93. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } } wisHidden͕trueͰଈ࣌ʹ஋͕ྲྀΕ͖ͯͯϨΠΞ΢τॲཧ
  94. class DetailViewController: UIViewController { @IBOutlet private weak var contentStackView: UIStackView!

    { didSet { contentStackView.addArrangedSubview(thumbnailView) contentStackView.addArrangedSubview(summaryView) contentStackView.addArrangedSubview(otherView) contentStackView.addArrangedSubview(recommendView) } } private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private let thumbnailView = ThumbnailView.makeFromNib() private let summaryView = SummaryView.makeFromNib() private let otherView = OtherView.makeFromNib() private let recommendView = RecommendView.makeFromNib() private lazy var viewModel = DetailViewModel( ... toggleContentInfo: summaryView.didTapContentInfo ) override func viewDidLoad() { super.viewDidLoad() viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: disposeBag) viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: disposeBag) viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: disposeBag) viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: disposeBag) ... } } wtoggleContentInfo͕ൃՐ͢ΔͱϨΠΞ΢τॲཧ
  95. վળޙͷ࣮૷

  96. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let otherView: OtherView let recommendView: RecommendView init(thumbnailView: ThumbnailView, summaryView: SummaryView, otherView: OtherView, recommendView: RecommendView) { ... } } class LazyRelays { let didTapContentInfo = PublishRelay<Void>() let didFinishInitializingViews = PublishRelay<Void>() } }
  97. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let otherView: OtherView let recommendView: RecommendView init(thumbnailView: ThumbnailView, summaryView: SummaryView, otherView: OtherView, recommendView: RecommendView) { ... } } class LazyRelays { let didTapContentInfo = PublishRelay<Void>() let didFinishInitializingViews = PublishRelay<Void>() } } ஗ԆॳظԽΛͨ͠ViewΛอ࣋͢ΔΫϥε
  98. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let otherView: OtherView let recommendView: RecommendView init(thumbnailView: ThumbnailView, summaryView: SummaryView, otherView: OtherView, recommendView: RecommendView) { ... } } class LazyRelays { let didTapContentInfo = PublishRelay<Void>() let didFinishInitializingViews = PublishRelay<Void>() } } ஗ԆॳظԽΛͨ͠ViewͷObservableͱ ViewModelΛܨ͙PublishRelayΛอ࣋͢ΔΫϥε
  99. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  100. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  101. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void> init(..., viewDidAppear: Observable<Void>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.readyForInitializingViews = Observable.combineLatest(episodeDetail, viewDidAppear) .map { _ in }.take(1) episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) didFinishInitializingViews.take(1).flatMap { _ in episodeAction.getRecommendList() } .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } }
  102. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void> init(..., viewDidAppear: Observable<Void>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.readyForInitializingViews = Observable.combineLatest(episodeDetail, viewDidAppear) .map { _ in }.take(1) episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) didFinishInitializingViews.take(1).flatMap { _ in episodeAction.getRecommendList() } .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wepisodeDetailͷऔಘ͕׬ྃ͢Δ͔ͭviewDidAppear
 ͕࣮ߦ͞ΕΔͱ౓͚ͩྲྀΕΔ
  103. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  104. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  105. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void> init(..., viewDidAppear: Observable<Void>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.readyForInitializingViews = Observable.combineLatest(episodeDetail, viewDidAppear) .map { _ in }.take(1) episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) didFinishInitializingViews.take(1).flatMap { _ in episodeAction.getRecommendList() } .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } }
  106. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void> init(..., viewDidAppear: Observable<Void>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.readyForInitializingViews = Observable.combineLatest(episodeDetail, viewDidAppear) .map { _ in }.take(1) episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) didFinishInitializingViews.take(1).flatMap { _ in episodeAction.getRecommendList() } .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wViewͷ஗ԆॳظԽ͕׬ྃ͢ΔͱྲྀΕΔ
  107. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Observable<[Episode]> let

    isRecommendListHidden: Observable<Bool> let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void> init(..., viewDidAppear: Observable<Void>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _episodeDetail = BehaviorRelay<EpisodeDetail?>(value: nil) let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) self.episodeDetail = _episodeDetail .flatMap { $0.map(Observable.just) ?? .empty() } self.recommendList = _recommendList.asObservable() self.isRecommendListHidden = _recommendList .map { $0.isEmpty } self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.readyForInitializingViews = Observable.combineLatest(episodeDetail, viewDidAppear) .map { _ in }.take(1) episodeAction.getEpisode(id: id) .bind(to: _episodeDetail) .disposed(by: disposeBag) didFinishInitializingViews.take(1).flatMap { _ in episodeAction.getRecommendList() } .bind(to: _recommendList) .disposed(by: disposeBag) toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) } } wViewͷ஗ԆॳظԽ͕׬ྃޙʹϨίϝϯυͷ औಘΛ࣮ߦ͢Δ
  108. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  109. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  110. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } } wViewͷ஗ԆॳظԽ͕׬ྃҎ߱ʹϨίϝϯυ ͷ஋ΛϨΠΞ΢τॲཧ
  111. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  112. final class DetailViewController: UIViewController { @IBOutlet weak var contentStackView: UIStackView!

    private(set) lazy var videoPlayerViewController = VideoPlayerViewController() private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) let recommendView = RecommendView.makeFromNib() me.viewModel.recommendList .bind(to: Binder(recommendView) { $0.setRecommendList($1) }) .disposed(by: me.disposeBag) me.viewModel.isRecommendListHidden .bind(to: recommendView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(...) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } } w஗ԆॳظԽ࣌ʹϨΠΞ΢τॲཧͱbind
  113. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class VideoPlayerViewController: UIViewController { private var videoPlayerView: VideoPlayerView? private var videoPlayerViewModel: VideoPlayerViewModel? func setup(completion: () -> Void) { DispatchQueue.main.async { let videoPlayerView = VideoPlayerView.makeFromNib() let viewModel = VideoPlayerViewModel() self.videoPlayerView = videoPlayerView self.videoPlayerViewModel = viewModel } ... completion() } }
  114. class VideoFullscreenViewController: UINavigationController { private let detailViewController: DetailViewController private let

    videoPlayerViewController: VideoPlayerViewController override func viewDidLoad() { super.viewDidLoad() detailViewController.loadViewIfNeeded() videoPlayerViewController.setup(completion: { ... }) } } class VideoPlayerViewController: UIViewController { private var videoPlayerView: VideoPlayerView? private var videoPlayerViewModel: VideoPlayerViewModel? func setup(completion: () -> Void) { DispatchQueue.main.async { let videoPlayerView = VideoPlayerView.makeFromNib() let viewModel = VideoPlayerViewModel() self.videoPlayerView = videoPlayerView self.videoPlayerViewModel = viewModel } ... completion() } }
  115. දࣔ଎౓ վળޙ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ

    776ms 1060ms ճ໨Ҏ߱ 580ms 653ms
  116. දࣔ଎౓ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ 1360ms


    ↓
 776ms 2100ms
 ↓
 1060ms ճ໨Ҏ߱ 760ms
 ↓
 580ms 1180ms
 ↓
 653ms
  117. վળ൛ iPhone6s iOS11 AppStore൛ iPhone8 iOS12

  118. վળ఺ͷҰཡ w UIStackViewͷdidSetͰarrangedSubview͍ͯͨ͠ViewΛɺviewDidAppearޙ ʹ஗ԆॳظԽͦ͠ͷλΠϛϯάͰarrangedSubview͢ΔΑ͏मਖ਼ w viewDidLoadͰbind͍ͯͨ͠ObserverɾObservableΛLazyRelaysͷ PublishRelayΛܦ༝͢ΔΑ͏ʹͯ͠஗ԆॳظԽʹରԠ w ϨίϝϯυͷऔಘΛViewͷ஗ԆॳظԽޙʹมߋʢϨίϝϯυͷඳը͕ͦͷ͋ ͱʹͳΔʣ

  119. ը໘ճసͷվળ

  120. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VerticalStackView { ScrollView { VerticalStackView

    { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  121. ը໘ճస଎౓ վળલ  ˞ճ࣮ࢪ iPhone 8 iOS12 ࠷খ 1100ms ฏۉ

    1130ms ࠷େ 1300ms
  122. import UIKit import os.signpost class VideoFullscreenViewController: UIViewController { let osLog

    = = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) os_signpost(.begin, log: osLog, name: "Rotation Measurement”) coordinator.animate(alongsideTransition: nil) { [osLog] _ in os_signpost(.end, log: osLog, name: "Rotation Measurement") } } }
  123. import UIKit import os.signpost class VideoFullscreenViewController: UIViewController { let osLog

    = = OSLog(subsystem: "jp.co.marty-suzuki", category: .pointsOfInterest) override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) os_signpost(.begin, log: osLog, name: "Rotation Measurement”) coordinator.animate(alongsideTransition: nil) { [osLog] _ in os_signpost(.end, log: osLog, name: "Rotation Measurement") } } }
  124. վળલͷ࣮૷

  125. class RecommendView: UIView { private let collectionView = UICollectionView(...) private

    lazy var collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 0) private var recommendList: [Episode] = [] init() { ... addSubview(collectionView) NSLayoutConstraint.activate([ ..., collectionViewHeightConstraint ]) } func setRecommendList(_ recommendList: [Episode]) { self.recommendList = recommendList collectionView.reloadData() collectionView.layoutIfNeeded() collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height layoutIfNeeded() } }
  126. class RecommendView: UIView { private let collectionView = UICollectionView(...) private

    lazy var collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 0) private var recommendList: [Episode] = [] init() { ... addSubview(collectionView) NSLayoutConstraint.activate([ ..., collectionViewHeightConstraint ]) } func setRecommendList(_ recommendList: [Episode]) { self.recommendList = recommendList collectionView.reloadData() collectionView.layoutIfNeeded() collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height layoutIfNeeded() } }
  127. class RecommendView: UIView { private let collectionView = UICollectionView(...) private

    lazy var collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 0) private var recommendList: [Episode] = [] init() { ... addSubview(collectionView) NSLayoutConstraint.activate([ ..., collectionViewHeightConstraint ]) } func setRecommendList(_ recommendList: [Episode]) { self.recommendList = recommendList collectionView.reloadData() collectionView.layoutIfNeeded() collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height layoutIfNeeded() } } $PMMFDUJPO7JFXͷ$POUFOU4J[Fͷߴ͞ʹ߹Θͤͯ 7JFXͷߴ͞ΛΞοϓσʔτ͢ΔͨΊ$FMMͷ࠶ར༻͸͞Ε͍ͯͳ͍ঢ়ଶ
  128. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } }
  129. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } } wtraitCollectionΛ΋ͱʹॎɾԣΛ൑ఆ
  130. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } } wViewͷ஗ԆॳظԽ׬ྃҎ߱ʹUIStackViewͷ ελοΫํ޲Λઃఆ
  131. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  132. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  133. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } }
  134. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } } wViewͷ஗ԆॳظԽ׬ྃҎ߱ʹॎ͔ԣ͔ʹΑͬͯ rootͷUIStackViewʹadd͢Δ͔contentͷ UIStackViewʹadd͢Δ͔ͷΠϕϯτΛྲྀ͢
  135. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  136. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  137. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } }
  138. class DetailViewModel { ... let isContentInfoHidden: Observable<Bool> let readyForInitializingViews: Observable<Void>

    let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let addThumbnailToContent: Observable<Void> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { ... let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) ... self.isContentInfoHidden = _isContentInfoHidden.asObservable() self.stackAxis = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .map { $0 ? .vertical : .horizontal } self.addThumbnailToRoot = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.empty() : .just(()) } self.addThumbnailToContent = Observable.combineLatest(didFinishInitializingViews.take(1), _isPortrait.asObservable()) { $1 } .flatMap { $0 ? Observable.just(()) : .empty() } ... toggleContentInfo.withLatestFrom(_isContentInfoHidden) { !$1 } .bind(to: _isContentInfoHidden) .disposed(by: disposeBag) traitCollectionDidChange .map { $0.verticalSizeClass == .regular && $0.horizontalSizeClass == .compact } .bind(to: _isPortrait) .disposed(by: disposeBag) } }
  139. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  140. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentStackView: UIStackView! ... private var lazyViews: LazyViews? private let lazyRelays = LazyRelays() private lazy var viewModel = DetailViewModel( ... traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) override func viewDidLoad() { super.viewDidLoad() viewModel.initializeViews .bind(to: Binder(self) { me, _ in ... summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.isContentInfoHidden .bind(to: summaryView.contentInfoView.rx.isHidden) .disposed(by: me.disposeBag) me.contentStackView.addArrangedSubview(…) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } if me.contentStackView.arrangedSubviews.contains(thumbnailView) { me.contentStackView.removeArrangedSubview(thumbnailView) thumbnailView.removeFromSuperview() } me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToContent .bind(to: ...) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  141. վળޙͷ࣮૷

  142. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VerticalStackView { CollectionView { DetailContentSection

    { ContentCell { ThumbnailView } ContentCell { SummaryView } ContentCell (Expand Only) { ContentInfoView } ContentCell { ActionButtonsView } ContentCell { EpisodeListView } } RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } VerticalStackView { ScrollView { VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  143. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let contentInfoView: ContentInfoView let otherView: OtherView init(...) { ... } } class ContentCell: UICollectionViewCell { private weak var view: UIView? { didSet { oldValue?.removeFromSuperview() guard let view = view else { return } view.removeFromSuperview() contentView.addSubview(view) } } func setContentView(_ view: UIView) { guard self.view != view else { return } self.view = view } } }
  144. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let contentInfoView: ContentInfoView let otherView: OtherView init(...) { ... } } class ContentCell: UICollectionViewCell { private weak var view: UIView? { didSet { oldValue?.removeFromSuperview() guard let view = view else { return } view.removeFromSuperview() contentView.addSubview(view) } } func setContentView(_ view: UIView) { guard self.view != view else { return } self.view = view } } } wsummaryViewͷUIStackView্ʹ഑ஔ͞Ε͍ͯͨViewΛ෼཭
  145. extension DetailViewController { class LazyViews { let thumbnailView: ThumbnailView let

    summaryView: SummaryView let contentInfoView: ContentInfoView let otherView: OtherView init(...) { ... } } class ContentCell: UICollectionViewCell { private weak var view: UIView? { didSet { oldValue?.removeFromSuperview() guard let view = view else { return } view.removeFromSuperview() contentView.addSubview(view) } } func setContentView(_ view: UIView) { guard self.view != view else { return } self.view = view } } } wLazyViewsͷViewΛUICollectionView্ʹ දࣔ͢ΔͨΊͷCell
  146. import RxDataSources enum SectionModel: Hashable { case content([Item]) case recommend([Item])

    enum Item: Hashable { case thumbnail case summary case contentInfo case other case recommend(IndexPath) } } extension SectionModel: AnimatableSectionModelType { var identity: AnyHashable { AnyHashable(self) } var items: [Item] { switch self { case let .content(items), let .recommend(items): return items } } init(original: SectionModel, items: [Item]) { switch original { case .content: self = .content(items) case .recommend: self = .recommend(items) } } } extension SectionModel.Item: IdentifiableType { var identity: AnyHashable { AnyHashable(self) } }
  147. import RxDataSources enum SectionModel: Hashable { case content([Item]) case recommend([Item])

    enum Item: Hashable { case thumbnail case summary case contentInfo case other case recommend(IndexPath) } } extension SectionModel: AnimatableSectionModelType { var identity: AnyHashable { AnyHashable(self) } var items: [Item] { switch self { case let .content(items), let .recommend(items): return items } } init(original: SectionModel, items: [Item]) { switch original { case .content: self = .content(items) case .recommend: self = .recommend(items) } } } extension SectionModel.Item: IdentifiableType { var identity: AnyHashable { AnyHashable(self) } } wίϯςϯπʹؔ͢ΔηΫγϣϯͱ Ϩίϝϯυʹؔ͢ΔηΫγϣϯΛදݱ
  148. import RxDataSources enum SectionModel: Hashable { case content([Item]) case recommend([Item])

    enum Item: Hashable { case thumbnail case summary case contentInfo case other case recommend(IndexPath) } } extension SectionModel: AnimatableSectionModelType { var identity: AnyHashable { AnyHashable(self) } var items: [Item] { switch self { case let .content(items), let .recommend(items): return items } } init(original: SectionModel, items: [Item]) { switch original { case .content: self = .content(items) case .recommend: self = .recommend(items) } } } extension SectionModel.Item: IdentifiableType { var identity: AnyHashable { AnyHashable(self) } } wίϯςϯπ·ͨ͸ϨίϝϯυͷηΫγϣϯͰ දࣔ͢ΔཁૉΛදݱ
  149. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } }
  150. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } }
  151. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } }
  152. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } }
  153. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } } wཁૉʹ֘౰͢ΔCellΛdequeue͠LazyViews ͔ΒରԠ͢ΔViewΛઃఆ͢Δ
  154. final class DetailViewController: UIViewController { @IBOutlet weak var rootStackView: UIStackView!

    @IBOutlet weak var contentCollectionView: UICollectionView! ... private var lazyViews: LazyViews? private lazy var viewModel = DetailViewModel( ..., viewDidAppear: rx.methodInvoked(#selector(viewDidAppear(_:))).map { _ in }, traitCollectionDidChange: rx.methodInvoked(#selector(traitCollectionDidChange(_:))).map { ... }, toggleContentInfo: lazyRelays.didTapContentInfo.asObservable(), didFinishInitializingViews: lazyRelays.didFinishInitializingViews.asObservable() ) private typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<SectionModel> private lazy var dataSource = DataSource(configureCell: configureCell) private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in guard let me = self, let lazyView = me.lazyViews else { return collectionView.fallback() } switch item { case .thumbnail: let cell = collectionView.dequeueReusableCell(ContentCell.self, for: indexPath) cell.setContentView(lazyView.thumbnailView) return cell ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } } wϨίϝϯυͷCellΛdequeue͠ίϯςϯπ৘ใΛઃఆ͢Δ
  155. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Property<[Episode]> let

    readyForInitializingViews: Observable<Void> let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let sectionModels: Observable<[SectionModel]> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) let _sectionModels = BehaviorRelay<[SectionModel]>(value: []) self.sectionModels = Observable.combineLatest(didFinishInitializingViews.take(1), _sectionModels.asObservable()) { $1 } self.recommendList = Property(_recommendList) ... Observable.combineLatest(_isPortrait, _isContentInfoHidden, _recommendList) .map { isPortrait, isContentInfoHidden, recommendList -> [SectionModel] in let contentItems: [SectionModel.Item] = [ isPortrait ? .thumbnail : nil, .summary, isContentInfoHidden ? nil : .contentInfo, .other, .episodeList ].compactMap { $0 } let recommendSections: [SectionModel] if recommendList.isEmpty { recommendSections = [] } else { recommendSections = [.recommend( recommendList.enumerated().map { args in .recommend(IndexPath(row: args.offset, section: 0)) } )] } return [SectionModel.content(contentItems)] + recommendSections } .bind(to: _sectionModels) .disposed(by: disposeBag) ... } }
  156. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Property<[Episode]> let

    readyForInitializingViews: Observable<Void> let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let sectionModels: Observable<[SectionModel]> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) let _sectionModels = BehaviorRelay<[SectionModel]>(value: []) self.sectionModels = Observable.combineLatest(didFinishInitializingViews.take(1), _sectionModels.asObservable()) { $1 } self.recommendList = Property(_recommendList) ... Observable.combineLatest(_isPortrait, _isContentInfoHidden, _recommendList) .map { isPortrait, isContentInfoHidden, recommendList -> [SectionModel] in let contentItems: [SectionModel.Item] = [ isPortrait ? .thumbnail : nil, .summary, isContentInfoHidden ? nil : .contentInfo, .other, .episodeList ].compactMap { $0 } let recommendSections: [SectionModel] if recommendList.isEmpty { recommendSections = [] } else { recommendSections = [.recommend( recommendList.enumerated().map { args in .recommend(IndexPath(row: args.offset, section: 0)) } )] } return [SectionModel.content(contentItems)] + recommendSections } .bind(to: _sectionModels) .disposed(by: disposeBag) ... } } wViewͷ஗ԆॳظԽ͕׬ྃͨ͠ΒsectionModelΛൃՐ
  157. class DetailViewModel { let episodeDetail: Observable<EpisodeDetail> let recommendList: Property<[Episode]> let

    readyForInitializingViews: Observable<Void> let stackAxis: Observable<NSLayoutConstraint.Axis> let addThumbnailToRoot: Observable<Void> let sectionModels: Observable<[SectionModel]> init(..., traitCollectionDidChange: Observable<UITraitCollection>, toggleContentInfo: Observable<Void>, didFinishInitializingViews: Observable<Void>) { let _recommendList = BehaviorRelay<[Episode]>(value: []) let _isContentInfoHidden = BehaviorRelay<Bool>(value: true) let _isPortrait = BehaviorRelay<Bool>(value: true) let _sectionModels = BehaviorRelay<[SectionModel]>(value: []) self.sectionModels = Observable.combineLatest(didFinishInitializingViews.take(1), _sectionModels.asObservable()) { $1 } self.recommendList = Property(_recommendList) ... Observable.combineLatest(_isPortrait, _isContentInfoHidden, _recommendList) .map { isPortrait, isContentInfoHidden, recommendList -> [SectionModel] in let contentItems: [SectionModel.Item] = [ isPortrait ? .thumbnail : nil, .summary, isContentInfoHidden ? nil : .contentInfo, .other, .episodeList ].compactMap { $0 } let recommendSections: [SectionModel] if recommendList.isEmpty { recommendSections = [] } else { recommendSections = [.recommend( recommendList.enumerated().map { args in .recommend(IndexPath(row: args.offset, section: 0)) } )] } return [SectionModel.content(contentItems)] + recommendSections } .bind(to: _sectionModels) .disposed(by: disposeBag) ... } } wঢ়ଶ͕ߋ৽͞ΕΔͱ഑ྻͷ಺༰΋ߋ৽
  158. final class DetailViewController: UIViewController { ... override func viewDidLoad() {

    super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let thumbnailView = ThumbnailView.makeFromNib() let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } thumbnailView.removeFromSuperview() me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.sectionModels .bind(to: me.contentCollectionView.rx.items(dataSource: me.dataSource)) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } }
  159. final class DetailViewController: UIViewController { ... override func viewDidLoad() {

    super.viewDidLoad() viewModel.readyForInitializingViews .bind(to: Binder(self) { me, _ in let thumbnailView = ThumbnailView.makeFromNib() let summaryView = SummaryView.makeFromNib() me.viewModel.episodeDetail .bind(to: Binder(summaryView) { $0.setEpisode($1) }) .disposed(by: me.disposeBag) summaryView.didTapContentInfo .bind(to: me.lazyRelays.didTapContentInfo) .disposed(by: me.disposeBag) me.viewModel.stackAxis .bind(to: me.rootStackView.rx[\.axis]) .disposed(by: me.disposeBag) me.viewModel.addThumbnailToRoot .bind(to: Binder(me) { me, _ in guard let thumbnailView = me.lazyViews?.thumbnailView, !me.rootStackView.arrangedSubviews.contains(thumbnailView) else { return } thumbnailView.removeFromSuperview() me.rootStackView.addArrangedSubview(thumbnailView) }) .disposed(by: me.disposeBag) me.viewModel.sectionModels .bind(to: me.contentCollectionView.rx.items(dataSource: me.dataSource)) .disposed(by: me.disposeBag) me.lazyViews = LazyViews(...) me.lazyRelays.didFinishInitializingViews.accept(()) }) .disposed(by: disposeBag) } } wࠓճͷ৔߹RxDataSourcesΛར༻͍ͯ͠ΔͷͰ ࠩ෼ߋ৽ͰΞχϝʔγϣϯ෇͖Ͱը໘Λඳը
  160. վળޙͷը໘ճస଎౓

  161. ը໘ճస଎౓ վળޙ  ˞ճ࣮ࢪ iPhone 8 iOS12 ࠷খ 858ms ฏۉ

    897ms ࠷େ 1140ms
  162. ը໘ճస଎౓ ˞ճ࣮ࢪ iPhone 8 iOS12 ࠷খ 1100ms→858ms ฏۉ 1130ms→897ms ࠷େ

    1300ms→1140ms
  163. վળ఺ͷҰཡ w UIScrollView + UIStackViewͷߏ੒͔ΒUICollectionViewʹมߋ w ֤View͔ΒUIStackView + arrangedSubviewsͷ࣮૷ΛผViewʹ෼཭ w

    UIStackViewͷarrangedSubviewͷisHiddenʹΑΔϨΠΞ΢τߋ৽Λ ࠩ෼ߋ৽ϥΠϒϥϦΛ༻͍ͨUICollectionViewͷΞχϝʔγϣϯʹมߋ
  164. ߏ଄มߋʹΑΔ෭࣍తͳޮՌ

  165. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ฒͼସ͑ ABςετΛ࣮ࢪ

  166. enum SectionModel: Hashable { case content([Item]) case recommend([Item]) enum Item:

    Hashable { case thumbnail case summary case contentInfo case other case episodeList case recommend(IndexPath) case recommendList(IndexPath) } } final class DetailViewController: UIViewController { ... private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in ... switch item { ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell case let .recommendList(indexPath): let cell = collectionView.dequeueReusableCell(RecommendListCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } }
  167. enum SectionModel: Hashable { case content([Item]) case recommend([Item]) enum Item:

    Hashable { case thumbnail case summary case contentInfo case other case episodeList case recommend(IndexPath) case recommendList(IndexPath) } } final class DetailViewController: UIViewController { ... private lazy var configureCell: DataSource.ConfigureCell = { [weak self] _, collectionView, indexPath, item in ... switch item { ... case let .recommend(indexPath): let cell = collectionView.dequeueReusableCell(RecommendCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell case let .recommendList(indexPath): let cell = collectionView.dequeueReusableCell(RecommendListCell.self, for: indexPath) cell.setEpisode(me.viewModel.recommendList.value[indexPath.row]) return cell } } } wABςετͷର৅ͱͳΔཁૉͱ࣮૷ͷ௥Ճ
  168. class DetailViewModel { ... init(..., abtestManager: ABTestManger, ...) { ...

    Observable.combineLatest(_isPortrait, _isContentInfoHidden, _recommendList) .map { isPortrait, isContentInfoHidden, recommendList -> [SectionModel] in let contentItems: [SectionModel.Item] = [ isPortrait ? .thumbnail : nil, .summary, isContentInfoHidden ? nil : .contentInfo, .other, .episodeList ].compactMap { $0 } let recommendSections: [SectionModel] if recommendList.isEmpty { recommendSections = [] } else { let recommendItems: [SectionModel.Item] switch abtestManager.recommendPattern { case .list: recommendItems = recommendList.enumerated().map { args in .recommendList(IndexPath(row: args.offset, section: 0)) } case .grid: recommendItems = recommendList.enumerated().map { args in .recommend(IndexPath(row: args.offset, section: 0)) } } recommendSections = [.recommend(recommendItems)] } return [SectionModel.content(contentItems)] + recommendSections } .bind(to: _sectionModels) .disposed(by: disposeBag) } }
  169. class DetailViewModel { ... init(..., abtestManager: ABTestManger, ...) { ...

    Observable.combineLatest(_isPortrait, _isContentInfoHidden, _recommendList) .map { isPortrait, isContentInfoHidden, recommendList -> [SectionModel] in let contentItems: [SectionModel.Item] = [ isPortrait ? .thumbnail : nil, .summary, isContentInfoHidden ? nil : .contentInfo, .other, .episodeList ].compactMap { $0 } let recommendSections: [SectionModel] if recommendList.isEmpty { recommendSections = [] } else { let recommendItems: [SectionModel.Item] switch abtestManager.recommendPattern { case .list: recommendItems = recommendList.enumerated().map { args in .recommendList(IndexPath(row: args.offset, section: 0)) } case .grid: recommendItems = recommendList.enumerated().map { args in .recommend(IndexPath(row: args.offset, section: 0)) } } recommendSections = [.recommend(recommendItems)] } return [SectionModel.content(contentItems)] + recommendSections } .bind(to: _sectionModels) .disposed(by: disposeBag) } } wABςετͷঢ়ଶʹΑͬͯ௥Ճ͢ΔཁૉΛมߋ
  170. ·ͱΊ

  171. ·ͱΊ w Xib͸ॳճϩʔυʹେ෯ʹ͕͔͔࣌ؒΔ w ը໘ͷදࣔ଎౓͕஗͘ͳͬͨ৔߹ʹViewͷੜ੒Λ஗Ԇͤ͞ΔͱޮՌ͕ग़΍͍͢ ʢbindͨ͠௚ޙʹॲཧ͕ൃՐ͠ɺϨΠΞ΢τॲཧ͕࣮ߦ͞Εͨ͜ͱʹΑͬͯදࣔ ͕஗Ԇͯ͠͠·͍ͬͯͨՄೳੑ΋͋Δʣ w UICollectionViewʢ·ͨ͸UITableViewʣͰσʔλιʔεͷड͚౉͠Λ஗Ԇͤ͞Ε ͹ɺViewͷੜ੒Λ஗Ԇͤ͞Δ͜ͱ΋Ͱ͖Δ

    w σʔλιʔεΛએݴతʹ͢Δ͜ͱͰɺϞδϡʔϧͷೖΕସ͑΍௥Ճ͕؆୯ʹͳΔ
  172. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠