Skip to content

Pause media playback when swiping to the next submission #3393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public protocol PagesViewControllerDataSource: AnyObject {
@objc optional func pagesViewController(_ pages: PagesViewController, didTransitionTo page: UIViewController)
}

public final class PagesViewController: UIViewController, UIScrollViewDelegate {
public class PagesViewController: UIViewController, UIScrollViewDelegate {
public weak var dataSource: PagesViewControllerDataSource?
public weak var delegate: PagesViewControllerDelegate?
public let scrollView = UIScrollView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import AVKit
import UIKit
import WebKit

Expand Down Expand Up @@ -76,6 +77,19 @@ extension UIViewController {
}
}

public func findAllChildViewControllers<T: UIViewController>(ofType type: T.Type) -> Set<T> {
var result = Set<T>()

for subview in children {
if let match = subview as? T {
result.insert(match)
}
result.formUnion(subview.findAllChildViewControllers(ofType: type))
}

return result
}

public func addNavigationButton(_ button: UIBarButtonItem, side: NavigationItemSide) {
switch side {
case .right:
Expand Down Expand Up @@ -223,11 +237,30 @@ extension UIViewController {
}
}

/// Pauses media playback on all WKWebView instances in the view hierarchy.
/// Pauses media playback on all `WKWebView` instances in the view hierarchy.
@objc
public func pauseWebViewPlayback() {
view.findAllSubviews(ofType: WKWebView.self).forEach { $0.pauseAllMediaPlayback() }
}

/// Pauses media playback on all `AVPlayerViewController` instances in the view hierarchy.
@objc
public func pauseMediaPlayback() {
findAllChildViewControllers(ofType: AVPlayerViewController.self).forEach { $0.player?.pause() }
}

#if DEBUG

public func printViewControllerHierarchy(nestingLevel: Int = 0) {
let prefix = (nestingLevel == 0) ? "" : String(repeating: " ", count: nestingLevel * 4).appending("|-")
print("\(prefix)\(type(of: self))")

for childViewController in children {
childViewController.printViewControllerHierarchy(nestingLevel: nestingLevel + 1)
}
}

#endif
}

public class ResetTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
Expand Down
16 changes: 14 additions & 2 deletions Core/Core/Common/Extensions/UIKit/UIViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,23 @@ extension UIView {
for subview in subviews {
if let match = subview as? T {
result.insert(match)
} else {
result.formUnion(subview.findAllSubviews(ofType: type))
}
result.formUnion(subview.findAllSubviews(ofType: type))
}

return result
}

#if DEBUG

public func printViewHierarchy(nestingLevel: Int = 0) {
let prefix = (nestingLevel == 0) ? "" : String(repeating: " ", count: nestingLevel * 4).appending("|-")
print("\(prefix)\(type(of: self))")

for subview in subviews {
subview.printViewHierarchy(nestingLevel: nestingLevel + 1)
}
}

#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import XCTest
import UIKit
@testable import Core
import WebKit
import AVKit

class UIViewControllerExtensionsTests: XCTestCase {
class MockController: UIViewController {
Expand Down Expand Up @@ -114,6 +115,48 @@ class UIViewControllerExtensionsTests: XCTestCase {
XCTAssertTrue(mockWebView.pauseAllMediaPlaybackCalled)
XCTAssertTrue(mockNestedWebView.pauseAllMediaPlaybackCalled)
}

func test_findAllChildViewControllers() {
let parent = UIViewController()

let child1 = UIViewController()
let child1_1 = UIViewController()
child1.addChild(child1_1)

let child2 = UIViewController()

parent.addChild(child1)
parent.addChild(child2)

// WHEN
let result = parent.findAllChildViewControllers(ofType: UIViewController.self)

// THEN
XCTAssertEqual(result, Set([child1, child1_1, child2]))
}

func test_pauseMediaPlayback() {
let parentViewController = UIViewController()

let mockPlayer1 = MockAVPlayerViewController()
let mockPlayer2 = MockAVPlayerViewController()
let deepNestedPlayer = MockAVPlayerViewController()

let containerController = UIViewController()
containerController.addChild(deepNestedPlayer)

parentViewController.addChild(mockPlayer1)
parentViewController.addChild(mockPlayer2)
parentViewController.addChild(containerController)

// WHEN
parentViewController.pauseMediaPlayback()

// THEN
XCTAssertTrue(mockPlayer1.playerMock.pauseCalled)
XCTAssertTrue(mockPlayer2.playerMock.pauseCalled)
XCTAssertTrue(deepNestedPlayer.playerMock.pauseCalled)
}
}

private class MockWKWebView: WKWebView {
Expand All @@ -125,3 +168,24 @@ private class MockWKWebView: WKWebView {
pauseAllMediaPlaybackCalled = true
}
}

private class MockAVPlayerViewController: AVPlayerViewController {
let playerMock = MockAVPlayer()

init() {
super.init(nibName: nil, bundle: nil)
player = playerMock
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

private class MockAVPlayer: AVPlayer {
var pauseCalled = false

override func pause() {
pauseCalled = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,23 @@ class UIViewExtensionsTests: XCTestCase {

func test_findAllSubviews() {
let label1 = UILabel()
let nestedLabel = UILabel()
let containerView = UIView()
containerView.addSubview(label1)
containerView.addSubview(nestedLabel)
let label1_1 = UILabel()
label1.addSubview(label1_1)

let containerView = UIView()
let label2 = UILabel()
let button = UIButton()
containerView.addSubview(label2)
containerView.addSubview(button)

let parentView = UIView()
parentView.addSubview(label2)
parentView.addSubview(button)
parentView.addSubview(label1)
parentView.addSubview(containerView)

// WHEN
let foundLabels = parentView.findAllSubviews(ofType: UILabel.self)

// THEN
XCTAssertEqual(foundLabels, Set([label1, label2, nestedLabel]))
XCTAssertEqual(foundLabels, Set([label1, label1_1, label2]))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Combine
import Core
import SwiftUI

class SpeedGraderViewModel: ObservableObject, PagesViewControllerDataSource, PagesViewControllerDelegate {
class SpeedGraderViewModel: ObservableObject {
typealias Page = CoreHostingController<SubmissionGraderView>

// MARK: - Outputs
Expand Down Expand Up @@ -67,58 +67,6 @@ class SpeedGraderViewModel: ObservableObject, PagesViewControllerDataSource, Pag
interactor.load()
}

// MARK: - PagesViewControllerDataSource

func pagesViewController(_ pages: PagesViewController, pageBefore page: UIViewController) -> UIViewController? {
(page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList - 1) }
}

func pagesViewController(_ pages: PagesViewController, pageAfter page: UIViewController) -> UIViewController? {
(page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList + 1) }
}

func controller(for index: Int) -> UIViewController? {
let controller = grader(for: index).map { CoreHostingController($0, env: environment) }
controller?.view.backgroundColor = nil
return controller
}

func grader(for index: Int) -> SubmissionGraderView? {
guard
let data = interactor.data,
index >= 0,
index < data.submissions.count
else { return nil }

return SubmissionGraderView(
env: environment,
userIndexInSubmissionList: index,
viewModel: SubmissionGraderViewModel(
assignment: data.assignment,
submission: data.submissions[index],
contextColor: interactor.contextInfo.compactMap { $0?.courseColor }.eraseToAnyPublisher()
),
landscapeSplitLayoutViewModel: landscapeSplitLayoutViewModel,
handleRefresh: { [weak self] in
self?.interactor.refreshSubmission(forUserId: data.submissions[index].userID)
}
)
}

private func updatePages(_ pages: PagesViewController) {
guard let data = interactor.data else { return }

if let page = controller(for: data.focusedSubmissionIndex) {
pages.setCurrentPage(page)
}

for page in pages.children.compactMap({ $0 as? Page }) {
if let grader = grader(for: page.rootView.content.userIndexInSubmissionList) {
page.rootView.content = grader
}
}
}

// MARK: - State Subscription

private func updateState(
Expand Down Expand Up @@ -196,3 +144,76 @@ class SpeedGraderViewModel: ObservableObject, PagesViewControllerDataSource, Pag
.store(in: &subscriptions)
}
}

// MARK: - PagesViewControllerDataSource

extension SpeedGraderViewModel: PagesViewControllerDataSource {

func pagesViewController(
_ pages: PagesViewController,
pageBefore page: UIViewController
) -> UIViewController? {
(page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList - 1) }
}

func pagesViewController(
_ pages: PagesViewController,
pageAfter page: UIViewController
) -> UIViewController? {
(page as? Page).flatMap { controller(for: $0.rootView.content.userIndexInSubmissionList + 1) }
}

private func controller(for index: Int) -> UIViewController? {
let controller = grader(for: index).map { CoreHostingController($0, env: environment) }
controller?.view.backgroundColor = nil
return controller
}

private func grader(for index: Int) -> SubmissionGraderView? {
guard
let data = interactor.data,
data.submissions.indices.contains(index)
else { return nil }

return SubmissionGraderView(
env: environment,
userIndexInSubmissionList: index,
viewModel: SubmissionGraderViewModel(
assignment: data.assignment,
submission: data.submissions[index],
contextColor: interactor.contextInfo.compactMap { $0?.courseColor }.eraseToAnyPublisher()
),
landscapeSplitLayoutViewModel: landscapeSplitLayoutViewModel,
handleRefresh: { [weak self] in
self?.interactor.refreshSubmission(forUserId: data.submissions[index].userID)
}
)
}

private func updatePages(_ pages: PagesViewController) {
guard let data = interactor.data else { return }

if let page = controller(for: data.focusedSubmissionIndex) {
pages.setCurrentPage(page)
}

for page in pages.children.compactMap({ $0 as? Page }) {
if let grader = grader(for: page.rootView.content.userIndexInSubmissionList) {
page.rootView.content = grader
}
}
}
}

// MARK: - PagesViewControllerDelegate

extension SpeedGraderViewModel: PagesViewControllerDelegate {

func pagesViewController(
_ pages: PagesViewController,
didTransitionTo page: UIViewController
) {
pages.pauseWebViewPlayback()
pages.pauseMediaPlayback()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import XCTest
import CoreData
import Combine
import Core
@testable import Core
@testable import Teacher
import TestsFoundation
import SwiftUI
Expand Down Expand Up @@ -83,6 +83,18 @@ class SpeedGraderViewModelTests: TeacherTestCase {
XCTAssertTrue(pagesController.children.allSatisfy { $0 is CoreHostingController<SubmissionGraderView> })
}

func test_didTransitionTo_pausesPlayback() {
let pagesViewController = MockPagesViewController()
let page = UIViewController()

// WHEN
testee.pagesViewController(pagesViewController, didTransitionTo: page)

// THEN
XCTAssertTrue(pagesViewController.webViewPlaybackPaused)
XCTAssertTrue(pagesViewController.mediaPlaybackPaused)
}

// MARK: - User Actions

func test_didTapPostPolicyButton() {
Expand Down Expand Up @@ -136,3 +148,16 @@ private class SpeedGraderInteractorMock: SpeedGraderInteractor {
)
}
}

private class MockPagesViewController: PagesViewController {
var webViewPlaybackPaused = false
var mediaPlaybackPaused = false

override func pauseWebViewPlayback() {
webViewPlaybackPaused = true
}

override func pauseMediaPlayback() {
mediaPlaybackPaused = true
}
}