Merge branch 'feature/video_molecule' into 'develop'
Feature/video molecule See merge request BPHV_MIPS/mvm_core_ui!665
This commit is contained in:
commit
8a7f11988a
@ -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 */; };
|
||||
@ -403,6 +405,11 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
@ -892,6 +899,8 @@
|
||||
D23EA7FF247EBD6C00D60C34 /* LabelBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
D23EA801247EBED400D60C34 /* ImageBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
D243859823A16B1800332775 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; };
|
||||
D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageVisibilityClosureBehavior.swift; sourceTree = "<group>"; };
|
||||
D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageScrolledClosureBehavior.swift; sourceTree = "<group>"; };
|
||||
D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModelProtocol.swift; sourceTree = "<group>"; };
|
||||
D2509ED52472EE2F001BFB9D /* NavigationImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationImageButtonModel.swift; sourceTree = "<group>"; };
|
||||
D253BB9B245874F8002DE544 /* BGImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageMolecule.swift; sourceTree = "<group>"; };
|
||||
@ -946,6 +955,11 @@
|
||||
D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageModelProtocol.swift; sourceTree = "<group>"; };
|
||||
D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = "<group>"; };
|
||||
D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = "<group>"; };
|
||||
D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMoleculeModel.swift; sourceTree = "<group>"; };
|
||||
D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMolecule.swift; sourceTree = "<group>"; };
|
||||
D29C558F25C095210082E7D6 /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
||||
D29C559225C0992D0082E7D6 /* VideoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoModel.swift; sourceTree = "<group>"; };
|
||||
D29C559525C099630082E7D6 /* VideoDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDataManager.swift; sourceTree = "<group>"; };
|
||||
D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = "<group>"; };
|
||||
D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MVMCoreUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUI.h; sourceTree = "<group>"; };
|
||||
@ -1228,6 +1242,8 @@
|
||||
children = (
|
||||
27F973522466074500CAB5C5 /* PageBehavior.swift */,
|
||||
27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */,
|
||||
D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */,
|
||||
D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */,
|
||||
);
|
||||
path = Behaviors;
|
||||
sourceTree = "<group>";
|
||||
@ -1679,6 +1695,8 @@
|
||||
D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */,
|
||||
D253BB9D2458751F002DE544 /* BGImageMoleculeModel.swift */,
|
||||
D253BB9B245874F8002DE544 /* BGImageMolecule.swift */,
|
||||
D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */,
|
||||
D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */,
|
||||
);
|
||||
path = OtherContainers;
|
||||
sourceTree = "<group>";
|
||||
@ -2023,6 +2041,9 @@
|
||||
AA37CBD42519072F0027344C /* Stars.swift */,
|
||||
AA07EA902510A442009A2AE3 /* StarModel.swift */,
|
||||
AA07EA922510A451009A2AE3 /* Star.swift */,
|
||||
D29C559525C099630082E7D6 /* VideoDataManager.swift */,
|
||||
D29C559225C0992D0082E7D6 /* VideoModel.swift */,
|
||||
D29C558F25C095210082E7D6 /* Video.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -2688,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 */,
|
||||
@ -2745,6 +2767,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 */,
|
||||
@ -2772,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 */,
|
||||
@ -2784,6 +2808,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 */,
|
||||
@ -2850,6 +2875,7 @@
|
||||
0AA4D2E125CAEC72008DB32D /* AccessibilityModelProtocol.swift 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 */,
|
||||
@ -2875,6 +2901,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 */,
|
||||
@ -2892,6 +2919,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 */,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
108
MVMCoreUI/Atomic/Atoms/Views/Video.swift
Normal file
108
MVMCoreUI/Atomic/Atoms/Views/Video.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// 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()
|
||||
private weak var containingView: UIView?
|
||||
|
||||
/// Used to track the state and respond..
|
||||
private var stateKVOToken: NSKeyValueObservation?
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
videoViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(videoViewController.view)
|
||||
NSLayoutConstraint.constraintPinSubview(toSuperview: videoViewController.view)
|
||||
videoViewController.videoGravity = .resizeAspectFill
|
||||
}
|
||||
|
||||
/// Checks if the video is visible in the molecule delegate
|
||||
open func isVisibleInDelegate() -> Bool {
|
||||
guard let containingView = containingView 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]?) {
|
||||
/// 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 }
|
||||
if let controller = delegateObject?.moleculeDelegate as? UIViewController {
|
||||
controller.addChild(videoViewController)
|
||||
videoViewController.didMove(toParent: controller)
|
||||
}
|
||||
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:
|
||||
guard isVisibleInDelegate() else { return }
|
||||
// Video loaded, unhalt it if necessary.
|
||||
model.halted = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.halted && model.autoPlay && self.isVisibleInDelegate() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
141
MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift
Normal file
141
MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift
Normal file
@ -0,0 +1,141 @@
|
||||
//
|
||||
// VideoDataManager.swift
|
||||
// MVMCoreUI
|
||||
//
|
||||
// Created by Scott Pfeil on 1/26/21.
|
||||
// Copyright © 2021 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
@objcMembers open class VideoDataManager: NSObject {
|
||||
|
||||
/// The state of the video.
|
||||
@objc public enum VideoState: 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 = 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: VideoState {
|
||||
get {
|
||||
var state = VideoState.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?
|
||||
|
||||
private var memoryWarningListener: Any?
|
||||
|
||||
public init(with videoURLString: String) {
|
||||
self.videoURLString = videoURLString
|
||||
super.init()
|
||||
self.addMemoryWarningListener()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private func addMemoryWarningListener() {
|
||||
memoryWarningListener = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
|
||||
self?.removeVideoObserver()
|
||||
self?.player = nil
|
||||
self?.videoState = .none
|
||||
}
|
||||
}
|
||||
|
||||
private func removeMemoryWarningListener() {
|
||||
guard let observer = memoryWarningListener else { return }
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
memoryWarningListener = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeVideoObserver()
|
||||
removeMemoryWarningListener()
|
||||
}
|
||||
}
|
||||
167
MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift
Normal file
167
MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift
Normal file
@ -0,0 +1,167 @@
|
||||
//
|
||||
// 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
|
||||
weak public var view: Video?
|
||||
|
||||
/// When the video is halted because it is no longer visible
|
||||
public var halted: Bool = false {
|
||||
didSet {
|
||||
guard halted != oldValue,
|
||||
videoDataManager.videoState == .loaded else { return }
|
||||
if halted {
|
||||
videoDataManager.player?.pause()
|
||||
} else {
|
||||
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
|
||||
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)
|
||||
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
|
||||
}
|
||||
videoDataManager = VideoDataManager(with: video)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
self.view = view
|
||||
halted = false
|
||||
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 view = self.view,
|
||||
view.isVisibleInDelegate() else { return }
|
||||
self.halted = false
|
||||
}
|
||||
let onHide: () -> Void = { [weak self] in
|
||||
self?.halted = true
|
||||
}
|
||||
|
||||
guard visibleBehavior == nil else {
|
||||
visibleBehavior?.pageShownHandler = onShow
|
||||
visibleBehavior?.pageHiddenHandler = onHide
|
||||
return
|
||||
}
|
||||
|
||||
guard var delegate = delegateObject?.behaviorTemplateDelegate else { return }
|
||||
let pauseBehavior = PageVisibilityClosureBehavior(with: onShow, onPageHiddenHandler: onHide)
|
||||
delegate.add(behavior: pauseBehavior)
|
||||
self.visibleBehavior = pauseBehavior
|
||||
}
|
||||
|
||||
/// 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,
|
||||
let view = self.view else { return }
|
||||
self.halted = !view.isVisible(in: scrollView)
|
||||
}
|
||||
|
||||
guard scrollBehavior == nil else {
|
||||
scrollBehavior?.pageScrolledHandler = onScroll
|
||||
return
|
||||
}
|
||||
|
||||
guard var delegate = delegateObject?.behaviorTemplateDelegate else { return }
|
||||
let scrollBehavior = PageScrolledClosureBehavior(with: onScroll)
|
||||
delegate.add(behavior: scrollBehavior)
|
||||
self.scrollBehavior = scrollBehavior
|
||||
}
|
||||
|
||||
open func addActiveListener(for view: Video, delegateObject: MVMCoreUIDelegateObject?) {
|
||||
removeActiveListener()
|
||||
|
||||
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
|
||||
guard let self = self,
|
||||
let view = self.view,
|
||||
view.isVisibleInDelegate() else { return }
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -73,6 +73,12 @@ open class ScrollingViewController: ViewController {
|
||||
scrollView.flashScrollIndicators()
|
||||
}
|
||||
|
||||
open func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
executeBehaviors { (behavior: PageScrolledBehavior) in
|
||||
behavior.pageScrolled(scrollView: scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Keyboard Handling
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -35,7 +35,29 @@ public protocol PageVisibilityBehavior: PageBehaviorProtocol {
|
||||
func onPageHidden()
|
||||
}
|
||||
|
||||
public protocol PageScrolledBehavior: PageBehaviorProtocol {
|
||||
|
||||
func pageScrolled(scrollView: UIScrollView)
|
||||
}
|
||||
|
||||
public protocol PageBehaviorsTemplateProtocol {
|
||||
|
||||
var behaviors: [PageBehaviorProtocol]? { get }
|
||||
var behaviors: [PageBehaviorProtocol]? { get set }
|
||||
|
||||
}
|
||||
|
||||
public extension PageBehaviorsTemplateProtocol {
|
||||
mutating func add(behavior: PageBehaviorProtocol) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift
Normal file
33
MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
40
MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift
Normal file
40
MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user