import Foundation import UIKit import AsyncDisplayKit import SwiftSignalKit private struct WindowLayout: Equatable { let size: CGSize let metrics: LayoutMetrics let statusBarHeight: CGFloat? let forceInCallStatusBarText: String? let inputHeight: CGFloat? let safeInsets: UIEdgeInsets let onScreenNavigationHeight: CGFloat? let upperKeyboardInputPositionBound: CGFloat? let inVoiceOver: Bool } private struct UpdatingLayout { var layout: WindowLayout var transition: ContainedViewLayoutTransition mutating func update(transition: ContainedViewLayoutTransition, override: Bool) { var update = false if case .immediate = self.transition { update = true } else if override { update = true } if update { self.transition = transition } } mutating func update(size: CGSize, metrics: LayoutMetrics, safeInsets: UIEdgeInsets, forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: size, metrics: metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(statusBarHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(inputHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(onScreenNavigationHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(upperKeyboardInputPositionBound: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: upperKeyboardInputPositionBound, inVoiceOver: self.layout.inVoiceOver) } mutating func update(inVoiceOver: Bool) { self.update(transition: transition, override: false) self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound, inVoiceOver: inVoiceOver) } } private let defaultStatusBarHeight: CGFloat = 20.0 private let statusBarHiddenInLandscape: Bool = UIDevice.current.userInterfaceIdiom == .phone private func inputHeightOffsetForLayout(_ layout: WindowLayout) -> CGFloat { if let inputHeight = layout.inputHeight, let upperBound = layout.upperKeyboardInputPositionBound { return max(0.0, upperBound - (layout.size.height - inputHeight)) } return 0.0 } private func containedLayoutForWindowLayout(_ layout: WindowLayout, deviceMetrics: DeviceMetrics) -> ContainerViewLayout { let resolvedStatusBarHeight: CGFloat? if let statusBarHeight = layout.statusBarHeight { if layout.forceInCallStatusBarText != nil { resolvedStatusBarHeight = max(40.0, layout.safeInsets.top) } else { resolvedStatusBarHeight = statusBarHeight } } else { resolvedStatusBarHeight = nil } var updatedInputHeight = layout.inputHeight if let inputHeight = updatedInputHeight, let _ = layout.upperKeyboardInputPositionBound { updatedInputHeight = inputHeight - inputHeightOffsetForLayout(layout) } let isLandscape = layout.size.width > layout.size.height var resolvedSafeInsets = layout.safeInsets if layout.safeInsets.left.isZero { resolvedSafeInsets = deviceMetrics.safeInsets(inLandscape: isLandscape) } return ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 0.0, right: 0.0), safeInsets: resolvedSafeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil, inVoiceOver: layout.inVoiceOver) } public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView, keyboardOnly: Bool = false) -> Bool { if view.disablesInteractiveTransitionGestureRecognizer && !keyboardOnly { return true } if view.disablesInteractiveKeyboardGestureRecognizer { return true } if let f = view.disablesInteractiveTransitionGestureRecognizerNow, f() { return true } if let superview = view.superview { return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview, keyboardOnly: keyboardOnly) } return false } public func getFirstResponderAndAccessoryHeight(_ view: UIView, _ accessoryHeight: CGFloat? = nil) -> (UIView?, CGFloat?) { if view.isFirstResponder { return (view, accessoryHeight) } else { var updatedAccessoryHeight = accessoryHeight if let view = view as? WindowInputAccessoryHeightProvider { updatedAccessoryHeight = view.getWindowInputAccessoryHeight() } for subview in view.subviews { let (result, resultHeight) = getFirstResponderAndAccessoryHeight(subview, updatedAccessoryHeight) if let result = result { return (result, resultHeight) } } return (nil, nil) } } public final class WindowHostView { public let containerView: UIView public let eventView: UIView public let isRotating: () -> Bool public let systemUserInterfaceStyle: Signal public let currentInterfaceOrientation: () -> UIInterfaceOrientation let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void let updatePrefersOnScreenNavigationHidden: (Bool) -> Void var present: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)? var presentInGlobalOverlay: ((_ controller: ContainableController) -> Void)? var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)? var presentNative: ((UIViewController) -> Void)? var nativeController: (() -> UIViewController?)? var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)? var layoutSubviews: (() -> Void)? var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? var isUpdatingOrientationLayout = false var hitTest: ((CGPoint, UIEvent?) -> UIView?)? var invalidateDeferScreenEdgeGesture: (() -> Void)? var invalidatePrefersOnScreenNavigationHidden: (() -> Void)? var invalidateSupportedOrientations: (() -> Void)? var cancelInteractiveKeyboardGestures: (() -> Void)? var forEachController: (((ContainableController) -> Void) -> Void)? var getAccessibilityElements: (() -> [Any]?)? init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal, currentInterfaceOrientation: @escaping () -> UIInterfaceOrientation, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) { self.containerView = containerView self.eventView = eventView self.isRotating = isRotating self.systemUserInterfaceStyle = systemUserInterfaceStyle self.currentInterfaceOrientation = currentInterfaceOrientation self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden } fileprivate var onScreenNavigationHeight: CGFloat? { if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { return self.eventView.safeAreaInsets.bottom.isLessThanOrEqualTo(0.0) ? nil : self.eventView.safeAreaInsets.bottom } else { return nil } } } public protocol WindowHost { func forEachController(_ f: (ContainableController) -> Void) func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) func presentInGlobalOverlay(_ controller: ContainableController) func addGlobalPortalHostView(sourceView: PortalSourceView) func invalidateDeferScreenEdgeGestures() func invalidatePrefersOnScreenNavigationHidden() func invalidateSupportedOrientations() func cancelInteractiveKeyboardGestures() } public extension UIView { var windowHost: WindowHost? { if let window = self.window as? WindowHost { return window } else if let result = findWindow(self) { return result } else { return nil } } } private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOrientation?) -> LayoutMetrics { if size.width > 690.0 && size.height > 650.0 { return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation) } else { return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation) } } public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if let view = gestureRecognizer.view { let location = touch.location(in: gestureRecognizer.view) if location.y > view.bounds.height - 44.0 { return false } } return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } } public class Window1 { public let hostView: WindowHostView public let badgeView: UIImageView private var deviceMetrics: DeviceMetrics public let statusBarHost: StatusBarHost? private let keyboardManager: KeyboardManager? private let keyboardViewManager: KeyboardViewManager? private var statusBarChangeObserver: AnyObject? private var keyboardRotationChangeObserver: AnyObject? private var keyboardFrameChangeObserver: AnyObject? private var keyboardTypeChangeObserver: AnyObject? private var voiceOverStatusObserver: AnyObject? private var windowLayout: WindowLayout private var updatingLayout: UpdatingLayout? private var updatedContainerLayout: ContainerViewLayout? private var upperKeyboardInputPositionBound: CGFloat? private let presentationContext: PresentationContext private let overlayPresentationContext: GlobalOverlayPresentationContext private let topPresentationContext: PresentationContext private var tracingStatusBarsInvalidated = false private var shouldUpdateDeferScreenEdgeGestures = false private var shouldInvalidatePrefersOnScreenNavigationHidden = false private var shouldInvalidateSupportedOrientations = false private var statusBarHidden = false private var shouldNotAnimateLikelyKeyboardAutocorrectionSwitch: Bool = false public private(set) var forceInCallStatusBarText: String? = nil public var inCallNavigate: (() -> Void)? private var debugTapCounter: (Double, Int) = (0.0, 0) private var debugTapRecognizer: UITapGestureRecognizer? public var debugAction: (() -> Void)? { didSet { if self.debugAction != nil { if self.debugTapRecognizer == nil { let debugTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.debugTapGesture(_:))) self.debugTapRecognizer = debugTapRecognizer self.hostView.containerView.addGestureRecognizer(debugTapRecognizer) } } else if let debugTapRecognizer = self.debugTapRecognizer { self.debugTapRecognizer = nil self.hostView.containerView.removeGestureRecognizer(debugTapRecognizer) } } } @objc private func debugTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let timestamp = CACurrentMediaTime() if self.debugTapCounter.0 < timestamp - 0.4 { self.debugTapCounter.0 = timestamp self.debugTapCounter.1 = 0 } if self.debugTapCounter.0 >= timestamp - 0.4 { self.debugTapCounter.0 = timestamp self.debugTapCounter.1 += 1 } if self.debugTapCounter.1 >= 10 { self.debugTapCounter.1 = 0 self.debugAction?() } } } public let systemUserInterfaceStyle: Signal private var windowPanRecognizer: WindowPanRecognizer? private let keyboardGestureRecognizerDelegate = WindowKeyboardGestureRecognizerDelegate() private var keyboardGestureBeginLocation: CGPoint? private var keyboardGestureAccessoryHeight: CGFloat? private var keyboardTypeChangeTimer: SwiftSignalKit.Timer? private var isInteractionBlocked = false public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) { self.hostView = hostView self.badgeView = UIImageView() self.badgeView.image = UIImage(bundleImageName: "Components/AppBadge") self.badgeView.isHidden = true self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle let boundsSize = self.hostView.eventView.bounds.size self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? 0.0, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight) self.statusBarHost = statusBarHost let statusBarHeight: CGFloat if let statusBarHost = statusBarHost { statusBarHeight = statusBarHost.statusBarFrame.size.height self.keyboardManager = KeyboardManager(host: statusBarHost) self.keyboardViewManager = KeyboardViewManager(host: statusBarHost) } else { statusBarHeight = 0.0 self.keyboardManager = nil self.keyboardViewManager = nil } let isLandscape = boundsSize.width > boundsSize.height let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape) let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) let orientation: UIInterfaceOrientation = self.hostView.currentInterfaceOrientation() self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(size: boundsSize, orientation: orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) self.presentationContext = PresentationContext() self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView) self.topPresentationContext = PresentationContext() self.presentationContext.topLevelSubview = { [weak self] in guard let strongSelf = self else { return nil } if let first = strongSelf.topPresentationContext.controllers.first { return first.0.displayNode.view } if let first = strongSelf._topLevelOverlayControllers.first { return first.view } return nil } self.presentationContext.updateIsInteractionBlocked = { [weak self] value in self?.isInteractionBlocked = value } let updateOpaqueOverlays: () -> Void = { [weak self] in guard let strongSelf = self else { return } strongSelf._rootController?.displayNode.accessibilityElementsHidden = strongSelf.presentationContext.hasOpaqueOverlay || strongSelf.topPresentationContext.hasOpaqueOverlay } self.presentationContext.updateHasOpaqueOverlay = { value in updateOpaqueOverlays() } self.topPresentationContext.updateHasOpaqueOverlay = { value in updateOpaqueOverlays() } self.hostView.present = { [weak self] controller, level, blockInteraction, completion in self?.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) } self.hostView.presentInGlobalOverlay = { [weak self] controller in self?.presentInGlobalOverlay(controller) } self.hostView.addGlobalPortalHostViewImpl = { [weak self] sourceView in self?.addGlobalPortalHostView(sourceView: sourceView) } self.hostView.presentNative = { [weak self] controller in self?.presentNative(controller) } self.hostView.updateSize = { [weak self] size, duration, orientation in self?.updateSize(size, duration: duration, orientation: orientation) } self.hostView.layoutSubviews = { [weak self] in self?.layoutSubviews(force: false) } self.hostView.updateToInterfaceOrientation = { [weak self] orientation in self?.updateToInterfaceOrientation(orientation) } self.hostView.hitTest = { [weak self] point, event in return self?.hitTest(point, with: event) } self.hostView.invalidateDeferScreenEdgeGesture = { [weak self] in self?.invalidateDeferScreenEdgeGestures() } self.hostView.invalidatePrefersOnScreenNavigationHidden = { [weak self] in self?.invalidatePrefersOnScreenNavigationHidden() } self.hostView.invalidateSupportedOrientations = { [weak self] in self?.invalidateSupportedOrientations() } self.hostView.cancelInteractiveKeyboardGestures = { [weak self] in self?.cancelInteractiveKeyboardGestures() } self.hostView.forEachController = { [weak self] f in self?.forEachViewController({ controller in f(controller) return true }) } self.presentationContext.view = self.hostView.containerView self.topPresentationContext.view = self.hostView.containerView self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate) self.topPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate) self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate) self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarFrameNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in if let strongSelf = self, strongSelf.statusBarHost != nil { let statusBarHeight: CGFloat = max(defaultStatusBarHeight, (notification.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? defaultStatusBarHeight) let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut) strongSelf.updateLayout { $0.update(statusBarHeight: statusBarHeight, transition: transition, overrideTransition: false) } } }) self.keyboardRotationChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name("UITextEffectsWindowDidRotateNotification"), object: nil, queue: nil, using: { [weak self] notification in if let strongSelf = self { if !strongSelf.hostView.isUpdatingOrientationLayout { return } var keyboardHeight = max(0.0, strongSelf.keyboardManager?.getCurrentKeyboardHeight() ?? 0.0) if strongSelf.deviceMetrics.type == .tablet, abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 { keyboardHeight = max(0.0, keyboardHeight - 24.0) } //print("rotation keyboardHeight: \(keyboardHeight)") var duration: Double = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 if duration > Double.ulpOfOne { duration = 0.5 } let curve: UInt = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 let transitionCurve: ContainedViewLayoutTransitionCurve if curve == 7 { transitionCurve = .spring } else { transitionCurve = .easeInOut } strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: .animated(duration: duration, curve: transitionCurve), overrideTransition: false) } } }) self.keyboardFrameChangeObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: nil, using: { [weak self] notification in if let strongSelf = self { var isTablet = false if case .regular = strongSelf.windowLayout.metrics.widthClass { isTablet = true } var keyboardFrame: CGRect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect() if isTablet && keyboardFrame.isEmpty { return } if #available(iOSApplicationExtension 14.2, iOS 14.2, *), UIAccessibility.prefersCrossFadeTransitions { } else if let keyboardView = strongSelf.statusBarHost?.keyboardView { if keyboardFrame.width.isEqual(to: keyboardView.bounds.width) && keyboardFrame.height.isEqual(to: keyboardView.bounds.height) && keyboardFrame.minX.isEqual(to: keyboardView.frame.minX) { keyboardFrame.origin.y = keyboardView.frame.minY } } var minKeyboardY: CGFloat? if #available(iOSApplicationExtension 16.1, iOS 16.1, *), let screen = notification.object as? UIScreen, let keyboardFrameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { let fromCoordinateSpace = screen.coordinateSpace let toCoordinateSpace: UICoordinateSpace = strongSelf.hostView.eventView let convertedKeyboardFrameEnd = fromCoordinateSpace.convert(keyboardFrameEnd, to: toCoordinateSpace) minKeyboardY = convertedKeyboardFrameEnd.minY } var windowedHeightDifference: CGFloat = 0.0 let screenHeight: CGFloat var isWindowed = false if keyboardFrame.width.isEqual(to: UIScreen.main.bounds.width) { let screenSize = UIScreen.main.bounds.size var portraitScreenSize = UIScreen.main.bounds.size if portraitScreenSize.width > portraitScreenSize.height { portraitScreenSize = CGSize(width: portraitScreenSize.height, height: portraitScreenSize.width) } var portraitLayoutSize = strongSelf.windowLayout.size if portraitLayoutSize.width > portraitLayoutSize.height { portraitLayoutSize = CGSize(width: portraitLayoutSize.height, height: portraitLayoutSize.width) } if strongSelf.windowLayout.size.height != screenSize.height { let heightDelta = screenSize.height - strongSelf.windowLayout.size.height //if heightDelta > 0.0 && heightDelta < 200.0 { isWindowed = true windowedHeightDifference = heightDelta / 2.0 //} } if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { if isWindowed, let _ = minKeyboardY { screenHeight = strongSelf.windowLayout.size.height } else { screenHeight = UIScreen.main.bounds.height } } else { screenHeight = strongSelf.windowLayout.size.height } } else { if let _ = minKeyboardY { screenHeight = strongSelf.windowLayout.size.height } else { if keyboardFrame.minX > 0.0 { screenHeight = UIScreen.main.bounds.height } else { screenHeight = UIScreen.main.bounds.width } } } var keyboardHeight: CGFloat if keyboardFrame.isEmpty || keyboardFrame.maxY < screenHeight { if isWindowed || (isTablet && screenHeight - keyboardFrame.maxY < 5.0) { if let minKeyboardY { keyboardFrame.origin.y = minKeyboardY } keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY) if isWindowed && !keyboardHeight.isZero, minKeyboardY == nil { keyboardHeight = max(0.0, keyboardHeight - windowedHeightDifference) } } else { keyboardHeight = 0.0 } } else { if let minKeyboardY { keyboardFrame.origin.y = minKeyboardY } keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY) if isWindowed && !keyboardHeight.isZero, minKeyboardY == nil { keyboardHeight = max(0.0, keyboardHeight - windowedHeightDifference) } } if strongSelf.hostView.containerView is ChildWindowHostView, !isTablet { keyboardHeight += 27.0 } var duration: Double = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 if duration > Double.ulpOfOne { duration = 0.5 } let curve: UInt = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 let transitionCurve: ContainedViewLayoutTransitionCurve if curve == 7 { transitionCurve = .spring } else { transitionCurve = .easeInOut } var transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: transitionCurve) if strongSelf.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch, let inputHeight = strongSelf.windowLayout.inputHeight { if abs(inputHeight - keyboardHeight) <= 44.1 { transition = .immediate } } strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: transition, overrideTransition: false) } } }) if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.keyboardTypeChangeObserver = NotificationCenter.default.addObserver(forName: UITextInputMode.currentInputModeDidChangeNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in if let strongSelf = self, let initialInputHeight = strongSelf.windowLayout.inputHeight, let firstResponder = getFirstResponderAndAccessoryHeight(strongSelf.hostView.eventView).0 { if firstResponder.textInputMode?.primaryLanguage != nil { return } strongSelf.keyboardTypeChangeTimer?.invalidate() let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { if let strongSelf = self, let firstResponder = getFirstResponderAndAccessoryHeight(strongSelf.hostView.eventView).0 { if firstResponder.textInputMode?.primaryLanguage != nil { return } if let keyboardManager = strongSelf.keyboardManager { var updatedKeyboardHeight = keyboardManager.getCurrentKeyboardHeight() if strongSelf.deviceMetrics.type == .tablet, abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 { updatedKeyboardHeight = max(0.0, updatedKeyboardHeight - 24.0) } if !updatedKeyboardHeight.isEqual(to: initialInputHeight) { strongSelf.updateLayout({ $0.update(inputHeight: updatedKeyboardHeight, transition: .immediate, overrideTransition: false) }) } } } }, queue: Queue.mainQueue()) strongSelf.keyboardTypeChangeTimer = timer timer.start() } }) } if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil, queue: OperationQueue.main, using: { [weak self] _ in if let strongSelf = self { strongSelf.updateLayout { $0.update(inVoiceOver: UIAccessibility.isVoiceOverRunning) } } }) } let recognizer = WindowPanRecognizer(target: self, action: #selector(self.panGesture(_:))) recognizer.cancelsTouchesInView = false recognizer.delaysTouchesBegan = false recognizer.delaysTouchesEnded = false recognizer.delegate = self.keyboardGestureRecognizerDelegate recognizer.isEnabled = self.deviceMetrics.type == .phone recognizer.began = { [weak self] point in self?.panGestureBegan(location: point) } recognizer.moved = { [weak self] point in self?.panGestureMoved(location: point) } recognizer.ended = { [weak self] point, velocity in self?.panGestureEnded(location: point, velocity: velocity) } self.windowPanRecognizer = recognizer self.hostView.containerView.addGestureRecognizer(recognizer) self.hostView.containerView.addSubview(self.badgeView) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { if let statusBarChangeObserver = self.statusBarChangeObserver { NotificationCenter.default.removeObserver(statusBarChangeObserver) } if let keyboardRotationChangeObserver = self.keyboardRotationChangeObserver { NotificationCenter.default.removeObserver(keyboardRotationChangeObserver) } if let keyboardFrameChangeObserver = self.keyboardFrameChangeObserver { NotificationCenter.default.removeObserver(keyboardFrameChangeObserver) } if let keyboardTypeChangeObserver = self.keyboardTypeChangeObserver { NotificationCenter.default.removeObserver(keyboardTypeChangeObserver) } if let voiceOverStatusObserver = self.voiceOverStatusObserver { NotificationCenter.default.removeObserver(voiceOverStatusObserver) } } private var forceBadgeHidden = true public func setForceBadgeHidden(_ hidden: Bool) { guard hidden != self.forceBadgeHidden else { return } self.forceBadgeHidden = hidden self.updateBadgeVisibility() } private var proximityDimController: CustomDimController? public func setProximityDimHidden(_ hidden: Bool) { if !hidden { if self.proximityDimController == nil { let proximityDimController = CustomDimController(navigationBarPresentationData: nil) self.proximityDimController = proximityDimController (self.viewController as? NavigationController)?.presentOverlay(controller: proximityDimController, inGlobal: true, blockInteraction: false) } } else if let proximityDimController = self.proximityDimController { self.proximityDimController = nil proximityDimController.dismiss() } } private func updateBadgeVisibility() { let badgeIsHidden = !self.deviceMetrics.showAppBadge || self.forceBadgeHidden || self.windowLayout.size.width > self.windowLayout.size.height if badgeIsHidden != self.badgeView.isHidden && !badgeIsHidden { Queue.mainQueue().after(0.4) { let badgeShouldBeHidden = !self.deviceMetrics.showAppBadge || self.forceBadgeHidden || self.windowLayout.size.width > self.windowLayout.size.height if badgeShouldBeHidden == badgeIsHidden { self.badgeView.isHidden = badgeIsHidden } } } else { self.badgeView.isHidden = badgeIsHidden } } public func setForceInCallStatusBar(_ forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) { if self.forceInCallStatusBarText != forceInCallStatusBarText { self.forceInCallStatusBarText = forceInCallStatusBarText self.updateLayout { $0.update(forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } self.invalidateTracingStatusBars() } } private func invalidateTracingStatusBars() { self.tracingStatusBarsInvalidated = true self.hostView.eventView.setNeedsLayout() } public func invalidateDeferScreenEdgeGestures() { self.shouldUpdateDeferScreenEdgeGestures = true self.hostView.eventView.setNeedsLayout() } public func invalidatePrefersOnScreenNavigationHidden() { self.shouldInvalidatePrefersOnScreenNavigationHidden = true self.hostView.eventView.setNeedsLayout() } public func invalidateSupportedOrientations() { self.shouldInvalidateSupportedOrientations = true self.hostView.eventView.setNeedsLayout() } public func cancelInteractiveKeyboardGestures() { self.windowPanRecognizer?.isEnabled = false self.windowPanRecognizer?.isEnabled = true if self.windowLayout.upperKeyboardInputPositionBound != nil { self.updateLayout { $0.update(upperKeyboardInputPositionBound: nil, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) } } if self.keyboardGestureBeginLocation != nil { self.keyboardGestureBeginLocation = nil } } public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.isInteractionBlocked { return nil } if let result = self.topPresentationContext.hitTest(view: self.hostView.containerView, point: point, with: event) { return result } if let coveringView = self.coveringView, !coveringView.isHidden, coveringView.superview != nil, coveringView.frame.contains(point) { return coveringView.hitTest(point, with: event) } for view in self.hostView.eventView.subviews.reversed() { let classString = NSStringFromClass(type(of: view)) if classString == "UITransitionView" || classString.contains("ContextMenuContainerView") { if let result = view.hitTest(point, with: event) { return result } } } if let result = self.overlayPresentationContext.hitTest(point, with: event) { return result } for controller in self._topLevelOverlayControllers.reversed() { if let result = controller.view.hitTest(point, with: event) { return result } } if let result = self.presentationContext.hitTest(view: self.hostView.containerView, point: point, with: event) { return result } return self.viewController?.view.hitTest(point, with: event) } func updateSize(_ value: CGSize, duration: Double, orientation: UIInterfaceOrientation) { let transition: ContainedViewLayoutTransition if !duration.isZero { transition = .animated(duration: duration, curve: .easeInOut) } else { transition = .immediate } self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(size: value, orientation: orientation), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground { self.layoutSubviews(force: true) } } private var _rootController: ContainableController? public var viewController: ContainableController? { get { return _rootController } set(value) { if let rootController = self._rootController { rootController.view.removeFromSuperview() } self._rootController = value if let rootController = self._rootController { if let rootController = rootController as? NavigationController { rootController.statusBarHost = self.statusBarHost rootController.updateSupportedOrientations = { [weak self] in guard let strongSelf = self else { return } var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) let orientationToLock: UIInterfaceOrientationMask if strongSelf.windowLayout.size.width < strongSelf.windowLayout.size.height { orientationToLock = .portrait } else { orientationToLock = .landscape } if let _rootController = strongSelf._rootController { supportedOrientations = supportedOrientations.intersection(_rootController.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) } supportedOrientations = supportedOrientations.intersection(strongSelf.presentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) supportedOrientations = supportedOrientations.intersection(strongSelf.overlayPresentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) var resolvedOrientations: UIInterfaceOrientationMask switch strongSelf.windowLayout.metrics.widthClass { case .regular: resolvedOrientations = supportedOrientations.regularSize case .compact: resolvedOrientations = supportedOrientations.compactSize } if resolvedOrientations.isEmpty { resolvedOrientations = [.portrait] } strongSelf.hostView.updateSupportedInterfaceOrientations(resolvedOrientations) } rootController.keyboardViewManager = self.keyboardViewManager rootController.inCallNavigate = { [weak self] in self?.inCallNavigate?() } } self.hostView.containerView.insertSubview(rootController.view, at: 0) if !self.windowLayout.size.width.isZero && !self.windowLayout.size.height.isZero { rootController.displayNode.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate) } } self.hostView.eventView.setNeedsLayout() } } private var _topLevelOverlayControllers: [ContainableController] = [] public var topLevelOverlayControllers: [ContainableController] { get { return _topLevelOverlayControllers } set(value) { for controller in self._topLevelOverlayControllers { if let controller = controller as? ViewController { controller.statusBar.alphaUpdated = nil } controller.view.removeFromSuperview() } self._topLevelOverlayControllers = value let layout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics) for controller in self._topLevelOverlayControllers { controller.displayNode.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) controller.containerLayoutUpdated(layout, transition: .immediate) if let coveringView = self.coveringView { self.hostView.containerView.insertSubview(controller.view, belowSubview: coveringView) } else { self.hostView.containerView.insertSubview(controller.view, belowSubview: self.badgeView) } if let controller = controller as? ViewController { controller.statusBar.alphaUpdated = { [weak self] transition in guard let strongSelf = self, let navigationController = strongSelf._rootController as? NavigationController else { return } var isStatusBarHidden: Bool = false for controller in strongSelf._topLevelOverlayControllers { if let controller = controller as? ViewController { if case .Hide = controller.statusBar.statusBarStyle { isStatusBarHidden = true } } } navigationController.updateExternalStatusBarHidden(isStatusBarHidden, transition: .animated(duration: 0.3, curve: .easeInOut)) } } } } } public var coveringView: WindowCoveringView? { didSet { if self.coveringView !== oldValue { if let oldValue = oldValue { oldValue.layer.allowsGroupOpacity = true oldValue.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak oldValue] _ in oldValue?.removeFromSuperview() }) } if let coveringView = self.coveringView { coveringView.layer.removeAnimation(forKey: "opacity") coveringView.layer.allowsGroupOpacity = false coveringView.alpha = 1.0 if let controller = self.topPresentationContext.controllers.first { self.hostView.containerView.insertSubview(coveringView, belowSubview: controller.0.displayNode.view) } else { self.hostView.containerView.insertSubview(coveringView, belowSubview: self.badgeView) } if !self.windowLayout.size.width.isZero { coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) coveringView.updateLayout(self.windowLayout.size) } } } } } private func layoutSubviews(force: Bool) { if self.tracingStatusBarsInvalidated, let _ = keyboardManager { self.tracingStatusBarsInvalidated = false var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) let orientationToLock: UIInterfaceOrientationMask if self.windowLayout.size.width < self.windowLayout.size.height { orientationToLock = .portrait } else { orientationToLock = .landscape } if let _rootController = self._rootController { supportedOrientations = supportedOrientations.intersection(_rootController.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) } supportedOrientations = supportedOrientations.intersection(self.presentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) supportedOrientations = supportedOrientations.intersection(self.overlayPresentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) var resolvedOrientations: UIInterfaceOrientationMask switch self.windowLayout.metrics.widthClass { case .regular: resolvedOrientations = supportedOrientations.regularSize case .compact: resolvedOrientations = supportedOrientations.compactSize } if resolvedOrientations.isEmpty { resolvedOrientations = [.portrait] } self.hostView.updateSupportedInterfaceOrientations(resolvedOrientations) self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) self.hostView.updatePrefersOnScreenNavigationHidden(self.collectPrefersOnScreenNavigationHidden()) self.shouldUpdateDeferScreenEdgeGestures = false self.shouldInvalidatePrefersOnScreenNavigationHidden = false self.shouldInvalidateSupportedOrientations = false } else if self.shouldUpdateDeferScreenEdgeGestures || self.shouldInvalidatePrefersOnScreenNavigationHidden || self.shouldInvalidateSupportedOrientations { self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) self.hostView.updatePrefersOnScreenNavigationHidden(self.collectPrefersOnScreenNavigationHidden()) self.shouldUpdateDeferScreenEdgeGestures = false self.shouldInvalidatePrefersOnScreenNavigationHidden = false if self.shouldInvalidateSupportedOrientations { var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) let orientationToLock: UIInterfaceOrientationMask if self.windowLayout.size.width < self.windowLayout.size.height { orientationToLock = .portrait } else { orientationToLock = .landscape } if let _rootController = self._rootController { supportedOrientations = supportedOrientations.intersection(_rootController.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) } supportedOrientations = supportedOrientations.intersection(self.presentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) supportedOrientations = supportedOrientations.intersection(self.overlayPresentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) var resolvedOrientations: UIInterfaceOrientationMask switch self.windowLayout.metrics.widthClass { case .regular: resolvedOrientations = supportedOrientations.regularSize case .compact: resolvedOrientations = supportedOrientations.compactSize } if resolvedOrientations.isEmpty { resolvedOrientations = [.portrait] } self.hostView.updateSupportedInterfaceOrientations(resolvedOrientations) self.shouldInvalidateSupportedOrientations = false } } if force { self.commitUpdatingLayout() } else if !UIWindow.isDeviceRotating() { if !self.hostView.isUpdatingOrientationLayout { self.commitUpdatingLayout() } else { self.addPostUpdateToInterfaceOrientationBlock(f: { [weak self] in if let strongSelf = self { strongSelf.hostView.eventView.setNeedsLayout() } }) } } else { UIWindow.addPostDeviceOrientationDidChange({ [weak self] in if let strongSelf = self { strongSelf.hostView.eventView.setNeedsLayout() } }) } } var postUpdateToInterfaceOrientationBlocks: [() -> Void] = [] private func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { let blocks = self.postUpdateToInterfaceOrientationBlocks self.postUpdateToInterfaceOrientationBlocks = [] for f in blocks { f() } self._rootController?.updateToInterfaceOrientation(orientation) self.presentationContext.updateToInterfaceOrientation(orientation) self.overlayPresentationContext.updateToInterfaceOrientation(orientation) self.topPresentationContext.updateToInterfaceOrientation(orientation) for controller in self.topLevelOverlayControllers { controller.updateToInterfaceOrientation(orientation) } } public func addPostUpdateToInterfaceOrientationBlock(f: @escaping () -> Void) { postUpdateToInterfaceOrientationBlocks.append(f) } private func updateLayout(_ update: (inout UpdatingLayout) -> ()) { if self.updatingLayout == nil { var updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) update(&updatingLayout) if updatingLayout.layout != self.windowLayout { self.updatingLayout = updatingLayout self.hostView.eventView.setNeedsLayout() } } else { update(&self.updatingLayout!) self.hostView.eventView.setNeedsLayout() } } private var isFirstLayout = true private func commitUpdatingLayout() { if let updatingLayout = self.updatingLayout { self.updatingLayout = nil if updatingLayout.layout != self.windowLayout || self.isFirstLayout { self.isFirstLayout = false let boundsSize = updatingLayout.layout.size let isLandscape = boundsSize.width > boundsSize.height var statusBarHeight: CGFloat? = self.deviceMetrics.statusBarHeight(for: boundsSize) if let statusBarHeightValue = statusBarHeight, let statusBarHost = self.statusBarHost { statusBarHeight = max(statusBarHeightValue, statusBarHost.statusBarFrame.size.height) } else { statusBarHeight = nil } if self.deviceMetrics.type == .tablet, let onScreenNavigationHeight = self.hostView.onScreenNavigationHeight, onScreenNavigationHeight != self.deviceMetrics.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) { self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? 0.0, onScreenNavigationHeight: onScreenNavigationHeight) } let statusBarWasHidden = self.statusBarHidden if statusBarHiddenInLandscape && isLandscape { statusBarHeight = nil self.statusBarHidden = true } else { self.statusBarHidden = false } if self.statusBarHidden != statusBarWasHidden { self.tracingStatusBarsInvalidated = true self.hostView.eventView.setNeedsLayout() } let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(size: updatingLayout.layout.size, orientation: updatingLayout.layout.metrics.orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) let childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics) let childLayoutUpdated = self.updatedContainerLayout != childLayout self.updatedContainerLayout = childLayout if childLayoutUpdated { var rootLayout = childLayout let rootTransition = updatingLayout.transition if self.presentationContext.isCurrentlyOpaque { rootLayout.inputHeight = nil } if let rootController = self._rootController { rootTransition.updateFrame(node: rootController.displayNode, frame: CGRect(origin: CGPoint(), size: self.windowLayout.size)) rootController.containerLayoutUpdated(rootLayout, transition: rootTransition) } self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) self.overlayPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) self.topPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) for controller in self.topLevelOverlayControllers { updatingLayout.transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(), size: self.windowLayout.size)) controller.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) } } let updatedInputOffset = inputHeightOffsetForLayout(self.windowLayout) if !previousInputOffset.isEqual(to: updatedInputOffset) { let hide = updatingLayout.transition.isAnimated && updatingLayout.layout.upperKeyboardInputPositionBound == updatingLayout.layout.size.height self.keyboardManager?.updateInteractiveInputOffset(updatedInputOffset, transition: updatingLayout.transition, completion: { [weak self] in if let strongSelf = self, hide { strongSelf.updateLayout { $0.update(upperKeyboardInputPositionBound: nil, transition: .immediate, overrideTransition: false) } strongSelf.hostView.eventView.endEditing(true) } }) } if let coveringView = self.coveringView { coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) coveringView.updateLayout(self.windowLayout.size) } if let image = self.badgeView.image { self.updateBadgeVisibility() self.badgeView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.windowLayout.size.width - image.size.width) / 2.0), y: 5.0), size: image.size) } } } } public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void = {}) { if level.rawValue <= 3, let controller = controller as? ViewController { for presentedController in self.presentationContext.controllers.reversed() { if let navigationController = presentedController.0 as? NavigationController { navigationController.presentOverlay(controller: controller, inGlobal: false, blockInteraction: blockInteraction) return } } if let navigationController = self._rootController as? NavigationController { navigationController.presentOverlay(controller: controller, inGlobal: false, blockInteraction: blockInteraction) } else { self.presentationContext.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) } } else { if let controller = controller as? ViewController, controller.presentedOverCoveringView { self.topPresentationContext.present(controller, on: level, completion: completion) } else { self.presentationContext.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) } } } public func presentInGlobalOverlay(_ controller: ContainableController) { if let controller = controller as? ViewController { if let navigationController = self._rootController as? NavigationController { navigationController.presentOverlay(controller: controller, inGlobal: true) return } } self.overlayPresentationContext.present(controller) } public func addGlobalPortalHostView(sourceView: PortalSourceView) { self.overlayPresentationContext.addGlobalPortalHostView(sourceView: sourceView) } public func presentNative(_ controller: UIViewController) { if let nativeController = self.hostView.nativeController?() { nativeController.present(controller, animated: true, completion: nil) } } private func panGestureBegan(location: CGPoint) { if self.windowLayout.upperKeyboardInputPositionBound != nil { return } let keyboardGestureBeginLocation = location let view = self.hostView.containerView let (firstResponder, accessoryHeight) = getFirstResponderAndAccessoryHeight(view) if let inputHeight = self.windowLayout.inputHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < self.windowLayout.size.height - inputHeight - (accessoryHeight ?? 0.0) { var enableGesture = true if let view = self.hostView.containerView.hitTest(location, with: nil) { if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view, keyboardOnly: true) { enableGesture = false } } if enableGesture, let _ = firstResponder { self.keyboardGestureBeginLocation = keyboardGestureBeginLocation self.keyboardGestureAccessoryHeight = accessoryHeight } } } private func panGestureMoved(location: CGPoint) { if let keyboardGestureBeginLocation = self.keyboardGestureBeginLocation { let currentLocation = location let deltaY = keyboardGestureBeginLocation.y - location.y if deltaY * deltaY >= 3.0 * 3.0 || self.windowLayout.upperKeyboardInputPositionBound != nil { self.updateLayout { $0.update(upperKeyboardInputPositionBound: currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0), transition: .immediate, overrideTransition: false) } } } } public func simulateKeyboardDismiss(transition: ContainedViewLayoutTransition) { var simulate = false for controller in self.overlayPresentationContext.controllers { if controller.isViewLoaded { if controller.view.window != self.hostView.containerView.window { simulate = true break } } } if simulate { self.updateLayout { $0.update(upperKeyboardInputPositionBound: self.windowLayout.size.height, transition: transition, overrideTransition: false) } } else { self.hostView.containerView.endEditing(true) } } private func panGestureEnded(location: CGPoint, velocity: CGPoint?) { if self.keyboardGestureBeginLocation == nil { return } self.keyboardGestureBeginLocation = nil let currentLocation = location let accessoryHeight = (self.keyboardGestureAccessoryHeight ?? 0.0) var canDismiss = false if let upperKeyboardInputPositionBound = self.windowLayout.upperKeyboardInputPositionBound, upperKeyboardInputPositionBound >= self.windowLayout.size.height - accessoryHeight { canDismiss = true } else if let velocity = velocity, velocity.y > 100.0 { canDismiss = true } if canDismiss, let inputHeight = self.windowLayout.inputHeight, currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) > self.windowLayout.size.height - inputHeight { self.updateLayout { $0.update(upperKeyboardInputPositionBound: self.windowLayout.size.height, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) } } else { self.updateLayout { $0.update(upperKeyboardInputPositionBound: nil, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) } } } @objc func panGesture(_ recognizer: WindowPanRecognizer) { switch recognizer.state { case .began: self.panGestureBegan(location: recognizer.location(in: recognizer.view)) case .changed: self.panGestureMoved(location: recognizer.location(in: recognizer.view)) case .ended: self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: recognizer.velocity(in: recognizer.view)) case .cancelled: self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: nil) default: break } } private func collectScreenEdgeGestures() -> UIRectEdge { var edges: UIRectEdge = [] if let navigationController = self._rootController as? NavigationController, let overlayController = navigationController.topOverlayController { edges = edges.union(overlayController.deferScreenEdgeGestures) } edges = edges.union(self.presentationContext.combinedDeferScreenEdgeGestures()) for controller in self.topLevelOverlayControllers { if let controller = controller as? ViewController { edges = edges.union(controller.deferScreenEdgeGestures) } } return edges } private func collectPrefersOnScreenNavigationHidden() -> Bool { var hidden = false if let navigationController = self._rootController as? NavigationController, let overlayController = navigationController.topOverlayController { hidden = hidden || overlayController.prefersOnScreenNavigationHidden } hidden = hidden || self.presentationContext.combinedPrefersOnScreenNavigationHidden() for controller in self.topLevelOverlayControllers { if let controller = controller as? ViewController { hidden = hidden || controller.prefersOnScreenNavigationHidden } } return hidden } public func forEachViewController(_ f: (ContainableController) -> Bool, excludeNavigationSubControllers: Bool = false) { if let navigationController = self._rootController as? NavigationController { if !excludeNavigationSubControllers { for case let controller as ContainableController in navigationController.viewControllers { let _ = f(controller) } } if let controller = navigationController.topOverlayController { let _ = f(controller) } } for (controller, _) in self.presentationContext.controllers { if !f(controller) { break } } for controller in self.topLevelOverlayControllers { if !f(controller) { break } } for (controller, _) in self.topPresentationContext.controllers { if !f(controller) { break } } } public func doNotAnimateLikelyKeyboardAutocorrectionSwitch() { self.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch = true DispatchQueue.main.async { self.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch = false } } } private class CustomDimController: ViewController { class Node: ASDisplayNode { override init() { super.init() self.backgroundColor = .black } } override init(navigationBarPresentationData: NavigationBarPresentationData?) { super.init(navigationBarPresentationData: nil) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadDisplayNode() { let node = Node() self.displayNode = node } }