From 879bbd37c0fac1373b138b0868b2cf11088d1d69 Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Thu, 2 Jan 2025 12:35:39 +0500 Subject: [PATCH 1/5] fix: auto scroll to current subtitle after some seconds --- .../Presentation/Video/SubtitlesView.swift | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 0783415d..a8082205 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -28,6 +28,8 @@ public struct SubtitlesView: View { @State var pause: Bool = false @State var languages: [SubtitleUrl] + @State private var autoScrollPublisher = PassthroughSubject() + public init(languages: [SubtitleUrl], currentTime: Binding, viewModel: VideoPlayerViewModel, @@ -79,13 +81,6 @@ public struct SubtitlesView: View { .onChange(of: currentTime, perform: { _ in if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { - if id != subtitle.id { - withAnimation { - if !pause { - scroll.scrollTo(subtitle.id, anchor: .top) - } - } - } self.id = subtitle.id } }) @@ -93,11 +88,27 @@ public struct SubtitlesView: View { }.id(subtitle.id) } } + .onChange(of: id) { _ in + withAnimation { + if !pause { + scroll.scrollTo(id, anchor: .top) + } + } + } } - }.simultaneousGesture( + } + .simultaneousGesture( DragGesture().onChanged({ _ in - pauseScrolling() - })) + pause = true + autoScrollPublisher.send() + }) + ) + .onReceive(autoScrollPublisher.debounce(for: .seconds(3), scheduler: DispatchQueue.main)) { _ in + if pause { + refreshID() + pause = false + } + } } }.padding(.horizontal, 24) .padding(.top, 16) @@ -105,12 +116,16 @@ public struct SubtitlesView: View { } } - private func pauseScrolling() { - pause = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.pause = false + private func refreshID() { + if let subtitle = subtitle(at: currentTime) { + id = subtitle.id } } + + private func subtitle(at time: Double) -> Subtitle? { + let date = Date(milliseconds: time) + return viewModel.subtitles.first { $0.fromTo.contains(date) } + } } #if DEBUG From 8161cab450b9a8c68d29400e833ba7188d291862 Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Tue, 7 Jan 2025 16:59:35 +0500 Subject: [PATCH 2/5] fix: ensure scroll to current subtitle based on changes in current time --- .../Presentation/Video/SubtitlesView.swift | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index a8082205..c59a5318 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -78,23 +78,10 @@ public struct SubtitlesView: View { .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime)) ? Theme.Colors.accentButtonColor : Theme.Colors.textPrimary) - - .onChange(of: currentTime, perform: { _ in - if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { - self.id = subtitle.id - } - }) }) }.id(subtitle.id) } } - .onChange(of: id) { _ in - withAnimation { - if !pause { - scroll.scrollTo(id, anchor: .top) - } - } - } } } .simultaneousGesture( @@ -103,6 +90,16 @@ public struct SubtitlesView: View { autoScrollPublisher.send() }) ) + .onChange(of: currentTime) { _ in + refreshID() + } + .onChange(of: id) { newID in + if !pause { + withAnimation { + scroll.scrollTo(newID, anchor: .top) + } + } + } .onReceive(autoScrollPublisher.debounce(for: .seconds(3), scheduler: DispatchQueue.main)) { _ in if pause { refreshID() @@ -117,15 +114,10 @@ public struct SubtitlesView: View { } private func refreshID() { - if let subtitle = subtitle(at: currentTime) { + if let subtitle = viewModel.findSubtitle(at: Date(milliseconds: currentTime)) { id = subtitle.id } } - - private func subtitle(at time: Double) -> Subtitle? { - let date = Date(milliseconds: time) - return viewModel.subtitles.first { $0.fromTo.contains(date) } - } } #if DEBUG From 6bb5a4fb5b508d86df8343680fbde9d983fc677c Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Wed, 8 Jan 2025 10:45:17 +0500 Subject: [PATCH 3/5] fix: scroll to current subtitle without animation when video is playing to avoid inconsistencies --- Course/Course/Presentation/Video/SubtitlesView.swift | 7 ++++++- .../Course/Presentation/Video/VideoPlayerViewModel.swift | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index c59a5318..483879d5 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -93,9 +93,14 @@ public struct SubtitlesView: View { .onChange(of: currentTime) { _ in refreshID() } + .onChange(of: viewModel.isPlaying) { isPlaying in + if !pause && isPlaying { + scroll.scrollTo(id, anchor: .top) + } + } .onChange(of: id) { newID in if !pause { - withAnimation { + withAnimation(viewModel.isPlaying ? .default : nil) { scroll.scrollTo(newID, anchor: .top) } } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index eb837d9b..263dbb43 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -11,6 +11,7 @@ import _AVKit_SwiftUI import Combine public class VideoPlayerViewModel: ObservableObject { + @Published private(set) var isPlaying: Bool = false @Published var pause: Bool = false @Published var currentTime: Double = 0 @Published var isLoading: Bool = true @@ -101,6 +102,7 @@ public class VideoPlayerViewModel: ObservableObject { playerHolder.getRatePublisher() .sink {[weak self] rate in guard self?.isLoading == false else { return } + self?.isPlaying = rate != 0 self?.trackVideoSpeedChange(rate: rate) } .store(in: &subscription) From 3a2520a480dbc8c1055bba1febc17fb51c1330a5 Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Wed, 8 Jan 2025 13:20:37 +0500 Subject: [PATCH 4/5] fix: skip scroll requests if an animation is already in progress --- .../Presentation/Video/SubtitlesView.swift | 44 ++++++++++++++++--- .../Video/VideoPlayerViewModel.swift | 3 +- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 483879d5..ea8c1c24 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -28,6 +28,9 @@ public struct SubtitlesView: View { @State var pause: Bool = false @State var languages: [SubtitleUrl] + @State private var isAnimating = false + @State private var syncBlock: (() -> Void)? + @State private var autoScrollPublisher = PassthroughSubject() public init(languages: [SubtitleUrl], @@ -94,16 +97,12 @@ public struct SubtitlesView: View { refreshID() } .onChange(of: viewModel.isPlaying) { isPlaying in - if !pause && isPlaying { - scroll.scrollTo(id, anchor: .top) + if isPlaying { + scrollTo(id, in: scroll) } } .onChange(of: id) { newID in - if !pause { - withAnimation(viewModel.isPlaying ? .default : nil) { - scroll.scrollTo(newID, anchor: .top) - } - } + scrollTo(newID, in: scroll) } .onReceive(autoScrollPublisher.debounce(for: .seconds(3), scheduler: DispatchQueue.main)) { _ in if pause { @@ -123,6 +122,37 @@ public struct SubtitlesView: View { id = subtitle.id } } + + private func scrollTo(_ viewID: Int, in scrollView: ScrollViewProxy) { + if !pause { + let scrollBlock = { + if viewModel.isPlaying { + isAnimating = true + + withAnimation(.linear(duration: 0.3)) { + scrollView.scrollTo(viewID, anchor: .top) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + isAnimating = false + + if let nextBlock = syncBlock { + syncBlock = nil + nextBlock() + } + } + } else { + scrollView.scrollTo(viewID, anchor: .top) + } + } + + if isAnimating { + syncBlock = scrollBlock + } else { + scrollBlock() + } + } + } } #if DEBUG diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index 263dbb43..e9cf02db 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -101,8 +101,9 @@ public class VideoPlayerViewModel: ObservableObject { playerHolder.getRatePublisher() .sink {[weak self] rate in - guard self?.isLoading == false else { return } self?.isPlaying = rate != 0 + + guard self?.isLoading == false else { return } self?.trackVideoSpeedChange(rate: rate) } .store(in: &subscription) From 214771449949348e9bec2727650b5309ef32bf16 Mon Sep 17 00:00:00 2001 From: Tayyab Akram Date: Tue, 21 Jan 2025 15:32:13 +0500 Subject: [PATCH 5/5] chore: put constants in a private enum --- .../Presentation/Video/SubtitlesView.swift | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index ea8c1c24..537ebd62 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -33,6 +33,12 @@ public struct SubtitlesView: View { @State private var autoScrollPublisher = PassthroughSubject() + private enum Constants { + static let autoScrollInterval: TimeInterval = 3.0 + static let animationDuration: TimeInterval = 0.3 + static let animationSkipInterval: TimeInterval = animationDuration + 0.05 + } + public init(languages: [SubtitleUrl], currentTime: Binding, viewModel: VideoPlayerViewModel, @@ -104,12 +110,18 @@ public struct SubtitlesView: View { .onChange(of: id) { newID in scrollTo(newID, in: scroll) } - .onReceive(autoScrollPublisher.debounce(for: .seconds(3), scheduler: DispatchQueue.main)) { _ in - if pause { - refreshID() - pause = false + .onReceive( + autoScrollPublisher.debounce( + for: .seconds(Constants.autoScrollInterval), + scheduler: DispatchQueue.main + ), + perform: { _ in + if pause { + refreshID() + pause = false + } } - } + ) } }.padding(.horizontal, 24) .padding(.top, 16) @@ -129,11 +141,11 @@ public struct SubtitlesView: View { if viewModel.isPlaying { isAnimating = true - withAnimation(.linear(duration: 0.3)) { + withAnimation(.linear(duration: Constants.animationDuration)) { scrollView.scrollTo(viewID, anchor: .top) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationSkipInterval) { isAnimating = false if let nextBlock = syncBlock {