From 4a85f7355af2f4b84ce0fc026f6cb3a6ef0c84e3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 23 Sep 2026 23:47:21 +0200 Subject: [PATCH] Track the active chapter in PlayerState instead of per-tick time reads Chapter lists highlighted the active row by reading currentTime in the view body, re-rendering every row many times per second during playback. PlayerState now stores a change-guarded activeChapterID updated when time or chapters change, so views observe only chapter transitions. --- Yattee/Services/Player/PlayerState.swift | 24 ++++++++++++++++--- .../Player/Panels/ChaptersPanelPage.swift | 6 ++--- .../Views/Player/Panels/InfoPanelPage.swift | 6 ++--- .../Player/tvOS/TVDetailsPanelPages.swift | 6 ++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Yattee/Services/Player/PlayerState.swift b/Yattee/Services/Player/PlayerState.swift index b753d87f..f53c7b9c 100644 --- a/Yattee/Services/Player/PlayerState.swift +++ b/Yattee/Services/Player/PlayerState.swift @@ -226,7 +226,9 @@ final class PlayerState { // MARK: - Time & Progress /// Current playback time in seconds. - var currentTime: TimeInterval = 0 + var currentTime: TimeInterval = 0 { + didSet { updateActiveChapter() } + } /// Total duration in seconds. var duration: TimeInterval = 0 @@ -356,11 +358,27 @@ final class PlayerState { // MARK: - Chapters /// Video chapters if available. - var chapters: [VideoChapter] = [] + var chapters: [VideoChapter] = [] { + didSet { updateActiveChapter() } + } + + /// ID of the chapter containing the current playback time. Stored and + /// change-guarded so views highlighting the active chapter observe only + /// chapter transitions, not every time-pos tick — reading `currentTime` + /// from a panel body re-renders it many times per second during playback. + private(set) var activeChapterID: UUID? /// Current chapter based on playback time. var currentChapter: VideoChapter? { - chapters.last { $0.startTime <= currentTime } + guard let activeChapterID else { return nil } + return chapters.first { $0.id == activeChapterID } + } + + private func updateActiveChapter() { + let id = chapters.last { $0.startTime <= currentTime }?.id + if activeChapterID != id { + activeChapterID = id + } } // MARK: - Storyboards diff --git a/Yattee/Views/Player/Panels/ChaptersPanelPage.swift b/Yattee/Views/Player/Panels/ChaptersPanelPage.swift index 2100de75..ee54910f 100644 --- a/Yattee/Views/Player/Panels/ChaptersPanelPage.swift +++ b/Yattee/Views/Player/Panels/ChaptersPanelPage.swift @@ -19,7 +19,6 @@ struct ChaptersPanelPage: View { private var playerState: PlayerState? { playerService?.state } private var chapters: [VideoChapter] { playerState?.chapters ?? [] } - private var currentTime: TimeInterval { playerState?.currentTime ?? 0 } private var storyboard: Storyboard? { playerState?.preferredStoryboard } var body: some View { @@ -50,9 +49,8 @@ struct ChaptersPanelPage: View { } private func isChapterActive(_ chapter: VideoChapter) -> Bool { - let nextChapter = chapters.first { $0.startTime > chapter.startTime } - let endTime = nextChapter?.startTime ?? .infinity - return currentTime >= chapter.startTime && currentTime < endTime + // Observes only chapter transitions, not per-tick currentTime updates. + playerState?.activeChapterID == chapter.id } private func preloadChapterSheets() async { diff --git a/Yattee/Views/Player/Panels/InfoPanelPage.swift b/Yattee/Views/Player/Panels/InfoPanelPage.swift index 1a92c421..dd7d573b 100644 --- a/Yattee/Views/Player/Panels/InfoPanelPage.swift +++ b/Yattee/Views/Player/Panels/InfoPanelPage.swift @@ -230,7 +230,6 @@ struct InfoPanelPage: View { // MARK: - Inline Sections (prototype) private var chapters: [VideoChapter] { playerState?.chapters ?? [] } - private var currentTime: TimeInterval { playerState?.currentTime ?? 0 } private var storyboard: Storyboard? { playerState?.preferredStoryboard } private var relatedVideos: [Video] { video.relatedVideos ?? [] } @@ -308,9 +307,8 @@ struct InfoPanelPage: View { } private func isChapterActive(_ chapter: VideoChapter) -> Bool { - let nextChapter = chapters.first { $0.startTime > chapter.startTime } - let endTime = nextChapter?.startTime ?? .infinity - return currentTime >= chapter.startTime && currentTime < endTime + // Observes only chapter transitions, not per-tick currentTime updates. + playerState?.activeChapterID == chapter.id } private func preloadChapterSheets() async { diff --git a/Yattee/Views/Player/tvOS/TVDetailsPanelPages.swift b/Yattee/Views/Player/tvOS/TVDetailsPanelPages.swift index 69055131..c854fa60 100644 --- a/Yattee/Views/Player/tvOS/TVDetailsPanelPages.swift +++ b/Yattee/Views/Player/tvOS/TVDetailsPanelPages.swift @@ -92,7 +92,6 @@ struct TVChaptersPanelList: View { private var playerState: PlayerState? { playerService?.state } private var chapters: [VideoChapter] { playerState?.chapters ?? [] } - private var currentTime: TimeInterval { playerState?.currentTime ?? 0 } var body: some View { ScrollViewReader { proxy in @@ -124,9 +123,8 @@ struct TVChaptersPanelList: View { } private func isChapterActive(_ chapter: VideoChapter) -> Bool { - let nextChapter = chapters.first { $0.startTime > chapter.startTime } - let endTime = nextChapter?.startTime ?? .infinity - return currentTime >= chapter.startTime && currentTime < endTime + // Observes only chapter transitions, not per-tick currentTime updates. + playerState?.activeChapterID == chapter.id } }