From be426c756fb10c5e41d03127623a1ee6c2de0c97 Mon Sep 17 00:00:00 2001 From: Subhankar Date: Wed, 27 Jan 2021 22:30:06 +0530 Subject: [PATCH 01/16] scope modification for product detail molecule --- .../Molecules/VerticalCombinationViews/HeadlineBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift index d111d1c5..ba95a5d3 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift @@ -54,7 +54,7 @@ open class HeadlineBody: View { } } - func styleLandingPageHeader() { + public func styleLandingPageHeader() { headlineLabel.setFontStyle(.Title2XLarge) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = PaddingTwo From a4c7c63ef30b25dfef10ce49fd51c23013679db3 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Thu, 28 Jan 2021 15:47:15 -0500 Subject: [PATCH 02/16] video molecule BGVideoImageMolecule --- MVMCoreUI.xcodeproj/project.pbxproj | 24 +++ MVMCoreUI/Atomic/Atoms/Views/Video.swift | 162 ++++++++++++++++++ .../Atomic/Atoms/Views/VideoDataManager.swift | 120 +++++++++++++ MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 58 +++++++ .../Atoms/Views/VideoPauseBehavior.swift | 110 ++++++++++++ .../BGVideoImageMolecule.swift | 98 +++++++++++ .../BGVideoImageMoleculeModel.swift | 38 ++++ .../ThreeLayerFillMiddleTemplate.swift | 8 + .../ScrollingViewController.swift | 6 + MVMCoreUI/Behaviors/PageBehavior.swift | 7 +- 10 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 MVMCoreUI/Atomic/Atoms/Views/Video.swift create mode 100644 MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift create mode 100644 MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift create mode 100644 MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift create mode 100644 MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift create mode 100644 MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 2dc86525..a1a735a7 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -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 = ""; }; D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = ""; }; D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = ""; }; + D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMoleculeModel.swift; sourceTree = ""; }; + D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMolecule.swift; sourceTree = ""; }; + D29C558F25C095210082E7D6 /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = ""; }; + D29C559225C0992D0082E7D6 /* VideoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoModel.swift; sourceTree = ""; }; + D29C559525C099630082E7D6 /* VideoDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDataManager.swift; sourceTree = ""; }; + D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPauseBehavior.swift; sourceTree = ""; }; D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = ""; }; 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 = ""; }; @@ -1676,6 +1688,8 @@ D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */, D253BB9D2458751F002DE544 /* BGImageMoleculeModel.swift */, D253BB9B245874F8002DE544 /* BGImageMolecule.swift */, + D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */, + D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */, ); path = OtherContainers; sourceTree = ""; @@ -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 = ""; @@ -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 */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift new file mode 100644 index 00000000..26904344 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -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 + } + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift new file mode 100644 index 00000000..ab6d74e4 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift @@ -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() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift new file mode 100644 index 00000000..d335f375 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -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) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift new file mode 100644 index 00000000..73293a8b --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift @@ -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) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift new file mode 100644 index 00000000..f57298aa --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift @@ -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() + } +} diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift new file mode 100644 index 00000000..f2ee20af --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift @@ -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) + } +} diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift index 50e55a45..81c3b6a8 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift @@ -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 diff --git a/MVMCoreUI/BaseControllers/ScrollingViewController.swift b/MVMCoreUI/BaseControllers/ScrollingViewController.swift index fcbcf878..9a9a42e3 100644 --- a/MVMCoreUI/BaseControllers/ScrollingViewController.swift +++ b/MVMCoreUI/BaseControllers/ScrollingViewController.swift @@ -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 //-------------------------------------------------- diff --git a/MVMCoreUI/Behaviors/PageBehavior.swift b/MVMCoreUI/Behaviors/PageBehavior.swift index d8fd99a3..ed17daff 100644 --- a/MVMCoreUI/Behaviors/PageBehavior.swift +++ b/MVMCoreUI/Behaviors/PageBehavior.swift @@ -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 } } From c473177832d345e6f243dab2c5b2663f37aa84cd Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Tue, 9 Feb 2021 13:18:29 -0500 Subject: [PATCH 03/16] image efficiency --- .../Molecules/OtherContainers/BGImageMoleculeModel.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift index 2dbfced5..56b77e3e 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift @@ -27,6 +27,12 @@ open class BGImageMoleculeModel: MoleculeContainerModel { if bottomPadding == nil { bottomPadding = PaddingDefaultVerticalSpacing3 } + if image.contentMode == nil { + image.contentMode = .scaleAspectFill + } + if image.imageFormat == nil { + image.imageFormat = "jpg" + } } private enum CodingKeys: String, CodingKey { From d333e449fec26df4a1293bbc0cc77b5a37e7d040 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Wed, 10 Feb 2021 12:40:23 -0500 Subject: [PATCH 04/16] move behavior --- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 67 +------------------ .../Atomic/Atoms/Views/VideoDataManager.swift | 2 + MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 64 +++++++++++++++++- .../Atoms/Views/VideoPauseBehavior.swift | 35 ---------- MVMCoreUI/Behaviors/PageBehavior.swift | 10 ++- 5 files changed, 76 insertions(+), 102 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index 044b199d..6e0fffbc 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -16,10 +16,6 @@ open class Video: View { /// 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() { @@ -54,9 +50,8 @@ open class Video: View { } // Handle pause behavior - addVisibleBehavoir(to: model, delegateObject: delegateObject) - addScrolledBehavoir(to: model, delegateObject: delegateObject) -// addForegroundCheck() + model.addVisibleBehavoir(for: self, delegateObject: delegateObject) + model.addScrollBehavoir(for: self, delegateObject: delegateObject) } /// Listens and responds to video loading state changes. @@ -102,62 +97,4 @@ open class Video: View { 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 - } - } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift index 754a6ef2..c616f4b6 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift @@ -127,6 +127,8 @@ import Foundation } private func removeMemoryWarningListener() { + guard let observer = memoryWarningListener else { return } + NotificationCenter.default.removeObserver(observer) memoryWarningListener = nil } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index d335f375..0c1033e8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -18,6 +18,9 @@ open class VideoModel: MoleculeModelProtocol { /// Keeps a reference to the video data. public var videoDataManager: VideoDataManager + + private weak var visibleBehavior: BlockPageVisibilityBehavior? + private weak var scrollBehavior: BlockPageScrolledBehavior? private enum CodingKeys: String, CodingKey { case moleculeName @@ -35,7 +38,6 @@ open class VideoModel: MoleculeModelProtocol { 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 } @@ -45,6 +47,7 @@ open class VideoModel: MoleculeModelProtocol { if let alwaysReset = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysReset) { self.alwaysReset = alwaysReset } + videoDataManager = VideoDataManager(with: video) } open func encode(to encoder: Encoder) throws { @@ -55,4 +58,63 @@ open class VideoModel: MoleculeModelProtocol { try container.encode(autoPlay, forKey: .autoPlay) try container.encode(alwaysReset, forKey: .alwaysReset) } + + // Temporary + func isVisible(view: UIView) -> Bool { + return true + } + + /// Adds a behavoir to pause the video on page hidden behavoir and unpause if necessary on page shown. + open func addVisibleBehavoir(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + let onShow = { [weak self] in + guard let self = self, + self.isVisible(view: view), + self.autoPlay else { return } + self.videoDataManager.player?.play() + } + let onHide: () -> Void = { [weak self] in + guard let self = self else { return } + self.videoDataManager.player?.pause() + } + + guard visibleBehavior == nil else { + visibleBehavior?.pageShownHandler = onShow + visibleBehavior?.pageHiddenHandler = onHide + return + } + + guard let delegate = delegateObject?.moleculeDelegate, + let page = delegate as? PageProtocol, + var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return } + let pauseBehavior = BlockPageVisibilityBehavior(with: onShow, onPageHiddenHandler: onHide) + pageModel.add(behavior: pauseBehavior) + self.visibleBehavior = pauseBehavior + } + + /// Adds a behavoir to pause the video if scrolled off of the page and unpause if necessary if scrolled on. + open func addScrollBehavoir(for view: Video, 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 + guard let self = self else { return } + if !self.isVisible(view: view) { + self.videoDataManager.player?.pause() + } else if self.autoPlay { + self.videoDataManager.player?.play() + } + } + + guard scrollBehavior == nil else { + scrollBehavior?.pageScrolledHandler = onScroll + return + } + + guard let delegate = delegateObject?.moleculeDelegate, + let page = delegate as? PageProtocol, + var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return } + let scrollBehavior = BlockPageScrolledBehavior(with: onScroll) + pageModel.add(behavior: scrollBehavior) + self.scrollBehavior = scrollBehavior + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift index 73293a8b..1524381e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift @@ -73,38 +73,3 @@ public class BlockPageScrolledBehavior: PageScrolledBehavior { 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) - } -} diff --git a/MVMCoreUI/Behaviors/PageBehavior.swift b/MVMCoreUI/Behaviors/PageBehavior.swift index ef80349e..59e41031 100644 --- a/MVMCoreUI/Behaviors/PageBehavior.swift +++ b/MVMCoreUI/Behaviors/PageBehavior.swift @@ -43,5 +43,13 @@ public protocol PageScrolledBehavior: PageBehaviorProtocol { public protocol PageBehaviorsTemplateProtocol { var behaviors: [PageBehaviorProtocol]? { get set } - + +} + +public extension PageBehaviorsTemplateProtocol { + mutating func add(behavior: PageBehaviorProtocol) { + var behavoirs = behaviors ?? [] + behavoirs.append(behavior) + self.behaviors = behavoirs + } } From f74fbc059c8b598f0645e943c8f2c9cfbb2f8ff3 Mon Sep 17 00:00:00 2001 From: "Khan, Arshad" Date: Thu, 11 Feb 2021 21:53:54 +0530 Subject: [PATCH 05/16] implementing feedback --- .../Molecules/VerticalCombinationViews/HeadlineBody.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift index f5c0c565..b3aed7fc 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift @@ -56,19 +56,19 @@ open class HeadlineBody: View { spaceBetweenLabelsConstant = Padding.Two } - func stylePageHeader() { + public func stylePageHeader() { headlineLabel.setFontStyle(.BoldTitleLarge) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = Padding.One } - func styleListItem() { + public func styleListItem() { headlineLabel.setFontStyle(.BoldBodySmall) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = 0 } - func styleListItemDivider() { + public func styleListItemDivider() { headlineLabel.setFontStyle(.BoldTitleMedium) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = 0 From c2f1cd200d6228c1dfb4651208346cf1895b8dc4 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Thu, 11 Feb 2021 17:18:06 -0500 Subject: [PATCH 06/16] review comments --- MVMCoreUI.xcodeproj/project.pbxproj | 12 ++- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 13 ++-- .../Atomic/Atoms/Views/VideoDataManager.swift | 4 +- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 72 +++++++++++------- .../Atoms/Views/VideoPauseBehavior.swift | 75 ------------------- MVMCoreUI/Behaviors/PageBehavior.swift | 14 +++- .../PageScrolledClosureBehavior.swift | 33 ++++++++ .../PageVisibilityClosureBehavior.swift | 40 ++++++++++ MVMCoreUI/OtherHandlers/CoreUIObject.swift | 2 + MVMCoreUI/Utility/MVMCoreUIUtility.h | 8 ++ MVMCoreUI/Utility/MVMCoreUIUtility.m | 45 +++++++++++ 11 files changed, 200 insertions(+), 118 deletions(-) delete mode 100644 MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift create mode 100644 MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift create mode 100644 MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 693b46ce..5c490b5a 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -347,6 +347,8 @@ D23EA800247EBD6C00D60C34 /* LabelBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23EA7FF247EBD6C00D60C34 /* LabelBarButtonItem.swift */; }; D23EA802247EBED400D60C34 /* ImageBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23EA801247EBED400D60C34 /* ImageBarButtonItem.swift */; }; D243859923A16B1800332775 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243859823A16B1800332775 /* Container.swift */; }; + D24918F625D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */; }; + D24918FA25D5ADBB00CAB4B1 /* PageScrolledClosureBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */; }; D2509ED12472ED9B001BFB9D /* NavigationItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */; }; D2509ED62472EE2F001BFB9D /* NavigationImageButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2509ED52472EE2F001BFB9D /* NavigationImageButtonModel.swift */; }; D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D260106423D0CEA700764D80 /* StackModel.swift */; }; @@ -408,7 +410,6 @@ 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 */; }; @@ -898,6 +899,8 @@ D23EA7FF247EBD6C00D60C34 /* LabelBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelBarButtonItem.swift; sourceTree = ""; }; D23EA801247EBED400D60C34 /* ImageBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBarButtonItem.swift; sourceTree = ""; }; D243859823A16B1800332775 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageVisibilityClosureBehavior.swift; sourceTree = ""; }; + D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageScrolledClosureBehavior.swift; sourceTree = ""; }; D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModelProtocol.swift; sourceTree = ""; }; D2509ED52472EE2F001BFB9D /* NavigationImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationImageButtonModel.swift; sourceTree = ""; }; D253BB9B245874F8002DE544 /* BGImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageMolecule.swift; sourceTree = ""; }; @@ -957,7 +960,6 @@ D29C558F25C095210082E7D6 /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = ""; }; D29C559225C0992D0082E7D6 /* VideoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoModel.swift; sourceTree = ""; }; D29C559525C099630082E7D6 /* VideoDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDataManager.swift; sourceTree = ""; }; - D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPauseBehavior.swift; sourceTree = ""; }; D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = ""; }; 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 = ""; }; @@ -1240,6 +1242,8 @@ children = ( 27F973522466074500CAB5C5 /* PageBehavior.swift */, 27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */, + D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */, + D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */, ); path = Behaviors; sourceTree = ""; @@ -2038,7 +2042,6 @@ AA07EA902510A442009A2AE3 /* StarModel.swift */, AA07EA922510A451009A2AE3 /* Star.swift */, D29C559525C099630082E7D6 /* VideoDataManager.swift */, - D29C559B25C20D6D0082E7D6 /* VideoPauseBehavior.swift */, D29C559225C0992D0082E7D6 /* VideoModel.swift */, D29C558F25C095210082E7D6 /* Video.swift */, ); @@ -2672,7 +2675,6 @@ 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 */, @@ -2707,6 +2709,7 @@ 94C2D9A523872C350006CF46 /* LabelAttributeFontModel.swift in Sources */, 011D958724042492000E3791 /* FormFieldProtocol.swift in Sources */, 011D95AF2407266E000E3791 /* RadioButtonModel.swift in Sources */, + D24918F625D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift in Sources */, D20492A624329CE200A5EED6 /* LoadImageView.swift in Sources */, 017BEB7F23676E870024EF95 /* MoleculeObjectMapping.swift in Sources */, D274CA332236A78900B01B62 /* FooterView.swift in Sources */, @@ -2792,6 +2795,7 @@ 012A88DB238ED45900FE3DA1 /* CarouselModel.swift in Sources */, D2092355244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift in Sources */, 0AE14F64238315D2005417F8 /* TextField.swift in Sources */, + D24918FA25D5ADBB00CAB4B1 /* PageScrolledClosureBehavior.swift in Sources */, 0A51F3E22475CB73002E08B6 /* LoadingSpinnerModel.swift in Sources */, D2169303251E53D9002A6324 /* SectionListTemplateModel.swift in Sources */, 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index 6e0fffbc..f869ab06 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -10,13 +10,8 @@ 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 var visible = true open override func setupView() { super.setupView() @@ -29,6 +24,10 @@ open class Video: View { 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 } + if let controller = delegateObject?.moleculeDelegate as? UIViewController { + controller.addChild(videoViewController) + videoViewController.didMove(toParent: controller) + } videoViewController.showsPlaybackControls = model.showControls videoViewController.player = model.videoDataManager.player addStateObserver() @@ -50,8 +49,8 @@ open class Video: View { } // Handle pause behavior - model.addVisibleBehavoir(for: self, delegateObject: delegateObject) - model.addScrollBehavoir(for: self, delegateObject: delegateObject) + model.addVisibleBehavior(for: self, delegateObject: delegateObject) + model.addScrollBehavior(for: self, delegateObject: delegateObject) } /// Listens and responds to video loading state changes. diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift index c616f4b6..046ba3bb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift @@ -7,9 +7,11 @@ // import Foundation +import AVFoundation @objcMembers open class VideoDataManager: NSObject { + /// The state of the video. @objc public enum State: Int { case none case loading @@ -22,7 +24,7 @@ import Foundation // Thread Safe video state handling. private var _videoState = State.none - private let videoStatueQueue = DispatchQueue(label: "concurrent.video.state.queue", attributes: .concurrent) + private let videoStatueQueue = DispatchQueue(label: "com.vzw.mvmcoreui.videoDataManager.state", attributes: .concurrent) /// The state of the video. Use KVO to listen for state changes. @objc public var videoState: State { diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index 0c1033e8..c64b13e5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -15,12 +15,15 @@ open class VideoModel: MoleculeModelProtocol { public var showControls = false public var autoPlay = true public var alwaysReset = false + + /// When the video is halted because it is no longer visible + private var halted = false /// Keeps a reference to the video data. public var videoDataManager: VideoDataManager - private weak var visibleBehavior: BlockPageVisibilityBehavior? - private weak var scrollBehavior: BlockPageScrolledBehavior? + private weak var visibleBehavior: PageVisibilityClosureBehavior? + private weak var scrollBehavior: PageScrolledClosureBehavior? private enum CodingKeys: String, CodingKey { case moleculeName @@ -59,22 +62,38 @@ open class VideoModel: MoleculeModelProtocol { try container.encode(alwaysReset, forKey: .alwaysReset) } - // Temporary - func isVisible(view: UIView) -> Bool { - return true + private func haltVideo() { + guard !halted else { return } + halted = true + print("halted \(video)") + videoDataManager.player?.pause() } - /// Adds a behavoir to pause the video on page hidden behavoir and unpause if necessary on page shown. - open func addVisibleBehavoir(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + private func unhaltVideo() { + guard halted else { return } + 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() + } + } + + /// 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, - self.isVisible(view: view), - self.autoPlay else { return } - self.videoDataManager.player?.play() + let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view, + MVMCoreUIUtility.isView(view, visibleIn: containingView) else { return } + self.unhaltVideo() } let onHide: () -> Void = { [weak self] in guard let self = self else { return } - self.videoDataManager.player?.pause() + self.haltVideo() } guard visibleBehavior == nil else { @@ -83,25 +102,24 @@ open class VideoModel: MoleculeModelProtocol { return } - guard let delegate = delegateObject?.moleculeDelegate, - let page = delegate as? PageProtocol, - var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return } - let pauseBehavior = BlockPageVisibilityBehavior(with: onShow, onPageHiddenHandler: onHide) - pageModel.add(behavior: pauseBehavior) + guard var delegate = delegateObject?.behaviorTemplateDelegate else { return } + let pauseBehavior = PageVisibilityClosureBehavior(with: onShow, onPageHiddenHandler: onHide) + delegate.add(behavior: pauseBehavior) self.visibleBehavior = pauseBehavior } - /// Adds a behavoir to pause the video if scrolled off of the page and unpause if necessary if scrolled on. - open func addScrollBehavoir(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + /// Adds a behavior to pause the video if scrolled off of the page and unpause if necessary if scrolled on. + open func addScrollBehavior(for view: Video, 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 - guard let self = self else { return } - if !self.isVisible(view: view) { - self.videoDataManager.player?.pause() - } else if self.autoPlay { - self.videoDataManager.player?.play() + guard let self = self, + let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return } + if !MVMCoreUIUtility.isView(view, visibleIn: containingView) { + self.haltVideo() + } else { + self.unhaltVideo() } } @@ -110,11 +128,9 @@ open class VideoModel: MoleculeModelProtocol { return } - guard let delegate = delegateObject?.moleculeDelegate, - let page = delegate as? PageProtocol, - var pageModel = page.pageModel as? PageBehaviorsTemplateProtocol else { return } - let scrollBehavior = BlockPageScrolledBehavior(with: onScroll) - pageModel.add(behavior: scrollBehavior) + guard var delegate = delegateObject?.behaviorTemplateDelegate else { return } + let scrollBehavior = PageScrolledClosureBehavior(with: onScroll) + delegate.add(behavior: scrollBehavior) self.scrollBehavior = scrollBehavior } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift deleted file mode 100644 index 1524381e..00000000 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// 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) - } -} diff --git a/MVMCoreUI/Behaviors/PageBehavior.swift b/MVMCoreUI/Behaviors/PageBehavior.swift index 59e41031..aaf915a1 100644 --- a/MVMCoreUI/Behaviors/PageBehavior.swift +++ b/MVMCoreUI/Behaviors/PageBehavior.swift @@ -48,8 +48,16 @@ public protocol PageBehaviorsTemplateProtocol { public extension PageBehaviorsTemplateProtocol { mutating func add(behavior: PageBehaviorProtocol) { - var behavoirs = behaviors ?? [] - behavoirs.append(behavior) - self.behaviors = behavoirs + var newBehaviors = behaviors ?? [] + newBehaviors.append(behavior) + self.behaviors = newBehaviors + } +} + +public extension MVMCoreUIDelegateObject { + weak var behaviorTemplateDelegate: (PageBehaviorsTemplateProtocol & NSObjectProtocol)? { + get { + return (moleculeDelegate as? PageProtocol)?.pageModel as? (PageBehaviorsTemplateProtocol & NSObjectProtocol) + } } } diff --git a/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift b/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift new file mode 100644 index 00000000..c43d4ecc --- /dev/null +++ b/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift @@ -0,0 +1,33 @@ +// +// PageScrolledClosureBehavior.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 2/11/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class PageScrolledClosureBehavior: PageScrolledBehavior { + + public static var identifier = "pageScrolledClosureBehavior" + + public var pageScrolledHandler: (_ scrollView: UIScrollView) -> Void + + public init(with onPageScrolledHandler: @escaping (_ scrollView: UIScrollView) -> Void) { + self.pageScrolledHandler = onPageScrolledHandler + } + + // This class is not meant to be decoded and encoded really. + public required init(from decoder: Decoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageScrolledClosureBehavior does not decode.") + } + + public func encode(to encoder: Encoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageScrolledClosureBehavior does not encode.") + } + + public func pageScrolled(scrollView: UIScrollView) { + pageScrolledHandler(scrollView) + } +} diff --git a/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift b/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift new file mode 100644 index 00000000..f5ecd82c --- /dev/null +++ b/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift @@ -0,0 +1,40 @@ +// +// PageVisibilityClosureBehavior.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 2/11/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class PageVisibilityClosureBehavior: PageVisibilityBehavior { + + public static var identifier = "pageVisibilityClosureBehavior" + + public var pageShownHandler: () -> Void + public var pageHiddenHandler: () -> Void + + public init(with onPageShownHandler: @escaping () -> Void, onPageHiddenHandler: @escaping () -> Void) { + self.pageShownHandler = onPageShownHandler + self.pageHiddenHandler = onPageHiddenHandler + } + + // This class is not meant to be decoded and encoded really. + public required init(from decoder: Decoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageVisibilityClosureBehavior does not decode.") + } + + public func encode(to encoder: Encoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageVisibilityClosureBehavior does not encode.") + } + + //MARK:- PageVisibilityBehavior + public func onPageShown() { + pageShownHandler() + } + + public func onPageHidden() { + pageHiddenHandler() + } +} diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index 3a4d2cbc..90532f98 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -13,6 +13,7 @@ import UIKit public var globalTopAlertDelegate: MVMCoreGlobalTopAlertDelegateProtocol? open override func defaultInitialSetup() { + loadHandler = MVMCoreLoadHandler() cache = MVMCoreCache() sessionHandler = MVMCoreSessionTimeHandler() actionHandler = MVMCoreActionHandler() @@ -21,5 +22,6 @@ import UIKit loggingDelegate = MVMCoreUILoggingHandler() moleculeMap = MoleculeObjectMapping() MoleculeObjectMapping.registerObjects() + clientParameterRegistry = ClientParameterRegistry() } } diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.h b/MVMCoreUI/Utility/MVMCoreUIUtility.h index 4bf6e8d5..74b19d0e 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.h +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.h @@ -37,6 +37,14 @@ NS_ASSUME_NONNULL_BEGIN /// Checks if the view or any descendents of the view is currently focused for voice over. + (BOOL)viewContainsAccessiblityFocus:(nonnull UIView *)view; ++ (BOOL)isView:(nonnull UIView *)view visibleIn:(nonnull UIView *)rootView; + ++ (BOOL)isViewVisibleInParent:(nonnull UIView *)view; + ++ (BOOL)doesView:(nonnull UIView *)overlappingView cover:(nonnull UIView *)view; + ++ (BOOL)isViewTransparent:(nonnull UIView *)view; + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom; diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.m b/MVMCoreUI/Utility/MVMCoreUIUtility.m index fed0bb30..fad2378c 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.m @@ -96,6 +96,51 @@ return containsFocus; } ++ (BOOL)isView:(nonnull UIView *)view visibleIn:(nonnull UIView *)rootView { + return [view isDescendantOfView:rootView] && [self isViewVisibleInParent:view]; +} + +// Climb up the tree. Break early. ++ (BOOL)isViewVisibleInParent:(nonnull UIView *)view { + UIView *superview = view.superview; + UIView *ancestor = view; + // Begin climbing its ancestor views, checking if it remains visible in *each* of their bounds and if the children at views closer to the front block the visibility of this view. + while (superview) { + if (superview.clipsToBounds && !CGRectIntersectsRect(superview.bounds, [view convertRect:view.bounds toView:superview])) { + return false; + } else { + // Check the superview's children up to the common ancestor. (Reworking the ancestor would put us in an infinite loop and we want to ignore the children with a lower z-order.) + for (UIView *subview in [superview.subviews reverseObjectEnumerator]) { + if (subview == ancestor) { + break; + } else if ([self doesView:subview cover:view]) { + return false; + } + } + } + ancestor = superview; + superview = superview.superview; + } + return true; +} + +// Climb down the tree. ++ (BOOL)doesView:(nonnull UIView *)overlappingView cover:(nonnull UIView *)view { + if (![self isViewTransparent:overlappingView] && CGRectContainsRect(overlappingView.bounds, [view convertRect:view.bounds toView:overlappingView])) { + return true; + } + for (UIView *subview in overlappingView.subviews) { + if ([self doesView:subview cover:view]) { + return true; + } + } + return false; +} + ++ (BOOL)isViewTransparent:(nonnull UIView *)view { + return view.alpha < 1 || view.backgroundColor == nil || CGColorGetAlpha(view.backgroundColor.CGColor) < 1; +} + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom { From 23a8c2c1baba15bd295bc32492b73fe7556964c0 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Thu, 11 Feb 2021 17:51:09 -0500 Subject: [PATCH 07/16] conflict update --- MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift | 8 ++++---- MVMCoreUI/BaseControllers/ScrollingViewController.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift index 046ba3bb..a4ba9235 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift @@ -12,7 +12,7 @@ import AVFoundation @objcMembers open class VideoDataManager: NSObject { /// The state of the video. - @objc public enum State: Int { + @objc public enum VideoState: Int { case none case loading case loaded @@ -23,13 +23,13 @@ import AVFoundation public var player: AVPlayer? // Thread Safe video state handling. - private var _videoState = State.none + private var _videoState = VideoState.none private let videoStatueQueue = DispatchQueue(label: "com.vzw.mvmcoreui.videoDataManager.state", attributes: .concurrent) /// The state of the video. Use KVO to listen for state changes. - @objc public var videoState: State { + @objc public var videoState: VideoState { get { - var state = State.none + var state = VideoState.none videoStatueQueue.sync { state = _videoState } diff --git a/MVMCoreUI/BaseControllers/ScrollingViewController.swift b/MVMCoreUI/BaseControllers/ScrollingViewController.swift index 9a9a42e3..19c9d001 100644 --- a/MVMCoreUI/BaseControllers/ScrollingViewController.swift +++ b/MVMCoreUI/BaseControllers/ScrollingViewController.swift @@ -73,7 +73,7 @@ open class ScrollingViewController: ViewController { scrollView.flashScrollIndicators() } - public func scrollViewDidScroll(_ scrollView: UIScrollView) { + open func scrollViewDidScroll(_ scrollView: UIScrollView) { executeBehaviors { (behavior: PageScrolledBehavior) in behavior.pageScrolled(scrollView: scrollView) } From 744398b7b8ac68cf1583054c09613dcc566c2f15 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Thu, 11 Feb 2021 19:44:24 -0500 Subject: [PATCH 08/16] Register class, remove debug --- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 3 +-- MVMCoreUI/Atomic/MoleculeObjectMapping.swift | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index c64b13e5..8bc3d827 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -65,14 +65,13 @@ open class VideoModel: MoleculeModelProtocol { private func haltVideo() { guard !halted else { return } halted = true - print("halted \(video)") + guard videoDataManager.videoState == .loaded else { return } videoDataManager.player?.pause() } private func unhaltVideo() { guard halted else { return } halted = false - print("unhalted \(video)") guard videoDataManager.videoState == .loaded else { return } if alwaysReset { // Always start video at the beginning. diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index 559b611b..e4d3a59a 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -106,6 +106,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: RadioButtonLabel.self, viewModelClass: RadioButtonLabelModel.self) MoleculeObjectMapping.shared()?.register(viewClass: WebView.self, viewModelClass: WebViewModel.self) MoleculeObjectMapping.shared()?.register(viewClass: LoadingSpinner.self, viewModelClass: LoadingSpinnerModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: Video.self, viewModelClass: VideoModel.self) // MARK:- Horizontal Combination Molecules MoleculeObjectMapping.shared()?.register(viewClass: StringAndMoleculeView.self, viewModelClass: StringAndMoleculeModel.self) @@ -151,6 +152,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: Scroller.self, viewModelClass: ScrollerModel.self) MoleculeObjectMapping.shared()?.register(viewClass: ModuleMolecule.self, viewModelClass: ModuleMoleculeModel.self) MoleculeObjectMapping.shared()?.register(viewClass: BGImageMolecule.self, viewModelClass: BGImageMoleculeModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: BGVideoImageMolecule.self, viewModelClass: BGVideoImageMoleculeModel.self) MoleculeObjectMapping.shared()?.register(viewClass: MoleculeSectionHeader.self, viewModelClass: MoleculeSectionHeaderModel.self) MoleculeObjectMapping.shared()?.register(viewClass: MoleculeSectionFooter.self, viewModelClass: MoleculeSectionFooterModel.self) From 0793498e3f0a4038e8543792bf5e9da1a767c6a2 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Thu, 11 Feb 2021 20:31:17 -0500 Subject: [PATCH 09/16] Active listener --- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 1 + MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 36 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index f869ab06..92ff3ecf 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -51,6 +51,7 @@ open class Video: View { // Handle pause behavior model.addVisibleBehavior(for: self, delegateObject: delegateObject) model.addScrollBehavior(for: self, delegateObject: delegateObject) + model.addActiveListener(for: self, delegateObject: delegateObject) } /// Listens and responds to video loading state changes. diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index 8bc3d827..9b1d4f71 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -24,6 +24,8 @@ open class VideoModel: MoleculeModelProtocol { 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 @@ -113,9 +115,8 @@ open class VideoModel: MoleculeModelProtocol { 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 - guard let self = self, - let containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return } - if !MVMCoreUIUtility.isView(view, visibleIn: containingView) { + guard let self = self else { return } + if !MVMCoreUIUtility.isView(view, visibleIn: scrollView) { self.haltVideo() } else { self.unhaltVideo() @@ -132,4 +133,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?.haltVideo() + } + activeListener = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + if MVMCoreUIUtility.isView(view, visibleIn: containingView) { + self?.unhaltVideo() + } + } + } + + 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() + } } From ff96fd284a716ae21c88737d784821fe8a2b57a6 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 11:46:05 -0500 Subject: [PATCH 10/16] load image needed --- MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 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 } From 0f0bc68f1eda8ad17e65abb3f84e78e6ee664984 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 14:19:33 -0500 Subject: [PATCH 11/16] 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() + } } From 4be36bf185ffaef0e5fa07304994b1bfbd657e6c Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 14:32:09 -0500 Subject: [PATCH 12/16] timing issue --- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index 7000bdd5..2ff3cdb1 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -104,11 +104,10 @@ open class VideoModel: MoleculeModelProtocol { open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { printThat(string: "before adding") // reset - halted = false (view.model as? VideoModel)?.view = nil self.view = view printThat(string: "after adding") - + halted = false addVisibleBehavior(for: view, delegateObject: delegateObject) addScrollBehavior(for: view, delegateObject: delegateObject) addActiveListener(for: view, delegateObject: delegateObject) From 742ea295b948dbb08b53d4bcb7675504903a4d77 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 14:55:14 -0500 Subject: [PATCH 13/16] ugh timing workaround --- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 1 + MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index 76b8231b..06474f95 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -35,6 +35,7 @@ open class Video: View { open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { self.delegateObject = delegateObject + (self.model as? VideoModel)?.view = nil super.set(with: model, delegateObject, additionalData) guard let model = model as? VideoModel else { return } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index 2ff3cdb1..68ec423a 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -103,8 +103,6 @@ open class VideoModel: MoleculeModelProtocol { open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { printThat(string: "before adding") - // reset - (view.model as? VideoModel)?.view = nil self.view = view printThat(string: "after adding") halted = false From 7e5aa638ddca2a4d15fda0c805935c072f57a01c Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 15:17:14 -0500 Subject: [PATCH 14/16] active fix --- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index 68ec423a..ac97eba0 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -161,14 +161,14 @@ open class VideoModel: MoleculeModelProtocol { 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 - } + guard let self = self, + let view = self.view, + view.isVisibleInDelegate() else { return } + self.halted = false } } From b5a00cb33fd92c78a9811442e67e019d2c12e125 Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 15:28:38 -0500 Subject: [PATCH 15/16] debug cleanup --- MVMCoreUI/Atomic/Atoms/Views/Video.swift | 9 ++++---- MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift | 22 ------------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift index 06474f95..baee04f6 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -9,7 +9,7 @@ import AVKit open class Video: View { public let videoViewController = AVPlayerViewController() - public var delegateObject: MVMCoreUIDelegateObject? + private weak var containingView: UIView? /// Used to track the state and respond.. private var stateKVOToken: NSKeyValueObservation? @@ -24,7 +24,7 @@ open class Video: View { /// 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 } + guard let containingView = containingView else { return true } return isVisible(in: containingView) } @@ -34,11 +34,12 @@ open class Video: View { } open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - self.delegateObject = delegateObject + /// Detach the view from it's previous model before setting. (self.model as? VideoModel)?.view = nil + containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view super.set(with: model, delegateObject, additionalData) - guard let model = model as? VideoModel else { return } + guard let model = model as? VideoModel else { return } if let controller = delegateObject?.moleculeDelegate as? UIViewController { controller.addChild(videoViewController) videoViewController.didMove(toParent: controller) diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index ac97eba0..1d079599 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -24,9 +24,7 @@ open class VideoModel: MoleculeModelProtocol { 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) @@ -83,28 +81,8 @@ open class VideoModel: MoleculeModelProtocol { try container.encode(alwaysReset, forKey: .alwaysReset) } - 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) - } - open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { - printThat(string: "before adding") self.view = view - printThat(string: "after adding") halted = false addVisibleBehavior(for: view, delegateObject: delegateObject) addScrollBehavior(for: view, delegateObject: delegateObject) From 0aa4e690ae5196c9d549fba3c25cbb8c3963284a Mon Sep 17 00:00:00 2001 From: "Pfeil, Scott Robert" Date: Fri, 12 Feb 2021 16:50:28 -0500 Subject: [PATCH 16/16] move flag set --- MVMCoreUI/BaseControllers/ViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 50172397..ecb4089a 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -348,8 +348,8 @@ import UIKit // First update should be explicit (hence the zero check) if needsUpdateUI || (previousScreenSize != .zero && screenSizeChanged()) { - updateViews() needsUpdateUI = false + updateViews() } previousScreenSize = view.bounds.size;