video molecule

BGVideoImageMolecule
This commit is contained in:
Pfeil, Scott Robert 2021-01-28 15:47:15 -05:00
parent ff8f82e19f
commit a4c7c63ef3
10 changed files with 630 additions and 1 deletions

View File

@ -402,6 +402,12 @@
D28BA74D248589C800B75CB8 /* TabPageModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */; };
D296E14722A5984C0051EBE7 /* MVMCoreUIViewConstrainingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */; };
D29C558A25C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */; };
D29C558D25C05C990082E7D6 /* BGVideoImageMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */; };
D29C559025C095210082E7D6 /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558F25C095210082E7D6 /* Video.swift */; };
D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C559225C0992D0082E7D6 /* VideoModel.swift */; };
D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C559525C099630082E7D6 /* VideoDataManager.swift */; };
D29C559C25C20D6D0082E7D6 /* VideoPauseBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */; };
D29C94D5242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */; };
D29DF0D121E404D4003B2FB9 /* MVMCoreUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */; settings = {ATTRIBUTES = (Public, ); }; };
D29DF0E621E4F3C7003B2FB9 /* MVMCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29DF0E521E4F3C7003B2FB9 /* MVMCore.framework */; };
@ -944,6 +950,12 @@
D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageModelProtocol.swift; sourceTree = "<group>"; };
D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = "<group>"; };
D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = "<group>"; };
D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMoleculeModel.swift; sourceTree = "<group>"; };
D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMolecule.swift; sourceTree = "<group>"; };
D29C558F25C095210082E7D6 /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
D29C559225C0992D0082E7D6 /* VideoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoModel.swift; sourceTree = "<group>"; };
D29C559525C099630082E7D6 /* VideoDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDataManager.swift; sourceTree = "<group>"; };
D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPauseBehavior.swift; sourceTree = "<group>"; };
D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = "<group>"; };
D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MVMCoreUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUI.h; sourceTree = "<group>"; };
@ -1676,6 +1688,8 @@
D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */,
D253BB9D2458751F002DE544 /* BGImageMoleculeModel.swift */,
D253BB9B245874F8002DE544 /* BGImageMolecule.swift */,
D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */,
D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */,
);
path = OtherContainers;
sourceTree = "<group>";
@ -2020,6 +2034,10 @@
AA37CBD42519072F0027344C /* Stars.swift */,
AA07EA902510A442009A2AE3 /* StarModel.swift */,
AA07EA922510A451009A2AE3 /* Star.swift */,
D29C559525C099630082E7D6 /* VideoDataManager.swift */,
D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */,
D29C559225C0992D0082E7D6 /* VideoModel.swift */,
D29C558F25C095210082E7D6 /* Video.swift */,
);
path = Views;
sourceTree = "<group>";
@ -2651,6 +2669,7 @@
0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */,
BB54C5212434D92F0038326C /* ListRightVariableButtonAllTextAndLinksModel.swift in Sources */,
D2092349244A51D40044AD09 /* RadioSwatchModel.swift in Sources */,
D29C559C25C20D6D0082E7D6 /* VideoPauseBehavior.swift in Sources */,
0A775F2824893937009EFB58 /* ThreeHeadlineBodyLinkModel.swift in Sources */,
8DD1E370243B3D0500D8F2DF /* ListThreeColumnInternationalData.swift in Sources */,
D2EC7BDD2527B83700F540AF /* SectionHeaderFooterView.swift in Sources */,
@ -2742,6 +2761,7 @@
AA104AC924472DC7004D2810 /* HeadersH1ButtonModel.swift in Sources */,
0ABD1371237DB0450081388D /* ItemDropdownEntryField.swift in Sources */,
D20C7009250BF99B0095B21C /* TopNotificationModel.swift in Sources */,
D29C558A25C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift in Sources */,
8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */,
BB2FB3BD247E7EF200DF73CD /* Tags.swift in Sources */,
AA104ADC244734EA004D2810 /* HeadersH1LandingPageHeaderModel.swift in Sources */,
@ -2781,6 +2801,7 @@
D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */,
AAB7EDF1246ADA2A00E54929 /* ListProgressBarThin.swift in Sources */,
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */,
D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */,
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */,
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */,
@ -2846,6 +2867,7 @@
D2ED2812254B0EB800A1C293 /* MVMCoreTopAlertObject.m in Sources */,
C003506123AA94CD00B6AC29 /* Button.swift in Sources */,
DBC4391B224421A0001AB423 /* CaretLink.swift in Sources */,
D29C559025C095210082E7D6 /* Video.swift in Sources */,
D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */,
AA104B1C24474A76004D2810 /* HeadersH2ButtonsModel.swift in Sources */,
0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */,
@ -2871,6 +2893,7 @@
BB6C6AC924225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShortModel.swift in Sources */,
C695A69423C9909000BFB94E /* DoughnutChartModel.swift in Sources */,
D2D3957D252FDBCD00047B11 /* ModalSectionListTemplateModel.swift in Sources */,
D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */,
8D4687E2242E2DE400802879 /* ListFourColumnDataUsageListItemModel.swift in Sources */,
D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */,
012A88C2238D7BCA00FE3DA1 /* CarouselItemModel.swift in Sources */,
@ -2888,6 +2911,7 @@
AA633B3324989ED500731E80 /* HeadersH2PricingTwoRows.swift in Sources */,
01509D8F2327EC6F00EF99AA /* MoleculeTableViewCell.swift in Sources */,
0A6682A22434DB4F00AD3CA1 /* ListLeftVariableRadioButtonBodyText.swift in Sources */,
D29C558D25C05C990082E7D6 /* BGVideoImageMolecule.swift in Sources */,
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */,
0105618D224BBE7700E1557D /* FormValidator.swift in Sources */,
01509D912327ECE600EF99AA /* CornerLabels.swift in Sources */,

View File

@ -0,0 +1,162 @@
//
// Video.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/26/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import AVKit
open class Video: View {
public let videoViewController = AVPlayerViewController()
// TODO: reuse.
private let videoQueue = DispatchQueue(label: "serial.video.queue")
/// Used to track the state and respond..
private var stateKVOToken: NSKeyValueObservation?
private weak var pauseBehavior: BlockPageVisibilityBehavior?
private weak var scrollBehavior: BlockPageScrolledBehavior?
private var visible = true
open override func setupView() {
super.setupView()
videoViewController.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(videoViewController.view)
NSLayoutConstraint.constraintPinSubview(toSuperview: videoViewController.view)
videoViewController.videoGravity = .resizeAspectFill
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? VideoModel else { return }
videoViewController.showsPlaybackControls = model.showControls
videoViewController.player = model.videoDataManager.player
addStateObserver()
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()
}
default:
break
}
// Handle pause behavior
addVisibleBehavoir(to: model, delegateObject: delegateObject)
addScrolledBehavoir(to: model, delegateObject: delegateObject)
}
/// Listens and responds to video loading state changes.
private func addStateObserver() {
removeStateObserver()
guard stateKVOToken == nil,
let model = model as? VideoModel else { return }
// To know when the video player item is done loading.
stateKVOToken =
model.videoDataManager.observe(\.videoState) { [weak self] (item, change) in
guard let self = self,
let model = self.model as? VideoModel,
item == model.videoDataManager else { return }
switch item.videoState {
case .loaded:
// Setting videoController's player must be in the main thread
MVMCoreDispatchUtility.performSyncBlock(onMainThread: {
// Play the video
self.videoViewController.player = item.player
if model.autoPlay {
item.player?.play()
}
UIAccessibility.post(notification: .screenChanged, argument: self)
})
case .failed:
if let errorObject = item.loadFailedError {
MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject)
}
default:
break
}
}
}
private func removeStateObserver() {
stateKVOToken?.invalidate()
stateKVOToken = nil
}
deinit {
removeStateObserver()
}
/// Adds a behavoir to pause the video on page hidden behavoir and unpause if necessary on page shown.
open func addVisibleBehavoir(to model: VideoModel, delegateObject: MVMCoreUIDelegateObject?) {
let onShow = { [weak self] in
guard self?.visible == true,
let model = self?.model as? VideoModel,
model.autoPlay else { return }
model.videoDataManager.player?.play()
}
let onHide: () -> Void = { [weak self] in
guard self?.visible == true,
let model = self?.model as? VideoModel else { return }
model.videoDataManager.player?.pause()
}
if pauseBehavior != nil {
pauseBehavior?.pageShownHandler = onShow
pauseBehavior?.pageHiddenHandler = onHide
} else {
guard let delegate = delegateObject?.moleculeDelegate,
let page = delegate as? PageProtocol,
var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return }
var behavoirs = pageModel.behaviors ?? []
let pauseBehavior = BlockPageVisibilityBehavior(with: onShow, onPageHiddenHandler: onHide)
behavoirs.append(pauseBehavior)
pageModel.behaviors = behavoirs
self.pauseBehavior = pauseBehavior
}
}
open func addScrolledBehavoir(to model: VideoModel, delegateObject: MVMCoreUIDelegateObject?) {
let onScroll = { [weak self] (scrollView: UIScrollView) in
// If visible to not visible, pause video.
// If not visible to visible, unpause if needed, add visible behavior
// visible ==
// if !visible {
// pause
// } else {
// let model = self?.model as? VideoModel,
// model.autoPlay else { return }
// model.videoDataManager.player?.play()
// }
}
if scrollBehavior != nil {
scrollBehavior?.pageScrolledHandler = onScroll
} else {
guard let delegate = delegateObject?.moleculeDelegate,
let page = delegate as? PageProtocol,
var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return }
var behavoirs = pageModel.behaviors ?? []
let scrollBehavior = BlockPageScrolledBehavior(with: onScroll)
behavoirs.append(scrollBehavior)
pageModel.behaviors = behavoirs
self.scrollBehavior = scrollBehavior
}
}
}

View File

@ -0,0 +1,120 @@
//
// VideoDataManager.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/26/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers open class VideoDataManager: NSObject {
@objc public enum State: Int {
case none
case loading
case loaded
case failed
}
public let videoURLString: String
public var player: AVPlayer?
// Thread Safe video state handling.
private var _videoState = State.none
private let videoStatueQueue = DispatchQueue(label: "concurrent.video.state.queue", attributes: .concurrent)
/// The state of the video. Use KVO to listen for state changes.
@objc public var videoState: State {
get {
var state = State.none
videoStatueQueue.sync {
state = _videoState
}
return state
}
set {
willChangeValue(for: \.videoState)
videoStatueQueue.async(flags: .barrier) {
self._videoState = newValue
}
didChangeValue(for: \.videoState)
}
}
/// Set when the state is set to failed. Follows the same pattern as apple's AVPlayerItem
public var loadFailedError: MVMCoreErrorObject?
private var kvoToken: NSKeyValueObservation?
public init(with videoURLString: String) {
self.videoURLString = videoURLString
}
public func loadVideo() {
guard videoState != .loading else { return }
removeVideoObserver()
player = nil
videoState = .loading
//Asset loading needs time, calling async method. by tracking asset's propety "duration", if we get the value of duration, we can treat asset load successfully.
let tracksKey = "duration"
MVMCoreCache.shared()?.playerAsset(fromFileName: videoURLString, trackKeys: [tracksKey], onComplete: { [weak self] (asset, fileName, errorObject) in
guard let asset = asset else {
self?.loadFailedError = errorObject
self?.videoState = .failed
return
}
var error: NSError? = nil
let tracksStatus = asset.statusOfValue(forKey: tracksKey, error: &error)
switch tracksStatus {
case .loaded:
//When Assets load successfully, we create playerItem and add playerItem into AVPlayer
self?.player = AVPlayer(playerItem: AVPlayerItem(asset: asset))
self?.addObserverToPlayerItem()
case .failed:
//Asset load fail
//Since checking asset status here, no need to check player.currenItem.asset's media tracks when play button is clicked anymore.
if let error = error,
let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: (asset as? AVURLAsset)?.url.absoluteString ?? self?.videoURLString) {
self?.loadFailedError = errorObject
}
self?.videoState = .failed
default:
break
}
})
}
private func addObserverToPlayerItem() {
removeVideoObserver()
// To know when the video player item is done loading.
guard kvoToken == nil else { return }
kvoToken = player?.currentItem?.observe(\.status) { [weak self] (item, change) in
guard item == self?.player?.currentItem else { return }
switch item.status {
case .readyToPlay:
self?.videoState = .loaded
case .failed:
if let error = item.error,
let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: (item.asset as? AVURLAsset)?.url.absoluteString ?? self?.videoURLString) {
self?.loadFailedError = errorObject
}
self?.videoState = .failed
default:
break
}
}
}
private func removeVideoObserver() {
kvoToken?.invalidate()
kvoToken = nil
}
deinit {
removeVideoObserver()
}
}

View File

@ -0,0 +1,58 @@
//
// VideoModel.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/26/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import Foundation
open class VideoModel: MoleculeModelProtocol {
public static var identifier = "video"
public var backgroundColor: Color?
public var video: String
public var showControls = false
public var autoPlay = true
public var alwaysReset = false
/// Keeps a reference to the video data.
public var videoDataManager: VideoDataManager
private enum CodingKeys: String, CodingKey {
case moleculeName
case video
case showControls
case autoPlay
case alwaysReset
}
public init(_ video: String) {
self.video = video
videoDataManager = VideoDataManager(with: video)
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
video = try typeContainer.decode(String.self, forKey:.video)
videoDataManager = VideoDataManager(with: video)
if let showControls = try typeContainer.decodeIfPresent(Bool.self, forKey: .showControls) {
self.showControls = showControls
}
if let autoPlay = try typeContainer.decodeIfPresent(Bool.self, forKey: .autoPlay) {
self.autoPlay = autoPlay
}
if let alwaysReset = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysReset) {
self.alwaysReset = alwaysReset
}
}
open func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(video, forKey: .video)
try container.encode(showControls, forKey: .showControls)
try container.encode(autoPlay, forKey: .autoPlay)
try container.encode(alwaysReset, forKey: .alwaysReset)
}
}

View File

@ -0,0 +1,110 @@
//
// VideoPauseBehavior.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/27/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import Foundation
public class BlockPageVisibilityBehavior: PageVisibilityBehavior {
public static var identifier = "blockPageVisibilityBehavior"
public var pageShownHandler: () -> Void
public var pageHiddenHandler: () -> Void
public init(with onPageShownHandler: @escaping () -> Void, onPageHiddenHandler: @escaping () -> Void) {
self.pageShownHandler = onPageShownHandler
self.pageHiddenHandler = onPageHiddenHandler
}
private enum CodingKeys: String, CodingKey {
case behaviorName
}
// This class is not meant to be decoded and encoded really.
public required init(from decoder: Decoder) throws {
pageShownHandler = {}
pageHiddenHandler = {}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(behaviorName, forKey: .behaviorName)
}
//MARK:- PageVisibilityBehavior
public func onPageShown() {
pageShownHandler()
}
public func onPageHidden() {
pageHiddenHandler()
}
}
public class BlockPageScrolledBehavior: PageScrolledBehavior {
public static var identifier = "blockPageScrolledBehavior"
public var pageScrolledHandler: (_ scrollView: UIScrollView) -> Void
public init(with onPageScrolledHandler: @escaping (_ scrollView: UIScrollView) -> Void) {
self.pageScrolledHandler = onPageScrolledHandler
}
private enum CodingKeys: String, CodingKey {
case behaviorName
}
// This class is not meant to be decoded and encoded really.
public required init(from decoder: Decoder) throws {
pageScrolledHandler = { (scrollView) in }
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(behaviorName, forKey: .behaviorName)
}
public func pageScrolled(scrollView: UIScrollView) {
pageScrolledHandler(scrollView)
}
}
public class VideoPauseBehavior: PageVisibilityBehavior {
public static var identifier = "videoPause"
public weak var videoModel: VideoModel?
public init(with videoModel: VideoModel) {
self.videoModel = videoModel
}
private enum CodingKeys: String, CodingKey {
case behaviorName
}
//MARK:- PageVisibilityBehavior
public func onPageShown() {
// TODO: Only do this if attached to a visible view...
if videoModel?.autoPlay == true {
videoModel?.videoDataManager.player?.play()
}
}
public func onPageHidden() {
videoModel?.videoDataManager.player?.pause()
}
public required init(from decoder: Decoder) throws {}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(behaviorName, forKey: .behaviorName)
}
}

View File

@ -0,0 +1,98 @@
//
// BGVideoImageMolecule.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/26/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import UIKit
open class BGVideoImageMolecule: BGImageMolecule {
public let video = Video()
/// Used to hide video after loaded.
private var stateKVOToken: NSKeyValueObservation?
private var endObserver: NSObjectProtocol?
open override func setupView() {
super.setupView()
insertSubview(video, aboveSubview: image)
NSLayoutConstraint.constraintPinSubview(toSuperview: video)
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? BGVideoImageMoleculeModel else { return }
video.set(with: model.video, delegateObject, additionalData)
video.isHidden = shouldVideoBeHidden()
addStateObserver()
}
open func shouldVideoBeHidden() -> Bool {
guard let model = model as? BGVideoImageMoleculeModel,
model.video.videoDataManager.videoState != .failed else {
return true
}
guard model.video.videoDataManager.videoState == .loaded,
let player = model.video.videoDataManager.player,
let item = player.currentItem,
item.currentTime() == item.duration else {
return false
}
return true
}
/// Listens and responds to video loading state changes to add the end observer.
private func addStateObserver() {
removeStateObserver()
guard stateKVOToken == nil,
let model = model as? BGVideoImageMoleculeModel else { return }
// To know when the video player item is done loading.
stateKVOToken =
model.video.videoDataManager.observe(\.videoState) { [weak self] (item, change) in
guard let self = self,
let model = self.model as? BGVideoImageMoleculeModel,
item == model.video.videoDataManager else { return }
switch item.videoState {
case .loaded:
self.addEndObserver()
case .failed:
MVMCoreDispatchUtility.performBlock(onMainThread: {
self.video.isHidden = true
})
default:
break
}
}
}
private func removeStateObserver() {
stateKVOToken?.invalidate()
stateKVOToken = nil
}
private func addEndObserver() {
removeStateObserver()
guard let model = model as? BGVideoImageMoleculeModel,
let item = model.video.videoDataManager.player?.currentItem else { return }
endObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: OperationQueue.main) { [weak self] (notification) in
self?.video.isHidden = true
}
}
private func removeEndObserver() {
guard let endObserver = endObserver else { return }
NotificationCenter.default.removeObserver(endObserver)
self.endObserver = nil
}
deinit {
removeStateObserver()
removeEndObserver()
}
}

View File

@ -0,0 +1,38 @@
//
// BGVideoImageMoleculeModel.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 1/26/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
import UIKit
open class BGVideoImageMoleculeModel: BGImageMoleculeModel {
open override class var identifier: String {
return "bgVideoImageContainer"
}
public var video: VideoModel
private enum CodingKeys: String, CodingKey {
case video
}
public init(_ video: VideoModel, image: ImageViewModel, molecule: MoleculeModelProtocol) {
self.video = video
super.init(image, molecule: molecule)
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
video = try typeContainer.decode(VideoModel.self, forKey:.video)
try super.init(from: decoder)
}
open override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(video, forKey: .video)
}
}

View File

@ -14,6 +14,14 @@ import Foundation
return 0
}
open override func loadView() {
super.loadView()
// The height is used to keep the bottom view at the bottom.
if let contentView = contentView, let scrollView = scrollView {
contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor).isActive = true
}
}
open override func handleNewData() {
super.handleNewData()
heightConstraint?.isActive = true

View File

@ -73,6 +73,12 @@ open class ScrollingViewController: ViewController {
scrollView.flashScrollIndicators()
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
executeBehaviors { (behavior: PageScrolledBehavior) in
behavior.pageScrolled(scrollView: scrollView)
}
}
//--------------------------------------------------
// MARK: - Keyboard Handling
//--------------------------------------------------

View File

@ -37,8 +37,13 @@ public protocol PageVisibilityBehavior: PageBehaviorProtocol {
}
public protocol PageScrolledBehavior: PageBehaviorProtocol {
func pageScrolled(scrollView: UIScrollView)
}
public protocol PageBehaviorsTemplateProtocol {
var behaviors: [PageBehaviorProtocol]? { get }
var behaviors: [PageBehaviorProtocol]? { get set }
}