Swiftgram/Display/Window.swift
2016-09-05 23:19:33 +03:00

355 lines
14 KiB
Swift

import Foundation
import AsyncDisplayKit
private class WindowRootViewController: UIViewController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .default
}
override var prefersStatusBarHidden: Bool {
return false
}
}
private struct WindowLayout: Equatable {
public let size: CGSize
public let statusBarHeight: CGFloat?
public let inputHeight: CGFloat?
}
private func ==(lhs: WindowLayout, rhs: WindowLayout) -> Bool {
if !lhs.size.equalTo(rhs.size) {
return false
}
if let lhsStatusBarHeight = lhs.statusBarHeight {
if let rhsStatusBarHeight = rhs.statusBarHeight {
if !lhsStatusBarHeight.isEqual(to: rhsStatusBarHeight) {
return false
}
} else {
return false
}
} else if let _ = rhs.statusBarHeight {
return false
}
if let lhsInputHeight = lhs.inputHeight {
if let rhsInputHeight = rhs.inputHeight {
if !lhsInputHeight.isEqual(to: rhsInputHeight) {
return false
}
} else {
return false
}
} else if let _ = rhs.inputHeight {
return false
}
return true
}
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, transition: ContainedViewLayoutTransition, overrideTransition: Bool) {
self.update(transition: transition, override: overrideTransition)
self.layout = WindowLayout(size: size, statusBarHeight: self.layout.statusBarHeight, inputHeight: self.layout.inputHeight)
}
mutating func update(statusBarHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) {
self.update(transition: transition, override: overrideTransition)
self.layout = WindowLayout(size: self.layout.size, statusBarHeight: statusBarHeight, inputHeight: self.layout.inputHeight)
}
mutating func update(inputHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) {
self.update(transition: transition, override: overrideTransition)
self.layout = WindowLayout(size: self.layout.size, statusBarHeight: self.layout.statusBarHeight, inputHeight: inputHeight)
}
}
private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3
private let statusBarHiddenInLandscape: Bool = UIDevice.current.userInterfaceIdiom == .phone
private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> ContainerViewLayout {
return ContainerViewLayout(size: layout.size, intrinsicInsets: UIEdgeInsets(), statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight)
}
public class Window: UIWindow {
private let statusBarHost: StatusBarHost?
private let statusBarManager: StatusBarManager?
private var statusBarChangeObserver: AnyObject?
private var keyboardFrameChangeObserver: AnyObject?
private var windowLayout: WindowLayout
private var updatingLayout: UpdatingLayout?
public var isUpdatingOrientationLayout = false
private let presentationContext: PresentationContext
private var tracingStatusBarsInvalidated = false
private var statusBarHidden = false
public init(frame: CGRect, statusBarHost: StatusBarHost?) {
self.statusBarHost = statusBarHost
let statusBarHeight: CGFloat
if let statusBarHost = statusBarHost {
self.statusBarManager = StatusBarManager(host: statusBarHost)
statusBarHeight = statusBarHost.statusBarFrame.size.height
} else {
self.statusBarManager = nil
statusBarHeight = 20.0
}
self.windowLayout = WindowLayout(size: frame.size, statusBarHeight: statusBarHeight, inputHeight: 0.0)
self.presentationContext = PresentationContext()
super.init(frame: frame)
self.layer.setInvalidateTracingSublayers { [weak self] in
self?.invalidateTracingStatusBars()
}
self.presentationContext.view = self
self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate)
super.rootViewController = WindowRootViewController()
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.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 keyboardHeight = max(0.0, UIScreen.main.bounds.size.height - keyboardFrame.minY)
var duration: Double = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0
if duration > DBL_EPSILON {
duration = 0.5
}
var 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) }
}
})
}
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 keyboardFrameChangeObserver = self.keyboardFrameChangeObserver {
NotificationCenter.default.removeObserver(keyboardFrameChangeObserver)
}
}
private func invalidateTracingStatusBars() {
self.tracingStatusBarsInvalidated = true
self.setNeedsLayout()
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.presentationContext.hitTest(point, with: event) {
return result
}
return self.viewController?.view.hitTest(point, with: event)
}
public override var frame: CGRect {
get {
return super.frame
}
set(value) {
let sizeUpdated = super.frame.size != value.size
super.frame = value
if sizeUpdated {
let transition: ContainedViewLayoutTransition
if self.isRotating() {
transition = .animated(duration: orientationChangeDuration, curve: .easeInOut)
} else {
transition = .immediate
}
self.updateLayout { $0.update(size: value.size, transition: transition, overrideTransition: true) }
}
}
}
public override var bounds: CGRect {
get {
return super.frame
}
set(value) {
let sizeUpdated = super.bounds.size != value.size
super.bounds = value
if sizeUpdated {
let transition: ContainedViewLayoutTransition
if self.isRotating() {
transition = .animated(duration: orientationChangeDuration, curve: .easeInOut)
} else {
transition = .immediate
}
self.updateLayout { $0.update(size: value.size, 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 {
rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate)
self.addSubview(rootController.view)
}
}
}
override public func layoutSubviews() {
super.layoutSubviews()
if self.tracingStatusBarsInvalidated, let statusBarManager = statusBarManager {
self.tracingStatusBarsInvalidated = false
if self.statusBarHidden {
statusBarManager.surfaces = []
} else {
var statusBarSurfaces: [StatusBarSurface] = []
for layers in self.layer.traceableLayerSurfaces() {
let surface = StatusBarSurface()
for layer in layers {
if let weakInfo = layer.traceableInfo() as? NSWeakReference {
if let statusBar = weakInfo.value as? StatusBar {
surface.addStatusBar(statusBar)
}
}
}
statusBarSurfaces.append(surface)
}
self.layer.adjustTraceableLayerTransforms(CGSize())
statusBarManager.surfaces = statusBarSurfaces
}
}
if !Window.isDeviceRotating() {
if !self.isUpdatingOrientationLayout {
self.commitUpdatingLayout()
} else {
self.addPostUpdateToInterfaceOrientationBlock(f: { [weak self] in
if let strongSelf = self {
strongSelf.setNeedsLayout()
}
})
}
} else {
Window.addPostDeviceOrientationDidChange({ [weak self] in
if let strongSelf = self {
strongSelf.setNeedsLayout()
}
})
}
}
var postUpdateToInterfaceOrientationBlocks: [(Void) -> Void] = []
override public func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) {
self.isUpdatingOrientationLayout = true
super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3)
self.isUpdatingOrientationLayout = false
let blocks = self.postUpdateToInterfaceOrientationBlocks
self.postUpdateToInterfaceOrientationBlocks = []
for f in blocks {
f()
}
}
public func addPostUpdateToInterfaceOrientationBlock(f: @escaping (Void) -> Void) {
postUpdateToInterfaceOrientationBlocks.append(f)
}
private func updateLayout(_ update: @noescape(inout UpdatingLayout) -> ()) {
if self.updatingLayout == nil {
self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate)
}
update(&self.updatingLayout!)
self.setNeedsLayout()
}
private func commitUpdatingLayout() {
if let updatingLayout = self.updatingLayout {
self.updatingLayout = nil
if updatingLayout.layout != self.windowLayout {
var statusBarHeight: CGFloat
if let statusBarHost = self.statusBarHost {
statusBarHeight = statusBarHost.statusBarFrame.size.height
} else {
statusBarHeight = 20.0
}
var statusBarWasHidden = self.statusBarHidden
if statusBarHiddenInLandscape && updatingLayout.layout.size.width > updatingLayout.layout.size.height {
statusBarHeight = 0.0
self.statusBarHidden = true
} else {
self.statusBarHidden = false
}
if self.statusBarHidden != statusBarWasHidden {
self.tracingStatusBarsInvalidated = true
self.setNeedsLayout()
}
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, statusBarHeight: statusBarHeight, inputHeight: updatingLayout.layout.inputHeight)
self.rootController?.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition)
self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition)
}
}
}
func present(_ controller: ViewController) {
self.presentationContext.present(controller)
}
}