diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index 8ff689f0b6..7007bb8f87 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ D02383861DE0E3B4004018B6 /* ListViewIntermediateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */; }; D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */; }; D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; }; + D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */; }; D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.swift */; }; D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */; }; D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */; }; @@ -116,6 +117,7 @@ D0E49C881B83A3580099E553 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E49C871B83A3580099E553 /* ImageCache.swift */; }; D0F1132F1D6F3C20008C3597 /* ContextMenuActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F1132E1D6F3C20008C3597 /* ContextMenuActionNode.swift */; }; D0F7AB371DCFF6F8009AD9A1 /* ListViewItemHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */; }; + D0FF9B301E7196F6000C66DB /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -145,6 +147,7 @@ D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewIntermediateState.swift; sourceTree = ""; }; D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuContainerNode.swift; sourceTree = ""; }; D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeUtils.swift; sourceTree = ""; }; + D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimizeKeyboardGestureRecognizer.swift; sourceTree = ""; }; D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = ""; }; D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuAction.swift; sourceTree = ""; }; D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuController.swift; sourceTree = ""; }; @@ -242,6 +245,7 @@ D0E49C871B83A3580099E553 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; D0F1132E1D6F3C20008C3597 /* ContextMenuActionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuActionNode.swift; sourceTree = ""; }; D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewItemHeader.swift; sourceTree = ""; }; + D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -397,6 +401,7 @@ D03BCCE91C72AE4B0097A291 /* Theme */, D05CC3001B6955D500E235A3 /* Utils */, D07921AA1B6FC911005C23D9 /* Status Bar */, + D0FF9B2E1E7196E2000C66DB /* Keyboard */, D05CC3211B695AA600E235A3 /* Navigation */, D0DC48521BF93D7C00F672FD /* Tabs */, D02BDAEC1B6A7053008AFAD2 /* Nodes */, @@ -584,6 +589,15 @@ name = "Image Cache"; sourceTree = ""; }; + D0FF9B2E1E7196E2000C66DB /* Keyboard */ = { + isa = PBXGroup; + children = ( + D0FF9B2F1E7196F6000C66DB /* KeyboardManager.swift */, + D036574A1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift */, + ); + name = Keyboard; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -770,6 +784,7 @@ D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */, D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */, D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */, + D0FF9B301E7196F6000C66DB /* KeyboardManager.swift in Sources */, D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */, D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */, D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */, @@ -784,6 +799,7 @@ D05BE4AE1D217F6B002BD72C /* MergedLayoutEvents.swift in Sources */, D0C0D2901C997110001D2851 /* FBAnimationPerformanceTracker.mm in Sources */, D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */, + D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */, D05CC2FE1B6955D000E235A3 /* UIWindow+OrientationChange.m in Sources */, D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */, D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */, diff --git a/Display/ActionSheetItemGroupNode.swift b/Display/ActionSheetItemGroupNode.swift index 9ac3c0230c..685e9bafa5 100644 --- a/Display/ActionSheetItemGroupNode.swift +++ b/Display/ActionSheetItemGroupNode.swift @@ -145,7 +145,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { if !self.scrollView.contentSize.equalTo(scrollViewContentSize) { self.scrollView.contentSize = scrollViewContentSize } - var scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0) + let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0) if !UIEdgeInsetsEqualToEdgeInsets(self.scrollView.contentInset, scrollViewContentInsets) { self.scrollView.contentInset = scrollViewContentInsets diff --git a/Display/AlertController.swift b/Display/AlertController.swift index 2e2ff29541..5e37e021c6 100644 --- a/Display/AlertController.swift +++ b/Display/AlertController.swift @@ -45,7 +45,7 @@ open class AlertController: ViewController { self.controllerNode.containerLayoutUpdated(layout, transition: transition) } - public func dismiss() { + override open func dismiss() { self.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 93c89b6d99..0a54ce4bd9 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -207,8 +207,8 @@ public extension CALayer { self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } - public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut) { - self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, additive: true) + public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) { @@ -251,4 +251,13 @@ public extension CALayer { partialCompletion() }) } + + public func cancelAnimationsRecursive(key: String) { + self.removeAnimation(forKey: key) + if let sublayers = self.sublayers { + for layer in sublayers { + layer.cancelAnimationsRecursive(key: key) + } + } + } } diff --git a/Display/CATracingLayer.h b/Display/CATracingLayer.h index f07f9b9aa2..0ca59876d3 100644 --- a/Display/CATracingLayer.h +++ b/Display/CATracingLayer.h @@ -4,6 +4,16 @@ @end +@interface CATracingLayerInfo : NSObject + +@property (nonatomic, readonly) bool shouldBeAdjustedToInverseTransform; +@property (nonatomic, weak, readonly) id _Nullable userData; +@property (nonatomic, readonly) int32_t tracingTag; + +- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag; + +@end + @interface UITracingLayerView : UIView - (void)scheduleWithLayout:(void (^_Nonnull)())block; @@ -12,15 +22,18 @@ @interface CALayer (Tracing) -- (id _Nullable)traceableInfo; -- (void)setTraceableInfo:(id _Nullable)info; +- (CATracingLayerInfo * _Nullable)traceableInfo; +- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info; - (bool)hasPositionOrOpacityAnimations; +- (bool)hasPositionAnimations; - (void)setInvalidateTracingSublayers:(void (^_Nullable)())block; -- (NSArray *> * _Nonnull)traceableLayerSurfaces; +- (NSArray *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag; - (void)adjustTraceableLayerTransforms:(CGSize)offset; +- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget; + - (void)invalidateUpTheTree; @end diff --git a/Display/CATracingLayer.m b/Display/CATracingLayer.m index 554655d57f..6bbc6fbd6e 100644 --- a/Display/CATracingLayer.m +++ b/Display/CATracingLayer.m @@ -4,7 +4,8 @@ static void *CATracingLayerInvalidatedKey = &CATracingLayerInvalidatedKey; static void *CATracingLayerIsInvalidatedBlock = &CATracingLayerIsInvalidatedBlock; -static void *CATracingLayerTraceablInfoKey = &CATracingLayerTraceablInfoKey; +static void *CATracingLayerTraceableInfoKey = &CATracingLayerTraceableInfoKey; +static void *CATracingLayerPositionAnimationMirrorTarget = &CATracingLayerPositionAnimationMirrorTarget; @implementation CALayer (Tracing) @@ -17,25 +18,30 @@ static void *CATracingLayerTraceablInfoKey = &CATracingLayerTraceablInfoKey; } - (bool)isTraceable { - return [self associatedObjectForKey:CATracingLayerTraceablInfoKey] != nil || [self isKindOfClass:[CATracingLayer class]]; + return [self associatedObjectForKey:CATracingLayerTraceableInfoKey] != nil || [self isKindOfClass:[CATracingLayer class]]; } -- (id _Nullable)traceableInfo { - return [self associatedObjectForKey:CATracingLayerTraceablInfoKey]; +- (CATracingLayerInfo * _Nullable)traceableInfo { + return [self associatedObjectForKey:CATracingLayerTraceableInfoKey]; } -- (void)setTraceableInfo:(id _Nullable)info { - [self setAssociatedObject:info forKey:CATracingLayerTraceablInfoKey]; +- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info { + [self setAssociatedObject:info forKey:CATracingLayerTraceableInfoKey]; } - (bool)hasPositionOrOpacityAnimations { return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil || [self animationForKey:@"sublayerTransform"] != nil || [self animationForKey:@"opacity"] != nil; } -static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDictionary *> *layersByDepth) { +- (bool)hasPositionAnimations { + return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil; +} + +static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull layer, NSMutableDictionary *> *layersByDepth) { bool hadTraceableSublayers = false; for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) { - if ([sublayer traceableInfo] != nil) { + CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo]; + if (sublayerTraceableInfo != nil && sublayerTraceableInfo.tracingTag == tracingTag) { NSMutableArray *array = layersByDepth[@(depth)]; if (array == nil) { array = [[NSMutableArray alloc] init]; @@ -49,16 +55,16 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic if (!hadTraceableSublayers) { for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) { if ([sublayer isKindOfClass:[CATracingLayer class]]) { - traceLayerSurfaces(depth + 1, sublayer, layersByDepth); + traceLayerSurfaces(tracingTag, depth + 1, sublayer, layersByDepth); } } } } -- (NSArray *> *)traceableLayerSurfaces { +- (NSArray *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag { NSMutableDictionary *> *layersByDepth = [[NSMutableDictionary alloc] init]; - traceLayerSurfaces(0, self, layersByDepth); + traceLayerSurfaces(tracingTag, 0, self, layersByDepth); NSMutableArray *> *result = [[NSMutableArray alloc] init]; @@ -73,7 +79,8 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic CGRect frame = self.frame; CGSize sublayerOffset = CGSizeMake(frame.origin.x + offset.width, frame.origin.y + offset.height); for (CALayer *sublayer in self.sublayers) { - if ([sublayer traceableInfo] != nil) { + CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo]; + if (sublayerTraceableInfo != nil && sublayerTraceableInfo.shouldBeAdjustedToInverseTransform) { sublayer.sublayerTransform = CATransform3DMakeTranslation(-sublayerOffset.width, -sublayerOffset.height, 0.0f); } else if ([sublayer isKindOfClass:[CATracingLayer class]]) { [(CATracingLayer *)sublayer adjustTraceableLayerTransforms:sublayerOffset]; @@ -81,6 +88,14 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic } } +- (CALayer * _Nullable)animationMirrorTarget { + return [self associatedObjectForKey:CATracingLayerPositionAnimationMirrorTarget]; +} + +- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget { + [self setAssociatedObject:animationMirrorTarget forKey:CATracingLayerPositionAnimationMirrorTarget associationPolicy:NSObjectAssociationPolicyRetain]; +} + - (void)invalidateUpTheTree { CALayer *superlayer = self; while (true) { @@ -235,9 +250,21 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic [super addAnimation:anim forKey:key]; + CABasicAnimation *positionAnimCopy = [animCopy copy]; + positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-to.x + from.x, 0.0, 0.0f)]; + positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; + positionAnimCopy.additive = true; + positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ + __strong CATracingLayer *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf invalidateUpTheTree]; + } + }]; + [self invalidateUpTheTree]; [self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"]; + [self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"]; } else if ([key isEqualToString:@"opacity"]) { __weak CATracingLayer *weakSelf = self; anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{ @@ -258,11 +285,25 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic } } +- (void)mirrorPositionAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key { + if ([animation isKindOfClass:[CABasicAnimation class]]) { + if ([((CABasicAnimation *)animation).keyPath isEqualToString:@"sublayerTransform"]) { + CALayer *positionAnimationMirrorTarget = [self animationMirrorTarget]; + if (positionAnimationMirrorTarget != nil) { + [positionAnimationMirrorTarget addAnimation:[animation copy] forKey:key]; + } + } + } +} + - (void)mirrorAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key { for (CALayer *sublayer in self.sublayers) { - if ([sublayer traceableInfo] != nil) { + CATracingLayerInfo *traceableInfo = [sublayer traceableInfo]; + if (traceableInfo != nil && traceableInfo.shouldBeAdjustedToInverseTransform) { [sublayer addAnimation:[animation copy] forKey:key]; - } else if ([sublayer isKindOfClass:[CATracingLayer class]]) { + } + + if ([sublayer isKindOfClass:[CATracingLayer class]]) { [(CATracingLayer *)sublayer mirrorAnimationDownTheTree:animation key:key]; } } @@ -270,6 +311,20 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic @end +@implementation CATracingLayerInfo + +- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag { + self = [super init]; + if (self != nil) { + _shouldBeAdjustedToInverseTransform = shouldBeAdjustedToInverseTransform; + _userData = userData; + _tracingTag = tracingTag; + } + return self; +} + +@end + @interface UITracingLayerView () { void (^_scheduledWithLayout)(); } diff --git a/Display/ContainerViewLayout.swift b/Display/ContainerViewLayout.swift index 74a06eda9b..134a038ed8 100644 --- a/Display/ContainerViewLayout.swift +++ b/Display/ContainerViewLayout.swift @@ -48,6 +48,10 @@ public struct ContainerViewLayout: Equatable { public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout { return ContainerViewLayout(size: self.size, 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), statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight) } + + public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, intrinsicInsets: self.intrinsicInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight) + } } public func ==(lhs: ContainerViewLayout, rhs: ContainerViewLayout) -> Bool { diff --git a/Display/ContextMenuNode.swift b/Display/ContextMenuNode.swift index 8f8aabcbad..06e8df983a 100644 --- a/Display/ContextMenuNode.swift +++ b/Display/ContextMenuNode.swift @@ -27,7 +27,7 @@ final class ContextMenuNode: ASDisplayNode { super.init() self.addSubnode(self.containerNode) - let dismissNode = { [weak self] in + let dismissNode = { dismiss() } for actionNode in self.actionNodes { diff --git a/Display/Font.swift b/Display/Font.swift index 25ab867276..d118f534af 100644 --- a/Display/Font.swift +++ b/Display/Font.swift @@ -16,7 +16,7 @@ public struct Font { public static func bold(_ size: CGFloat) -> UIFont { if #available(iOS 8.2, *) { - return UIFont.systemFont(ofSize: size, weight: UIFontWeightBold) + return UIFont.boldSystemFont(ofSize: size) } else { return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil) } diff --git a/Display/GenerateImage.swift b/Display/GenerateImage.swift index 6119a70611..2e0a7e3cb1 100644 --- a/Display/GenerateImage.swift +++ b/Display/GenerateImage.swift @@ -114,6 +114,25 @@ public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, backgr }) } +public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let color = color { + context.setStrokeColor(color.cgColor) + } else { + context.setStrokeColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.setLineWidth(lineWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth))) + }) +} + public func generateStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { let intRadius = Int(radius) let cap = intRadius == 1 ? 2 : intRadius @@ -283,7 +302,7 @@ public class DrawingContext { } public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) { - if abs(other.scale - self.scale) < CGFloat(FLT_EPSILON) { + if abs(other.scale - self.scale) < CGFloat.ulpOfOne { let srcX = 0 var srcY = 0 let dstX = Int(at.x * self.scale) diff --git a/Display/GridNode.swift b/Display/GridNode.swift index 057b1454fb..0d8adc8e6d 100644 --- a/Display/GridNode.swift +++ b/Display/GridNode.swift @@ -35,13 +35,15 @@ public struct GridNodeScrollToItem { public let transition: ContainedViewLayoutTransition public let directionHint: GridNodePreviousItemsTransitionDirectionHint public let adjustForSection: Bool + public let adjustForTopInset: Bool - public init(index: Int, position: GridNodeScrollToItemPosition, transition: ContainedViewLayoutTransition, directionHint: GridNodePreviousItemsTransitionDirectionHint, adjustForSection: Bool) { + public init(index: Int, position: GridNodeScrollToItemPosition, transition: ContainedViewLayoutTransition, directionHint: GridNodePreviousItemsTransitionDirectionHint, adjustForSection: Bool, adjustForTopInset: Bool = false) { self.index = index self.position = position self.transition = transition self.directionHint = directionHint self.adjustForSection = adjustForSection + self.adjustForTopInset = adjustForTopInset } } @@ -156,6 +158,12 @@ private struct GridNodePresentationLayoutTransition { let transition: ContainedViewLayoutTransition } +public struct GridNodeCurrentPresentationLayout { + public let layout: GridNodeLayout + public let contentOffset: CGPoint + public let contentSize: CGSize +} + private final class GridNodeItemLayout { let contentSize: CGSize let items: [GridNodePresentationItem] @@ -225,6 +233,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { private var applyingContentOffset = false public var visibleItemsUpdated: ((GridNodeVisibleItems) -> Void)? + public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)? public override init() { super.init() @@ -241,6 +250,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) { if transaction.deleteItems.isEmpty && transaction.insertItems.isEmpty && transaction.scrollToItem == nil && transaction.updateItems.isEmpty && (transaction.updateLayout == nil || transaction.updateLayout!.layout == self.gridLayout && (transaction.updateFirstIndexInSectionOffset == nil || transaction.updateFirstIndexInSectionOffset == self.firstIndexInSectionOffset)) { + if let presentationLayoutUpdated = self.presentationLayoutUpdated { + presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: self.gridLayout, contentOffset: self.scrollView.contentOffset, contentSize: self.itemLayout.contentSize), .immediate) + } completion(self.displayedItemRange()) return } @@ -249,7 +261,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.firstIndexInSectionOffset = updateFirstIndexInSectionOffset } + var layoutTransactionOffset: CGFloat = 0.0 if let updateLayout = transaction.updateLayout { + layoutTransactionOffset += updateLayout.layout.insets.top - self.gridLayout.insets.top self.gridLayout = updateLayout.layout } @@ -316,7 +330,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.itemNodes = remappedInsertionItemNodes } - var previousLayoutWasEmpty = self.itemLayout.items.isEmpty + let previousLayoutWasEmpty = self.itemLayout.items.isEmpty self.itemLayout = self.generateItemLayout() @@ -324,19 +338,19 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { if let scrollToItem = transaction.scrollToItem { generatedScrollToItem = scrollToItem } else if previousLayoutWasEmpty { - generatedScrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .immediate, directionHint: .up, adjustForSection: true) + generatedScrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .immediate, directionHint: .up, adjustForSection: true, adjustForTopInset: true) } else { generatedScrollToItem = nil } - self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, scrollToItem: generatedScrollToItem), removedNodes: removedNodes) + self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: transaction.updateLayout?.transition) completion(self.displayedItemRange()) } public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.applyingContentOffset { - self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(), removedNodes: []) + self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(layoutTransactionOffset: 0.0), removedNodes: [], updateLayoutTransition: nil) } } @@ -427,7 +441,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { } } - private func generatePresentationLayoutTransition(stationaryItems: GridNodeStationaryItems = .none, scrollToItem: GridNodeScrollToItem? = nil) -> GridNodePresentationLayoutTransition { + private func generatePresentationLayoutTransition(stationaryItems: GridNodeStationaryItems = .none, layoutTransactionOffset: CGFloat, scrollToItem: GridNodeScrollToItem? = nil) -> GridNodePresentationLayoutTransition { if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) && !self.itemLayout.items.isEmpty { var transitionDirectionHint: GridNodePreviousItemsTransitionDirectionHint = .up var transition: ContainedViewLayoutTransition = .immediate @@ -438,7 +452,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { let itemFrame = self.itemLayout.items[scrollToItem.index] var additionalOffset: CGFloat = 0.0 - if scrollToItem.adjustForSection { + if scrollToItem.adjustForTopInset { + additionalOffset = -gridLayout.insets.top + } else if scrollToItem.adjustForSection { var adjustForSection: GridSection? if scrollToItem.index == 0 { if let itemSection = self.items[scrollToItem.index].section { @@ -485,7 +501,18 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { contentOffset = CGPoint(x: 0.0, y: verticalOffset) } else { - contentOffset = self.scrollView.contentOffset + if !layoutTransactionOffset.isZero { + var verticalOffset = self.scrollView.contentOffset.y - layoutTransactionOffset + if verticalOffset > self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height { + verticalOffset = self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height + } + if verticalOffset < -self.gridLayout.insets.top { + verticalOffset = -self.gridLayout.insets.top + } + contentOffset = CGPoint(x: 0.0, y: verticalOffset) + } else { + contentOffset = self.scrollView.contentOffset + } } case let .indices(stationaryItemIndices): var selectedContentOffset: CGPoint? @@ -548,7 +575,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { } } - private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode]) { + private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?) { var previousItemFrames: ([WrappedGridItemNode: CGRect])? switch presentationLayoutTransition.transition { case .animated: @@ -783,6 +810,10 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { visibleItemsUpdated(GridNodeVisibleItems(top: nil, bottom: nil, topVisible: nil, bottomVisible: nil, topSectionVisible: nil, count: self.items.count)) } } + + if let presentationLayoutUpdated = self.presentationLayoutUpdated { + presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: presentationLayoutTransition.layout.layout, contentOffset: presentationLayoutTransition.layout.contentOffset, contentSize: presentationLayoutTransition.layout.contentSize), updateLayoutTransition ?? presentationLayoutTransition.transition) + } } private func addItemNode(index: Int, itemNode: GridItemNode) { @@ -817,9 +848,28 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { } } - public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) { + public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { for (_, node) in self.itemNodes { f(node) } } + + public func forEachRow(_ f: ([ASDisplayNode]) -> Void) { + var row: [ASDisplayNode] = [] + var previousMinY: CGFloat? + for index in self.itemNodes.keys.sorted() { + let itemNode = self.itemNodes[index]! + if let previousMinY = previousMinY, !previousMinY.isEqual(to: itemNode.frame.minY) { + if !row.isEmpty { + f(row) + row.removeAll() + } + } + previousMinY = itemNode.frame.minY + row.append(itemNode) + } + if !row.isEmpty { + f(row) + } + } } diff --git a/Display/KeyboardManager.swift b/Display/KeyboardManager.swift new file mode 100644 index 0000000000..6abee24933 --- /dev/null +++ b/Display/KeyboardManager.swift @@ -0,0 +1,147 @@ +import Foundation +import AsyncDisplayKit + +struct KeyboardSurface { + let host: UIView +} + +private func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } else { + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false + } +} + +private func findKeyboardBackdrop(_ view: UIView) -> UIView? { + if NSStringFromClass(type(of: view)) == "UIKBInputBackdropView" { + return view + } + for subview in view.subviews { + if let result = findKeyboardBackdrop(subview) { + return result + } + } + return nil +} + +class KeyboardManager { + private let host: StatusBarHost + + private weak var previousPositionAnimationMirrorSource: CATracingLayer? + private weak var previousFirstResponderView: UIView? + + var gestureRecognizer: MinimizeKeyboardGestureRecognizer? = nil + + var minimized: Bool = false + var minimizedUpdated: (() -> Void)? + + var updatedMinimizedBackdrop = false + + var surfaces: [KeyboardSurface] = [] { + didSet { + self.updateSurfaces(oldValue) + } + } + + init(host: StatusBarHost) { + self.host = host + } + + private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) { + guard let keyboardWindow = self.host.keyboardWindow else { + return + } + + if let keyboardView = self.host.keyboardView { + if self.minimized { + let normalizedHeight = floor(0.85 * keyboardView.frame.size.height) + let factor = normalizedHeight / keyboardView.frame.size.height + let scaleTransform = CATransform3DMakeScale(factor, factor, 1.0) + let horizontalOffset = (keyboardView.frame.size.width - keyboardView.frame.size.width * factor) / 2.0 + let verticalOffset = (keyboardView.frame.size.height - keyboardView.frame.size.height * factor) / 2.0 + let translate = CATransform3DMakeTranslation(horizontalOffset, verticalOffset, 0.0) + keyboardView.layer.sublayerTransform = CATransform3DConcat(scaleTransform, translate) + + self.updatedMinimizedBackdrop = false + + if let backdrop = findKeyboardBackdrop(keyboardView) { + let scale = CATransform3DMakeScale(1.0 / factor, 1.0, 0.0) + let translate = CATransform3DMakeTranslation(-horizontalOffset * (1.0 / factor), 0.0, 0.0) + backdrop.layer.sublayerTransform = CATransform3DConcat(scale, translate) + } + } else { + keyboardView.layer.sublayerTransform = CATransform3DIdentity + if !self.updatedMinimizedBackdrop { + if let backdrop = findKeyboardBackdrop(keyboardView) { + backdrop.layer.sublayerTransform = CATransform3DIdentity + } + + self.updatedMinimizedBackdrop = true + } + } + } + + if let gestureRecognizer = self.gestureRecognizer { + if keyboardWindow.gestureRecognizers == nil || !keyboardWindow.gestureRecognizers!.contains(gestureRecognizer) { + keyboardWindow.addGestureRecognizer(gestureRecognizer) + } + } else { + let gestureRecognizer = MinimizeKeyboardGestureRecognizer(target: self, action: #selector(self.minimizeGesture(_:))) + self.gestureRecognizer = gestureRecognizer + keyboardWindow.addGestureRecognizer(gestureRecognizer) + } + + var firstResponderView: UIView? + for surface in surfaces { + if hasFirstResponder(surface.host) { + firstResponderView = surface.host + break + } + } + + if let firstResponderView = firstResponderView { + let containerOrigin = firstResponderView.convert(CGPoint(), to: nil) + let horizontalTranslation = CATransform3DMakeTranslation(containerOrigin.x, 0.0, 0.0) + keyboardWindow.layer.sublayerTransform = horizontalTranslation + if let tracingLayer = firstResponderView.layer as? CATracingLayer { + if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + } + tracingLayer.setPositionAnimationMirrorTarget(keyboardWindow.layer) + self.previousPositionAnimationMirrorSource = tracingLayer + } else if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + self.previousPositionAnimationMirrorSource = nil + } + } else { + keyboardWindow.layer.sublayerTransform = CATransform3DIdentity + if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource { + previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) + self.previousPositionAnimationMirrorSource = nil + } + if let previousFirstResponderView = previousFirstResponderView { + if previousFirstResponderView.window == nil { + keyboardWindow.isHidden = true + keyboardWindow.layer.cancelAnimationsRecursive(key: "position") + keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds") + keyboardWindow.isHidden = false + } + } + } + + self.previousFirstResponderView = firstResponderView + } + + @objc func minimizeGesture(_ recognizer: UISwipeGestureRecognizer) { + if case .ended = recognizer.state { + self.minimized = !self.minimized + self.minimizedUpdated?() + } + } +} diff --git a/Display/LegacyPresentedController.swift b/Display/LegacyPresentedController.swift index f2c4b1b798..eec8e3ada7 100644 --- a/Display/LegacyPresentedController.swift +++ b/Display/LegacyPresentedController.swift @@ -126,7 +126,7 @@ open class LegacyPresentedController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } - public func dismiss() { + override open func dismiss() { switch self.presentation { case .modal: self.controllerNode.animateModalOut { [weak self] in diff --git a/Display/ListView.swift b/Display/ListView.swift index cd32b3223f..2b48c1b71b 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -118,6 +118,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel public final var stackFromBottomInsetItemFactor: CGFloat = 0.0 public final var limitHitTestToNodes: Bool = false public final var keepBottomItemOverscrollBackground: Bool = false + public final var snapToBottomInsetUntilFirstInteraction: Bool = false private var bottomItemOverscrollBackground: ASDisplayNode? @@ -359,6 +360,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.resetHeaderItemsFlashTimer(start: false) self.updateHeaderItemsFlashing(animated: true) + if self.snapToBottomInsetUntilFirstInteraction { + self.snapToBottomInsetUntilFirstInteraction = false + } + /*if usePerformanceTracker { self.performanceTracker.start() }*/ @@ -640,7 +645,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } - if abs(offset) > CGFloat(FLT_EPSILON) { + if abs(offset) > CGFloat.ulpOfOne { for itemNode in self.itemNodes { var frame = itemNode.frame frame.origin.y += offset @@ -914,7 +919,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel let widthUpdated: Bool if let updateSizeAndInsets = updateSizeAndInsets { - widthUpdated = abs(state.visibleSize.width - updateSizeAndInsets.size.width) > CGFloat(FLT_EPSILON) + widthUpdated = abs(state.visibleSize.width - updateSizeAndInsets.size.width) > CGFloat.ulpOfOne state.visibleSize = updateSizeAndInsets.size state.insets = updateSizeAndInsets.insets @@ -1142,9 +1147,9 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if options.contains(.PreferSynchronousResourceLoading) { var currentReadySignals: [Signal] = [] for i in 0 ..< updatedOperations.count { - if case let .InsertNode(index, offsetDirection, node, layout, apply) = updatedOperations[i] { + if case let .InsertNode(index, offsetDirection, nodeAnimated, node, layout, apply) = updatedOperations[i] { let (ready, commitApply) = apply() - updatedOperations[i] = .InsertNode(index: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: { + updatedOperations[i] = .InsertNode(index: index, offsetDirection: offsetDirection, animated: nodeAnimated, node: node, layout: layout, apply: { return (nil, commitApply) }) if let ready = ready { @@ -1409,7 +1414,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel let nextNode = self.itemNodes[nodeIndex + 1] if nextNode.index == nil { let nextHeight = nextNode.apparentHeight - if abs(nextHeight - previousApparentHeight) < CGFloat(FLT_EPSILON) { + if abs(nextHeight - previousApparentHeight) < CGFloat.ulpOfOne { if let animation = nextNode.animationForKey("apparentHeight") { node.apparentHeight = previousApparentHeight @@ -1424,7 +1429,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel takenAnimation = true - if abs(layout.size.height - previousApparentHeight) > CGFloat(FLT_EPSILON) { + if abs(layout.size.height - previousApparentHeight) > CGFloat.ulpOfOne { node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in if let node = node { node.animateFrameTransition(progress, currentValue) @@ -1490,7 +1495,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } - if node.apparentHeight > CGFloat(FLT_EPSILON) { + if node.apparentHeight > CGFloat.ulpOfOne { switch offsetDirection { case .Up: var i = nodeIndex - 1 @@ -1587,7 +1592,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var takenPreviousNodes = Set() for operation in operations { - if case let .InsertNode(_, _, node, _, _) = operation { + if case let .InsertNode(_, _, _, node, _, _) = operation { takenPreviousNodes.insert(node) } } @@ -1596,7 +1601,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel for operation in operations { switch operation { - case let .InsertNode(index, offsetDirection, node, layout, apply): + case let .InsertNode(index, offsetDirection, nodeAnimated, node, layout, apply): var previousFrame: CGRect? for (previousNode, frame) in previousApparentFrames { if previousNode === node { @@ -1613,8 +1618,8 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel updatedPreviousFrame = nil } - self.insertNodeAtIndex(animated: animated, animateAlpha: animateAlpha, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp) - if let updatedPreviousFrame = updatedPreviousFrame { + self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp) + if let _ = updatedPreviousFrame { if let lowestHeaderNode = lowestHeaderNode { self.insertSubnode(node, belowSubnode: lowestHeaderNode) } else { @@ -1706,7 +1711,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel node.addInsetsAnimationToValue(updatedInsets, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) } - if abs(updatedApparentHeight - previousApparentHeight) > CGFloat(FLT_EPSILON) { + if abs(updatedApparentHeight - previousApparentHeight) > CGFloat.ulpOfOne { node.apparentHeight = previousApparentHeight node.animateFrameTransition(0.0, previousApparentHeight) node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in @@ -1779,7 +1784,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel offset = self.insets.top - itemNode.apparentFrame.minY - itemNode.scrollPositioningInsets.top case let .Center(overflow): let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top - if itemNode.apparentFrame.size.height <= contentAreaHeight + CGFloat(FLT_EPSILON) { + if itemNode.apparentFrame.size.height <= contentAreaHeight + CGFloat.ulpOfOne { offset = self.insets.top + floor(((self.visibleSize.height - self.insets.bottom - self.insets.top) - itemNode.frame.size.height) / 2.0) - itemNode.apparentFrame.minY } else { switch overflow { @@ -1807,7 +1812,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if previousNode === itemNode { let offset = previousFrame.minY - itemNode.frame.minY - if abs(offset) > CGFloat(FLT_EPSILON) { + if abs(offset) > CGFloat.ulpOfOne { for itemNode in self.itemNodes { var frame = itemNode.frame frame.origin.y += offset @@ -1835,7 +1840,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel let previousVisibleSize = self.visibleSize self.visibleSize = updateSizeAndInsets.size - var offsetFix = updateSizeAndInsets.insets.top - self.insets.top + var offsetFix: CGFloat + if self.snapToBottomInsetUntilFirstInteraction { + offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom + } else { + offsetFix = updateSizeAndInsets.insets.top - self.insets.top + } self.insets = updateSizeAndInsets.insets self.visibleSize = updateSizeAndInsets.size @@ -2025,7 +2035,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.updateItemHeaders(headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) - if let offset = offset , abs(offset) > CGFloat(FLT_EPSILON) { + if let offset = offset , abs(offset) > CGFloat.ulpOfOne { let lowestHeaderNode = self.lowestHeaderNode() for itemNode in temporaryPreviousNodes { itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: offset) @@ -2144,7 +2154,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if self.debugInfo { var previousMaxY: CGFloat? for node in self.itemNodes { - if let previousMaxY = previousMaxY , abs(previousMaxY - node.apparentFrame.minY) > CGFloat(FLT_EPSILON) { + if let previousMaxY = previousMaxY , abs(previousMaxY - node.apparentFrame.minY) > CGFloat.ulpOfOne { print("monotonity violated") break } @@ -2454,7 +2464,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var i = 0 while i < self.itemNodes.count { let node = self.itemNodes[i] - if node.index == nil && node.apparentHeight <= CGFloat(FLT_EPSILON) { + if node.index == nil && node.apparentHeight <= CGFloat.ulpOfOne { self.removeItemNodeAtIndex(i) ASDeallocQueue.sharedDeallocation().releaseObject(inBackground: node) } else { @@ -2590,15 +2600,15 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } let updatedApparentHeight = itemNode.apparentHeight let apparentHeightDelta = updatedApparentHeight - previousApparentHeight - if abs(apparentHeightDelta) > CGFloat(FLT_EPSILON) { - if itemNode.apparentFrame.maxY < self.insets.top + CGFloat(FLT_EPSILON) { + if abs(apparentHeightDelta) > CGFloat.ulpOfOne { + if itemNode.apparentFrame.maxY < self.insets.top + CGFloat.ulpOfOne { offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) } else { offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: apparentHeightDelta) } } - if itemNode.index == nil && updatedApparentHeight <= CGFloat(FLT_EPSILON) { + if itemNode.index == nil && updatedApparentHeight <= CGFloat.ulpOfOne { requestUpdateVisibleItems = true } diff --git a/Display/ListViewAnimation.swift b/Display/ListViewAnimation.swift index 91b91d9c0e..2d7d954a98 100644 --- a/Display/ListViewAnimation.swift +++ b/Display/ListViewAnimation.swift @@ -133,10 +133,10 @@ public final class ListViewAnimation { public func applyAt(_ timestamp: Double) { var t = CGFloat((timestamp - self.startTime) / self.duration) let ct: CGFloat - if t <= 0.0 + CGFloat(FLT_EPSILON) { + if t <= 0.0 + CGFloat.ulpOfOne { t = 0.0 ct = 0.0 - } else if t >= 1.0 - CGFloat(FLT_EPSILON) { + } else if t >= 1.0 - CGFloat.ulpOfOne { t = 1.0 ct = 1.0 } else { diff --git a/Display/ListViewIntermediateState.swift b/Display/ListViewIntermediateState.swift index 99e60d79d9..e59bf1dadf 100644 --- a/Display/ListViewIntermediateState.swift +++ b/Display/ListViewIntermediateState.swift @@ -312,7 +312,7 @@ struct ListViewState { offset = self.insets.top - node.frame.minY case let .Center(overflow): let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top - if node.frame.size.height <= contentAreaHeight + CGFloat(FLT_EPSILON) { + if node.frame.size.height <= contentAreaHeight + CGFloat.ulpOfOne { offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY } else { switch overflow { @@ -340,7 +340,7 @@ struct ListViewState { additionalOffset = self.insets.top - minY } - if abs(additionalOffset) > CGFloat(FLT_EPSILON) { + if abs(additionalOffset) > CGFloat.ulpOfOne { for i in 0 ..< self.nodes.count { var frame = self.nodes[i].frame frame = frame.offsetBy(dx: 0.0, dy: additionalOffset) @@ -358,7 +358,7 @@ struct ListViewState { if node.index == stationaryIndex { let offset = stationaryOffset - node.frame.minY - if abs(offset) > CGFloat(FLT_EPSILON) { + if abs(offset) > CGFloat.ulpOfOne { for i in 0 ..< self.nodes.count { var frame = self.nodes[i].frame frame = frame.offsetBy(dx: 0.0, dy: offset) @@ -447,7 +447,7 @@ struct ListViewState { } } - if abs(offset) > CGFloat(FLT_EPSILON) { + if abs(offset) > CGFloat.ulpOfOne { for i in 0 ..< self.nodes.count { var frame = self.nodes[i].frame frame.origin.y += offset @@ -511,9 +511,9 @@ struct ListViewState { let node = self.nodes[i] if let index = node.index { if index != currentUpperNode.index - 1 { - if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat(FLT_EPSILON) { + if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne { var directionHint: ListViewInsertionOffsetDirection? - if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat(FLT_EPSILON) { + if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat.ulpOfOne { directionHint = ListViewInsertionOffsetDirection(hint) } return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up) @@ -525,9 +525,9 @@ struct ListViewState { } } - if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat(FLT_EPSILON) { + if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne { var directionHint: ListViewInsertionOffsetDirection? - if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat(FLT_EPSILON) { + if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat.ulpOfOne { directionHint = ListViewInsertionOffsetDirection(hint) } @@ -540,9 +540,9 @@ struct ListViewState { let node = self.nodes[i] if let index = node.index { if index != currentLowerNode.index + 1 { - if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat(FLT_EPSILON) { + if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne { var directionHint: ListViewInsertionOffsetDirection? - if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat(FLT_EPSILON) { + if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { directionHint = ListViewInsertionOffsetDirection(hint) } return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down) @@ -555,9 +555,9 @@ struct ListViewState { } } - if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat(FLT_EPSILON) { + if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne { var directionHint: ListViewInsertionOffsetDirection? - if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat(FLT_EPSILON) { + if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { directionHint = ListViewInsertionOffsetDirection(hint) } return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down) @@ -593,7 +593,7 @@ struct ListViewState { } } - let upperBound = -self.invisibleInset + CGFloat(FLT_EPSILON) + let upperBound = -self.invisibleInset + CGFloat.ulpOfOne for i in 0 ..< self.nodes.count { let node = self.nodes[i] if let index = node.index , node.frame.maxY > upperBound { @@ -617,7 +617,7 @@ struct ListViewState { } } - let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat(FLT_EPSILON) + let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne for i in (0 ..< self.nodes.count).reversed() { let node = self.nodes[i] if let index = node.index , node.frame.minY < lowerBound { @@ -717,7 +717,7 @@ struct ListViewState { let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height)) - operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply)) + operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, animated: animated, node: node, layout: layout, apply: apply)) self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil), at: insertionIndex) if !animated { @@ -770,7 +770,7 @@ struct ListViewState { if let direction = direction { offsetDirection = ListViewInsertionOffsetDirection(direction) } else { - if nodeFrame.maxY < self.insets.top + CGFloat(FLT_EPSILON) { + if nodeFrame.maxY < self.insets.top + CGFloat.ulpOfOne { offsetDirection = .Down } else { offsetDirection = .Up @@ -782,8 +782,8 @@ struct ListViewState { self.nodes.insert(.Placeholder(frame: nodeFrame), at: index) operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted())) } else { - if nodeFrame.maxY > self.insets.top - CGFloat(FLT_EPSILON) { - if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat(FLT_EPSILON) { + if nodeFrame.maxY > self.insets.top - CGFloat.ulpOfOne { + if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne { for i in (0 ..< index).reversed() { var frame = self.nodes[i].frame frame.origin.y += nodeFrame.size.height @@ -820,7 +820,7 @@ struct ListViewState { if let direction = direction { offsetDirection = ListViewInsertionOffsetDirection(direction) } else { - if node.frame.maxY < self.insets.top + CGFloat(FLT_EPSILON) { + if node.frame.maxY < self.insets.top + CGFloat.ulpOfOne { offsetDirection = .Down } else { offsetDirection = .Up @@ -865,7 +865,7 @@ struct ListViewState { } enum ListViewStateOperation { - case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (Signal?, () -> Void)) + case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (Signal?, () -> Void)) case InsertDisappearingPlaceholder(index: Int, referenceNode: ListViewItemNode, offsetDirection: ListViewInsertionOffsetDirection) case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection) case Remap([Int: Int]) diff --git a/Display/ListViewItemNode.swift b/Display/ListViewItemNode.swift index e7994277c4..59580cf9c8 100644 --- a/Display/ListViewItemNode.swift +++ b/Display/ListViewItemNode.swift @@ -408,7 +408,7 @@ open class ListViewItemNode: ASDisplayNode { public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) { if let previousAnimation = self.animationForKey("apparentHeight") { var duration = previousAnimation.startTime + previousAnimation.duration - beginAt - if abs(self.apparentHeight - value) < CGFloat(FLT_EPSILON) { + if abs(self.apparentHeight - value) < CGFloat.ulpOfOne { duration = 0.0 } diff --git a/Display/MinimizeKeyboardGestureRecognizer.swift b/Display/MinimizeKeyboardGestureRecognizer.swift new file mode 100644 index 0000000000..b8c2ed10c5 --- /dev/null +++ b/Display/MinimizeKeyboardGestureRecognizer.swift @@ -0,0 +1,20 @@ +import Foundation +import UIKit + +final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.cancelsTouchesInView = false + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + self.delegate = self + + self.direction = [.left, .right] + self.numberOfTouchesRequired = 2 + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/Display/NavigationController.swift b/Display/NavigationController.swift index c90f621782..33d6396360 100644 --- a/Display/NavigationController.swift +++ b/Display/NavigationController.swift @@ -210,9 +210,14 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle } public func pushViewController(_ controller: ViewController) { - controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) + self.view.endEditing(true) + let appliedLayout = self.containerLayout.withUpdatedInputHeight(nil) + controller.containerLayoutUpdated(appliedLayout, transition: .immediate) self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: {[weak self] _ in if let strongSelf = self { + if strongSelf.containerLayout.withUpdatedInputHeight(nil) != appliedLayout { + controller.containerLayoutUpdated(strongSelf.containerLayout.withUpdatedInputHeight(nil), transition: .immediate) + } strongSelf.pushViewController(controller, animated: true) } })) @@ -227,6 +232,7 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle } public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { + self.view.endEditing(true) controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { @@ -240,6 +246,7 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle } public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { + self.view.endEditing(true) controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { @@ -262,6 +269,26 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle self.setViewControllers(controllers, animated: animated) } + override open func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { + var poppedControllers: [UIViewController] = [] + var found = false + var controllers = self.viewControllers + while !controllers.isEmpty { + if controllers[controllers.count - 1] === viewController { + found = true + break + } + poppedControllers.insert(controllers[controllers.count - 1], at: 0) + controllers.removeLast() + } + if found { + self.setViewControllers(controllers, animated: animated) + return poppedControllers + } else { + return nil + } + } + open override func popViewController(animated: Bool) -> UIViewController? { var controller: UIViewController? var controllers = self.viewControllers diff --git a/Display/NavigationTransitionCoordinator.swift b/Display/NavigationTransitionCoordinator.swift index d1fe4b5e0c..104743d4bf 100644 --- a/Display/NavigationTransitionCoordinator.swift +++ b/Display/NavigationTransitionCoordinator.swift @@ -204,7 +204,7 @@ class NavigationTransitionCoordinator { completion() } - if abs(velocity) < CGFloat(FLT_EPSILON) && abs(self.progress) < CGFloat(FLT_EPSILON) { + if abs(velocity) < CGFloat.ulpOfOne && abs(self.progress) < CGFloat.ulpOfOne { UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { self.progress = 1.0 }, completion: { _ in diff --git a/Display/StatusBar.swift b/Display/StatusBar.swift index aa6deb337b..c5aa85610f 100644 --- a/Display/StatusBar.swift +++ b/Display/StatusBar.swift @@ -40,7 +40,7 @@ public class StatusBar: ASDisplayNode { return UITracingLayerView() }, didLoad: nil) - self.layer.setTraceableInfo(NSWeakReference(value: self)) + self.layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: true, userData: self, tracingTag: Window.statusBarTracingTag)) self.clipsToBounds = true self.isUserInteractionEnabled = false diff --git a/Display/StatusBarHost.swift b/Display/StatusBarHost.swift index 073aeaf6c9..4b79aa1845 100644 --- a/Display/StatusBarHost.swift +++ b/Display/StatusBarHost.swift @@ -6,5 +6,6 @@ public protocol StatusBarHost { var statusBarWindow: UIView? { get } var statusBarView: UIView? { get } + var keyboardWindow: UIWindow? { get } var keyboardView: UIView? { get } } diff --git a/Display/StatusBarManager.swift b/Display/StatusBarManager.swift index 693435cc4d..69af044c0b 100644 --- a/Display/StatusBarManager.swift +++ b/Display/StatusBarManager.swift @@ -24,7 +24,7 @@ private func mappedSurface(_ surface: StatusBarSurface) -> MappedStatusBarSurfac private func optimizeMappedSurface(statusBarSize: CGSize, surface: MappedStatusBarSurface) -> MappedStatusBarSurface { if surface.statusBars.count > 1 { for i in 1 ..< surface.statusBars.count { - if surface.statusBars[i].style != surface.statusBars[i - 1].style || abs(surface.statusBars[i].frame.origin.y - surface.statusBars[i - 1].frame.origin.y) > CGFloat(FLT_EPSILON) { + if surface.statusBars[i].style != surface.statusBars[i - 1].style || abs(surface.statusBars[i].frame.origin.y - surface.statusBars[i - 1].frame.origin.y) > CGFloat.ulpOfOne { return surface } if let lhsStatusBar = surface.statusBars[i - 1].statusBar, let rhsStatusBar = surface.statusBars[i].statusBar , !lhsStatusBar.alpha.isEqual(to: rhsStatusBar.alpha) { diff --git a/Display/Theme.swift b/Display/Theme.swift index cf8ea4883c..fd448fb1fb 100644 --- a/Display/Theme.swift +++ b/Display/Theme.swift @@ -1,9 +1,9 @@ import Foundation public class Theme { - public let tintColor: UIColor + public let accentColor: UIColor - public init(tintColor: UIColor) { - self.tintColor = tintColor + public init(accentColor: UIColor) { + self.accentColor = accentColor } } diff --git a/Display/UIKitUtils.m b/Display/UIKitUtils.m index 484ac2549f..e236dab2c7 100644 --- a/Display/UIKitUtils.m +++ b/Display/UIKitUtils.m @@ -53,8 +53,8 @@ CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPat }); if (canSetInitialVelocity) { springAnimation.initialVelocity = initialVelocity; + springAnimation.duration = springAnimation.settlingDuration; } - springAnimation.duration = springAnimation.settlingDuration; springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; return springAnimation; } diff --git a/Display/UIViewController+Navigation.m b/Display/UIViewController+Navigation.m index 247bf14f26..dade219ffc 100644 --- a/Display/UIViewController+Navigation.m +++ b/Display/UIViewController+Navigation.m @@ -177,7 +177,7 @@ static bool notyfyingShiftState = false; return controller; } - UIView *result = [self associatedObjectForKey:UIViewControllerPresentingProxyControllerKey]; + UIViewController *result = [self associatedObjectForKey:UIViewControllerPresentingProxyControllerKey]; if (result != nil) { return result; } diff --git a/Display/ViewController.swift b/Display/ViewController.swift index 497e2141bf..daf8347c18 100644 --- a/Display/ViewController.swift +++ b/Display/ViewController.swift @@ -168,6 +168,9 @@ open class ViewControllerPresentationArguments { } open func displayNodeDidLoad() { + if let layer = self.displayNode.layer as? CATracingLayer { + layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: false, userData: self.displayNode.layer, tracingTag: Window.keyboardTracingTag)) + } self.updateScrollToTopView() } @@ -243,4 +246,7 @@ open class ViewControllerPresentationArguments { super.viewDidAppear(animated) } + + open func dismiss() { + } } diff --git a/Display/Window.swift b/Display/Window.swift index 4ab3fd3c2b..60c55e5a8a 100644 --- a/Display/Window.swift +++ b/Display/Window.swift @@ -23,6 +23,7 @@ private struct WindowLayout: Equatable { public let size: CGSize public let statusBarHeight: CGFloat? public let inputHeight: CGFloat? + public let inputMinimized: Bool } private func ==(lhs: WindowLayout, rhs: WindowLayout) -> Bool { @@ -54,6 +55,10 @@ private func ==(lhs: WindowLayout, rhs: WindowLayout) -> Bool { return false } + if lhs.inputMinimized != rhs.inputMinimized { + return false + } + return true } @@ -76,19 +81,25 @@ private struct UpdatingLayout { 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) + self.layout = WindowLayout(size: size, statusBarHeight: self.layout.statusBarHeight, inputHeight: self.layout.inputHeight, inputMinimized: self.layout.inputMinimized) } 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) + self.layout = WindowLayout(size: self.layout.size, statusBarHeight: statusBarHeight, inputHeight: self.layout.inputHeight, inputMinimized: self.layout.inputMinimized) } 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) + self.layout = WindowLayout(size: self.layout.size, statusBarHeight: self.layout.statusBarHeight, inputHeight: inputHeight, inputMinimized: self.layout.inputMinimized) + } + + mutating func update(inputMinimized: Bool, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, statusBarHeight: self.layout.statusBarHeight, inputHeight: self.layout.inputHeight, inputMinimized: inputMinimized) } } @@ -96,12 +107,21 @@ private let orientationChangeDuration: Double = UIDevice.current.userInterfaceId 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) + var inputHeight: CGFloat? = layout.inputHeight + if let inputHeightValue = inputHeight, layout.inputMinimized { + inputHeight = floor(0.85 * inputHeightValue) + } + + return ContainerViewLayout(size: layout.size, intrinsicInsets: UIEdgeInsets(), statusBarHeight: layout.statusBarHeight, inputHeight: inputHeight) } public class Window: UIWindow { + public static let statusBarTracingTag: Int32 = 0 + public static let keyboardTracingTag: Int32 = 1 + private let statusBarHost: StatusBarHost? private let statusBarManager: StatusBarManager? + private let keyboardManager: KeyboardManager? private var statusBarChangeObserver: AnyObject? private var keyboardFrameChangeObserver: AnyObject? @@ -122,11 +142,21 @@ public class Window: UIWindow { if let statusBarHost = statusBarHost { self.statusBarManager = StatusBarManager(host: statusBarHost) statusBarHeight = statusBarHost.statusBarFrame.size.height + self.keyboardManager = KeyboardManager(host: statusBarHost) } else { self.statusBarManager = nil + self.keyboardManager = nil statusBarHeight = 20.0 } - self.windowLayout = WindowLayout(size: frame.size, statusBarHeight: statusBarHeight, inputHeight: 0.0) + + let minimized: Bool + if let keyboardManager = self.keyboardManager { + minimized = keyboardManager.minimized + } else { + minimized = false + } + + self.windowLayout = WindowLayout(size: frame.size, statusBarHeight: statusBarHeight, inputHeight: 0.0, inputMinimized: minimized) self.presentationContext = PresentationContext() super.init(frame: frame) @@ -135,6 +165,14 @@ public class Window: UIWindow { self?.invalidateTracingStatusBars() } + self.keyboardManager?.minimizedUpdated = { [weak self] in + if let strongSelf = self { + strongSelf.updateLayout { current in + current.update(inputMinimized: strongSelf.keyboardManager!.minimized, transition: .immediate, overrideTransition: false) + } + } + } + self.presentationContext.view = self self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate) @@ -170,7 +208,7 @@ public class Window: UIWindow { if duration > DBL_EPSILON { duration = 0.5 } - var curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 + let curve: UInt = (notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 let transitionCurve: ContainedViewLayoutTransitionCurve if curve == 7 { @@ -271,20 +309,19 @@ public class Window: UIWindow { override public func layoutSubviews() { super.layoutSubviews() - if self.tracingStatusBarsInvalidated, let statusBarManager = statusBarManager { + if self.tracingStatusBarsInvalidated, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { self.tracingStatusBarsInvalidated = false if self.statusBarHidden { statusBarManager.surfaces = [] } else { var statusBarSurfaces: [StatusBarSurface] = [] - for layers in self.layer.traceableLayerSurfaces() { + for layers in self.layer.traceableLayerSurfaces(withTag: Window.statusBarTracingTag) { let surface = StatusBarSurface() for layer in layers { - if let weakInfo = layer.traceableInfo() as? NSWeakReference { - if let statusBar = weakInfo.value as? StatusBar { - surface.addStatusBar(statusBar) - } + let traceableInfo = layer.traceableInfo() + if let statusBar = traceableInfo?.userData as? StatusBar { + surface.addStatusBar(statusBar) } } statusBarSurfaces.append(surface) @@ -292,6 +329,16 @@ public class Window: UIWindow { self.layer.adjustTraceableLayerTransforms(CGSize()) statusBarManager.surfaces = statusBarSurfaces } + + var keyboardSurfaces: [KeyboardSurface] = [] + for layers in self.layer.traceableLayerSurfaces(withTag: Window.keyboardTracingTag) { + for layer in layers { + if let view = layer.delegate as? UITracingLayerView { + keyboardSurfaces.append(KeyboardSurface(host: view)) + } + } + } + keyboardManager.surfaces = keyboardSurfaces } if !Window.isDeviceRotating() { @@ -331,7 +378,7 @@ public class Window: UIWindow { postUpdateToInterfaceOrientationBlocks.append(f) } - private func updateLayout(_ update: @noescape(inout UpdatingLayout) -> ()) { + private func updateLayout(_ update: (inout UpdatingLayout) -> ()) { if self.updatingLayout == nil { self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) } @@ -349,7 +396,7 @@ public class Window: UIWindow { } else { statusBarHeight = 20.0 } - var statusBarWasHidden = self.statusBarHidden + let statusBarWasHidden = self.statusBarHidden if statusBarHiddenInLandscape && updatingLayout.layout.size.width > updatingLayout.layout.size.height { statusBarHeight = 0.0 self.statusBarHidden = true @@ -360,7 +407,7 @@ public class Window: UIWindow { self.tracingStatusBarsInvalidated = true self.setNeedsLayout() } - self.windowLayout = WindowLayout(size: updatingLayout.layout.size, statusBarHeight: statusBarHeight, inputHeight: updatingLayout.layout.inputHeight) + self.windowLayout = WindowLayout(size: updatingLayout.layout.size, statusBarHeight: statusBarHeight, inputHeight: updatingLayout.layout.inputHeight, inputMinimized: updatingLayout.layout.inputMinimized) self._rootController?.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition) self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition)