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 26904344..f869ab06 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -10,17 +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 weak var pauseBehavior: BlockPageVisibilityBehavior? - - private weak var scrollBehavior: BlockPageScrolledBehavior? - - private var visible = true open override func setupView() { super.setupView() @@ -33,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() @@ -54,8 +49,8 @@ open class Video: View { } // Handle pause behavior - addVisibleBehavoir(to: model, delegateObject: delegateObject) - addScrolledBehavoir(to: model, delegateObject: delegateObject) + model.addVisibleBehavior(for: self, delegateObject: delegateObject) + model.addScrollBehavior(for: self, delegateObject: delegateObject) } /// Listens and responds to video loading state changes. @@ -101,62 +96,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 ab6d74e4..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 { @@ -47,8 +49,12 @@ import Foundation private var kvoToken: NSKeyValueObservation? + private var memoryWarningListener: Any? + public init(with videoURLString: String) { self.videoURLString = videoURLString + super.init() + self.addMemoryWarningListener() } public func loadVideo() { @@ -114,7 +120,22 @@ import Foundation 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() } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift index d335f375..c64b13e5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -15,9 +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: PageVisibilityClosureBehavior? + private weak var scrollBehavior: PageScrolledClosureBehavior? private enum CodingKeys: String, CodingKey { case moleculeName @@ -35,7 +41,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 +50,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 +61,76 @@ open class VideoModel: MoleculeModelProtocol { try container.encode(autoPlay, forKey: .autoPlay) try container.encode(alwaysReset, forKey: .alwaysReset) } + + private func haltVideo() { + guard !halted else { return } + halted = true + print("halted \(video)") + 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. + 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, + 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.haltVideo() + } + + 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 containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view else { return } + if !MVMCoreUIUtility.isView(view, visibleIn: containingView) { + self.haltVideo() + } else { + self.unhaltVideo() + } + } + + 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 + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift deleted file mode 100644 index 73293a8b..00000000 --- a/MVMCoreUI/Atomic/Atoms/Views/VideoPauseBehavior.swift +++ /dev/null @@ -1,110 +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) - } -} - - -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/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 { diff --git a/MVMCoreUI/Behaviors/PageBehavior.swift b/MVMCoreUI/Behaviors/PageBehavior.swift index 95fce406..aaf915a1 100644 --- a/MVMCoreUI/Behaviors/PageBehavior.swift +++ b/MVMCoreUI/Behaviors/PageBehavior.swift @@ -43,4 +43,21 @@ public protocol PageScrolledBehavior: PageBehaviorProtocol { public protocol PageBehaviorsTemplateProtocol { 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) + } + } } 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 {