From cc5aed6c132f9b7df4ea2f0edf83bc363defaf88 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Tue, 28 May 2024 12:22:58 -0300 Subject: [PATCH] Improved handling of schedule event hero --- WWDC/AppCoordinator.swift | 14 +++- WWDC/EventHeroViewController.swift | 85 +++++++++++----------- WWDC/Main.xcconfig | 2 +- WWDC/ScheduleContainerViewController.swift | 21 +++--- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/WWDC/AppCoordinator.swift b/WWDC/AppCoordinator.swift index 3858edcd..381289a3 100644 --- a/WWDC/AppCoordinator.swift +++ b/WWDC/AppCoordinator.swift @@ -303,13 +303,21 @@ final class AppCoordinator: Logging, Signposting { // Wait for the data to be loaded to hide the loading spinner // this avoids some jittery UI. Technically this could be changed to only watch // the tab that will be visible during startup. - Publishers.CombineLatest( + Publishers.CombineLatest3( self.videosController.listViewController.$hasPerformedInitialRowDisplay, - self.scheduleController.splitViewController.listViewController.$hasPerformedInitialRowDisplay + self.scheduleController.splitViewController.listViewController.$hasPerformedInitialRowDisplay, + self.scheduleController.$isShowingHeroView ) .replaceErrorWithEmpty() .drop { - $0.0 == false || $0.1 == false + /// The videos tab has content. + let videosAvailable = $0.0 + /// The schedule tab has content. + let scheduleAvailable = $0.1 + /// The schedule tab has an event hero landing screen. + let scheduleHeroAvailable = $0.2 + /// We want to reveal the UI once the videos tab has content and the schedule tab has content, be it a schedule or a landing screen. + return videosAvailable == false || (scheduleAvailable == false && scheduleHeroAvailable == false) } .prefix(1) // Happens once then automatically completes .receive(on: DispatchQueue.main) diff --git a/WWDC/EventHeroViewController.swift b/WWDC/EventHeroViewController.swift index 7f0029af..4736cf78 100644 --- a/WWDC/EventHeroViewController.swift +++ b/WWDC/EventHeroViewController.swift @@ -142,49 +142,48 @@ public final class EventHeroViewController: NSViewController { private lazy var cancellables: Set = [] private func bindViews() { - let image = $hero.compactMap({ $0?.backgroundImage }).compactMap(URL.init) - - image.driveUI { [weak self] imageUrl in - guard let self = self else { return } - - self.imageDownloadOperation?.cancel() - - self.imageDownloadOperation = ImageDownloadCenter.shared.downloadImage(from: imageUrl, thumbnailHeight: Constants.thumbnailHeight) { url, result in - guard url == imageUrl, result.original != nil else { return } - - self.backgroundImageView.image = result.original - } - }.store(in: &cancellables) - - let heroUnavailable = $hero.map({ $0 == nil }) - heroUnavailable.replaceError(with: true).driveUI(\.isHidden, on: backgroundImageView).store(in: &cancellables) - heroUnavailable.toggled().replaceError(with: false).driveUI(\.isHidden, on: placeholderImageView).store(in: &cancellables) - - $hero.map(\.?.title).replaceNil(with: "Schedule not available").replaceError(with: "Schedule not available").driveUI(\.stringValue, on: titleLabel).store(in: &cancellables) - $hero.map({ hero in - let unavailable = "The schedule is not currently available. Check back later." - guard let hero = hero else { return unavailable } - if hero.textComponents.isEmpty { - return hero.body - } else { - return hero.textComponents.joined(separator: "\n\n") - } - }).replaceError(with: "").driveUI(\.stringValue, on: bodyLabel).store(in: &cancellables) - - $hero.compactMap({ $0?.titleColor }).driveUI { [weak self] colorHex in - guard let self = self else { return } - self.titleLabel.textColor = NSColor.fromHexString(hexString: colorHex) - }.store(in: &cancellables) - - // Dim background when there's a lot of text to show - $hero.compactMap({ $0 }).map({ $0.textComponents.count > 2 }).driveUI { [weak self] largeText in - self?.backgroundImageView.alphaValue = 0.5 - }.store(in: &cancellables) - - $hero.compactMap({ $0?.bodyColor }).driveUI { [weak self] colorHex in - guard let self = self else { return } - self.bodyLabel.textColor = NSColor.fromHexString(hexString: colorHex) - }.store(in: &cancellables) + $hero + .drop(while: { $0 == nil }) + .sink + { [weak self] hero in + guard let self else { return } + + /// A lack of event hero is handled by the app coordinator / schedule controller by hiding this view controller, + /// so there's no special handling required when the hero is not available. + guard let hero else { return } + + setupBackground(with: hero) + setupText(with: hero) + } + .store(in: &cancellables) + } + + private func setupBackground(with hero: EventHero) { + guard let imageUrl = URL(string: hero.backgroundImage) else { return } + + placeholderImageView.isHidden = true + + backgroundImageView.alphaValue = hero.textComponents.count > 1 ? 0.5 : 1 + backgroundImageView.isHidden = false + + imageDownloadOperation?.cancel() + + imageDownloadOperation = ImageDownloadCenter.shared.downloadImage(from: imageUrl, thumbnailHeight: Constants.thumbnailHeight) { url, result in + guard url == imageUrl, result.original != nil else { return } + + self.backgroundImageView.image = result.original + } + } + + private func setupText(with hero: EventHero) { + titleLabel.stringValue = hero.title + if hero.textComponents.isEmpty { + bodyLabel.stringValue = hero.body + } else { + bodyLabel.stringValue = hero.textComponents.joined(separator: "\n\n") + } + titleLabel.textColor = hero.titleColor.flatMap { NSColor.fromHexString(hexString: $0) } + bodyLabel.textColor = hero.bodyColor.flatMap { NSColor.fromHexString(hexString: $0) } } } diff --git a/WWDC/Main.xcconfig b/WWDC/Main.xcconfig index 8767a61f..12302a11 100644 --- a/WWDC/Main.xcconfig +++ b/WWDC/Main.xcconfig @@ -3,7 +3,7 @@ #include "TeamID.xcconfig" MARKETING_VERSION = 7.4.2 -CURRENT_PROJECT_VERSION = 1040 +CURRENT_PROJECT_VERSION = 1041 MACOSX_DEPLOYMENT_TARGET = 12.0 diff --git a/WWDC/ScheduleContainerViewController.swift b/WWDC/ScheduleContainerViewController.swift index 3ad3cba8..3308435e 100644 --- a/WWDC/ScheduleContainerViewController.swift +++ b/WWDC/ScheduleContainerViewController.swift @@ -69,15 +69,18 @@ final class ScheduleContainerViewController: WWDCWindowContentViewController { } private func bindViews() { - $isShowingHeroView.replaceError(with: false).driveUI(\.view.isHidden, on: splitViewController) - .store(in: &cancellables) - - $isShowingHeroView.toggled().replaceError(with: true) - .driveUI(\.view.isHidden, on: heroController) - .store(in: &cancellables) - - $isShowingHeroView.driveUI { [weak self] _ in - self?.view.needsUpdateConstraints = true + /// The debounce in here prevents a little UI flicker when the event hero is available. + $isShowingHeroView + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .removeDuplicates() + .sink + { [weak self] isShowingHeroView in + guard let self else { return } + + splitViewController.view.isHidden = isShowingHeroView + heroController.view.isHidden = !isShowingHeroView + + view.needsUpdateConstraints = true } .store(in: &cancellables) }