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

iOSDC Japan 2020 Day 1 Track B 10:50

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

Taiki Suzuki

September 20, 2020
Tweet

More Decks by Taiki Suzuki

Other Decks in Programming

Transcript

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

    γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ ϑϧεΫϦʔϯ    ίϯςϯπ͕ࢹௌՄೳͳ৔߹
  2. class VideoFullscreenViewController: UIViewController { init() { super.init(nibName: nil, bundle: nil)

    } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } } init͔ΒviewDidAppear·ͰΛը໘͕දࣔ͞Ε·Ͱͱఆٛ
  3. 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") } }
  4. 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") } }
  5. 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") } }
  6. 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") } }
  7. 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() }
  8. 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() }
  9. 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() }
  10. 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() }
  11. 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() }
  12. 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ΛॳظԽͯ͠ϨΠΞ΢τ ॲཧΛ࣮ߦ͢Δ
  13. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

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

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

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

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

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

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

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

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

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

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  23. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ 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ॳظ࣮૷౰ॳ͸݅·Ͱ͔͠දࣔ͠ͳ͍࢓༷ͩͬͨͷͰڐ༰Ͱ͖͕ͨ ͕࣌ܦͬͯमਖ਼લͷஈ֊Ͱ݅·ͰϨίϝϯυ͕දࣔ͞ΕΔΑ͏ʹͳ͍ͬͯͨ
  24. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VideoPlayerViewController.view VerticalStackView { ScrollView {

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

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

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

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

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

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

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

    VerticalStackView { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }   
  32. දࣔ଎౓ վળલ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ

    1360ms 2100ms ճ໨Ҏ߱ 760ms 1180ms ॳճͱճ໨Ҏ߱Ͱදࣔ଎౓ͷ͕ࠩ͋Δ
  33. 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.
  34. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ 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 ... } ... } } } } }   
  35. 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 } }
  36. 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 } }
  37. 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 } }
  38. ॳظԽ࣌ؒ (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
  39. 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) ... } }
  40. 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) ... } }
  41. 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) } }
  42. 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Ͱͳ͔ͬͨͱ͖ʹ஋Λྲྀ͢
  43. 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ॳظԽ͞ΕͨλΠϛϯάͰৄࡉ৘ใऔಘΛ࣮ߦ
  44. 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) ... } }
  45. 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ͷऔಘ׬ྃޙʹϨΠΞ΢τॲཧ
  46. 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) } }
  47. 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ଈ࣌ʹϨίϝϯυͷ஋͕ྲྀΕΔ
  48. 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Ϩίϝϯυ͕ۭ͔Ͳ͏͔͔Βม׵ͨ͠ ஋͕ଈ࣌ʹྲྀΕΔ
  49. 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ॳظԽ͞ΕͨλΠϛϯάͰϨίϝϯυऔಘΛ࣮ߦ
  50. 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) ... } }
  51. 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ͰϨΠΞ΢τॲཧ
  52. 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) } }
  53. 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͕λοϓ͞ΕΔͱൃՐ
  54. 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ͷ஋͕ྲྀΕΔ
  55. 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ͷ ஋Λ൓సͤ͞Δ
  56. 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) ... } }
  57. 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Ͱଈ࣌ʹ஋͕ྲྀΕ͖ͯͯϨΠΞ΢τॲཧ
  58. 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͕ൃՐ͢ΔͱϨΠΞ΢τॲཧ
  59. 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>() } }
  60. 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Λอ࣋͢ΔΫϥε
  61. 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Λอ࣋͢ΔΫϥε
  62. 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) } }
  63. 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) } }
  64. 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) } }
  65. 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
 ͕࣮ߦ͞ΕΔͱ౓͚ͩྲྀΕΔ
  66. 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) } }
  67. 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) } }
  68. 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) } }
  69. 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ͷ஗ԆॳظԽ͕׬ྃ͢ΔͱྲྀΕΔ
  70. 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ͷ஗ԆॳظԽ͕׬ྃޙʹϨίϝϯυͷ औಘΛ࣮ߦ͢Δ
  71. 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) } }
  72. 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) } }
  73. 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ͷ஗ԆॳظԽ͕׬ྃҎ߱ʹϨίϝϯυ ͷ஋ΛϨΠΞ΢τॲཧ
  74. 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) } }
  75. 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
  76. 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() } }
  77. 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() } }
  78. දࣔ଎౓ iPhone XS Max iOS13 iPhone 8 iOS12 ॳճ 1360ms


    ↓
 776ms 2100ms
 ↓
 1060ms ճ໨Ҏ߱ 760ms
 ↓
 580ms 1180ms
 ↓
 653ms
  79. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ VerticalStackView { ScrollView { VerticalStackView

    { ThumbnailView SummaryView { VerticalStackView { Label ContentInfoView (Expand Only) } } OtherView { VerticalStackView { ActionButtonsView EpisodeListView } } RecommendView { CollectionView { RecommendSection { RecommendCell RecommendCell ... } RecommendSection { RecommendCell RecommendCell ... } ... } } } } }
  80. 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") } } }
  81. 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") } } }
  82. 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() } }
  83. 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() } }
  84. 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ͷ࠶ར༻͸͞Ε͍ͯͳ͍ঢ়ଶ
  85. 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) } }
  86. 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Λ΋ͱʹॎɾԣΛ൑ఆ
  87. 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ͷ ελοΫํ޲Λઃఆ
  88. 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) } }
  89. 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) } }
  90. 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) } }
  91. 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͢Δ͔ͷΠϕϯτΛྲྀ͢
  92. 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) } }
  93. 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) } }
  94. 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) } }
  95. 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) } }
  96. 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) } }
  97. 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) } }
  98. ϚΠϏσΦ μ΢ϯϩʔυ γΣΞ ΤϐιʔυҰཡ ΋ͬͱΈΔʼ 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 ... } ... } } } } }
  99. 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 } } }
  100. 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Λ෼཭
  101. 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
  102. 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) } }
  103. 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ίϯςϯπʹؔ͢ΔηΫγϣϯͱ Ϩίϝϯυʹؔ͢ΔηΫγϣϯΛදݱ
  104. 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ίϯςϯπ·ͨ͸ϨίϝϯυͷηΫγϣϯͰ දࣔ͢ΔཁૉΛදݱ
  105. 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 } } }
  106. 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 } } }
  107. 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 } } }
  108. 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 } } }
  109. 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Λઃఆ͢Δ
  110. 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͠ίϯςϯπ৘ใΛઃఆ͢Δ
  111. 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) ... } }
  112. 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ΛൃՐ
  113. 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ঢ়ଶ͕ߋ৽͞ΕΔͱ഑ྻͷ಺༰΋ߋ৽
  114. 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) } }
  115. 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Λར༻͍ͯ͠ΔͷͰ ࠩ෼ߋ৽ͰΞχϝʔγϣϯ෇͖Ͱը໘Λඳը
  116. վળ఺ͷҰཡ w UIScrollView + UIStackViewͷߏ੒͔ΒUICollectionViewʹมߋ w ֤View͔ΒUIStackView + arrangedSubviewsͷ࣮૷ΛผViewʹ෼཭ w

    UIStackViewͷarrangedSubviewͷisHiddenʹΑΔϨΠΞ΢τߋ৽Λ ࠩ෼ߋ৽ϥΠϒϥϦΛ༻͍ͨUICollectionViewͷΞχϝʔγϣϯʹมߋ
  117. 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 } } }
  118. 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ςετͷର৅ͱͳΔཁૉͱ࣮૷ͷ௥Ճ
  119. 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) } }
  120. 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ςετͷঢ়ଶʹΑͬͯ௥Ճ͢ΔཁૉΛมߋ