Swiftgram/Display/WindowContent.swift
2019-03-19 01:22:50 +04:00

1073 lines
53 KiB
Swift

import Foundation
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 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, hasOnScreenNavigation: Bool) -> 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)
}
var resolvedSafeInsets = layout.safeInsets
var standardInputHeight: CGFloat = 216.0
var predictiveHeight: CGFloat = 42.0
if let metrics = DeviceMetrics.forScreenSize(layout.size, hintHasOnScreenNavigation: hasOnScreenNavigation) {
let isLandscape = layout.size.width > layout.size.height
let safeAreaInsets = metrics.safeAreaInsets(inLandscape: isLandscape)
if !safeAreaInsets.left.isZero {
resolvedSafeInsets.left = safeAreaInsets.left
resolvedSafeInsets.right = safeAreaInsets.right
}
standardInputHeight = metrics.standardInputHeight(inLandscape: isLandscape)
predictiveHeight = metrics.predictiveInputHeight(inLandscape: isLandscape)
}
standardInputHeight += predictiveHeight
return ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 00, right: 0.0), safeInsets: resolvedSafeInsets, statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, standardInputHeight: standardInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil, inVoiceOver: layout.inVoiceOver)
}
private func encodeText(_ string: String, _ key: Int) -> String {
var result = ""
for c in string.unicodeScalars {
result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!))
}
return result
}
public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView) -> Bool {
if view.disablesInteractiveTransitionGestureRecognizer {
return true
}
if let f = view.disablesInteractiveTransitionGestureRecognizerNow, f() {
return true
}
if let superview = view.superview {
return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview)
}
return false
}
private let transitionClass: AnyClass? = NSClassFromString(encodeText("VJUsbotjujpoWjfx", -1))
private let previewingClass: AnyClass? = NSClassFromString("UIVisualEffectView")
private let previewingActionGroupClass: AnyClass? = NSClassFromString("UIInterfaceActionGroupView")
private func checkIsPreviewingView(_ view: UIView) -> Bool {
if let transitionClass = transitionClass, view.isKind(of: transitionClass) {
for subview in view.subviews {
if let previewingClass = previewingClass, subview.isKind(of: previewingClass) {
return true
}
}
}
return false
}
private func applyThemeToPreviewingView(_ view: UIView, accentColor: UIColor, darkBlur: Bool) {
if let previewingActionGroupClass = previewingActionGroupClass, view.isKind(of: previewingActionGroupClass) {
view.tintColor = accentColor
if darkBlur {
applyThemeToPreviewingEffectView(view)
}
return
}
for subview in view.subviews {
applyThemeToPreviewingView(subview, accentColor: accentColor, darkBlur: darkBlur)
}
}
private func applyThemeToPreviewingEffectView(_ view: UIView) {
if let previewingClass = previewingClass, view.isKind(of: previewingClass) {
if let view = view as? UIVisualEffectView {
view.effect = UIBlurEffect(style: .dark)
}
}
for subview in view.subviews {
applyThemeToPreviewingEffectView(subview)
}
}
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
let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void
let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void
let updatePreferNavigationUIHidden: (Bool) -> Void
var present: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
var presentInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
var presentNative: ((UIViewController) -> Void)?
var updateSize: ((CGSize, Double) -> Void)?
var layoutSubviews: (() -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var isUpdatingOrientationLayout = false
var hitTest: ((CGPoint, UIEvent?) -> UIView?)?
var invalidateDeferScreenEdgeGesture: (() -> Void)?
var invalidatePreferNavigationUIHidden: (() -> Void)?
var cancelInteractiveKeyboardGestures: (() -> Void)?
var forEachController: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElements: (() -> [Any]?)?
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePreferNavigationUIHidden: @escaping (Bool) -> Void) {
self.containerView = containerView
self.eventView = eventView
self.isRotating = isRotating
self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations
self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures
self.updatePreferNavigationUIHidden = updatePreferNavigationUIHidden
}
var hasOnScreenNavigation: Bool {
if #available(iOSApplicationExtension 11.0, *) {
return !self.eventView.safeAreaInsets.bottom.isZero
} else {
return false
}
}
}
public struct WindowTracingTags {
public static let statusBar: Int32 = 1 << 0
public static let keyboard: Int32 = 1 << 1
}
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 invalidateDeferScreenEdgeGestures()
func invalidatePreferNavigationUIHidden()
func cancelInteractiveKeyboardGestures()
}
private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics {
if size.width > 690.0 && size.height > 690.0 {
return LayoutMetrics(widthClass: .regular, heightClass: .regular)
} else {
return LayoutMetrics(widthClass: .compact, heightClass: .compact)
}
}
private func safeInsetsForScreenSize(_ size: CGSize, hasOnScreenNavigation: Bool) -> UIEdgeInsets {
return DeviceMetrics.forScreenSize(size, hintHasOnScreenNavigation: hasOnScreenNavigation)?.safeAreaInsets(inLandscape: size.width > size.height) ?? UIEdgeInsets.zero
}
public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
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
private let statusBarHost: StatusBarHost?
private let statusBarManager: StatusBarManager?
private let keyboardManager: KeyboardManager?
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 var cachedWindowSubviewCount: Int = 0
private var cachedHasPreview: Bool = false
private let presentationContext: PresentationContext
private let overlayPresentationContext: GlobalOverlayPresentationContext
private var tracingStatusBarsInvalidated = false
private var shouldUpdateDeferScreenEdgeGestures = false
private var shouldInvalidatePreferNavigationUIHidden = false
private var statusBarHidden = false
public var previewThemeAccentColor: UIColor = .blue
public var previewThemeDarkBlur: Bool = false
public private(set) var forceInCallStatusBarText: String? = nil
public var inCallNavigate: (() -> Void)? {
didSet {
self.statusBarManager?.inCallNavigate = self.inCallNavigate
}
}
private var windowPanRecognizer: WindowPanRecognizer?
private let keyboardGestureRecognizerDelegate = WindowKeyboardGestureRecognizerDelegate()
private var keyboardGestureBeginLocation: CGPoint?
private var keyboardGestureAccessoryHeight: CGFloat?
private var keyboardTypeChangeTimer: SwiftSignalKit.Timer?
private let volumeControlStatusBar: VolumeControlStatusBar
private let volumeControlStatusBarNode: VolumeControlStatusBarNode
private var isInteractionBlocked = false
/*private var accessibilityElements: [Any]? {
return self.viewController?.view.accessibilityElements
}*/
public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) {
self.hostView = hostView
self.volumeControlStatusBar = VolumeControlStatusBar(frame: CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: CGSize(width: 100.0, height: 20.0)), shouldBeVisible: statusBarHost?.handleVolumeControl ?? .single(false))
self.volumeControlStatusBarNode = VolumeControlStatusBarNode()
self.volumeControlStatusBarNode.isHidden = true
let boundsSize = self.hostView.eventView.bounds.size
let deviceMetrics = DeviceMetrics.forScreenSize(boundsSize, hintHasOnScreenNavigation: self.hostView.hasOnScreenNavigation)
self.statusBarHost = statusBarHost
let statusBarHeight: CGFloat
if let statusBarHost = statusBarHost {
statusBarHeight = statusBarHost.statusBarFrame.size.height
self.statusBarManager = StatusBarManager(host: statusBarHost, volumeControlStatusBar: self.volumeControlStatusBar, volumeControlStatusBarNode: self.volumeControlStatusBarNode)
self.keyboardManager = KeyboardManager(host: statusBarHost)
} else {
statusBarHeight = deviceMetrics?.statusBarHeight ?? 20.0
self.statusBarManager = nil
self.keyboardManager = nil
}
let onScreenNavigationHeight = deviceMetrics?.onScreenNavigationHeight(inLandscape: boundsSize.width > boundsSize.height)
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsetsForScreenSize(boundsSize, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning)
self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate)
self.presentationContext = PresentationContext()
self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost)
self.presentationContext.updateIsInteractionBlocked = { [weak self] value in
self?.isInteractionBlocked = value
}
self.presentationContext.updateHasOpaqueOverlay = { [weak self] value in
self?._rootController?.displayNode.accessibilityElementsHidden = value
}
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.presentNative = { [weak self] controller in
self?.presentNative(controller)
}
self.hostView.updateSize = { [weak self] size, duration in
self?.updateSize(size, duration: duration)
}
self.hostView.eventView.layer.setInvalidateTracingSublayers { [weak self] in
self?.invalidateTracingStatusBars()
}
self.hostView.layoutSubviews = { [weak self] in
self?.layoutSubviews()
}
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.invalidatePreferNavigationUIHidden = { [weak self] in
self?.invalidatePreferNavigationUIHidden()
}
self.hostView.cancelInteractiveKeyboardGestures = { [weak self] in
self?.cancelInteractiveKeyboardGestures()
}
self.hostView.forEachController = { [weak self] f in
self?.forEachViewController({ controller in
f(controller)
return true
})
}
/*self.hostView.getAccessibilityElements = { [weak self] in
return self?.accessibilityElements
}*/
self.presentationContext.view = self.hostView.containerView
self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate)
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate)
self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil, queue: OperationQueue.main, using: { [weak self] notification in
if let strongSelf = self {
let statusBarHeight: CGFloat = max(20.0, (notification.userInfo?[UIApplicationStatusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? 20.0)
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
}
let keyboardHeight = max(0.0, strongSelf.keyboardManager?.getCurrentKeyboardHeight() ?? 0.0)
var duration: Double = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0
if duration > Double.ulpOfOne {
duration = 0.5
}
let curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] 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: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil, queue: nil, using: { [weak self] notification in
if let strongSelf = self {
let keyboardFrame: CGRect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect()
let screenHeight: CGFloat
if keyboardFrame.width.isEqual(to: UIScreen.main.bounds.width) {
if abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 {
screenHeight = UIScreen.main.bounds.height
} else {
screenHeight = strongSelf.windowLayout.size.height
}
} else {
screenHeight = UIScreen.main.bounds.width
}
let keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY)
var duration: Double = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0
if duration > Double.ulpOfOne {
duration = 0.5
}
let curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] 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) }
}
})
if #available(iOSApplicationExtension 11.0, *) {
self.keyboardTypeChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UITextInputCurrentInputModeDidChange, 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 {
let updatedKeyboardHeight = keyboardManager.getCurrentKeyboardHeight()
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, *) {
self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIAccessibilityVoiceOverStatusDidChange, 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.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.volumeControlStatusBar)
self.hostView.containerView.addSubview(self.volumeControlStatusBarNode.view)
}
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)
}
}
public func setupVolumeControlStatusBarGraphics(_ graphics: (UIImage, UIImage, UIImage)) {
self.volumeControlStatusBarNode.graphics = graphics
}
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 invalidatePreferNavigationUIHidden() {
self.shouldInvalidatePreferNavigationUIHidden = 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 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() {
if NSStringFromClass(type(of: view)) == "UITransitionView" {
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(point, with: event) {
return result
}
return self.viewController?.view.hitTest(point, with: event)
}
func updateSize(_ value: CGSize, duration: Double) {
let transition: ContainedViewLayoutTransition
if !duration.isZero {
transition = .animated(duration: duration, curve: .easeInOut)
} else {
transition = .immediate
}
self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: safeInsetsForScreenSize(value, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: 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 !self.windowLayout.size.width.isZero && !self.windowLayout.size.height.isZero {
rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate)
}
self.hostView.containerView.insertSubview(rootController.view, at: 0)
}
self.hostView.eventView.setNeedsLayout()
}
}
private var _topLevelOverlayControllers: [ContainableController] = []
public var topLevelOverlayControllers: [ContainableController] {
get {
return _topLevelOverlayControllers
}
set(value) {
for controller in self._topLevelOverlayControllers {
controller.view.removeFromSuperview()
}
self._topLevelOverlayControllers = value
let layout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation)
for controller in self._topLevelOverlayControllers {
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.volumeControlStatusBarNode.view)
}
}
self.presentationContext.topLevelSubview = self._topLevelOverlayControllers.first?.view
}
}
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
self.hostView.containerView.insertSubview(coveringView, belowSubview: self.volumeControlStatusBarNode.view)
if !self.windowLayout.size.width.isZero {
coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size)
coveringView.updateLayout(self.windowLayout.size)
}
}
}
}
}
private func layoutSubviews() {
var hasPreview = false
var updatedHasPreview = false
for subview in self.hostView.eventView.subviews {
if checkIsPreviewingView(subview) {
applyThemeToPreviewingView(subview, accentColor: self.previewThemeAccentColor, darkBlur: self.previewThemeDarkBlur)
hasPreview = true
break
}
}
if hasPreview != self.cachedHasPreview {
self.cachedHasPreview = hasPreview
updatedHasPreview = true
}
if self.tracingStatusBarsInvalidated || updatedHasPreview, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager {
self.tracingStatusBarsInvalidated = false
if self.statusBarHidden {
statusBarManager.updateState(surfaces: [], withSafeInsets: false, forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, animated: false)
} else {
var statusBarSurfaces: [StatusBarSurface] = []
for layers in self.hostView.containerView.layer.traceableLayerSurfaces(withTag: WindowTracingTags.statusBar) {
let surface = StatusBarSurface()
for layer in layers {
let traceableInfo = layer.traceableInfo()
if let statusBar = traceableInfo?.userData as? StatusBar {
surface.addStatusBar(statusBar)
}
}
statusBarSurfaces.append(surface)
}
self.hostView.containerView.layer.adjustTraceableLayerTransforms(CGSize())
var animatedUpdate = false
if let updatingLayout = self.updatingLayout {
if case .animated = updatingLayout.transition {
animatedUpdate = true
}
}
self.cachedWindowSubviewCount = self.hostView.containerView.window?.subviews.count ?? 0
statusBarManager.updateState(surfaces: statusBarSurfaces, withSafeInsets: !self.windowLayout.safeInsets.top.isZero, forceInCallStatusBarText: self.forceInCallStatusBarText, forceHiddenBySystemWindows: hasPreview, animated: animatedUpdate)
}
var keyboardSurfaces: [KeyboardSurface] = []
for layers in self.hostView.containerView.layer.traceableLayerSurfaces(withTag: WindowTracingTags.keyboard) {
for layer in layers {
if let view = layer.delegate as? UITracingLayerView {
keyboardSurfaces.append(KeyboardSurface(host: view))
}
}
}
keyboardManager.surfaces = keyboardSurfaces
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.updatePreferNavigationUIHidden(self.collectPreferNavigationUIHidden())
self.shouldUpdateDeferScreenEdgeGestures = false
self.shouldInvalidatePreferNavigationUIHidden = false
} else if self.shouldUpdateDeferScreenEdgeGestures || self.shouldInvalidatePreferNavigationUIHidden {
self.shouldUpdateDeferScreenEdgeGestures = false
self.shouldInvalidatePreferNavigationUIHidden = false
self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures())
self.hostView.updatePreferNavigationUIHidden(self.collectPreferNavigationUIHidden())
}
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)
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 deviceMetrics = DeviceMetrics.forScreenSize(boundsSize, hintHasOnScreenNavigation: self.hostView.hasOnScreenNavigation)
var statusBarHeight: CGFloat?
if let statusBarHost = self.statusBarHost {
statusBarHeight = statusBarHost.statusBarFrame.size.height
} else {
statusBarHeight = deviceMetrics?.statusBarHeight ?? 20.0
}
let statusBarWasHidden = self.statusBarHidden
if statusBarHiddenInLandscape && updatingLayout.layout.size.width > updatingLayout.layout.size.height {
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)
let onScreenNavigationHeight = deviceMetrics?.onScreenNavigationHeight(inLandscape: boundsSize.width > boundsSize.height)
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver)
let childLayout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation)
let childLayoutUpdated = self.updatedContainerLayout != childLayout
self.updatedContainerLayout = childLayout
if childLayoutUpdated {
var rootLayout = childLayout
let rootTransition = updatingLayout.transition
if self.presentationContext.isCurrentlyOpaque {
rootLayout.inputHeight = nil
}
self._rootController?.containerLayoutUpdated(rootLayout, transition: rootTransition)
self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition)
self.overlayPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition)
for controller in self.topLevelOverlayControllers {
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)
}
})
}
self.volumeControlStatusBarNode.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size)
self.volumeControlStatusBarNode.updateLayout(layout: childLayout, transition: updatingLayout.transition)
if let coveringView = self.coveringView {
coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size)
coveringView.updateLayout(self.windowLayout.size)
}
}
}
}
public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void = {}) {
self.presentationContext.present(controller, on: level, blockInteraction: blockInteraction, completion: completion)
}
public func presentInGlobalOverlay(_ controller: ContainableController) {
self.overlayPresentationContext.present(controller)
}
public func presentNative(_ controller: UIViewController) {
}
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) {
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)
}
}
}
}
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: UIPanGestureRecognizer) {
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 = self.presentationContext.combinedDeferScreenEdgeGestures()
for controller in self.topLevelOverlayControllers {
if let controller = controller as? ViewController {
edges = edges.union(controller.deferScreenEdgeGestures)
}
}
return edges
}
private func collectPreferNavigationUIHidden() -> Bool {
return false
}
public func forEachViewController(_ f: (ContainableController) -> Bool) {
for (controller, _) in self.presentationContext.controllers {
if !f(controller) {
break
}
}
}
}