diff --git a/Signal/src/ViewControllers/Call/CallViewController.swift b/Signal/src/ViewControllers/Call/CallViewController.swift index 24e14e8865..35aa893379 100644 --- a/Signal/src/ViewControllers/Call/CallViewController.swift +++ b/Signal/src/ViewControllers/Call/CallViewController.swift @@ -31,6 +31,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, // MARK: - Views + private lazy var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) private lazy var backgroundAvatarView = UIImageView() private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() @@ -190,6 +191,13 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, self.shouldUseTheme = false } + deinit { + // These views might be in the return to call PIP's hierarchy, + // we want to remove them so they are free'd when the call ends + remoteVideoView.removeFromSuperview() + localVideoView.removeFromSuperview() + } + @objc func didBecomeActive() { if self.isViewLoaded { shouldRemoteVideoControlsBeHidden = false @@ -213,6 +221,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, override func loadView() { view = UIView() + view.clipsToBounds = true view.backgroundColor = UIColor.black view.layoutMargins = UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20) @@ -271,8 +280,6 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, backgroundAvatarView.autoPinEdgesToSuperviewEdges() // Dark blurred background. - let blurEffect = UIBlurEffect(style: .dark) - let blurView = UIVisualEffectView(effect: blurEffect) blurView.isUserInteractionEnabled = false view.addSubview(blurView) blurView.autoPinEdgesToSuperviewEdges() @@ -587,11 +594,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, private func nearestValidLocalVideoFrame(for origin: CGPoint) -> CGRect { var newFrame = CGRect( origin: origin, - // The PIP has a fixed size for iPhone and iPad - size: CGSize( - width: UIDevice.current.isIPad ? 272 : 90, - height: UIDevice.current.isIPad ? 154 : 160 - ) + size: ReturnToCallViewController.pipSize ) let boundingRect = localVideoBoundingRect @@ -631,6 +634,8 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, } private func updateLocalVideoLayout() { + guard localVideoView.superview == view else { return } + guard !isRenderingLocalVanityVideo else { view.layoutIfNeeded() localVideoView.frame = view.frame @@ -1431,3 +1436,57 @@ extension CallViewController: UIGestureRecognizerDelegate { } } } + +extension CallViewController: CallViewControllerWindowReference { + var remoteVideoViewReference: UIView { remoteVideoView } + var localVideoViewReference: UIView { localVideoView } + + @objc + public func returnFromPip(pipWindow: UIWindow) { + // The call "pip" uses our remote and local video views since only + // one `AVCaptureVideoPreviewLayer` per capture session is supported. + // We need to re-add them when we return to this view. + guard remoteVideoView.superview != view && localVideoView.superview != view else { + return owsFailDebug("unexpectedly returned to call while we own the video views") + } + + guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else { + return owsFailDebug("failed to snapshot rootViewController") + } + + guard let pipSnapshot = pipWindow.snapshotView(afterScreenUpdates: false) else { + return owsFailDebug("failed to snapshot pip") + } + + view.insertSubview(remoteVideoView, aboveSubview: blurView) + remoteVideoView.autoPinEdgesToSuperviewEdges() + + view.insertSubview(localVideoView, aboveSubview: contactAvatarContainerView) + + shouldRemoteVideoControlsBeHidden = false + + animateReturnFromPip(pipSnapshot: pipSnapshot, pipFrame: pipWindow.frame, splitViewSnapshot: splitViewSnapshot) + } + + private func animateReturnFromPip(pipSnapshot: UIView, pipFrame: CGRect, splitViewSnapshot: UIView) { + guard let window = view.window else { return owsFailDebug("missing window") } + view.superview?.insertSubview(splitViewSnapshot, belowSubview: view) + splitViewSnapshot.autoPinEdgesToSuperviewEdges() + + view.frame = pipFrame + view.addSubview(pipSnapshot) + pipSnapshot.autoPinEdgesToSuperviewEdges() + + view.layoutIfNeeded() + + UIView.animate(withDuration: 0.2, animations: { + pipSnapshot.alpha = 0 + self.view.frame = window.frame + self.view.layoutIfNeeded() + }) { _ in + self.updateCallUI() + splitViewSnapshot.removeFromSuperview() + pipSnapshot.removeFromSuperview() + } + } +} diff --git a/Signal/src/environment/SignalApp.h b/Signal/src/environment/SignalApp.h index f14460175a..7b806b4508 100644 --- a/Signal/src/environment/SignalApp.h +++ b/Signal/src/environment/SignalApp.h @@ -55,6 +55,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)receivedVerificationCode:(NSString *)verificationCode; - (void)applicationWillTerminate; +- (nullable UIView *)snapshotSplitViewControllerAfterScreenUpdates:(BOOL)afterScreenUpdates; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/environment/SignalApp.m b/Signal/src/environment/SignalApp.m index 3889ad42c1..5e8d3e4df2 100644 --- a/Signal/src/environment/SignalApp.m +++ b/Signal/src/environment/SignalApp.m @@ -365,6 +365,11 @@ NSString *const kNSUserDefaults_DidTerminateKey = @"kNSUserDefaults_DidTerminate [self.conversationSplitViewController showNewConversationView]; } +- (nullable UIView *)snapshotSplitViewControllerAfterScreenUpdates:(BOOL)afterScreenUpdates +{ + return [self.conversationSplitViewController.view snapshotViewAfterScreenUpdates:afterScreenUpdates]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/ViewControllers/OWSNavigationController.m b/SignalMessaging/ViewControllers/OWSNavigationController.m index cdad0b7b1c..86d3d9f16b 100644 --- a/SignalMessaging/ViewControllers/OWSNavigationController.m +++ b/SignalMessaging/ViewControllers/OWSNavigationController.m @@ -7,7 +7,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface UINavigationController (OWSNavigationController) +@interface UINavigationController (OWSNavigationController) @end @@ -30,7 +30,11 @@ NS_ASSUME_NONNULL_BEGIN if (!self) { return self; } - [self setupNavbar]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(themeDidChange:) + name:ThemeDidChangeNotification + object:nil]; return self; } @@ -77,24 +81,6 @@ NS_ASSUME_NONNULL_BEGIN return [super prefersStatusBarHidden]; } -#pragma mark - UINavigationBarDelegate - -- (void)setupNavbar -{ - if (![self.navigationBar isKindOfClass:[OWSNavigationBar class]]) { - OWSFailDebug(@"navigationBar was unexpected class: %@", self.navigationBar); - return; - } - OWSNavigationBar *navbar = (OWSNavigationBar *)self.navigationBar; - navbar.navBarLayoutDelegate = self; - [self updateLayoutForNavbar:navbar]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(themeDidChange:) - name:ThemeDidChangeNotification - object:nil]; -} - // All OWSNavigationController serve as the UINavigationBarDelegate for their navbar. // We override shouldPopItem: in order to cancel some back button presses - for example, // if a view has unsaved changes. @@ -148,21 +134,10 @@ NS_ASSUME_NONNULL_BEGIN } } -#pragma mark - NavBarLayoutDelegate - -- (void)navBarCallLayoutDidChangeWithNavbar:(OWSNavigationBar *)navbar -{ - [self updateLayoutForNavbar:navbar]; - [self setNeedsStatusBarAppearanceUpdate]; -} - - (UIStatusBarStyle)preferredStatusBarStyle { if (!CurrentAppContext().isMainApp) { return super.preferredStatusBarStyle; - } else if (OWSWindowManager.sharedManager.hasCall) { - // Status bar is overlaying the green "call banner" - return UIStatusBarStyleLightContent; } else { UIViewController *presentedViewController = self.presentedViewController; if (presentedViewController != nil && !presentedViewController.isBeingDismissed) { @@ -173,26 +148,6 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)updateLayoutForNavbar:(OWSNavigationBar *)navbar -{ - OWSLogDebug(@""); - - [UIView setAnimationsEnabled:NO]; - - if (!CurrentAppContext().isMainApp) { - self.additionalSafeAreaInsets = UIEdgeInsetsZero; - } else if (OWSWindowManager.sharedManager.hasCall) { - self.additionalSafeAreaInsets = UIEdgeInsetsMake(20, 0, 0, 0); - } else { - self.additionalSafeAreaInsets = UIEdgeInsetsZero; - } - - // we have to ensure the navbar frame *in* layoutSubviews. - [navbar layoutSubviews]; - - [UIView setAnimationsEnabled:YES]; -} - #pragma mark - Orientation - (UIInterfaceOrientationMask)supportedInterfaceOrientations diff --git a/SignalMessaging/ViewControllers/ReturnToCallViewController.swift b/SignalMessaging/ViewControllers/ReturnToCallViewController.swift index ad52e08137..15dfee5102 100644 --- a/SignalMessaging/ViewControllers/ReturnToCallViewController.swift +++ b/SignalMessaging/ViewControllers/ReturnToCallViewController.swift @@ -1,9 +1,19 @@ // -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. // import Foundation +@objc +public protocol CallViewControllerWindowReference: class { + var localVideoViewReference: UIView { get } + var remoteVideoViewReference: UIView { get } + var thread: TSContactThread { get } + var view: UIView { get } + + func returnFromPip(pipWindow: UIWindow) +} + @objc public protocol ReturnToCallViewControllerDelegate: class { func returnToCallWasTapped(_ viewController: ReturnToCallViewController) @@ -12,79 +22,207 @@ public protocol ReturnToCallViewControllerDelegate: class { @objc public class ReturnToCallViewController: UIViewController { + @objc + public static var pipSize = UIDevice.current.isIPad ? CGSize(width: 272, height: 204) : CGSize(width: 90, height: 160) + @objc public weak var delegate: ReturnToCallViewControllerDelegate? - let returnToCallLabel = UILabel() + private weak var callViewController: CallViewControllerWindowReference? @objc - public func startAnimating() { - NotificationCenter.default.addObserver(self, selector: #selector(didTapStatusBar(notification:)), name: .TappedStatusBar, object: nil) - self.returnToCallLabel.layer.removeAllAnimations() - self.returnToCallLabel.alpha = 1 - UIView.animate(withDuration: 1, - delay: 0, - options: [.repeat, .autoreverse], - animations: { self.returnToCallLabel.alpha = 0 }, - completion: { _ in self.returnToCallLabel.alpha = 1 }) + public func displayForCallViewController(_ callViewController: CallViewControllerWindowReference) { + guard callViewController !== self.callViewController else { return } + + guard let callViewSnapshot = callViewController.view.snapshotView(afterScreenUpdates: false) else { + return owsFailDebug("failed to snapshot call view") + } + + self.callViewController = callViewController + + view.addSubview(callViewController.remoteVideoViewReference) + callViewController.remoteVideoViewReference.autoPinEdgesToSuperviewEdges() + + view.addSubview(callViewController.localVideoViewReference) + callViewController.localVideoViewReference.layer.cornerRadius = 6 + + let localVideoSize = CGSizeScale(Self.pipSize, 0.3) + callViewController.localVideoViewReference.frame = CGRect( + origin: CGPoint( + x: Self.pipSize.width - 6 - localVideoSize.width, + y: Self.pipSize.height - 6 - localVideoSize.height + ), + size: localVideoSize + ) + + backgroundAvatarView.image = Environment.shared.contactsManager.profileImageForAddress( + withSneakyTransaction: callViewController.thread.contactAddress + ) + avatarView.image = OWSAvatarBuilder.buildImage(thread: callViewController.thread, diameter: 60) + + animatePipPresentation(snapshot: callViewSnapshot) } - @objc - public func stopAnimating() { - NotificationCenter.default.removeObserver(self, name: .TappedStatusBar, object: nil) - self.returnToCallLabel.layer.removeAllAnimations() - } + private lazy var avatarView = AvatarImageView() + private lazy var backgroundAvatarView = UIImageView() + private lazy var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) override public func loadView() { - self.view = UIView() + view = UIView() - // This is the color of the iOS "return to call" banner. - view.backgroundColor = UIColor(rgbHex: 0x4cd964) - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView)) - view.addGestureRecognizer(tapGesture) + view.backgroundColor = .black + view.clipsToBounds = true + view.layer.cornerRadius = 8 - view.addSubview(returnToCallLabel) + backgroundAvatarView.contentMode = .scaleAspectFill + view.addSubview(backgroundAvatarView) + backgroundAvatarView.autoPinEdgesToSuperviewEdges() - // System UI doesn't use dynamic type for status bar; neither do we. - returnToCallLabel.font = UIFont.ows_regularFont(withSize: 14) - returnToCallLabel.text = NSLocalizedString("CALL_WINDOW_RETURN_TO_CALL", comment: "Label for the 'return to call' banner.") - returnToCallLabel.textColor = .white + view.addSubview(blurView) + blurView.autoPinEdgesToSuperviewEdges() - returnToCallLabel.autoPinEdge(toSuperviewEdge: .bottom, withInset: 2) - returnToCallLabel.setCompressionResistanceHigh() - returnToCallLabel.setContentHuggingHigh() - returnToCallLabel.autoHCenterInSuperview() + view.addSubview(avatarView) + avatarView.autoSetDimensions(to: CGSize(square: 60)) + avatarView.autoCenterInSuperview() + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap))) + view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan))) } public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: { _ in - OWSWindowManager.shared.ensureReturnToCallWindowFrame() + coordinator.animate(alongsideTransition: { [weak self] _ in + self?.updatePipLayout() }, completion: nil) } - @objc - public func didTapView(gestureRecognizer: UITapGestureRecognizer) { - self.delegate?.returnToCallWasTapped(self) + // MARK: - + + private func animatePipPresentation(snapshot: UIView) { + guard let window = view.window else { return owsFailDebug("missing window") } + let previousOrigin = window.frame.origin + window.frame = OWSWindowManager.shared.rootWindow.bounds + + view.addSubview(snapshot) + snapshot.autoPinEdgesToSuperviewEdges() + + window.layoutIfNeeded() + + UIView.animate(withDuration: 0.2, animations: { + snapshot.alpha = 0 + window.frame = self.nearestValidPipFrame(for: previousOrigin) + window.layoutIfNeeded() + }) { _ in + snapshot.removeFromSuperview() + } + } + + private var pipBoundingRect: CGRect { + let padding: CGFloat = 4 + var rect = CurrentAppContext().frame + + let safeAreaInsets = OWSWindowManager.shared.rootWindow.safeAreaInsets + + let leftInset = safeAreaInsets.left + padding + let rightInset = safeAreaInsets.right + padding + rect.origin.x += leftInset + rect.size.width -= leftInset + rightInset + + let topInset = safeAreaInsets.top + padding + let bottomInset = safeAreaInsets.bottom + padding + rect.origin.y += topInset + rect.size.height -= topInset + bottomInset + + return rect + } + + private func nearestValidPipFrame(for origin: CGPoint) -> CGRect { + var newFrame = CGRect(origin: origin, size: Self.pipSize) + + let boundingRect = pipBoundingRect + + // If the origin is zero, we always want to position + // the pip in the top right + let hasZeroOrigin = newFrame.origin == .zero + + // If we're positioned outside of the vertical bounds, we + // want to position the pip at the nearest bound + let positionedOutOfVerticalBounds = newFrame.minY < boundingRect.minY || newFrame.maxY > boundingRect.maxY + + // If we're position anywhere but exactly at the horizontal + // edges, we want to position the pip at the nearest edge + let positionedAwayFromHorizontalEdges = boundingRect.minX != newFrame.minX && boundingRect.maxX != newFrame.maxX + + if positionedOutOfVerticalBounds { + if newFrame.minY < boundingRect.minY || hasZeroOrigin { + newFrame.origin.y = boundingRect.minY + } else { + newFrame.origin.y = boundingRect.maxY - newFrame.height + } + } + + if positionedAwayFromHorizontalEdges { + let distanceFromLeading = newFrame.minX - boundingRect.minX + let distanceFromTrailing = boundingRect.maxX - newFrame.maxX + + if distanceFromLeading > distanceFromTrailing || hasZeroOrigin { + newFrame.origin.x = boundingRect.maxX - newFrame.width + } else { + newFrame.origin.x = boundingRect.minX + } + } + + return newFrame + } + + private func updatePipLayout() { + guard let window = view.window else { return owsFailDebug("missing window") } + let newFrame = nearestValidPipFrame(for: window.frame.origin) + UIView.animate(withDuration: 0.25) { window.frame = newFrame } + } + + private var startingTranslation: CGPoint? + @objc func handlePan(sender: UIPanGestureRecognizer) { + guard let window = view.window else { return owsFailDebug("missing window") } + + switch sender.state { + case .began, .changed: + let translation = sender.translation(in: view) + sender.setTranslation(.zero, in: view) + + window.frame.origin.y += translation.y + window.frame.origin.x += translation.x + case .ended, .cancelled, .failed: + let velocity = sender.velocity(in: view) + + // TODO: maybe do more sophisticated deceleration + + let duration: CGFloat = 0.35 + + let additionalDistanceX = velocity.x * duration + let additionalDistanceY = velocity.y * duration + + let finalDestination = CGPoint( + x: window.frame.origin.x + additionalDistanceX, + y: window.frame.origin.y + additionalDistanceY + ) + + let finalFrame = nearestValidPipFrame(for: finalDestination) + + UIView.animate(withDuration: TimeInterval(duration)) { window.frame = finalFrame } + default: + break + } } @objc - public func didTapStatusBar(notification: Notification) { + private func handleTap(sender: UITapGestureRecognizer) { + callViewController?.localVideoViewReference.removeFromSuperview() + callViewController?.remoteVideoViewReference.removeFromSuperview() + callViewController = nil self.delegate?.returnToCallWasTapped(self) } - override public func viewWillLayoutSubviews() { - Logger.debug("") - - super.viewWillLayoutSubviews() - } - - override public func viewDidLayoutSubviews() { - Logger.debug("") - - super.viewDidLayoutSubviews() - } - // MARK: Orientation public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { diff --git a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift index ccae4c6a81..373ba8dbd3 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift @@ -37,7 +37,6 @@ public class StickerPackViewController: OWSViewController { stickerCollectionView.show(dataSource: dataSource) dataSource.add(delegate: self) - NotificationCenter.default.addObserver(self, selector: #selector(callDidChange), name: .OWSWindowManagerCallDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didChangeStatusBarFrame), name: UIApplication.didChangeStatusBarFrameNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(stickersOrPacksDidChange), @@ -409,13 +408,6 @@ public class StickerPackViewController: OWSViewController { StickerSharingViewController.shareStickerPack(stickerPack.info, from: self) } - @objc - public func callDidChange() { - Logger.debug("") - - updateContent() - } - @objc public func didChangeStatusBarFrame() { Logger.debug("") diff --git a/SignalMessaging/Views/OWSNavigationBar.swift b/SignalMessaging/Views/OWSNavigationBar.swift index af1c39937a..fb1fed7aec 100644 --- a/SignalMessaging/Views/OWSNavigationBar.swift +++ b/SignalMessaging/Views/OWSNavigationBar.swift @@ -5,25 +5,12 @@ import Foundation import UIKit -@objc -public protocol NavBarLayoutDelegate: class { - func navBarCallLayoutDidChange(navbar: OWSNavigationBar) -} - @objc public class OWSNavigationBar: UINavigationBar { - @objc - public weak var navBarLayoutDelegate: NavBarLayoutDelegate? - @objc public let navbarWithoutStatusHeight: CGFloat = 44 - @objc - public var callBannerHeight: CGFloat { - return OWSWindowManagerCallBannerHeight() - } - @objc public var statusBarHeight: CGFloat { return CurrentAppContext().statusBarHeight @@ -47,8 +34,6 @@ public class OWSNavigationBar: UINavigationBar { applyTheme() - NotificationCenter.default.addObserver(self, selector: #selector(callDidChange), name: .OWSWindowManagerCallDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didChangeStatusBarFrame), name: UIApplication.didChangeStatusBarFrameNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .ThemeDidChange, @@ -110,36 +95,6 @@ public class OWSNavigationBar: UINavigationBar { } } - public func snapshotViewIncludingBackground(afterScreenUpdates: Bool) -> UIView? { - let originalFrame = self.frame - let originalBounds = self.bounds - - // NavigationBars are weird because though it appears as though the status bar - // content is "in" or maybe "above" (z-index) the navbar, the navbar frame is strictly - // lower (y-index) than the status bar. - // To work with that, the navbar background, including the blur effect when transparency - // is enabled, extends beyond the navbars's bounds. This allows the background to extend - // up (y-index) and under (z-index) the status bar, without clips to bounds. - // - // Snapshots, however, clip to bounds. So as to capture the full size of the background - // in our snapshot we temporarily adjust the navbars frame. - self.frame = CGRect(x: 0, y: callBannerHeight, width: fullWidth, height: navbarWithoutStatusHeight + statusBarHeight) - self.bounds = self.frame - defer { - self.frame = originalFrame - self.bounds = originalBounds - } - - guard let barSnapshot = self.snapshotView(afterScreenUpdates: afterScreenUpdates) else { - owsFailDebug("barSnapshot was unexpectedly nil") - return nil - } - - barSnapshot.frame.origin = .zero - - return barSnapshot - } - @objc public func themeDidChange() { Logger.debug("") @@ -153,60 +108,6 @@ public class OWSNavigationBar: UINavigationBar { } } - // MARK: Layout - - @objc - public func callDidChange() { - Logger.debug("") - self.navBarLayoutDelegate?.navBarCallLayoutDidChange(navbar: self) - } - - @objc - public func didChangeStatusBarFrame() { - Logger.debug("") - self.navBarLayoutDelegate?.navBarCallLayoutDidChange(navbar: self) - } - - public override func layoutSubviews() { - guard CurrentAppContext().isMainApp else { - super.layoutSubviews() - return - } - guard OWSWindowManager.shared.hasCall else { - super.layoutSubviews() - return - } - - super.layoutSubviews() - - self.frame = CGRect(x: 0, y: callBannerHeight, width: fullWidth, height: navbarWithoutStatusHeight) - self.bounds = CGRect(x: 0, y: 0, width: fullWidth, height: navbarWithoutStatusHeight) - - // This is only necessary on iOS11, which has some private views within that lay outside of the navbar. - // They aren't actually visible behind the call status bar, but they looks strange during present/dismiss - // animations for modal VC's - for subview in self.subviews { - let stringFromClass = NSStringFromClass(subview.classForCoder) - if stringFromClass.contains("BarBackground") { - subview.frame = self.bounds - } else if stringFromClass.contains("BarContentView") { - subview.frame = self.bounds - } - } - } - - public override func didMoveToSuperview() { - super.didMoveToSuperview() - - // `fullWidth` will not be accurate until the navbar has a defined - // superview. By forcing a re-layout after moving to a superview, we - // ensure that the correct width is always used. This works around a bug - // on iPad where the navigation bar is larger than the application frame - // when in slide over mode. - setNeedsLayout() - layoutIfNeeded() - } - // MARK: Override Theme @objc diff --git a/SignalMessaging/utils/OWSWindowManager.h b/SignalMessaging/utils/OWSWindowManager.h index ca9f92659b..5a916be6f4 100644 --- a/SignalMessaging/utils/OWSWindowManager.h +++ b/SignalMessaging/utils/OWSWindowManager.h @@ -16,10 +16,10 @@ extern NSString *const IsScreenBlockActiveDidChangeNotification; #pragma mark - -const CGFloat OWSWindowManagerCallBannerHeight(void); - extern const UIWindowLevel UIWindowLevel_Background; +@protocol CallViewControllerWindowReference; + @interface OWSWindowManager : NSObject + (instancetype)new NS_UNAVAILABLE; @@ -36,15 +36,14 @@ extern const UIWindowLevel UIWindowLevel_Background; - (BOOL)isAppWindow:(UIWindow *)window; - (void)updateWindowFrames; -- (void)ensureReturnToCallWindowFrame; #pragma mark - Calls @property (nonatomic, readonly) BOOL shouldShowCallView; @property (nonatomic, readonly) UIWindow *callViewWindow; -- (void)startCall:(UIViewController *)callViewController; -- (void)endCall:(UIViewController *)callViewController; +- (void)startCall:(UIViewController *)callViewController; +- (void)endCall:(UIViewController *)callViewController; - (void)leaveCallView; @property (nonatomic, readonly) BOOL hasCall; diff --git a/SignalMessaging/utils/OWSWindowManager.m b/SignalMessaging/utils/OWSWindowManager.m index 8110dec6f8..9b22978d45 100644 --- a/SignalMessaging/utils/OWSWindowManager.m +++ b/SignalMessaging/utils/OWSWindowManager.m @@ -14,26 +14,6 @@ NSString *const OWSWindowManagerCallDidChangeNotification = @"OWSWindowManagerCa NSString *const IsScreenBlockActiveDidChangeNotification = @"IsScreenBlockActiveDidChangeNotification"; -const CGFloat OWSWindowManagerCallBannerHeight(void) -{ - if (@available(iOS 11.4, *)) { - return CurrentAppContext().statusBarHeight + 20; - } - - if (![UIDevice currentDevice].hasIPhoneXNotch) { - return CurrentAppContext().statusBarHeight + 20; - } - - // Hardcode CallBanner height for iPhone X's on older iOS. - // - // As of iOS11.4 and iOS12, this no longer seems to be an issue, but previously statusBarHeight returned - // something like 20pts (IIRC), meaning our call banner did not extend sufficiently past the iPhone X notch. - // - // Before noticing that this behavior changed, I actually assumed that notch height was intentionally excluded from - // the statusBarHeight, and that this was not a bug, else I'd have taken better notes. - return 64; -} - // Behind everything, especially the root window. const UIWindowLevel UIWindowLevel_Background = -1.f; @@ -110,7 +90,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) @property (nonatomic) BOOL shouldShowCallView; -@property (nonatomic, nullable) UIViewController *callViewController; +@property (nonatomic, nullable) UIViewController *callViewController; @end @@ -153,39 +133,15 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) self.returnToCallWindow = [self createReturnToCallWindow:rootWindow]; self.callViewWindow = [self createCallViewWindow:rootWindow]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didChangeStatusBarFrame:) - name:UIApplicationDidChangeStatusBarFrameNotification - object:nil]; - [self ensureWindowState]; } -- (void)didChangeStatusBarFrame:(NSNotification *)notification -{ - // Apple bug? Upon returning from landscape, this method *is* fired, but both the notification and [UIApplication - // sharedApplication].statusBarFrame still show a height of 0. So to work around we also call - // `ensureReturnToCallWindowFrame` before showing the call banner. - [self ensureReturnToCallWindowFrame]; -} - -- (void)ensureReturnToCallWindowFrame -{ - CGRect newFrame = CurrentAppContext().frame; - newFrame.size.height = OWSWindowManagerCallBannerHeight(); - OWSLogDebug(@"returnToCallWindowFrame: %@", NSStringFromCGRect(newFrame)); - self.returnToCallWindow.frame = newFrame; -} - - (UIWindow *)createReturnToCallWindow:(UIWindow *)rootWindow { OWSAssertIsOnMainThread(); OWSAssertDebug(rootWindow); - // "Return to call" should remain at the top of the screen. - CGRect applicationFrame = CurrentAppContext().frame; - applicationFrame.size.height = OWSWindowManagerCallBannerHeight(); - UIWindow *window = [[OWSWindow alloc] initWithFrame:applicationFrame]; + UIWindow *window = [[OWSWindow alloc] initWithFrame:rootWindow.bounds]; window.hidden = YES; window.windowLevel = UIWindowLevel_ReturnToCall(); window.opaque = YES; @@ -266,7 +222,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) #pragma mark - Calls -- (void)setCallViewController:(nullable UIViewController *)callViewController +- (void)setCallViewController:(nullable UIViewController *)callViewController { OWSAssertIsOnMainThread(); @@ -279,7 +235,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) [NSNotificationCenter.defaultCenter postNotificationName:OWSWindowManagerCallDidChangeNotification object:nil]; } -- (void)startCall:(UIViewController *)callViewController +- (void)startCall:(UIViewController *)callViewController { OWSAssertIsOnMainThread(); OWSAssertDebug(callViewController); @@ -298,7 +254,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) [self ensureWindowState]; } -- (void)endCall:(UIViewController *)callViewController +- (void)endCall:(UIViewController *)callViewController { OWSAssertIsOnMainThread(); OWSAssertDebug(callViewController); @@ -337,6 +293,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) self.shouldShowCallView = YES; + [self.callViewController returnFromPipWithPipWindow:self.returnToCallWindow]; [self ensureWindowState]; } @@ -373,14 +330,13 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) // Show Call View. [self ensureRootWindowHidden]; - [self ensureReturnToCallWindowHidden]; [self ensureCallViewWindowShown]; + [self ensureReturnToCallWindowHidden]; [self ensureScreenBlockWindowHidden]; } else { // Show Root Window [self ensureRootWindowShown]; - [self ensureCallViewWindowHidden]; [self ensureScreenBlockWindowHidden]; if (self.callViewController) { @@ -390,6 +346,8 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) } else { [self ensureReturnToCallWindowHidden]; } + + [self ensureCallViewWindowHidden]; } } @@ -430,10 +388,9 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) return; } - [self ensureReturnToCallWindowFrame]; OWSLogInfo(@"showing 'return to call' window."); self.returnToCallWindow.hidden = NO; - [self.returnToCallViewController startAnimating]; + [self.returnToCallViewController displayForCallViewController:self.callViewController]; } - (void)ensureReturnToCallWindowHidden @@ -446,7 +403,6 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void) OWSLogInfo(@"hiding 'return to call' window."); self.returnToCallWindow.hidden = YES; - [self.returnToCallViewController stopAnimating]; } - (void)ensureCallViewWindowShown