From 0f0bc68f1eda8ad17e65abb3f84e78e6ee664984 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 14:19:33 -0500 Subject: [PATCH] restructure video --- .../Atomic/Atoms/Views/LoadImageView.swift | 5 +- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 35 +++--- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 114 +++++++++++++----- 3 files changed, 109 insertions(+), 45 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index aafddb00..8d8ef49e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -21,7 +21,8 @@ public var addSizeConstraintsForAspectRatio = true public var shouldNotifyDelegateOnUpdate = true - + public var shouldNotifyDelegateOnDefaultSizeChange = false + // Allows for a view to hardcode which height to use if there is none in the json. var imageWidth: CGFloat? var imageHeight: CGFloat? @@ -228,7 +229,7 @@ let widthWillChange = !MVMCoreGetterUtility.cgfequal(widthConstraint?.constant ?? 0, width ?? 0) let heightWillChange = !MVMCoreGetterUtility.cgfequal(heightConstraint?.constant ?? 0, height ?? 0) - let sizeWillChange = (width == nil || height == nil) && !(size?.equalTo(imageView.image?.size ?? CGSize.zero) ?? false) + let sizeWillChange = shouldNotifyDelegateOnDefaultSizeChange && (width == nil || height == nil) && !(size?.equalTo(imageView.image?.size ?? CGSize.zero) ?? false) let heightChangeFromSpinner = (heightConstraint?.isActive ?? false) ? false : ((height ?? size?.height) ?? 0) < loadingSpinnerHeightConstraint?.constant ?? CGFloat.leastNormalMagnitude return widthWillChange || heightWillChange || sizeWillChange || heightChangeFromSpinner } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index f869ab06..76b8231b 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -9,6 +9,7 @@ import AVKit open class Video: View { public let videoViewController = AVPlayerViewController() + public var delegateObject: MVMCoreUIDelegateObject? /// Used to track the state and respond.. private var stateKVOToken: NSKeyValueObservation? @@ -21,9 +22,22 @@ open class Video: View { videoViewController.videoGravity = .resizeAspectFill } + /// Checks if the video is visible in the molecule delegate + open func isVisibleInDelegate() -> Bool { + guard let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return true } + return isVisible(in: containingView) + } + + /// Checks if the video is visible in the passed in view + open func isVisible(in view: UIView) -> Bool { + return MVMCoreUIUtility.isView(self, visibleIn: view) + } + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + self.delegateObject = delegateObject super.set(with: model, delegateObject, additionalData) guard let model = model as? VideoModel else { return } + if let controller = delegateObject?.moleculeDelegate as? UIViewController { controller.addChild(videoViewController) videoViewController.didMove(toParent: controller) @@ -31,26 +45,19 @@ open class Video: View { videoViewController.showsPlaybackControls = model.showControls videoViewController.player = model.videoDataManager.player addStateObserver() + model.addVisibilityHalting(for: self, delegateObject: delegateObject) + switch (model.videoDataManager.videoState) { case .none: // Begin loading the video model.videoDataManager.loadVideo() case .loaded: - // Video loaded - if model.alwaysReset { - // Always start video at the beginning. - model.videoDataManager.player?.seek(to: .zero) - } - if model.autoPlay { - model.videoDataManager.player?.play() - } + guard isVisibleInDelegate() else { return } + // Video loaded, unhalt it if necessary. + model.halted = false default: break } - - // Handle pause behavior - model.addVisibleBehavior(for: self, delegateObject: delegateObject) - model.addScrollBehavior(for: self, delegateObject: delegateObject) } /// Listens and responds to video loading state changes. @@ -73,10 +80,10 @@ open class Video: View { MVMCoreDispatchUtility.performSyncBlock(onMainThread: { // Play the video self.videoViewController.player = item.player - if model.autoPlay { + if !model.halted && model.autoPlay && self.isVisibleInDelegate() { item.player?.play() + UIAccessibility.post(notification: .screenChanged, argument: self) } - UIAccessibility.post(notification: .screenChanged, argument: self) }) case .failed: if let errorObject = item.loadFailedError { diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index c64b13e5..7000bdd5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -15,15 +15,36 @@ open class VideoModel: MoleculeModelProtocol { public var showControls = false public var autoPlay = true public var alwaysReset = false + weak public var view: Video? /// When the video is halted because it is no longer visible - private var halted = false - + public var halted: Bool = false { + didSet { + guard halted != oldValue, + videoDataManager.videoState == .loaded else { return } + if halted { + videoDataManager.player?.pause() + printThat(string: "halted") + } else { + printThat(string: "unhalted") + if alwaysReset { + // Always start video at the beginning. + videoDataManager.player?.seek(to: .zero) + } + if autoPlay { + videoDataManager.player?.play() + } + } + } + } + /// Keeps a reference to the video data. public var videoDataManager: VideoDataManager private weak var visibleBehavior: PageVisibilityClosureBehavior? private weak var scrollBehavior: PageScrolledClosureBehavior? + private var activeListener: Any? + private var resignActiveListener: Any? private enum CodingKeys: String, CodingKey { case moleculeName @@ -62,38 +83,48 @@ open class VideoModel: MoleculeModelProtocol { try container.encode(alwaysReset, forKey: .alwaysReset) } - private func haltVideo() { - guard !halted else { return } - halted = true - print("halted \(video)") - videoDataManager.player?.pause() + func printThat(string: String) { + var newString = "sss" + string + " model:\(Unmanaged.passUnretained(self).toOpaque())" + if var view: UIView = self.view { + newString = newString + " view:\(Unmanaged.passUnretained(view).toOpaque())" + while (view as? BGVideoImageMolecule) == nil { + if let superView = view.superview { + view = superView + } else { + break + } + } + if let title = (((view as? BGVideoImageMolecule)?.view as? FooterView)?.view as? TwoButtonView)?.primaryButton.title(for: .normal) { + newString = newString + " \(title)" + } + } + print(newString) } - private func unhaltVideo() { - guard halted else { return } + open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + printThat(string: "before adding") + // reset halted = false - print("unhalted \(video)") - guard videoDataManager.videoState == .loaded else { return } - if alwaysReset { - // Always start video at the beginning. - videoDataManager.player?.seek(to: .zero) - } - if autoPlay { - videoDataManager.player?.play() - } + (view.model as? VideoModel)?.view = nil + self.view = view + printThat(string: "after adding") + + addVisibleBehavior(for: view, delegateObject: delegateObject) + addScrollBehavior(for: view, delegateObject: delegateObject) + addActiveListener(for: view, delegateObject: delegateObject) } /// Adds a behavior to pause the video on page hidden behavior and unpause if necessary on page shown. open func addVisibleBehavior(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + let onShow = { [weak self] in guard let self = self, - let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view, - MVMCoreUIUtility.isView(view, visibleIn: containingView) else { return } - self.unhaltVideo() + let view = self.view, + view.isVisibleInDelegate() else { return } + self.halted = false } let onHide: () -> Void = { [weak self] in - guard let self = self else { return } - self.haltVideo() + self?.halted = true } guard visibleBehavior == nil else { @@ -115,12 +146,8 @@ open class VideoModel: MoleculeModelProtocol { // If visible to not visible, pause video. // If not visible to visible, unpause if needed, add visible behavior guard let self = self, - let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return } - if !MVMCoreUIUtility.isView(view, visibleIn: containingView) { - self.haltVideo() - } else { - self.unhaltVideo() - } + let view = self.view else { return } + self.halted = !view.isVisible(in: scrollView) } guard scrollBehavior == nil else { @@ -133,4 +160,33 @@ open class VideoModel: MoleculeModelProtocol { delegate.add(behavior: scrollBehavior) self.scrollBehavior = scrollBehavior } + + open func addActiveListener(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + removeActiveListener() + + guard let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return } + resignActiveListener = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + self?.halted = true + } + activeListener = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + if MVMCoreUIUtility.isView(view, visibleIn: containingView) { + self?.halted = false + } + } + } + + private func removeActiveListener() { + if let observer = activeListener { + NotificationCenter.default.removeObserver(observer) + activeListener = nil + } + if let observer = resignActiveListener { + NotificationCenter.default.removeObserver(observer) + resignActiveListener = nil + } + } + + deinit { + removeActiveListener() + } }