no message

This commit is contained in:
Peter Iakovlev
2018-02-21 01:46:32 +04:00
parent 2d79df7e75
commit 14f0fc94f4
32 changed files with 1681 additions and 142 deletions

View File

@@ -367,6 +367,14 @@ static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull
@implementation UITracingLayerView
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
}
- (void)setAutoresizingMask:(UIViewAutoresizing)autoresizingMask {
[super setAutoresizingMask:0];
}
+ (Class)layerClass {
return [CATracingLayer class];
}

View File

@@ -45,25 +45,17 @@ public struct ContainerViewLayout: Equatable {
public let safeInsets: UIEdgeInsets
public let statusBarHeight: CGFloat?
public let inputHeight: CGFloat?
public let standardInputHeight: CGFloat
public let inputHeightIsInteractivellyChanging: Bool
public init() {
self.size = CGSize()
self.metrics = LayoutMetrics()
self.intrinsicInsets = UIEdgeInsets()
self.safeInsets = UIEdgeInsets()
self.statusBarHeight = nil
self.inputHeight = nil
self.inputHeightIsInteractivellyChanging = false
}
public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, inputHeightIsInteractivellyChanging: Bool) {
public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool) {
self.size = size
self.metrics = metrics
self.intrinsicInsets = intrinsicInsets
self.safeInsets = safeInsets
self.statusBarHeight = statusBarHeight
self.inputHeight = inputHeight
self.standardInputHeight = standardInputHeight
self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging
}
@@ -79,15 +71,15 @@ public struct ContainerViewLayout: Equatable {
}
public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
}
public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
}
public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging)
}
public static func ==(lhs: ContainerViewLayout, rhs: ContainerViewLayout) -> Bool {
@@ -131,6 +123,10 @@ public struct ContainerViewLayout: Equatable {
return false
}
if !lhs.standardInputHeight.isEqual(to: rhs.standardInputHeight) {
return false
}
if lhs.inputHeightIsInteractivellyChanging != rhs.inputHeightIsInteractivellyChanging {
return false
}

View File

@@ -0,0 +1,160 @@
import Foundation
import AsyncDisplayKit
import SwiftSignalKit
final class GlobalOverlayPresentationContext {
private let statusBarHost: StatusBarHost?
private var controllers: [ViewController] = []
private var presentationDisposables = DisposableSet()
private var layout: ContainerViewLayout?
private var ready: Bool {
return self.currentPresentationView() != nil && self.layout != nil
}
init(statusBarHost: StatusBarHost?) {
self.statusBarHost = statusBarHost
}
private func currentPresentationView() -> UIView? {
if let statusBarHost = self.statusBarHost {
if let keyboardWindow = statusBarHost.keyboardWindow {
return keyboardWindow
} else {
return statusBarHost.statusBarWindow
}
}
return nil
}
func present(_ controller: ViewController) {
let controllerReady = controller.ready.get()
|> filter({ $0 })
|> take(1)
|> deliverOnMainQueue
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true))
if let _ = self.currentPresentationView(), let initialLayout = self.layout {
controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size)
controller.containerLayoutUpdated(initialLayout, transition: .immediate)
self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in
if let strongSelf = self {
if strongSelf.controllers.contains(where: { $0 === controller }) {
return
}
strongSelf.controllers.append(controller)
if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout {
controller.navigation_setDismiss({ [weak controller] in
if let strongSelf = self, let controller = controller {
strongSelf.dismiss(controller)
}
}, rootController: nil)
controller.setIgnoreAppearanceMethodInvocations(true)
if layout != initialLayout {
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
view.addSubview(controller.view)
controller.containerLayoutUpdated(layout, transition: .immediate)
} else {
view.addSubview(controller.view)
}
controller.setIgnoreAppearanceMethodInvocations(false)
view.layer.invalidateUpTheTree()
controller.viewWillAppear(false)
controller.viewDidAppear(false)
}
}
}))
} else {
self.controllers.append(controller)
}
}
deinit {
self.presentationDisposables.dispose()
}
private func dismiss(_ controller: ViewController) {
if let index = self.controllers.index(where: { $0 === controller }) {
self.controllers.remove(at: index)
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let wasReady = self.ready
self.layout = layout
if wasReady != self.ready {
self.readyChanged(wasReady: wasReady)
} else if self.ready {
for controller in self.controllers {
controller.containerLayoutUpdated(layout, transition: transition)
}
}
}
private func readyChanged(wasReady: Bool) {
if !wasReady {
self.addViews()
} else {
self.removeViews()
}
}
private func addViews() {
if let view = self.currentPresentationView(), let layout = self.layout {
for controller in self.controllers {
controller.viewWillAppear(false)
view.addSubview(controller.view)
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
controller.containerLayoutUpdated(layout, transition: .immediate)
controller.viewDidAppear(false)
}
}
}
private func removeViews() {
for controller in self.controllers {
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
}
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for controller in self.controllers.reversed() {
if controller.isViewLoaded {
if let result = controller.view.hitTest(point, with: event) {
return result
}
}
}
return nil
}
func combinedSupportedOrientations() -> UIInterfaceOrientationMask {
var mask: UIInterfaceOrientationMask = .all
for controller in self.controllers {
mask = mask.intersection(controller.supportedInterfaceOrientations)
}
return mask
}
func combinedDeferScreenEdgeGestures() -> UIRectEdge {
var edges: UIRectEdge = []
for controller in self.controllers {
edges = edges.union(controller.deferScreenEdgeGestures)
}
return edges
}
}

View File

@@ -102,34 +102,6 @@ public struct GridNodeUpdateLayout {
}
}
/*private func binarySearch(_ inputArr: [GridNodePresentationItem], searchItem: CGFloat) -> Int? {
if inputArr.isEmpty {
return nil
}
var lowerPosition = inputArr[0].frame.origin.y + inputArr[0].frame.size.height
var upperPosition = inputArr[inputArr.count - 1].frame.origin.y
if lowerPosition > upperPosition {
return nil
}
while (true) {
let currentPosition = (lowerIndex + upperIndex) / 2
if (inputArr[currentIndex] == searchItem) {
return currentIndex
} else if (lowerIndex > upperIndex) {
return nil
} else {
if (inputArr[currentIndex] > searchItem) {
upperIndex = currentIndex - 1
} else {
lowerIndex = currentIndex + 1
}
}
}
}*/
public enum GridNodeStationaryItems {
case none
case all
@@ -263,6 +235,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
public var visibleItemsUpdated: ((GridNodeVisibleItems) -> Void)?
public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)?
public var scrollingCompleted: (() -> Void)?
public final var floatingSections = false
@@ -397,11 +370,13 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.updateItemNodeVisibilititesAndScrolling()
self.scrollingCompleted?()
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.updateItemNodeVisibilititesAndScrolling()
self.scrollingCompleted?()
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {

View File

@@ -1,10 +1,6 @@
#if os(macOS)
import SwiftSignalKitMac
#else
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
#endif
private let useBackgroundDeallocation = false
@@ -48,7 +44,6 @@ final class ListViewBackingView: UIView {
override func setNeedsDisplay() {
}
#if os(iOS)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.target?.touchesBegan(touches, with: event)
}
@@ -73,7 +68,6 @@ final class ListViewBackingView: UIView {
}
return super.hitTest(point, with: event)
}
#endif
}
private final class ListViewTimerProxy: NSObject {
@@ -197,6 +191,8 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
public final var beganInteractiveDragging: () -> Void = { }
public final var reorderItem: (Int, Int, Any?) -> Void = { _, _, _ in }
private final var animations: [ListViewAnimation] = []
private final var actionsForVSync: [() -> ()] = []
private final var inVSync = false
@@ -211,6 +207,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
private var selectionLongTapDelayTimer: Foundation.Timer?
private var flashNodesDelayTimer: Foundation.Timer?
private var highlightedItemIndex: Int?
private var reorderNode: ListViewReorderingItemNode?
private let waitingForNodesDisposable = MetaDisposable()
@@ -266,13 +263,37 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
trackingRecognizer.delegate = self
self.view.addGestureRecognizer(trackingRecognizer)
self.view.addGestureRecognizer(ListViewReorderingGestureRecognizer(shouldBegin: { [weak self] point in
if let strongSelf = self {
if let index = strongSelf.itemIndexAtPoint(point) {
for i in 0 ..< strongSelf.itemNodes.count {
if strongSelf.itemNodes[i].index == index {
let itemNode = strongSelf.itemNodes[i]
let itemNodeFrame = itemNode.frame
let itemNodeBounds = itemNode.bounds
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
strongSelf.beginReordering(itemNode: itemNode)
return true
}
break
}
}
}
}
return false
}, ended: { [weak self] in
self?.endReordering()
}, moved: { [weak self] offset in
self?.updateReordering(offset: offset)
}))
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
#if os(iOS)
if #available(iOS 10.0, *) {
self.displayLink.preferredFramesPerSecond = 60
}
#endif
self.displayLink.isPaused = true
}
@@ -334,6 +355,86 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
}
private func beginReordering(itemNode: ListViewItemNode) {
if let reorderNode = self.reorderNode {
reorderNode.removeFromSupernode()
}
let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin)
self.reorderNode = reorderNode
self.addSubnode(reorderNode)
itemNode.isHidden = true
}
private func endReordering() {
if let reorderNode = self.reorderNode {
self.reorderNode = nil
if let itemNode = reorderNode.itemNode, itemNode.supernode == self {
self.view.bringSubview(toFront: itemNode.view)
reorderNode.animateCompletion(completion: { [weak itemNode, weak reorderNode] in
//itemNode?.isHidden = false
reorderNode?.removeFromSupernode()
})
self.setNeedsAnimations()
} else {
reorderNode.removeFromSupernode()
}
}
}
private func updateReordering(offset: CGFloat) {
if let reorderNode = self.reorderNode {
reorderNode.updateOffset(offset: offset)
self.checkItemReordering()
}
}
private func checkItemReordering() {
if let reorderNode = self.reorderNode, let reorderItemNode = reorderNode.itemNode, let reorderItemIndex = reorderItemNode.index, reorderItemNode.supernode == self {
guard let verticalTopOffset = reorderNode.currentOffset() else {
return
}
let verticalOffset = verticalTopOffset
var closestIndex: (Int, CGFloat)?
for i in 0 ..< self.itemNodes.count {
if let itemNodeIndex = self.itemNodes[i].index, itemNodeIndex != reorderItemIndex {
let itemOffset = self.itemNodes[i].frame.midY
let deltaOffset = itemOffset - verticalOffset
if let (_, closestOffset) = closestIndex {
if abs(deltaOffset) < abs(closestOffset) {
closestIndex = (itemNodeIndex, deltaOffset)
}
} else {
closestIndex = (itemNodeIndex, deltaOffset)
}
}
}
if let (closestIndexValue, offset) = closestIndex {
//print("closest \(closestIndexValue) offset \(offset)")
var toIndex: Int
if offset > 0 {
toIndex = closestIndexValue
if toIndex > reorderItemIndex {
toIndex -= 1
}
} else {
toIndex = closestIndexValue + 1
if toIndex > reorderItemIndex {
toIndex -= 1
}
}
if toIndex != reorderItemNode.index {
if reorderNode.currentState?.0 != reorderItemIndex || reorderNode.currentState?.1 != toIndex {
reorderNode.currentState = (reorderItemIndex, toIndex)
//print("reorder \(reorderItemIndex) to \(toIndex) offset \(offset)")
self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState)
}
}
}
self.setNeedsAnimations()
}
}
private func resetHeaderItemsFlashTimer(start: Bool) {
if let flashNodesDelayTimer = self.flashNodesDelayTimer {
flashNodesDelayTimer.invalidate()
@@ -1524,7 +1625,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
var offsetHeight = node.apparentHeight
var takenAnimation = false
if let _ = previousFrame , animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 {
if let _ = previousFrame, animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 {
let nextNode = self.itemNodes[nodeIndex + 1]
if nextNode.index == nil && nextNode.subnodes.isEmpty {
let nextHeight = nextNode.apparentHeight
@@ -1560,8 +1661,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
if node.index == nil {
node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
if node.animationForKey("height") == nil || !(node is ListViewTempItemNode) {
node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) {
node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor())
} else if animated {
if takenAnimation {
@@ -1656,7 +1761,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
}
private func lowestHeaderNode() -> ASDisplayNode? {
private func lowestNodeToInsertBelow() -> ASDisplayNode? {
if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self {
//return itemNode
}
var lowestHeaderNode: ASDisplayNode?
var lowestHeaderNodeIndex: Int?
for (_, headerNode) in self.itemHeaderNodes {
@@ -1709,6 +1817,18 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set<Int>, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) {
/*if true {
print("----------")
for itemNode in self.itemNodes {
var anim = ""
if let animation = itemNode.animationForKey("apparentHeight") {
anim = "\(animation.from)->\(animation.to)"
}
print("\(itemNode.index) \(itemNode.apparentFrame.height) \(anim)")
}
print("----------")
}*/
let timestamp = CACurrentMediaTime()
let listInsets = updateSizeAndInsets?.insets ?? self.insets
@@ -1718,14 +1838,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
var previousTopItemVerticalOrigin: CGFloat?
var previousBottomItemMaxY: CGFloat?
var snapshotView: UIView?
if animateCrossfade {
snapshotView = self.view.snapshotView(afterScreenUpdates: false)
}
if animateTopItemVerticalOrigin {
previousTopItemVerticalOrigin = self.topItemVerticalOrigin()
previousBottomItemMaxY = self.bottomItemMaxY()
}
var previousApparentFrames: [(ListViewItemNode, CGRect)] = []
@@ -1740,7 +1858,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
}
let lowestHeaderNode = self.lowestHeaderNode()
let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow()
for operation in operations {
switch operation {
@@ -1763,8 +1881,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp, listInsets: listInsets)
if let _ = updatedPreviousFrame {
if let lowestHeaderNode = lowestHeaderNode {
self.insertSubnode(node, belowSubnode: lowestHeaderNode)
if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self {
self.insertSubnode(node, belowSubnode: itemNode)
} else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow {
self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow)
} else {
self.addSubnode(node)
}
@@ -1776,8 +1896,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
self.insertSubnode(node, at: 0)
}
} else {
if let lowestHeaderNode = lowestHeaderNode {
self.insertSubnode(node, belowSubnode: lowestHeaderNode)
if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self {
self.insertSubnode(node, belowSubnode: itemNode)
} else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow {
self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow)
} else {
self.addSubnode(node)
}
@@ -1797,7 +1919,9 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
if let height = height, let previousLayout = previousLayout {
if takenPreviousNodes.contains(referenceNode) {
self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: ListViewItemNode(layerBacked: true), layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets)
let tempNode = ListViewTempItemNode(layerBacked: true)
//referenceNode.copyHeightAndApparentHeightAnimations(to: tempNode)
self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: tempNode, layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets)
} else {
referenceNode.index = nil
self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: referenceNode, layout: previousLayout, apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets)
@@ -2212,11 +2336,11 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty)
if let offset = offset , abs(offset) > CGFloat.ulpOfOne {
let lowestHeaderNode = self.lowestHeaderNode()
let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow()
for itemNode in temporaryPreviousNodes {
itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: offset)
if let lowestHeaderNode = lowestHeaderNode {
self.insertSubnode(itemNode, belowSubnode: lowestHeaderNode)
if let lowestNodeToInsertBelow = lowestNodeToInsertBelow {
self.insertSubnode(itemNode, belowSubnode: lowestNodeToInsertBelow)
} else {
self.addSubnode(itemNode)
}
@@ -2714,7 +2838,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
private func immediateDisplayedItemRange() -> ListViewDisplayedItemRange {
var loadedRange: ListViewItemRange?
var visibleRange: ListViewItemRange?
var visibleRange: ListViewVisibleItemRange?
if self.itemNodes.count != 0 {
var firstIndex: (nodeIndex: Int, index: Int)?
var lastIndex: (nodeIndex: Int, index: Int)?
@@ -2736,12 +2860,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
i -= 1
}
if let firstIndex = firstIndex, let lastIndex = lastIndex {
var firstVisibleIndex: Int?
var firstVisibleIndex: (Int, Bool)?
for i in firstIndex.nodeIndex ... lastIndex.nodeIndex {
if let index = self.itemNodes[i].index {
let frame = self.itemNodes[i].apparentFrame
if frame.maxY >= self.insets.top && frame.minY < self.visibleSize.height + self.insets.bottom {
firstVisibleIndex = index
firstVisibleIndex = (index, frame.minY >= self.insets.top)
break
}
}
@@ -2760,7 +2884,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
if let lastVisibleIndex = lastVisibleIndex {
visibleRange = ListViewItemRange(firstIndex: firstVisibleIndex, lastIndex: lastVisibleIndex)
visibleRange = ListViewVisibleItemRange(firstIndex: firstVisibleIndex.0, firstIndexFullyVisible: firstVisibleIndex.1, lastIndex: lastVisibleIndex)
}
}
@@ -2807,6 +2931,20 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
var offsetRanges = OffsetRanges()
if let reorderOffset = self.reorderNode?.currentOffset(), !self.itemNodes.isEmpty {
if reorderOffset < self.insets.top + 10.0 {
if self.itemNodes[0].apparentFrame.minY < self.insets.top {
continueAnimations = true
offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: 6.0)
}
} else if reorderOffset > self.visibleSize.height - self.insets.bottom - 10.0 {
if self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY > self.visibleSize.height - self.insets.bottom {
continueAnimations = true
offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: -6.0)
}
}
}
var requestUpdateVisibleItems = false
var index = 0
while index < self.itemNodes.count {
@@ -2871,9 +3009,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
if requestUpdateVisibleItems {
self.enqueueUpdateVisibleItems()
}
self.checkItemReordering()
}
#if os(iOS)
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchesPosition = touches.first!.location(in: self.view)
@@ -2963,7 +3102,6 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
self.updateScroller(transition: .immediate)
}
#endif
public func clearHighlightAnimated(_ animated: Bool) {
if let highlightedItemIndex = self.highlightedItemIndex {
@@ -3016,8 +3154,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
return nil
}
#if os(iOS)
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let selectionTouchLocation = self.selectionTouchLocation {
let location = touches.first!.location(in: self.view)
@@ -3099,7 +3236,6 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
break
}
}
#endif
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
@@ -3111,8 +3247,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
completion()
}
}
#if os(iOS)
fileprivate func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> Bool {
if self.limitHitTestToNodes {
var foundHit = false
@@ -3128,5 +3263,4 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel
}
return true
}
#endif
}

View File

@@ -124,6 +124,19 @@ public final class ListViewAnimation {
self.completed = completed
}
init<T: Interpolatable>(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
self.from = copying.from
self.to = copying.to
self.duration = copying.duration
self.curve = copying.curve
self.startTime = copying.startTime
self.interpolator = copying.interpolator
self.update = { progress, value in
update(progress, value as! T)
}
self.completed = completed
}
public func completeAt(_ timestamp: Double) -> Bool {
if timestamp >= self.startTime + self.duration {
self.completed(true)

View File

@@ -145,15 +145,25 @@ public struct ListViewUpdateSizeAndInsets {
public struct ListViewItemRange: Equatable {
public let firstIndex: Int
public let lastIndex: Int
public static func ==(lhs: ListViewItemRange, rhs: ListViewItemRange) -> Bool {
return lhs.firstIndex == rhs.firstIndex && lhs.lastIndex == rhs.lastIndex
}
}
public func ==(lhs: ListViewItemRange, rhs: ListViewItemRange) -> Bool {
return lhs.firstIndex == rhs.firstIndex && lhs.lastIndex == rhs.lastIndex
public struct ListViewVisibleItemRange: Equatable {
public let firstIndex: Int
public let firstIndexFullyVisible: Bool
public let lastIndex: Int
public static func ==(lhs: ListViewVisibleItemRange, rhs: ListViewVisibleItemRange) -> Bool {
return lhs.firstIndex == rhs.firstIndex && lhs.firstIndexFullyVisible == rhs.firstIndexFullyVisible && lhs.lastIndex == rhs.lastIndex
}
}
public struct ListViewDisplayedItemRange: Equatable {
public let loadedRange: ListViewItemRange?
public let visibleRange: ListViewItemRange?
public let visibleRange: ListViewVisibleItemRange?
}
public func ==(lhs: ListViewDisplayedItemRange, rhs: ListViewDisplayedItemRange) -> Bool {

View File

@@ -430,6 +430,27 @@ open class ListViewItemNode: ASDisplayNode {
self.setAnimationForKey("height", animation: animation)
}
func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) {
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
}
})
otherNode.setAnimationForKey("height", animation: updatedAnimation)
}
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
strongSelf.apparentHeight = currentValue
}
})
otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation)
}
}
public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
@@ -484,6 +505,10 @@ open class ListViewItemNode: ASDisplayNode {
open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
}
open func isReorderable(at point: CGPoint) -> Bool {
return false
}
open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
}

View File

@@ -0,0 +1,65 @@
import Foundation
import UIKit
final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> Bool
private let ended: () -> Void
private let moved: (CGFloat) -> Void
private var initialLocation: CGPoint?
init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
self.shouldBegin = shouldBegin
self.ended = ended
self.moved = moved
super.init(target: nil, action: nil)
}
override func reset() {
super.reset()
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.state == .possible {
if let location = touches.first?.location(in: self.view), self.shouldBegin(location) {
self.initialLocation = location
self.state = .began
} else {
self.state = .failed
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
let offset = location.y - initialLocation.y
self.moved(offset)
}
}
}

View File

@@ -0,0 +1,48 @@
import Foundation
import AsyncDisplayKit
final class ListViewReorderingItemNode: ASDisplayNode {
weak var itemNode: ListViewItemNode?
var currentState: (Int, Int)?
private let copyView: UIView?
private let initialLocation: CGPoint
init(itemNode: ListViewItemNode, initialLocation: CGPoint) {
self.itemNode = itemNode
self.copyView = itemNode.view.snapshotContentTree()
self.initialLocation = initialLocation
super.init()
if let copyView = self.copyView {
self.view.addSubview(copyView)
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size)
}
}
func updateOffset(offset: CGFloat) {
if let copyView = self.copyView {
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size)
}
}
func currentOffset() -> CGFloat? {
if let copyView = self.copyView {
return copyView.center.y
}
return nil
}
func animateCompletion(completion: @escaping () -> Void) {
if let copyView = self.copyView, let itemNode = self.itemNode {
itemNode.isHidden = false
itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY
itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime())
completion()
} else {
completion()
}
}
}

View File

@@ -0,0 +1,5 @@
import Foundation
final class ListViewTempItemNode: ListViewItemNode {
}

View File

@@ -11,6 +11,7 @@ private let defaultOrientations: UIInterfaceOrientationMask = {
private class WindowRootViewController: UIViewController {
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)?
var orientations: UIInterfaceOrientationMask = defaultOrientations {
didSet {
if oldValue != self.orientations {
@@ -73,6 +74,7 @@ private final class NativeWindow: UIWindow, WindowHost {
var updateIsUpdatingOrientationLayout: ((Bool) -> Void)?
var updateToInterfaceOrientation: (() -> Void)?
var presentController: ((ViewController, PresentationSurfaceLevel) -> Void)?
var presentControllerInGlobalOverlay: ((_ controller: ViewController) -> Void)?
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
var presentNativeImpl: ((UIViewController) -> Void)?
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
@@ -150,6 +152,10 @@ private final class NativeWindow: UIWindow, WindowHost {
self.presentController?(controller, level)
}
func presentInGlobalOverlay(_ controller: ViewController) {
self.presentControllerInGlobalOverlay?(controller)
}
func presentNative(_ controller: UIViewController) {
self.presentNativeImpl?(controller)
}
@@ -226,6 +232,10 @@ public func nativeWindowHostView() -> WindowHostView {
hostView?.present?(controller, level)
}
window.presentControllerInGlobalOverlay = { [weak hostView] controller in
hostView?.presentInGlobalOverlay?(controller)
}
window.presentNativeImpl = { [weak hostView] controller in
hostView?.presentNative?(controller)
}

View File

@@ -21,7 +21,7 @@ private class NavigationControllerView: UIView {
open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
public private(set) weak var overlayPresentingController: ViewController?
private var containerLayout = ContainerViewLayout()
private var validLayout: ContainerViewLayout?
private var navigationTransitionCoordinator: NavigationTransitionCoordinator?
@@ -72,10 +72,10 @@ open class NavigationController: UINavigationController, ContainableController,
if !self.isViewLoaded {
self.loadView()
}
self.containerLayout = layout
self.validLayout = layout
transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size))
let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging)
let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging)
if let topViewController = self.topViewController {
if let topViewController = topViewController as? ContainableController {
@@ -101,6 +101,7 @@ open class NavigationController: UINavigationController, ContainableController,
open override func loadView() {
self.view = NavigationControllerView()
self.view.clipsToBounds = true
self.view.autoresizingMask = []
if #available(iOSApplicationExtension 11.0, *) {
self.navigationBar.prefersLargeTitles = false
@@ -224,17 +225,21 @@ open class NavigationController: UINavigationController, ContainableController,
if !controller.hasActiveInput {
self.view.endEditing(true)
}
let appliedLayout = self.containerLayout.withUpdatedInputHeight(controller.hasActiveInput ? self.containerLayout.inputHeight : nil)
controller.containerLayoutUpdated(appliedLayout, transition: .immediate)
self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in
if let strongSelf = self {
let containerLayout = strongSelf.containerLayout.withUpdatedInputHeight(controller.hasActiveInput ? strongSelf.containerLayout.inputHeight : nil)
if containerLayout != appliedLayout {
controller.containerLayoutUpdated(containerLayout, transition: .immediate)
if let validLayout = self.validLayout {
let appliedLayout = validLayout.withUpdatedInputHeight(controller.hasActiveInput ? validLayout.inputHeight : nil)
controller.containerLayoutUpdated(appliedLayout, transition: .immediate)
self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in
if let strongSelf = self, let validLayout = strongSelf.validLayout {
let containerLayout = validLayout.withUpdatedInputHeight(controller.hasActiveInput ? validLayout.inputHeight : nil)
if containerLayout != appliedLayout {
controller.containerLayoutUpdated(containerLayout, transition: .immediate)
}
strongSelf.pushViewController(controller, animated: true)
}
strongSelf.pushViewController(controller, animated: true)
}
}))
}))
} else {
self.pushViewController(controller, animated: false)
}
}
open override func pushViewController(_ viewController: UIViewController, animated: Bool) {
@@ -247,7 +252,9 @@ open class NavigationController: UINavigationController, ContainableController,
public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise<Bool>? = nil) {
self.view.endEditing(true)
controller.containerLayoutUpdated(self.containerLayout, transition: .immediate)
if let validLayout = self.validLayout {
controller.containerLayoutUpdated(validLayout, transition: .immediate)
}
self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in
if let strongSelf = self {
ready?.set(true)
@@ -261,7 +268,9 @@ open class NavigationController: UINavigationController, ContainableController,
public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise<Bool>? = nil) {
self.view.endEditing(true)
controller.containerLayoutUpdated(self.containerLayout, transition: .immediate)
if let validLayout = self.validLayout {
controller.containerLayoutUpdated(validLayout, transition: .immediate)
}
self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in
if let strongSelf = self {
ready?.set(true)
@@ -323,15 +332,17 @@ open class NavigationController: UINavigationController, ContainableController,
let topViewController = viewControllers[viewControllers.count - 1] as UIViewController
if let controller = topViewController as? ContainableController {
var layoutToApply = self.containerLayout
var hasActiveInput = false
if let controller = controller as? ViewController {
hasActiveInput = controller.hasActiveInput
if let validLayout = self.validLayout {
var layoutToApply = validLayout
var hasActiveInput = false
if let controller = controller as? ViewController {
hasActiveInput = controller.hasActiveInput
}
if !hasActiveInput {
layoutToApply = layoutToApply.withUpdatedInputHeight(nil)
}
controller.containerLayoutUpdated(layoutToApply, transition: .immediate)
}
if !hasActiveInput {
layoutToApply = layoutToApply.withUpdatedInputHeight(nil)
}
controller.containerLayoutUpdated(layoutToApply, transition: .immediate)
} else {
topViewController.view.frame = CGRect(origin: CGPoint(), size: self.view.bounds.size)
}
@@ -452,7 +463,9 @@ open class NavigationController: UINavigationController, ContainableController,
self._presentedViewController = controller
self.view.endEditing(true)
controller.containerLayoutUpdated(self.containerLayout, transition: .immediate)
if let validLayout = self.validLayout {
controller.containerLayoutUpdated(validLayout, transition: .immediate)
}
var ready: Signal<Bool, Void> = .single(true)

View File

@@ -0,0 +1,81 @@
import Foundation
import AsyncDisplayKit
public final class PeekControllerTheme {
public let isDark: Bool
public let menuBackgroundColor: UIColor
public let menuItemHighligtedColor: UIColor
public let menuItemSeparatorColor: UIColor
public let accentColor: UIColor
public let destructiveColor: UIColor
public init(isDark: Bool, menuBackgroundColor: UIColor, menuItemHighligtedColor: UIColor, menuItemSeparatorColor: UIColor, accentColor: UIColor, destructiveColor: UIColor) {
self.isDark = isDark
self.menuBackgroundColor = menuBackgroundColor
self.menuItemHighligtedColor = menuItemHighligtedColor
self.menuItemSeparatorColor = menuItemSeparatorColor
self.accentColor = accentColor
self.destructiveColor = destructiveColor
}
}
public final class PeekController: ViewController {
private var controllerNode: PeekControllerNode {
return self.displayNode as! PeekControllerNode
}
private let theme: PeekControllerTheme
private let content: PeekControllerContent
private let sourceNode: () -> ASDisplayNode?
private var animatedIn = false
public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) {
self.theme = theme
self.content = content
self.sourceNode = sourceNode
super.init(navigationBarTheme: nil)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in
self?.dismiss()
})
self.displayNodeDidLoad()
}
private func getSourceRect() -> CGRect {
if let sourceNode = self.sourceNode() {
return sourceNode.view.convert(sourceNode.bounds, to: self.view)
} else {
let size = self.displayNode.bounds.size
return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0))
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn(from: self.getSourceRect())
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
}

View File

@@ -0,0 +1,23 @@
import Foundation
import AsyncDisplayKit
public enum PeekControllerContentPresentation {
case contained
case freeform
}
public enum PeerkControllerMenuActivation {
case drag
case press
}
public protocol PeekControllerContent {
func presentation() -> PeekControllerContentPresentation
func menuActivation() -> PeerkControllerMenuActivation
func menuItems() -> [PeekControllerMenuItem]
func node() -> PeekControllerContentNode & ASDisplayNode
}
public protocol PeekControllerContentNode {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
}

View File

@@ -0,0 +1,228 @@
import Foundation
import UIKit
import SwiftSignalKit
private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool {
if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating {
return true
}
for subview in view.subviews {
let subviewPoint = view.convert(point, to: subview)
if traceDeceleratingScrollView(subview, at: subviewPoint) {
return true
}
}
return false
}
public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer {
private let contentAtPoint: (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?
private let present: (PeekControllerContent, ASDisplayNode) -> PeekController?
private var tapLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var pressTimer: SwiftSignalKit.Timer?
private let candidateContentDisposable = MetaDisposable()
private var candidateContent: (ASDisplayNode, PeekControllerContent)?
private var menuActivation: PeerkControllerMenuActivation?
private weak var presentedController: PeekController?
public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> PeekController?) {
self.contentAtPoint = contentAtPoint
self.present = present
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
self.candidateContentDisposable.dispose()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func startPressTimer() {
self.pressTimer?.invalidate()
let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
self?.pressTimerFired()
}, queue: Queue.mainQueue())
self.pressTimer = pressTimer
pressTimer.start()
}
private func stopLongTapTimer() {
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func stopPressTimer() {
self.pressTimer?.invalidate()
self.pressTimer = nil
}
override public func reset() {
super.reset()
self.stopLongTapTimer()
self.stopPressTimer()
self.tapLocation = nil
self.candidateContent = nil
self.menuActivation = nil
self.presentedController = nil
}
private func longTapTimerFired() {
guard let _ = self.tapLocation, let (sourceNode, content) = self.candidateContent else {
return
}
self.state = .began
if let presentedController = self.present(content, sourceNode) {
self.menuActivation = content.menuActivation()
self.presentedController = presentedController
switch content.menuActivation() {
case .drag:
break
case .press:
if #available(iOSApplicationExtension 9.0, *) {
if presentedController.traitCollection.forceTouchCapability != .available {
self.startPressTimer()
}
} else {
self.startPressTimer()
}
}
}
}
private func pressTimerFired() {
if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation {
if let presentedController = self.presentedController {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.activateMenu()
}
self.menuActivation = nil
self.presentedController = nil
self.state = .ended
}
}
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let view = self.view, let tapLocation = touches.first?.location(in: view) {
if traceDeceleratingScrollView(view, at: tapLocation) {
self.candidateContent = nil
self.state = .failed
} else {
if let contentSignal = self.contentAtPoint(tapLocation) {
self.candidateContentDisposable.set((contentSignal |> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
switch strongSelf.state {
case .possible, .changed:
if let (sourceNode, content) = result {
strongSelf.tapLocation = tapLocation
strongSelf.candidateContent = (sourceNode, content)
strongSelf.menuActivation = content.menuActivation()
strongSelf.startLongTapTimer()
} else {
strongSelf.state = .failed
}
default:
break
}
}
}))
} else {
self.state = .failed
}
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
let velocity = self.velocity(in: self.view)
if let presentedController = self.presentedController, presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.endDraggingWithVelocity(velocity.y)
self.presentedController = nil
self.menuActivation = nil
}
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
if let presentedController = self.presentedController {
self.menuActivation = nil
self.presentedController = nil
presentedController.dismiss()
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first, let initialTapLocation = self.tapLocation, let menuActivation = self.menuActivation {
let touchLocation = touch.location(in: self.view)
if let presentedController = self.presentedController {
switch menuActivation {
case .drag:
var offset = touchLocation.y - initialTapLocation.y
let delta = abs(offset)
let factor: CGFloat = 60.0
offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0)
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset)
}
case .press:
if #available(iOSApplicationExtension 9.0, *) {
if touch.force >= 2.5 {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.activateMenu()
self.menuActivation = nil
self.presentedController = nil
self.state = .ended
}
}
}
break
}
} else {
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
import Foundation
import AsyncDisplayKit
public enum PeekControllerMenuItemColor {
case accent
case destructive
}
public struct PeekControllerMenuItem {
public let title: String
public let color: PeekControllerMenuItemColor
public let action: () -> Void
public init(title: String, color: PeekControllerMenuItemColor, action: @escaping () -> Void) {
self.title = title
self.color = color
self.action = action
}
}
final class PeekControllerMenuItemNode: HighlightTrackingButtonNode {
private let item: PeekControllerMenuItem
private let activatedAction: () -> Void
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let textNode: ASTextNode
init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) {
self.item = item
self.activatedAction = activatedAction
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.separatorNode.backgroundColor = theme.menuItemSeparatorColor
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor
self.highlightedBackgroundNode.alpha = 0.0
self.textNode = ASTextNode()
self.textNode.isLayerBacked = true
self.textNode.displaysAsynchronously = false
let textColor: UIColor
switch item.color {
case .accent:
textColor = theme.accentColor
case .destructive:
textColor = theme.destructiveColor
}
self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(20.0), textColor: textColor)
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.view.superview?.bringSubview(toFront: strongSelf.view)
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let height: CGFloat = 57.0
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel)))
let textSize = self.textNode.measure(CGSize(width: width - 10.0, height: height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((height - textSize.height) / 2.0)), size: textSize))
return height
}
@objc func buttonPressed() {
self.activatedAction()
self.item.action()
}
}

View File

@@ -0,0 +1,30 @@
import Foundation
import AsyncDisplayKit
final class PeekControllerMenuNode: ASDisplayNode {
private let itemNodes: [PeekControllerMenuItemNode]
init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) {
self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) }
super.init()
self.backgroundColor = theme.menuBackgroundColor
self.cornerRadius = 16.0
self.clipsToBounds = true
for itemNode in self.itemNodes {
self.addSubnode(itemNode)
}
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
var verticalOffset: CGFloat = 0.0
for itemNode in self.itemNodes {
let itemHeight = itemNode.updateLayout(width: width, transition: transition)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight)))
verticalOffset += itemHeight
}
return verticalOffset - UIScreenPixel
}
}

View File

@@ -0,0 +1,206 @@
import Foundation
import AsyncDisplayKit
final class PeekControllerNode: ViewControllerTracingNode {
private let requestDismiss: () -> Void
private let theme: PeekControllerTheme
private let blurView: UIView
private let dimNode: ASDisplayNode
private let containerBackgroundNode: ASImageNode
private let containerNode: ASDisplayNode
private var validLayout: ContainerViewLayout?
private var containerOffset: CGFloat = 0.0
private let content: PeekControllerContent
private let contentNode: PeekControllerContentNode & ASDisplayNode
private let menuNode: PeekControllerMenuNode?
private var displayingMenu = false
init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) {
self.theme = theme
self.requestDismiss = requestDismiss
self.dimNode = ASDisplayNode()
self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light))
self.blurView.isUserInteractionEnabled = false
switch content.menuActivation() {
case .drag:
self.dimNode.backgroundColor = nil
self.blurView.alpha = 1.0
case .press:
self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5)
self.blurView.alpha = 0.0
}
self.containerBackgroundNode = ASImageNode()
self.containerBackgroundNode.isLayerBacked = true
self.containerBackgroundNode.displayWithoutProcessing = true
self.containerBackgroundNode.displaysAsynchronously = false
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.containerNode.cornerRadius = 16.0
self.content = content
self.contentNode = content.node()
var activatedActionImpl: (() -> Void)?
let menuItems = content.menuItems()
if menuItems.isEmpty {
self.menuNode = nil
} else {
self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: {
activatedActionImpl?()
})
}
super.init()
self.addSubnode(self.dimNode)
self.view.addSubview(self.blurView)
self.containerNode.addSubnode(self.contentNode)
self.addSubnode(self.containerNode)
if let menuNode = self.menuNode {
self.addSubnode(menuNode)
}
activatedActionImpl = { [weak self] in
self?.requestDismiss()
}
}
deinit {
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size))
let layoutInsets = layout.insets(options: [])
let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0)
var menuSize: CGSize?
let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: transition)
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
var containerFrame: CGRect
switch self.content.presentation() {
case .contained:
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: self.containerOffset + floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
case .freeform:
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: self.containerOffset + floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize)
}
if let menuNode = self.menuNode {
let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0
let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition)
menuSize = CGSize(width: menuWidth, height: menuHeight)
if self.displayingMenu {
containerFrame.origin.y = min(containerFrame.origin.y, layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height)
transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0)
}
}
transition.updateFrame(node: self.containerNode, frame: containerFrame)
if let menuNode = self.menuNode, let menuSize = menuSize {
let menuY: CGFloat
if self.displayingMenu {
menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height)
} else {
menuY = layout.size.height + 14.0
}
transition.updateFrame(node: menuNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize))
}
}
func animateIn(from rect: CGRect) {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3)
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
func animateOut(to rect: CGRect, completion: @escaping () -> Void) {
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, force: true, completion: { _ in
completion()
})
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
if let menuNode = self.menuNode {
menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false)
}
}
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.requestDismiss()
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .changed:
break
case .cancelled, .ended:
break
default:
break
}
}
func applyDraggingOffset(_ offset: CGFloat) {
self.containerOffset = min(0.0, offset)
if self.containerOffset < -25.0 {
//self.displayingMenu = true
} else {
//self.displayingMenu = false
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
}
func activateMenu() {
if let layout = self.validLayout {
self.displayingMenu = true
self.containerOffset = 0.0
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring))
}
}
func endDraggingWithVelocity(_ velocity: CGFloat) {
if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 {
if let layout = self.validLayout {
self.displayingMenu = true
self.containerOffset = 0.0
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring))
}
} else {
self.requestDismiss()
}
}
}

View File

@@ -226,15 +226,15 @@ class StatusBarManager {
self.host.statusBarStyle = statusBarStyle
}
if let statusBarWindow = self.host.statusBarWindow {
statusBarWindow.alpha = globalStatusBar.1
statusBarView.alpha = globalStatusBar.1
var statusBarBounds = statusBarWindow.bounds
if !statusBarBounds.origin.y.isEqual(to: globalStatusBar.2) {
statusBarBounds.origin.y = globalStatusBar.2
statusBarWindow.bounds = statusBarBounds
}
}
} else if let statusBarWindow = self.host.statusBarWindow {
statusBarWindow.alpha = 0.0
} else {
statusBarView.alpha = 0.0
}
}
}

View File

@@ -34,6 +34,7 @@ public enum StatusBarStyle {
private enum StatusBarItemType {
case Generic
case Battery
case Activity
}
func makeStatusBarProxy(_ statusBarStyle: StatusBarStyle, statusBar: UIView) -> StatusBarProxyNode {
@@ -90,7 +91,22 @@ private class StatusBarItemNode: ASDisplayNode {
UIGraphicsPopContext()
}
}
let type: StatusBarItemType = self.targetView.checkIsKind(of: batteryItemClass!) ? .Battery : .Generic
//dumpViews(self.targetView)
var type: StatusBarItemType = self.targetView.checkIsKind(of: batteryItemClass!) ? .Battery : .Generic
if case .Generic = type {
var hasActivityBackground = false
var hasText = false
for subview in self.targetView.subviews {
if let stringClass = stringClass, subview.checkIsKind(of: stringClass) {
hasText = true
} else if let activityClass = activityClass, subview.checkIsKind(of: activityClass) {
hasActivityBackground = true
}
}
if hasActivityBackground && hasText {
type = .Activity
}
}
tintStatusBarItem(context, type: type, style: statusBarStyle)
self.contents = context.generateImage()?.cgImage
@@ -229,6 +245,8 @@ private func tintStatusBarItem(_ context: DrawingContext, type: StatusBarItemTyp
}
}
}
case .Activity:
break
case .Generic:
var pixel = context.bytes.assumingMemoryBound(to: UInt32.self)
let end = context.bytes.advanced(by: context.length).assumingMemoryBound(to: UInt32.self)
@@ -259,7 +277,15 @@ private func tintStatusBarItem(_ context: DrawingContext, type: StatusBarItemTyp
}
}
private let batteryItemClass: AnyClass? = { () -> AnyClass? in
private let foregroundClass: AnyClass? = {
var nameString = "StatusBar"
if CFAbsoluteTimeGetCurrent() > 0 {
nameString += "ForegroundView"
}
return NSClassFromString("_UI" + nameString)
}()
private let batteryItemClass: AnyClass? = {
var nameString = "StatusBarBattery"
if CFAbsoluteTimeGetCurrent() > 0 {
nameString += "ItemView"
@@ -267,6 +293,22 @@ private let batteryItemClass: AnyClass? = { () -> AnyClass? in
return NSClassFromString("UI" + nameString)
}()
private let activityClass: AnyClass? = {
var nameString = "StatusBarBackground"
if CFAbsoluteTimeGetCurrent() > 0 {
nameString += "ActivityView"
}
return NSClassFromString("_UI" + nameString)
}()
private let stringClass: AnyClass? = {
var nameString = "StatusBar"
if CFAbsoluteTimeGetCurrent() > 0 {
nameString += "StringView"
}
return NSClassFromString("_UI" + nameString)
}()
private class StatusBarProxyNodeTimerTarget: NSObject {
let action: () -> Void
@@ -327,7 +369,15 @@ class StatusBarProxyNode: ASDisplayNode {
self.clipsToBounds = true
//self.backgroundColor = UIColor.blueColor().colorWithAlphaComponent(0.2)
var rootView: UIView = statusBar
for subview in statusBar.subviews {
if let foregroundClass = foregroundClass, subview.checkIsKind(of: foregroundClass) {
rootView = subview
break
}
}
for subview in rootView.subviews {
let itemNode = StatusBarItemNode(statusBarStyle: statusBarStyle, targetView: subview)
self.itemNodes.append(itemNode)
self.addSubnode(itemNode)
@@ -343,10 +393,20 @@ class StatusBarProxyNode: ASDisplayNode {
private func updateItems() {
let statusBar = self.statusBar
var rootView: UIView = statusBar
for subview in statusBar.subviews {
if let foregroundClass = foregroundClass, subview.checkIsKind(of: foregroundClass) {
rootView = subview
break
}
}
//dumpViews(self.statusBar)
var i = 0
while i < self.itemNodes.count {
var found = false
for subview in statusBar.subviews {
for subview in rootView.subviews {
if self.itemNodes[i].targetView == subview {
found = true
break
@@ -362,7 +422,7 @@ class StatusBarProxyNode: ASDisplayNode {
}
}
for subview in statusBar.subviews {
for subview in rootView.subviews {
var found = false
for itemNode in self.itemNodes {
if itemNode.targetView == subview {

View File

@@ -26,7 +26,7 @@ public final class TabBarControllerTheme {
}
open class TabBarController: ViewController {
private var containerLayout = ContainerViewLayout()
private var validLayout: ContainerViewLayout?
private var tabBarControllerNode: TabBarControllerNode {
get {
@@ -88,10 +88,32 @@ open class TabBarController: ViewController {
}
}
private var debugTapCounter: (Double, Int) = (0.0, 0)
override open func loadDisplayNode() {
self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index in
if let strongSelf = self {
strongSelf.controllers[index].containerLayoutUpdated(strongSelf.containerLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate)
if strongSelf.selectedIndex == index {
let timestamp = CACurrentMediaTime()
if strongSelf.debugTapCounter.0 < timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 = 0
}
if strongSelf.debugTapCounter.0 >= timestamp - 0.4 {
strongSelf.debugTapCounter.0 = timestamp
strongSelf.debugTapCounter.1 += 1
}
if strongSelf.debugTapCounter.1 >= 10 {
strongSelf.debugTapCounter.1 = 0
strongSelf.controllers[index].tabBarItemDebugTapAction?()
}
}
if let validLayout = strongSelf.validLayout {
strongSelf.controllers[index].containerLayoutUpdated(validLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate)
}
strongSelf.pendingControllerDisposable.set((strongSelf.controllers[index].ready.get() |> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self {
strongSelf.selectedIndex = index
@@ -127,7 +149,9 @@ open class TabBarController: ViewController {
var displayNavigationBar = false
if let currentController = self.currentController {
currentController.willMove(toParentViewController: self)
currentController.containerLayoutUpdated(self.containerLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate)
if let validLayout = self.validLayout {
currentController.containerLayoutUpdated(validLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate)
}
self.tabBarControllerNode.currentControllerView = currentController.view
currentController.navigationBar?.isHidden = true
self.addChildViewController(currentController)
@@ -149,13 +173,15 @@ open class TabBarController: ViewController {
self.setDisplayNavigationBar(displayNavigationBar)
}
self.tabBarControllerNode.containerLayoutUpdated(self.containerLayout, transition: .immediate)
if let validLayout = self.validLayout {
self.tabBarControllerNode.containerLayoutUpdated(validLayout, transition: .immediate)
}
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.containerLayout = layout
self.validLayout = layout
self.tabBarControllerNode.containerLayoutUpdated(layout, transition: transition)

View File

@@ -197,6 +197,16 @@ class TabBarNode: ASDisplayNode {
self.addSubnode(self.separatorNode)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(TabBarTapRecognizer(tap: { [weak self] point in
if let strongSelf = self {
strongSelf.tapped(at: point)
}
}))
}
func updateTheme(_ theme: TabBarControllerTheme) {
if self.theme !== theme {
self.theme = theme
@@ -354,11 +364,8 @@ class TabBarNode: ASDisplayNode {
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first, let bottomInset = self.validLayout?.3 {
let location = touch.location(in: self.view)
private func tapped(at location: CGPoint) {
if let bottomInset = self.validLayout?.3 {
if location.y > self.bounds.size.height - bottomInset {
return
}

View File

@@ -0,0 +1,58 @@
import Foundation
import UIKit
final class TabBarTapRecognizer: UIGestureRecognizer {
private let tap: (CGPoint) -> Void
private var initialLocation: CGPoint?
init(tap: @escaping (CGPoint) -> Void) {
self.tap = tap
super.init(target: nil, action: nil)
}
override func reset() {
super.reset()
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.initialLocation == nil {
self.initialLocation = touches.first?.location(in: self.view)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if let initialLocation = self.initialLocation {
self.initialLocation = nil
self.tap(initialLocation)
self.state = .ended
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
let deltaX = initialLocation.x - location.x
let deltaY = initialLocation.y - location.y
if deltaX * deltaX + deltaY * deltaY > 4.0 {
self.initialLocation = nil
self.state = .failed
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.state = .failed
}
}

View File

@@ -134,7 +134,7 @@ public extension UIImage {
private func makeSubtreeSnapshot(layer: CALayer) -> UIView? {
let view = UIView()
//view.layer.isHidden = layer.isHidden
view.layer.isHidden = layer.isHidden
view.layer.opacity = layer.opacity
view.layer.contents = layer.contents
view.layer.contentsRect = layer.contentsRect
@@ -143,6 +143,7 @@ private func makeSubtreeSnapshot(layer: CALayer) -> UIView? {
view.layer.contentsGravity = layer.contentsGravity
view.layer.masksToBounds = layer.masksToBounds
view.layer.cornerRadius = layer.cornerRadius
view.layer.backgroundColor = layer.backgroundColor
if let sublayers = layer.sublayers {
for sublayer in sublayers {
let subtree = makeSubtreeSnapshot(layer: sublayer)
@@ -160,7 +161,7 @@ private func makeSubtreeSnapshot(layer: CALayer) -> UIView? {
private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
let view = CALayer()
//view.layer.isHidden = layer.isHidden
view.isHidden = layer.isHidden
view.opacity = layer.opacity
view.contents = layer.contents
view.contentsRect = layer.contentsRect
@@ -169,6 +170,7 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
view.contentsGravity = layer.contentsGravity
view.masksToBounds = layer.masksToBounds
view.cornerRadius = layer.cornerRadius
view.backgroundColor = layer.backgroundColor
if let sublayers = layer.sublayers {
for sublayer in sublayers {
let subtree = makeLayerSubtreeSnapshot(layer: sublayer)
@@ -185,24 +187,40 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
}
public extension UIView {
public func snapshotContentTree() -> UIView? {
if let snapshot = makeSubtreeSnapshot(layer: self.layer) {
public func snapshotContentTree(unhide: Bool = false) -> UIView? {
let wasHidden = self.isHidden
if unhide && wasHidden {
self.isHidden = false
}
let snapshot = makeSubtreeSnapshot(layer: self.layer)
if unhide && wasHidden {
self.isHidden = true
}
if let snapshot = snapshot {
snapshot.frame = self.frame
return snapshot
} else {
return nil
}
return nil
}
}
public extension CALayer {
public func snapshotContentTree() -> CALayer? {
if let snapshot = makeLayerSubtreeSnapshot(layer: self) {
public func snapshotContentTree(unhide: Bool = false) -> CALayer? {
let wasHidden = self.isHidden
if unhide && wasHidden {
self.isHidden = false
}
let snapshot = makeLayerSubtreeSnapshot(layer: self)
if unhide && wasHidden {
self.isHidden = true
}
if let snapshot = snapshot {
snapshot.frame = self.frame
return snapshot
} else {
return nil
}
return nil
}
}

View File

@@ -7,6 +7,7 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) {
@interface UIViewController (Navigation)
- (BOOL)isPresentedInPreviewingContext;
- (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations;
- (BOOL)ignoreAppearanceMethodInvocations;
- (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller;

View File

@@ -106,6 +106,10 @@ static bool notyfyingShiftState = false;
});
}
- (BOOL)isPresentedInPreviewingContext {
return ![self.presentingViewController isKindOfClass:[UIViewControllerPresentingProxy class]];
}
- (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations
{
[self setAssociatedObject:@(ignoreAppearanceMethodInvocations) forKey:UIViewControllerIgnoreAppearanceMethodInvocationsKey];

View File

@@ -30,7 +30,7 @@ open class ViewControllerPresentationArguments {
}
@objc open class ViewController: UIViewController, ContainableController {
private var containerLayout = ContainerViewLayout()
private var validLayout: ContainerViewLayout?
private let presentationContext: PresentationContext
public final var supportedOrientations: UIInterfaceOrientationMask = .all
@@ -60,6 +60,8 @@ open class ViewControllerPresentationArguments {
public private(set) var presentationArguments: Any?
public var tabBarItemDebugTapAction: (() -> Void)?
private var _displayNode: ASDisplayNode?
public final var displayNode: ASDisplayNode {
get {
@@ -86,6 +88,8 @@ open class ViewControllerPresentationArguments {
public let statusBar: StatusBar
public let navigationBar: NavigationBar?
private var previewingContext: Any?
public var displayNavigationBar = true
private weak var activeInputViewCandidate: UIResponder?
@@ -172,7 +176,7 @@ open class ViewControllerPresentationArguments {
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.containerLayout = layout
self.validLayout = layout
if !self.isViewLoaded {
self.loadView()
@@ -221,6 +225,7 @@ open class ViewControllerPresentationArguments {
self.displayNode.addSubnode(navigationBar)
}
}
self.view.autoresizingMask = []
self.view.addSubview(self.statusBar.view)
self.presentationContext.view = self.view
}
@@ -238,8 +243,8 @@ open class ViewControllerPresentationArguments {
}
public func requestLayout(transition: ContainedViewLayoutTransition) {
if self.isViewLoaded {
self.containerLayoutUpdated(self.containerLayout, transition: transition)
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
}
@@ -293,6 +298,11 @@ open class ViewControllerPresentationArguments {
}
}
public func presentInGlobalOverlay(_ controller: ViewController, with arguments: Any? = nil) {
controller.presentationArguments = arguments
self.window?.presentInGlobalOverlay(controller)
}
open override func viewWillDisappear(_ animated: Bool) {
self.activeInputViewCandidate = findCurrentResponder(self.view)
@@ -313,4 +323,27 @@ open class ViewControllerPresentationArguments {
open func dismiss(completion: (() -> Void)? = nil) {
}
@available(iOSApplicationExtension 9.0, *)
open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) {
if self.traitCollection.forceTouchCapability == .available {
let _ = super.registerForPreviewing(with: delegate, sourceView: sourceView)
} else if !onlyNative {
if self.previewingContext == nil {
let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a)
})
self.previewingContext = previewingContext
}
}
}
@available(iOSApplicationExtension 9.0, *)
open override func unregisterForPreviewing(withContext previewing: UIViewControllerPreviewing) {
if self.previewingContext != nil {
self.previewingContext = nil
} else {
super.unregisterForPreviewing(withContext: previewing)
}
}
}

View File

@@ -0,0 +1,118 @@
import Foundation
import UIKit
import SwiftSignalKit
@available(iOSApplicationExtension 9.0, *)
private final class ViewControllerPeekContent: PeekControllerContent {
private let controller: ViewController
private let menu: [PeekControllerMenuItem]
init(controller: ViewController) {
self.controller = controller
var menu: [PeekControllerMenuItem] = []
for item in controller.previewActionItems {
menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] in
if let controller = controller, let item = item as? UIPreviewAction {
item.handler(item, controller)
}
}))
}
self.menu = menu
}
func presentation() -> PeekControllerContentPresentation {
return .contained
}
func menuActivation() -> PeerkControllerMenuActivation {
return .drag
}
func menuItems() -> [PeekControllerMenuItem] {
return self.menu
}
func node() -> PeekControllerContentNode & ASDisplayNode {
return ViewControllerPeekContentNode(controller: self.controller)
}
}
private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode {
private let controller: ViewController
private var hasValidLayout = false
init(controller: ViewController) {
self.controller = controller
super.init()
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
if !self.hasValidLayout {
self.hasValidLayout = true
self.controller.view.frame = CGRect(origin: CGPoint(), size: size)
self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate)
self.controller.setIgnoreAppearanceMethodInvocations(true)
self.view.addSubview(self.controller.view)
self.controller.setIgnoreAppearanceMethodInvocations(false)
self.controller.viewWillAppear(false)
self.controller.viewDidAppear(false)
} else {
self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: transition)
}
return size
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return self.view
}
return nil
}
}
@available(iOSApplicationExtension 9.0, *)
final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing {
weak var delegateImpl: UIViewControllerPreviewingDelegate?
var delegate: UIViewControllerPreviewingDelegate {
return self.delegateImpl!
}
let recognizer: PeekControllerGestureRecognizer
var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer {
return self.recognizer
}
let sourceView: UIView
let node: ASDisplayNode
var sourceRect: CGRect = CGRect()
init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void) {
self.delegateImpl = delegate
self.sourceView = sourceView
self.node = node
var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)?
self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in
return contentAtPointImpl?(point)
}, present: { content, sourceNode in
let controller = PeekController(theme: theme, content: content, sourceNode: {
return sourceNode
})
present(controller, nil)
return controller
})
node.view.addGestureRecognizer(self.recognizer)
super.init()
contentAtPointImpl = { [weak self] point in
if let strongSelf = self, let delegate = strongSelf.delegateImpl {
if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController {
return .single((strongSelf.node, ViewControllerPeekContent(controller: controller)))
}
}
return nil
}
}
}

View File

@@ -162,7 +162,26 @@ private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> Container
resolvedSafeInsets.right = 44.0
}
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, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil)
var standardInputHeight: CGFloat = 216.0
var predictiveHeight: CGFloat = 42.0
if layout.size.width.isEqual(to: 320.0) || layout.size.width.isEqual(to: 375.0) {
standardInputHeight = 216.0
predictiveHeight = 42.0
} else if layout.size.width.isEqual(to: 414.0) {
standardInputHeight = 226.0
predictiveHeight = 42.0
} else if layout.size.width.isEqual(to: 480.0) || layout.size.width.isEqual(to: 568.0) || layout.size.width.isEqual(to: 667.0) || layout.size.width.isEqual(to: 736.0) {
standardInputHeight = 162.0
predictiveHeight = 38.0
} else if layout.size.width.isEqual(to: 768.0) || layout.size.width.isEqual(to: 1024.0) {
standardInputHeight = 264.0
predictiveHeight = 42.0
}
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)
}
private func encodeText(_ string: String, _ key: Int) -> String {
@@ -250,6 +269,7 @@ public final class WindowHostView {
let updatePreferNavigationUIHidden: (Bool) -> Void
var present: ((ViewController, PresentationSurfaceLevel) -> Void)?
var presentInGlobalOverlay: ((_ controller: ViewController) -> Void)?
var presentNative: ((UIViewController) -> Void)?
var updateSize: ((CGSize) -> Void)?
var layoutSubviews: (() -> Void)?
@@ -276,6 +296,7 @@ public struct WindowTracingTags {
public protocol WindowHost {
func present(_ controller: ViewController, on level: PresentationSurfaceLevel)
func presentInGlobalOverlay(_ controller: ViewController)
func invalidateDeferScreenEdgeGestures()
func invalidatePreferNavigationUIHidden()
func cancelInteractiveKeyboardGestures()
@@ -324,6 +345,7 @@ public class Window1 {
private var cachedHasPreview: Bool = false
private let presentationContext: PresentationContext
private let overlayPresentationContext: GlobalOverlayPresentationContext
private var tracingStatusBarsInvalidated = false
private var shouldUpdateDeferScreenEdgeGestures = false
@@ -372,11 +394,16 @@ public class Window1 {
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsetsForScreenSize(boundsSize), onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil)
self.presentationContext = PresentationContext()
self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost)
self.hostView.present = { [weak self] controller, level in
self?.present(controller, on: level)
}
self.hostView.presentInGlobalOverlay = { [weak self] controller in
self?.presentInGlobalOverlay(controller)
}
self.hostView.presentNative = { [weak self] controller in
self?.presentNative(controller)
}
@@ -415,6 +442,7 @@ public class Window1 {
self.presentationContext.view = self.hostView.view
self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate)
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), 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 {
@@ -570,6 +598,10 @@ public class Window1 {
}
}
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
@@ -802,6 +834,7 @@ public class Window1 {
if childLayoutUpdated {
self._rootController?.containerLayoutUpdated(childLayout, transition: updatingLayout.transition)
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)
@@ -833,6 +866,10 @@ public class Window1 {
self.presentationContext.present(controller, on: level)
}
public func presentInGlobalOverlay(_ controller: ViewController) {
self.overlayPresentationContext.present(controller)
}
public func presentNative(_ controller: UIViewController) {
}