diff --git a/ios/example-pip-app/src/Info.plist b/ios/example-pip-app/src/Info.plist index 8a99dee66..ff67a0ed6 100644 --- a/ios/example-pip-app/src/Info.plist +++ b/ios/example-pip-app/src/Info.plist @@ -51,6 +51,8 @@ NSAllowsArbitraryLoads + UIViewControllerBasedStatusBarAppearance + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/ios/example-pip-app/src/ViewController.swift b/ios/example-pip-app/src/ViewController.swift index af4f03983..7d608f4f2 100644 --- a/ios/example-pip-app/src/ViewController.swift +++ b/ios/example-pip-app/src/ViewController.swift @@ -21,19 +21,73 @@ class ViewController: UIViewController { @IBOutlet weak var videoButton: UIButton? - private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator? + fileprivate var pipViewCoordinator: PiPViewCoordinator? + fileprivate var jitsiMeetView: JitsiMeetView? override func viewDidLoad() { super.viewDidLoad() } + override func viewWillTransition(to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + let rect = CGRect(origin: CGPoint.zero, size: size) + pipViewCoordinator?.resetBounds(bounds: rect) + } + // MARK: - Actions @IBAction func openJitsiMeet(sender: Any?) { - let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator() - self.jitsiMeetCoordinator = jitsiMeetCoordinator - jitsiMeetCoordinator.jitsiMeetView.welcomePageEnabled = true - jitsiMeetCoordinator.jitsiMeetView.load(nil) - jitsiMeetCoordinator.show() + cleanUp() + + // create and configure jitsimeet view + let jitsiMeetView = JitsiMeetView() + jitsiMeetView.welcomePageEnabled = true + jitsiMeetView.pictureInPictureEnabled = true + jitsiMeetView.load(nil) + jitsiMeetView.delegate = self + self.jitsiMeetView = jitsiMeetView + + // Enable jitsimeet view to be a view that can be displayed + // on top of all the things, and let the coordinator to manage + // the view state and interactions + pipViewCoordinator = PiPViewCoordinator(withView: jitsiMeetView) + pipViewCoordinator?.configureAsStickyView(withParentView: view) + + // animate in + jitsiMeetView.alpha = 0 + pipViewCoordinator?.show() + } + + fileprivate func cleanUp() { + jitsiMeetView?.removeFromSuperview() + jitsiMeetView = nil + pipViewCoordinator = nil + } +} + +extension ViewController: JitsiMeetViewDelegate { + + func conferenceFailed(_ data: [AnyHashable : Any]!) { + hideJitsiMeetViewAndCleanUp() + } + + func conferenceLeft(_ data: [AnyHashable : Any]!) { + hideJitsiMeetViewAndCleanUp() + } + + func enterPicture(inPicture data: [AnyHashable : Any]!) { + DispatchQueue.main.async { + self.pipViewCoordinator?.enterPictureInPicture() + } + } + + private func hideJitsiMeetViewAndCleanUp() { + DispatchQueue.main.async { + self.pipViewCoordinator?.hide() { _ in + self.cleanUp() + } + } } } diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index b0d098d87..e5b4b731d 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -31,10 +31,8 @@ 75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; }; C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; }; C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; }; - C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; }; - C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; }; C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; }; - C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; }; + C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; }; C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; }; /* End PBXBuildFile section */ @@ -68,10 +66,8 @@ 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = ""; }; C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = ""; }; C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = ""; }; - C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = ""; }; - C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = ""; }; C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; - C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = ""; }; + C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = ""; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -171,9 +167,7 @@ isa = PBXGroup; children = ( C6A3425E204EF76800E062DD /* DragGestureController.swift */, - C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */, - C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */, - C6A3425C204EF76800E062DD /* PiPWindow.swift */, + C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */, ); path = "picture-in-picture"; sourceTree = ""; @@ -345,14 +339,12 @@ buildActionMask = 2147483647; files = ( 0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */, - C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */, 0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */, 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */, - C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */, - C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */, + C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, diff --git a/ios/sdk/src/picture-in-picture/JitsiMeetPresentationCoordinator.swift b/ios/sdk/src/picture-in-picture/JitsiMeetPresentationCoordinator.swift deleted file mode 100644 index ad8ac92c9..000000000 --- a/ios/sdk/src/picture-in-picture/JitsiMeetPresentationCoordinator.swift +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright @ 2017-present Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Foundation - -/// Coordinates the presentation of JitsiMeetViewController inside of -/// an external window that can be resized and dragged with custom PiP mode -open class JitsiMeetPresentationCoordinator: NSObject { - - public let meetViewController: JitsiMeetViewController - public let meetWindow: PiPWindow - - public var isInPiP: Bool { - get { - return meetWindow.isInPiP - } - } - - public var jitsiMeetView: JitsiMeetView { - get { - return meetViewController.jitsiMeetView - } - } - - public init(meetViewController: JitsiMeetViewController? = nil, - meetWindow: PiPWindow? = nil) { - self.meetViewController = meetViewController ?? JitsiMeetViewController() - self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds) - - super.init() - - configureMeetWindow() - configureMeetViewController() - } - - /// Show window with jitsi meet and perform a completion closure - open func show(completion: CompletionAction? = nil) { - meetWindow.show(completion: completion) - } - - /// Hide window with jitsi meet and perform a completion closure - open func hide(completion: CompletionAction? = nil) { - meetWindow.hide(completion: completion) - } - - open func cleanUp() { - // TODO: more clean up work on this - - meetWindow.isHidden = true - meetWindow.stopDragGesture() - } - - deinit { - cleanUp() - } - - // MARK: - helpers - - private func configureMeetViewController() { - meetViewController.jitsiMeetView.pictureInPictureEnabled = true - meetViewController.delegate = self - } - - private func configureMeetWindow() { - meetWindow.backgroundColor = .clear - meetWindow.windowLevel = UIWindowLevelStatusBar + 100 - meetWindow.rootViewController = self.meetViewController - } -} - -extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate { - - open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) { - switch to { - case .enterPictureInPicture: - meetWindow.enterPictureInPicture() - case .sizeChange: - // resize to full screen if rotation happens - if meetWindow.isInPiP { - meetWindow.exitPictureInPicture() - } - } - } - - open func conferenceStarted() { - if meetWindow.isHidden { - meetWindow.show() - } - } - - open func conferenceEnded(didFail: Bool) { - cleanUp() - } -} diff --git a/ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift b/ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift deleted file mode 100644 index 6e2b952be..000000000 --- a/ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright @ 2017-present Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -public enum JitsiMeetPresentationUpdate { - - /// The conference wants to enter Picture-in-Picture - case enterPictureInPicture - - /// A screen size change (usually screen rotation) - case sizeChange -} - -public protocol JitsiMeetViewControllerDelegate: class { - - /// Notifies a change of the conference presentation style. - /// - /// - Parameter to: The presentation state that will be changed to - func performPresentationUpdate(to: JitsiMeetPresentationUpdate) - - /// The conference started - func conferenceStarted() - - /// The conference ended - /// - /// - Parameter didFail: The reason of ending the conference - func conferenceEnded(didFail: Bool) -} - -/// Wrapper ViewController of a JitsiMeetView -/// -/// Is suggested to override this class and implement some customization -/// on how to handle the JitsiMeetView delegate events -open class JitsiMeetViewController: UIViewController { - - open weak var delegate: JitsiMeetViewControllerDelegate? - - private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView() - - override open func loadView() { - super.loadView() - self.view = jitsiMeetView - } - - open override func viewDidLoad() { - super.viewDidLoad() - - jitsiMeetView.delegate = self - } - - open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - delegate?.performPresentationUpdate(to: .sizeChange) - } -} - -extension JitsiMeetViewController: JitsiMeetViewDelegate { - - open func conferenceWillJoin(_ data: [AnyHashable : Any]!) { - // do something - } - - open func conferenceJoined(_ data: [AnyHashable : Any]!) { - DispatchQueue.main.async { - self.delegate?.conferenceStarted() - } - } - - open func conferenceWillLeave(_ data: [AnyHashable : Any]!) { - // do something - } - - open func conferenceLeft(_ data: [AnyHashable : Any]!) { - DispatchQueue.main.async { - self.delegate?.conferenceEnded(didFail: false) - } - } - - open func conferenceFailed(_ data: [AnyHashable : Any]!) { - DispatchQueue.main.async { - self.delegate?.conferenceEnded(didFail: true) - } - } - - open func loadConfigError(_ data: [AnyHashable : Any]!) { - DispatchQueue.main.async { - self.delegate?.conferenceEnded(didFail: true) - } - } - - open func enterPicture(inPicture data: [AnyHashable : Any]!) { - DispatchQueue.main.async { - self.delegate?.performPresentationUpdate(to: .enterPictureInPicture) - } - } -} diff --git a/ios/sdk/src/picture-in-picture/PiPWindow.swift b/ios/sdk/src/picture-in-picture/PiPViewCoordinator.swift similarity index 60% rename from ios/sdk/src/picture-in-picture/PiPWindow.swift rename to ios/sdk/src/picture-in-picture/PiPViewCoordinator.swift index c5c518fc9..2514fd122 100644 --- a/ios/sdk/src/picture-in-picture/PiPWindow.swift +++ b/ios/sdk/src/picture-in-picture/PiPViewCoordinator.swift @@ -14,14 +14,15 @@ * limitations under the License. */ -/// Alias defining a completion closure that returns a Bool -public typealias CompletionAction = (Bool) -> Void +public typealias AnimationCompletion = (Bool) -> Void -/// A window that allows its root view controller to be presented -/// in full screen or in a custom Picture in Picture mode -open class PiPWindow: UIWindow { +/// Coordinates the view state of a specified view to allow +/// to be presented in full screen or in a custom Picture in Picture mode. +/// This object will also provide the drag and tap interactions of the view +/// when is presented in Picure in Picture mode. +public class PiPViewCoordinator { - /// Limits the boundries of root view position on screen when minimized + /// Limits the boundries of view position on screen when minimized public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25, left: 5, bottom: 5, @@ -31,10 +32,10 @@ open class PiPWindow: UIWindow { } } - /// The size ratio for root view controller view when in PiP mode + /// The size ratio of the view when in PiP mode public var pipSizeRatio: CGFloat = { let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom - switch (deviceIdiom) { + switch deviceIdiom { case .pad: return 0.25 case .phone: @@ -44,50 +45,62 @@ open class PiPWindow: UIWindow { } }() - /// The PiP state of this contents of the window - private(set) var isInPiP: Bool = false + private(set) var isInPiP: Bool = false // true if view is in PiP mode - private let dragController: DragGestureController = DragGestureController() + private(set) var view: UIView + private var currentBounds: CGRect = CGRect.zero - /// Used when in PiP mode to enable/disable exit PiP UI private var tapGestureRecognizer: UITapGestureRecognizer? private var exitPiPButton: UIButton? - /// Help out to bubble up the gesture detection outside of the rootVC frame - open override func point(inside point: CGPoint, - with event: UIEvent?) -> Bool { - guard let vc = rootViewController else { - return super.point(inside: point, with: event) - } - return vc.view.frame.contains(point) + private let dragController: DragGestureController = DragGestureController() + + public init(withView view: UIView) { + self.view = view } - /// animate in the window - open func show(completion: CompletionAction? = nil) { - if self.isHidden || self.alpha < 1 { - self.isHidden = false - self.alpha = 0 + /// Configure the view to be always on top of all the contents + /// of the provided parent view. + /// If a parentView is not provided it will try to use the main window + public func configureAsStickyView(withParentView parentView: UIView? = nil) { + guard + let parentView = parentView ?? UIApplication.shared.keyWindow + else { return } + + parentView.addSubview(view) + currentBounds = parentView.bounds + view.frame = currentBounds + view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude) + } + + /// Show view with fade in animation + public func show(completion: AnimationCompletion? = nil) { + if view.isHidden || view.alpha < 1 { + view.isHidden = false + view.alpha = 0 - animateTransition(animations: { - self.alpha = 1 + animateTransition(animations: { [weak self] in + self?.view.alpha = 1 }, completion: completion) } } - /// animate out the window - open func hide(completion: CompletionAction? = nil) { - if !self.isHidden || self.alpha > 0 { - animateTransition(animations: { - self.alpha = 1 + /// Hide view with fade out animation + public func hide(completion: AnimationCompletion? = nil) { + if view.isHidden || view.alpha > 0 { + animateTransition(animations: { [weak self] in + self?.view.alpha = 0 + self?.view.isHidden = true }, completion: completion) } } - /// Resize the root view to PiP mode - open func enterPictureInPicture() { - guard let view = rootViewController?.view else { return } + /// Resize view to and change state to custom PictureInPicture mode + /// This will resize view, add a gesture to enable user to "drag" view + /// around screen, and add a button of top of the view to be able to exit mode + public func enterPictureInPicture() { isInPiP = true - animateRootViewChange() + animateViewChange() dragController.startDragListener(inView: view) dragController.insets = dragBoundInsets @@ -99,10 +112,11 @@ open class PiPWindow: UIWindow { view.addGestureRecognizer(tapGestureRecognizer) } - /// Resize the root view to full screen - open func exitPictureInPicture() { + /// Exit Picture in picture mode, this will resize view, remove + /// exit pip button, and disable the drag gesture + @objc public func exitPictureInPicture() { isInPiP = false - animateRootViewChange() + animateViewChange() dragController.stopDragListener() // hide PiP UI @@ -115,6 +129,13 @@ open class PiPWindow: UIWindow { tapGestureRecognizer = nil } + /// Reset view to provide bounds, use this method on rotation or + /// screen size changes + public func resetBounds(bounds: CGRect) { + currentBounds = bounds + exitPictureInPicture() + } + /// Stop the dragging gesture of the root view public func stopDragGesture() { dragController.stopDragListener() @@ -132,41 +153,14 @@ open class PiPWindow: UIWindow { button.backgroundColor = .gray button.layer.cornerRadius = size.width / 2 button.frame = CGRect(origin: CGPoint.zero, size: size) - if let view = rootViewController?.view { - button.center = view.convert(view.center, from:view.superview) - } + button.center = view.convert(view.center, from: view.superview) button.addTarget(target, action: action, for: .touchUpInside) return button } - // MARK: - Manage presentation switching - - private func animateRootViewChange() { - UIView.animate(withDuration: 0.25) { - self.rootViewController?.view.frame = self.changeRootViewRect() - self.rootViewController?.view.setNeedsLayout() - } - } - - private func changeRootViewRect() -> CGRect { - guard isInPiP else { - return self.bounds - } - - // resize to suggested ratio and position to the bottom right - let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets) - let size = CGSize(width: bounds.size.width * pipSizeRatio, - height: bounds.size.height * pipSizeRatio) - let x: CGFloat = adjustedBounds.maxX - size.width - let y: CGFloat = adjustedBounds.maxY - size.height - return CGRect(x: x, y: y, width: size.width, height: size.height) - } - - // MARK: - Exit PiP + // MARK: - Interactions @objc private func toggleExitPiP() { - guard let view = rootViewController?.view else { return } - if exitPiPButton == nil { // show button let exitSelector = #selector(exitPictureInPicture) @@ -182,18 +176,40 @@ open class PiPWindow: UIWindow { } } - @objc private func exitPiP() { - exitPictureInPicture() + // MARK: - Size calculation + + private func animateViewChange() { + UIView.animate(withDuration: 0.25) { + self.view.frame = self.changeViewRect() + self.view.setNeedsLayout() + } } - // MARK: - Animation transition + private func changeViewRect() -> CGRect { + let bounds = currentBounds + + guard isInPiP else { + return bounds + } + + // resize to suggested ratio and position to the bottom right + let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets) + let size = CGSize(width: bounds.size.width * pipSizeRatio, + height: bounds.size.height * pipSizeRatio) + let x: CGFloat = adjustedBounds.maxX - size.width + let y: CGFloat = adjustedBounds.maxY - size.height + return CGRect(x: x, y: y, width: size.width, height: size.height) + } + + // MARK: - Animation helpers private func animateTransition(animations: @escaping () -> Void, - completion: CompletionAction?) { + completion: AnimationCompletion?) { UIView.animate(withDuration: 0.1, delay: 0, options: .beginFromCurrentState, animations: animations, completion: completion) } + }