diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index 8b141de67b..e9eca87f96 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */; }; D0AE3D4D1D25C816001CCE13 /* NavigationBarTransitionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */; }; D0B367201C94A53A00346D2E /* StatusBarProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */; }; + D0BB4EBA1F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */; }; D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */; }; D0C0B5991EDF3BC9000F4D2C /* ActionSheetTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */; }; D0C0B59D1EE022CC000F4D2C /* NavigationBarContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B59C1EE022CC000F4D2C /* NavigationBarContentNode.swift */; }; @@ -113,7 +114,9 @@ D0C85DD01D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DCF1D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift */; }; D0C85DD21D1C08AE00124894 /* ActionSheetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DD11D1C08AE00124894 /* ActionSheetItemNode.swift */; }; D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DD31D1C1E6A00124894 /* ActionSheetItemGroupNode.swift */; }; + D0CB78901F9822F8004AB79B /* WindowPanRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */; }; D0CD12161CCFEB4E000DE7BC /* ScrollToTopProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */; }; + D0CE67921F7DA11700FFB557 /* ActionSheetTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */; }; D0CE8CE91F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE81F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift */; }; D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */; }; D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */; }; @@ -233,6 +236,7 @@ D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateImage.swift; sourceTree = ""; }; D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarTransitionState.swift; sourceTree = ""; }; D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarProxyNode.swift; sourceTree = ""; }; + D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInputAccessoryHeightProvider.swift; sourceTree = ""; }; D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeWindowHostView.swift; sourceTree = ""; }; D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetTextItem.swift; sourceTree = ""; }; D0C0B59C1EE022CC000F4D2C /* NavigationBarContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarContentNode.swift; sourceTree = ""; }; @@ -253,7 +257,9 @@ D0C85DCF1D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroupsContainerNode.swift; sourceTree = ""; }; D0C85DD11D1C08AE00124894 /* ActionSheetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemNode.swift; sourceTree = ""; }; D0C85DD31D1C1E6A00124894 /* ActionSheetItemGroupNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroupNode.swift; sourceTree = ""; }; + D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowPanRecognizer.swift; sourceTree = ""; }; D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollToTopProxyView.swift; sourceTree = ""; }; + D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionSheetTheme.swift; sourceTree = ""; }; D0CE8CE81F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarTitleView.swift; sourceTree = ""; }; D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalTapRecognizer.swift; sourceTree = ""; }; D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertController.swift; sourceTree = ""; }; @@ -315,6 +321,8 @@ D05CC2A11B69326C00E235A3 /* WindowContent.swift */, D0BE93181E8ED71100DCC1E6 /* NativeWindowHostView.swift */, D087BFB41F75181D003FD209 /* ChildWindowHostView.swift */, + D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */, + D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */, ); name = Window; sourceTree = ""; @@ -342,6 +350,7 @@ D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */, D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */, D0C0B5981EDF3BC9000F4D2C /* ActionSheetTextItem.swift */, + D0CE67911F7DA11700FFB557 /* ActionSheetTheme.swift */, ); name = "Action Sheet"; sourceTree = ""; @@ -822,6 +831,7 @@ D05CC2F81B6955D000E235A3 /* UIViewController+Navigation.m in Sources */, D0F1132F1D6F3C20008C3597 /* ContextMenuActionNode.swift in Sources */, D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */, + D0BB4EBA1F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift in Sources */, D05CC31F1B695A9600E235A3 /* NavigationControllerProxy.m in Sources */, D05CC3031B69568600E235A3 /* NotificationCenterUtils.m in Sources */, D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */, @@ -894,10 +904,12 @@ D0C2DFC61CC4431D0044FF83 /* ASTransformLayerNode.swift in Sources */, D05CC3291B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift in Sources */, D077B8E91F4637040046D27A /* NavigationBarBadge.swift in Sources */, + D0CE67921F7DA11700FFB557 /* ActionSheetTheme.swift in Sources */, D05BE4AE1D217F6B002BD72C /* MergedLayoutEvents.swift in Sources */, D0C0D2901C997110001D2851 /* FBAnimationPerformanceTracker.mm in Sources */, D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */, D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */, + D0CB78901F9822F8004AB79B /* WindowPanRecognizer.swift in Sources */, D05CC2FE1B6955D000E235A3 /* UIWindow+OrientationChange.m in Sources */, D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */, D0C0B5991EDF3BC9000F4D2C /* ActionSheetTextItem.swift in Sources */, diff --git a/Display/ActionSheetButtonItem.swift b/Display/ActionSheetButtonItem.swift index ede39695f3..ac5e3b9cbe 100644 --- a/Display/ActionSheetButtonItem.swift +++ b/Display/ActionSheetButtonItem.swift @@ -20,8 +20,8 @@ public class ActionSheetButtonItem: ActionSheetItem { self.action = action } - public func node() -> ActionSheetItemNode { - let node = ActionSheetButtonNode() + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetButtonNode(theme: theme) node.setItem(self) return node } @@ -37,6 +37,8 @@ public class ActionSheetButtonItem: ActionSheetItem { } public class ActionSheetButtonNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + public static let defaultFont: UIFont = Font.regular(20.0) private var item: ActionSheetButtonItem? @@ -44,7 +46,9 @@ public class ActionSheetButtonNode: ActionSheetItemNode { private let button: HighlightTrackingButton private let label: ASTextNode - override public init() { + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.button = HighlightTrackingButton() self.label = ASTextNode() @@ -52,7 +56,7 @@ public class ActionSheetButtonNode: ActionSheetItemNode { self.label.maximumNumberOfLines = 1 self.label.displaysAsynchronously = false - super.init() + super.init(theme: theme) self.view.addSubview(self.button) @@ -62,10 +66,10 @@ public class ActionSheetButtonNode: ActionSheetItemNode { self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor } else { UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor }) } } @@ -80,11 +84,11 @@ public class ActionSheetButtonNode: ActionSheetItemNode { let textColor: UIColor switch item.color { case .accent: - textColor = UIColor(rgb: 0x007ee5) + textColor = self.theme.standardActionTextColor case .destructive: - textColor = .red + textColor = self.theme.destructiveActionTextColor case .disabled: - textColor = .gray + textColor = self.theme.disabledActionTextColor } self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: textColor) diff --git a/Display/ActionSheetCheckboxItem.swift b/Display/ActionSheetCheckboxItem.swift index a946c34625..26a3cfe2d9 100644 --- a/Display/ActionSheetCheckboxItem.swift +++ b/Display/ActionSheetCheckboxItem.swift @@ -1,16 +1,6 @@ import Foundation import AsyncDisplayKit -private let checkIcon = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0x007ee5).cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() -}) - public class ActionSheetCheckboxItem: ActionSheetItem { public let title: String public let label: String @@ -24,8 +14,8 @@ public class ActionSheetCheckboxItem: ActionSheetItem { self.action = action } - public func node() -> ActionSheetItemNode { - let node = ActionSheetCheckboxItemNode() + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetCheckboxItemNode(theme: theme) node.setItem(self) return node } @@ -43,6 +33,8 @@ public class ActionSheetCheckboxItem: ActionSheetItem { public class ActionSheetCheckboxItemNode: ActionSheetItemNode { public static let defaultFont: UIFont = Font.regular(20.0) + private let theme: ActionSheetControllerTheme + private var item: ActionSheetCheckboxItem? private let button: HighlightTrackingButton @@ -50,7 +42,9 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { private let labelNode: ASTextNode private let checkNode: ASImageNode - public override init() { + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.button = HighlightTrackingButton() self.titleNode = ASTextNode() @@ -67,9 +61,17 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { self.checkNode.isUserInteractionEnabled = false self.checkNode.displayWithoutProcessing = true self.checkNode.displaysAsynchronously = false - self.checkNode.image = checkIcon + self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.controlAccentColor.cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() + }) - super.init() + super.init(theme: theme) self.view.addSubview(self.button) self.addSubnode(self.titleNode) @@ -79,10 +81,10 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor } else { UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor }) } } @@ -94,8 +96,8 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { func setItem(_ item: ActionSheetCheckboxItem) { self.item = item - self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: .black) - self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: UIColor(rgb: 0x8e8e93)) + self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.primaryTextColor) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.secondaryTextColor) self.checkNode.isHidden = !item.value self.setNeedsLayout() diff --git a/Display/ActionSheetController.swift b/Display/ActionSheetController.swift index 5ac67b2859..b3643e8b07 100644 --- a/Display/ActionSheetController.swift +++ b/Display/ActionSheetController.swift @@ -5,9 +5,13 @@ open class ActionSheetController: ViewController { return self.displayNode as! ActionSheetControllerNode } + private let theme: ActionSheetControllerTheme + private var groups: [ActionSheetItemGroup] = [] - public init() { + public init(theme: ActionSheetControllerTheme) { + self.theme = theme + super.init(navigationBarTheme: nil) } @@ -20,7 +24,7 @@ open class ActionSheetController: ViewController { } open override func loadDisplayNode() { - self.displayNode = ActionSheetControllerNode() + self.displayNode = ActionSheetControllerNode(theme: self.theme) self.displayNodeDidLoad() self.actionSheetNode.dismiss = { [weak self] in diff --git a/Display/ActionSheetControllerNode.swift b/Display/ActionSheetControllerNode.swift index 054884fcd5..7f9fb5e152 100644 --- a/Display/ActionSheetControllerNode.swift +++ b/Display/ActionSheetControllerNode.swift @@ -1,7 +1,7 @@ import UIKit import AsyncDisplayKit -private let containerInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 8.0, right: 16.0) +private let containerInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) private class ActionSheetControllerNodeScrollView: UIScrollView { override func touchesShouldCancel(in view: UIView) -> Bool { @@ -10,7 +10,7 @@ private class ActionSheetControllerNodeScrollView: UIScrollView { } final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { - static let dimColor: UIColor = UIColor(white: 0.0, alpha: 0.4) + private let theme: ActionSheetControllerTheme private let dismissTapView: UIView @@ -25,7 +25,9 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { var dismiss: () -> Void = { } - override init() { + init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.scrollView = ActionSheetControllerNodeScrollView() if #available(iOSApplicationExtension 11.0, *) { @@ -38,22 +40,22 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { self.dismissTapView = UIView() self.leftDimView = UIView() - self.leftDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.leftDimView.backgroundColor = self.theme.dimColor self.leftDimView.isUserInteractionEnabled = false self.rightDimView = UIView() - self.rightDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.rightDimView.backgroundColor = self.theme.dimColor self.rightDimView.isUserInteractionEnabled = false self.topDimView = UIView() - self.topDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.topDimView.backgroundColor = self.theme.dimColor self.topDimView.isUserInteractionEnabled = false self.bottomDimView = UIView() - self.bottomDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.bottomDimView.backgroundColor = self.theme.dimColor self.bottomDimView.isUserInteractionEnabled = false - self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode() + self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode(theme: self.theme) super.init() @@ -74,7 +76,7 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - var insets = layout.insets(options: [.statusBar]) + let insets = layout.insets(options: [.statusBar]) self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size) self.dismissTapView.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -88,7 +90,7 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { func animateIn() { let tempDimView = UIView() - tempDimView.backgroundColor = ActionSheetControllerNode.dimColor + tempDimView.backgroundColor = self.theme.dimColor tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) self.view.addSubview(tempDimView) @@ -105,7 +107,7 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { func animateOut() { let tempDimView = UIView() - tempDimView.backgroundColor = ActionSheetControllerNode.dimColor + tempDimView.backgroundColor = self.theme.dimColor tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) self.view.addSubview(tempDimView) @@ -138,7 +140,7 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { let contentOffset = self.scrollView.contentOffset - var additionalTopHeight = max(0.0, -contentOffset.y) + let additionalTopHeight = max(0.0, -contentOffset.y) if additionalTopHeight >= 30.0 { self.animateOut() @@ -146,8 +148,8 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate { } func updateScrollDimViews(size: CGSize) { - var additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y) - var additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y) + let additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y) + let additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y) self.topDimView.frame = CGRect(x: containerInsets.left, y: -additionalTopHeight, width: size.width - containerInsets.left - containerInsets.right, height: max(0.0, self.itemGroupsContainerNode.frame.minY + additionalTopHeight)) self.bottomDimView.frame = CGRect(x: containerInsets.left, y: self.itemGroupsContainerNode.frame.maxY, width: size.width - containerInsets.left - containerInsets.right, height: max(0.0, size.height - self.itemGroupsContainerNode.frame.maxY + additionalBottomHeight)) diff --git a/Display/ActionSheetItem.swift b/Display/ActionSheetItem.swift index fdac4f2386..291b3478a6 100644 --- a/Display/ActionSheetItem.swift +++ b/Display/ActionSheetItem.swift @@ -1,6 +1,6 @@ import Foundation public protocol ActionSheetItem { - func node() -> ActionSheetItemNode + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode func updateNode(_ node: ActionSheetItemNode) -> Void } diff --git a/Display/ActionSheetItemGroupNode.swift b/Display/ActionSheetItemGroupNode.swift index b340085195..771f541177 100644 --- a/Display/ActionSheetItemGroupNode.swift +++ b/Display/ActionSheetItemGroupNode.swift @@ -8,6 +8,8 @@ private class ActionSheetItemGroupNodeScrollView: UIScrollView { } final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { + private let theme: ActionSheetControllerTheme + private let centerDimView: UIImageView private let topDimView: UIView private let bottomDimView: UIView @@ -22,26 +24,28 @@ final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { var respectInputHeight = true - override init() { + init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.centerDimView = UIImageView() - self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: ActionSheetControllerNode.dimColor) + self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor) self.topDimView = UIView() - self.topDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.topDimView.backgroundColor = self.theme.dimColor self.topDimView.isUserInteractionEnabled = false self.bottomDimView = UIView() - self.bottomDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.bottomDimView.backgroundColor = self.theme.dimColor self.bottomDimView.isUserInteractionEnabled = false self.trailingDimView = UIView() - self.trailingDimView.backgroundColor = ActionSheetControllerNode.dimColor + self.trailingDimView.backgroundColor = self.theme.dimColor self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.clippingNode.cornerRadius = 16.0 - self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark)) self.scrollView = ActionSheetItemGroupNodeScrollView() if #available(iOSApplicationExtension 11.0, *) { @@ -107,7 +111,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate { i += 1 } - return CGSize(width: constrainedSize.width, height: min(itemNodesHeight, constrainedSize.height)) + return CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height)) } override func layout() { diff --git a/Display/ActionSheetItemGroupsContainerNode.swift b/Display/ActionSheetItemGroupsContainerNode.swift index 21805872d0..080024b0f9 100644 --- a/Display/ActionSheetItemGroupsContainerNode.swift +++ b/Display/ActionSheetItemGroupsContainerNode.swift @@ -1,13 +1,17 @@ import UIKit import AsyncDisplayKit -private let groupSpacing: CGFloat = 16.0 +private let groupSpacing: CGFloat = 8.0 final class ActionSheetItemGroupsContainerNode: ASDisplayNode { + private let theme: ActionSheetControllerTheme + private var groups: [ActionSheetItemGroup] = [] private var groupNodes: [ActionSheetItemGroupNode] = [] - override init() { + init(theme: ActionSheetControllerTheme) { + self.theme = theme + super.init() } @@ -20,8 +24,8 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode { self.groupNodes.removeAll() for group in groups { - let groupNode = ActionSheetItemGroupNode() - groupNode.updateItemNodes(group.items.map({ $0.node() }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0) + let groupNode = ActionSheetItemGroupNode(theme: self.theme) + groupNode.updateItemNodes(group.items.map({ $0.node(theme: self.theme) }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0) self.groupNodes.append(groupNode) self.addSubnode(groupNode) } diff --git a/Display/ActionSheetItemNode.swift b/Display/ActionSheetItemNode.swift index 22214130a3..178bfc0f05 100644 --- a/Display/ActionSheetItemNode.swift +++ b/Display/ActionSheetItemNode.swift @@ -2,18 +2,19 @@ import UIKit import AsyncDisplayKit open class ActionSheetItemNode: ASDisplayNode { - public static let defaultBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 0.8) - public static let highlightedBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 0.7) + private let theme: ActionSheetControllerTheme public let backgroundNode: ASDisplayNode private let overflowSeparatorNode: ASDisplayNode - public override init() { + public init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor self.overflowSeparatorNode = ASDisplayNode() - self.overflowSeparatorNode.backgroundColor = UIColor(white: 0.5, alpha: 0.3) + self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor super.init() diff --git a/Display/ActionSheetTextItem.swift b/Display/ActionSheetTextItem.swift index 49dfda6f6c..6155bb1072 100644 --- a/Display/ActionSheetTextItem.swift +++ b/Display/ActionSheetTextItem.swift @@ -8,8 +8,8 @@ public class ActionSheetTextItem: ActionSheetItem { self.title = title } - public func node() -> ActionSheetItemNode { - let node = ActionSheetTextNode() + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetTextNode(theme: theme) node.setItem(self) return node } @@ -27,18 +27,22 @@ public class ActionSheetTextItem: ActionSheetItem { public class ActionSheetTextNode: ActionSheetItemNode { public static let defaultFont: UIFont = Font.regular(13.0) + private let theme: ActionSheetControllerTheme + private var item: ActionSheetTextItem? private let label: ASTextNode - override public init() { + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.label = ASTextNode() self.label.isLayerBacked = true self.label.maximumNumberOfLines = 1 self.label.displaysAsynchronously = false self.label.truncationMode = .byTruncatingTail - super.init() + super.init(theme: theme) self.label.isUserInteractionEnabled = false self.addSubnode(self.label) @@ -47,9 +51,7 @@ public class ActionSheetTextNode: ActionSheetItemNode { func setItem(_ item: ActionSheetTextItem) { self.item = item - let textColor = UIColor(rgb: 0x7c7c7c) - - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: textColor) + self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor) self.setNeedsLayout() } diff --git a/Display/ActionSheetTheme.swift b/Display/ActionSheetTheme.swift new file mode 100644 index 0000000000..934485d135 --- /dev/null +++ b/Display/ActionSheetTheme.swift @@ -0,0 +1,33 @@ +import Foundation +import UIKit + +public enum ActionSheetControllerThemeBackgroundType { + case light + case dark +} + +public final class ActionSheetControllerTheme { + public let dimColor: UIColor + public let backgroundType: ActionSheetControllerThemeBackgroundType + public let itemBackgroundColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let standardActionTextColor: UIColor + public let destructiveActionTextColor: UIColor + public let disabledActionTextColor: UIColor + public let primaryTextColor: UIColor + public let secondaryTextColor: UIColor + public let controlAccentColor: UIColor + + public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor) { + self.dimColor = dimColor + self.backgroundType = backgroundType + self.itemBackgroundColor = itemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.standardActionTextColor = standardActionTextColor + self.destructiveActionTextColor = destructiveActionTextColor + self.disabledActionTextColor = disabledActionTextColor + self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor + self.controlAccentColor = controlAccentColor + } +} diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 3937eace9d..de7cf0a653 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -39,7 +39,7 @@ public extension CAAnimation { } public extension CALayer { - public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { + public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { if timingFunction == kCAMediaTimingFunctionSpring { let animation = makeSpringAnimation(keyPath) animation.fromValue = from @@ -59,6 +59,11 @@ public extension CALayer { animation.speed = speed * Float(animation.duration / duration) animation.isAdditive = additive + if !delay.isZero { + animation.beginTime = CACurrentMediaTime() + delay + animation.fillMode = kCAFillModeBoth + } + return animation } else { let k = Float(UIView.animationDurationFactor()) @@ -84,12 +89,17 @@ public extension CALayer { animation.delegate = CALayerAnimationDelegate(completion: completion) } + if !delay.isZero { + animation.beginTime = CACurrentMediaTime() + delay + animation.fillMode = kCAFillModeBoth + } + return animation } } - public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) self.add(animation, forKey: additive ? nil : keyPath) } @@ -184,8 +194,8 @@ public extension CALayer { self.add(animation, forKey: key) } - public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) { - self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, completion: completion) + public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion) } public func animateScale(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { diff --git a/Display/ContainableController.swift b/Display/ContainableController.swift index 4f6593b787..f77ca45763 100644 --- a/Display/ContainableController.swift +++ b/Display/ContainableController.swift @@ -62,8 +62,8 @@ public extension ContainedViewLayoutTransition { } } - func updateBounds(node: ASDisplayNode, bounds: CGRect, completion: ((Bool) -> Void)? = nil) { - if node.bounds.equalTo(bounds) { + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.bounds.equalTo(bounds) && !force { completion?(true) } else { switch self { @@ -75,7 +75,7 @@ public extension ContainedViewLayoutTransition { case let .animated(duration, curve): let previousBounds = node.bounds node.bounds = bounds - node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, completion: { result in + node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, force: force, completion: { result in if let completion = completion { completion(result) } @@ -140,6 +140,21 @@ public extension ContainedViewLayoutTransition { } } + func animateFrame(node: ASDisplayNode, from frame: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + node.layer.animateFrame(from: frame, to: node.layer.frame, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: removeOnCompletion, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: @@ -171,10 +186,10 @@ public extension ContainedViewLayoutTransition { } } - func animateOffsetAdditive(layer: CALayer, offset: CGFloat) { + func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { switch self { case .immediate: - break + completion?() case let .animated(duration, curve): let timingFunction: String switch curve { @@ -183,7 +198,9 @@ public extension ContainedViewLayoutTransition { case .spring: timingFunction = kCAMediaTimingFunctionSpring } - layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: timingFunction) + layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: timingFunction, completion: { _ in + completion?() + }) } } @@ -203,6 +220,28 @@ public extension ContainedViewLayoutTransition { } } + func updateFrame(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if view.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + view.frame = frame + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousFrame = view.frame + view.frame = frame + view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) { completion?(true) @@ -331,6 +370,34 @@ public extension ContainedViewLayoutTransition { }) } } + + func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint, completion: ((Bool) -> Void)? = nil) { + print("update to \(offset) animated: \(self.isAnimated)") + let t = layer.transform + let currentOffset = CGPoint(x: t.m41, y: t.m42) + if currentOffset == offset { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0) + layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } } public extension ContainedViewLayoutTransition { diff --git a/Display/ContainerViewLayout.swift b/Display/ContainerViewLayout.swift index d1c9b3d372..4680a6c64a 100644 --- a/Display/ContainerViewLayout.swift +++ b/Display/ContainerViewLayout.swift @@ -42,23 +42,29 @@ public struct ContainerViewLayout: Equatable { public let size: CGSize public let metrics: LayoutMetrics public let intrinsicInsets: UIEdgeInsets + public let safeInsets: UIEdgeInsets public let statusBarHeight: CGFloat? public let inputHeight: 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, statusBarHeight: CGFloat?, inputHeight: CGFloat?) { + public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, inputHeightIsInteractivellyChanging: Bool) { self.size = size self.metrics = metrics self.intrinsicInsets = intrinsicInsets + self.safeInsets = safeInsets self.statusBarHeight = statusBarHeight self.inputHeight = inputHeight + self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging } public func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets { @@ -73,54 +79,62 @@ 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), statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight) + 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) } public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { - return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight) + return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) } public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout { - return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight) + return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) } -} -public func ==(lhs: ContainerViewLayout, rhs: ContainerViewLayout) -> Bool { - if !lhs.size.equalTo(rhs.size) { - return false - } - - if lhs.metrics != rhs.metrics { - return false - } - - if lhs.intrinsicInsets != rhs.intrinsicInsets { - return false - } - - if let lhsStatusBarHeight = lhs.statusBarHeight { - if let rhsStatusBarHeight = rhs.statusBarHeight { - if !lhsStatusBarHeight.isEqual(to: rhsStatusBarHeight) { - return false - } - } else { + public static func ==(lhs: ContainerViewLayout, rhs: ContainerViewLayout) -> Bool { + if !lhs.size.equalTo(rhs.size) { return false } - } else if let _ = rhs.statusBarHeight { - return false - } - - if let lhsInputHeight = lhs.inputHeight { - if let rhsInputHeight = rhs.inputHeight { - if !lhsInputHeight.isEqual(to: rhsInputHeight) { - return false - } - } else { + + if lhs.metrics != rhs.metrics { return false } - } else if let _ = rhs.inputHeight { - return false + + if lhs.intrinsicInsets != rhs.intrinsicInsets { + return false + } + + if lhs.safeInsets != rhs.safeInsets { + return false + } + + if let lhsStatusBarHeight = lhs.statusBarHeight { + if let rhsStatusBarHeight = rhs.statusBarHeight { + if !lhsStatusBarHeight.isEqual(to: rhsStatusBarHeight) { + return false + } + } else { + return false + } + } else if let _ = rhs.statusBarHeight { + return false + } + + if let lhsInputHeight = lhs.inputHeight { + if let rhsInputHeight = rhs.inputHeight { + if !lhsInputHeight.isEqual(to: rhsInputHeight) { + return false + } + } else { + return false + } + } else if let _ = rhs.inputHeight { + return false + } + + if lhs.inputHeightIsInteractivellyChanging != rhs.inputHeightIsInteractivellyChanging { + return false + } + + return true } - - return true } diff --git a/Display/ContextMenuNode.swift b/Display/ContextMenuNode.swift index 06e8df983a..21f7fadd53 100644 --- a/Display/ContextMenuNode.swift +++ b/Display/ContextMenuNode.swift @@ -2,11 +2,139 @@ import Foundation import UIKit import AsyncDisplayKit +private func generateShadowImage() -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(white: 0.18, alpha: 1.0).cgColor) + context.setFillColor(UIColor(white: 0.18, alpha: 1.0).cgColor) + context.fill(CGRect(origin: CGPoint(x: -15.0, y: 0.0), size: CGSize(width: 30.0, height: 1.0))) + }) +} + +private final class ContextMenuContentScrollNode: ASDisplayNode { + var contentWidth: CGFloat = 0.0 + + private var initialOffset: CGFloat = 0.0 + + private let leftShadow: ASImageNode + private let rightShadow: ASImageNode + private let leftOverscrollNode: ASDisplayNode + private let rightOverscrollNode: ASDisplayNode + let contentNode: ASDisplayNode + + override init() { + self.contentNode = ASDisplayNode() + + let shadowImage = generateShadowImage() + + self.leftShadow = ASImageNode() + self.leftShadow.displayWithoutProcessing = true + self.leftShadow.displaysAsynchronously = false + self.leftShadow.image = shadowImage + self.rightShadow = ASImageNode() + self.rightShadow.displayWithoutProcessing = true + self.rightShadow.displaysAsynchronously = false + self.rightShadow.image = shadowImage + self.rightShadow.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + + self.leftOverscrollNode = ASDisplayNode() + self.leftOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + self.rightOverscrollNode = ASDisplayNode() + self.rightOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + + super.init() + + self.contentNode.addSubnode(self.leftOverscrollNode) + self.contentNode.addSubnode(self.rightOverscrollNode) + self.addSubnode(self.contentNode) + + self.addSubnode(self.leftShadow) + self.addSubnode(self.rightShadow) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.view.addGestureRecognizer(panRecognizer) + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.initialOffset = self.contentNode.bounds.origin.x + case .changed: + var bounds = self.contentNode.bounds + bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x + if bounds.origin.x > self.contentWidth - bounds.size.width { + let delta = bounds.origin.x - (self.contentWidth - bounds.size.width) + bounds.origin.x = self.contentWidth - bounds.size.width + ((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + if bounds.origin.x < 0.0 { + let delta = -bounds.origin.x + bounds.origin.x = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) + } + self.contentNode.bounds = bounds + self.updateShadows(.immediate) + case .ended, .cancelled: + var bounds = self.contentNode.bounds + bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x + + var duration = 0.4 + + if abs(bounds.origin.x - self.initialOffset) > 10.0 || abs(recognizer.velocity(in: self.view).x) > 100.0 { + duration = 0.2 + if bounds.origin.x < self.initialOffset { + bounds.origin.x = 0.0 + } else { + bounds.origin.x = self.contentWidth - bounds.size.width + } + } else { + bounds.origin.x = self.initialOffset + } + + if bounds.origin.x > self.contentWidth - bounds.size.width { + bounds.origin.x = self.contentWidth - bounds.size.width + } + if bounds.origin.x < 0.0 { + bounds.origin.x = 0.0 + } + let previousBounds = self.contentNode.bounds + self.contentNode.bounds = bounds + self.contentNode.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.updateShadows(.animated(duration: duration, curve: .spring)) + default: + break + } + } + + override func layout() { + let bounds = self.bounds + self.contentNode.frame = bounds + self.leftShadow.frame = CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: bounds.height)) + self.rightShadow.frame = CGRect(origin: CGPoint(x: bounds.size.width - 30.0, y: 0.0), size: CGSize(width: 30.0, height: bounds.height)) + self.leftOverscrollNode.frame = bounds.offsetBy(dx: -bounds.width, dy: 0.0) + self.rightOverscrollNode.frame = bounds.offsetBy(dx: self.contentWidth, dy: 0.0) + self.updateShadows(.immediate) + } + + private func updateShadows(_ transition: ContainedViewLayoutTransition) { + let bounds = self.contentNode.bounds + + let leftAlpha = max(0.0, min(1.0, bounds.minX / 20.0)) + transition.updateAlpha(node: self.leftShadow, alpha: leftAlpha) + + let rightAlpha = max(0.0, min(1.0, (self.contentWidth - bounds.maxX) / 20.0)) + transition.updateAlpha(node: self.rightShadow, alpha: rightAlpha) + } +} + final class ContextMenuNode: ASDisplayNode { private let actions: [ContextMenuAction] private let dismiss: () -> Void private let containerNode: ContextMenuContainerNode + private let scrollNode: ContextMenuContentScrollNode private let actionNodes: [ContextMenuActionNode] var sourceRect: CGRect? @@ -19,6 +147,7 @@ final class ContextMenuNode: ASDisplayNode { self.dismiss = dismiss self.containerNode = ContextMenuContainerNode() + self.scrollNode = ContextMenuContentScrollNode() self.actionNodes = actions.map { action in return ContextMenuActionNode(action: action) @@ -26,28 +155,33 @@ final class ContextMenuNode: ASDisplayNode { super.init() + self.containerNode.addSubnode(self.scrollNode) + self.addSubnode(self.containerNode) let dismissNode = { dismiss() } for actionNode in self.actionNodes { actionNode.dismiss = dismissNode - self.containerNode.addSubnode(actionNode) + self.scrollNode.contentNode.addSubnode(actionNode) } } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - var actionsWidth: CGFloat = 0.0 + var unboundActionsWidth: CGFloat = 0.0 let actionSeparatorWidth: CGFloat = UIScreenPixel for actionNode in self.actionNodes { - if !actionsWidth.isZero { - actionsWidth += actionSeparatorWidth + if !unboundActionsWidth.isZero { + unboundActionsWidth += actionSeparatorWidth } let actionSize = actionNode.measure(CGSize(width: layout.size.width, height: 54.0)) - actionNode.frame = CGRect(origin: CGPoint(x: actionsWidth, y: 0.0), size: actionSize) - actionsWidth += actionSize.width + actionNode.frame = CGRect(origin: CGPoint(x: unboundActionsWidth, y: 0.0), size: actionSize) + unboundActionsWidth += actionSize.width } + let maxActionsWidth = layout.size.width - 20.0 + let actionsWidth = min(unboundActionsWidth, maxActionsWidth) + let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize()) let insets = layout.insets(options: [.statusBar, .input]) @@ -67,7 +201,11 @@ final class ContextMenuNode: ASDisplayNode { self.containerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: actionsWidth, height: 54.0)) self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) + self.scrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: actionsWidth, height: 54.0)) + self.scrollNode.contentWidth = unboundActionsWidth + self.containerNode.layout() + self.scrollNode.layout() } func animateIn() { diff --git a/Display/GenerateImage.swift b/Display/GenerateImage.swift index b41b717307..833a594ac0 100644 --- a/Display/GenerateImage.swift +++ b/Display/GenerateImage.swift @@ -300,6 +300,9 @@ public class DrawingContext { } public func generateImage() -> UIImage? { + if self.scaledSize.width.isZero || self.scaledSize.height.isZero { + return nil + } if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) { return UIImage(cgImage: image, scale: scale, orientation: .up) } else { diff --git a/Display/GridNode.swift b/Display/GridNode.swift index b87337fe5a..4d36d1e715 100644 --- a/Display/GridNode.swift +++ b/Display/GridNode.swift @@ -373,16 +373,21 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.itemLayout = self.generateItemLayout() + var updateLayoutTransition = transaction.updateLayout?.transition + let generatedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transaction.scrollToItem { generatedScrollToItem = scrollToItem + if updateLayoutTransition == nil { + updateLayoutTransition = scrollToItem.transition + } } else if previousLayoutWasEmpty { generatedScrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .immediate, directionHint: .up, adjustForSection: true, adjustForTopInset: true) } else { generatedScrollToItem = nil } - self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: transaction.updateLayout?.transition, itemTransition: transaction.itemTransition, completion: completion) + self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: updateLayoutTransition, customScrollToItem: transaction.scrollToItem != nil, itemTransition: transaction.itemTransition, completion: completion) } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -401,7 +406,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.applyingContentOffset { - self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(layoutTransactionOffset: 0.0), removedNodes: [], updateLayoutTransition: nil, itemTransition: .immediate, completion: { _ in }) + self.applyPresentaionLayoutTransition(self.generatePresentationLayoutTransition(layoutTransactionOffset: 0.0), removedNodes: [], updateLayoutTransition: nil, customScrollToItem: false, itemTransition: .immediate, completion: { _ in }) } } @@ -764,7 +769,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { return lowestHeaderNode } - private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, itemTransition: ContainedViewLayoutTransition, completion: (GridNodeDisplayedItemRange) -> Void) { + private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, customScrollToItem: Bool, itemTransition: ContainedViewLayoutTransition, completion: (GridNodeDisplayedItemRange) -> Void) { let boundsTransition: ContainedViewLayoutTransition = updateLayoutTransition ?? .immediate var previousItemFrames: [WrappedGridItemNode: CGRect]? @@ -806,14 +811,14 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.scrollView.scrollIndicatorInsets = presentationLayoutTransition.layout.layout.insets } var boundsOffset: CGFloat = 0.0 + var shouldAnimateBounds = false if !self.scrollView.contentOffset.equalTo(presentationLayoutTransition.layout.contentOffset) || self.bounds.size != presentationLayoutTransition.layout.layout.size { let updatedBounds = CGRect(origin: presentationLayoutTransition.layout.contentOffset, size: presentationLayoutTransition.layout.layout.size) boundsOffset = updatedBounds.origin.y - previousBounds.origin.y self.bounds = updatedBounds - //boundsTransition.animateOffsetAdditive(layer: self.layer, offset: -boundsOffset - insetsOffset) - boundsTransition.animateBounds(layer: self.layer, from: previousBounds) + shouldAnimateBounds = true } - applyingContentOffset = false + self.applyingContentOffset = false let lowestSectionNode: ASDisplayNode? = self.lowestSectionNode() @@ -860,6 +865,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { if let previousItemFrames = previousItemFrames, case let .animated(duration, curve) = presentationLayoutTransition.transition { let contentOffset = presentationLayoutTransition.layout.contentOffset + boundsOffset = 0.0 + shouldAnimateBounds = false + var offset: CGFloat? for (index, itemNode) in self.itemNodes { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)], existingItemIndices.contains(index) { @@ -934,7 +942,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { self.removeItemNodeWithIndex(index, removeNode: false) let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) - itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak itemNode] _ in + itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { @@ -946,7 +954,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { for itemNode in removedNodes { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: itemNode)] { let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) - itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak itemNode] _ in + itemNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } else { @@ -960,7 +968,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { if let previousFrame = previousItemFrames[WrappedGridItemNode(node: sectionNode)] { self.removeSectionNodeWithSection(wrappedSection, removeNode: false) let position = CGPoint(x: previousFrame.midX, y: previousFrame.midY) - sectionNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak sectionNode] _ in + sectionNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + contentOffset.y), to: CGPoint(x: position.x, y: position.y + contentOffset.y - offset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, force: true, completion: { [weak sectionNode] _ in sectionNode?.removeFromSupernode() }) } else { @@ -1062,6 +1070,10 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { } } + if shouldAnimateBounds { + boundsTransition.animateBounds(layer: self.layer, from: previousBounds) + } + completion(self.displayedItemRange()) self.updateItemNodeVisibilititesAndScrolling() diff --git a/Display/KeyboardManager.swift b/Display/KeyboardManager.swift index 3ffd75f8bc..dfca12345d 100644 --- a/Display/KeyboardManager.swift +++ b/Display/KeyboardManager.swift @@ -5,43 +5,25 @@ struct KeyboardSurface { let host: UIView } -private func hasFirstResponder(_ view: UIView) -> Bool { +private func getFirstResponder(_ view: UIView) -> UIView? { if view.isFirstResponder { - return true + return view } else { for subview in view.subviews { - if hasFirstResponder(subview) { - return true + if let result = getFirstResponder(subview) { + return result } } - return false + return nil } } -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 + private var interactiveInputOffset: CGFloat = 0.0 var surfaces: [KeyboardSurface] = [] { didSet { @@ -53,63 +35,49 @@ class KeyboardManager { self.host = host } + func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + guard let keyboardView = self.host.keyboardView else { + return + } + + self.interactiveInputOffset = offset + + let previousBounds = keyboardView.bounds + let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size) + keyboardView.layer.bounds = updatedBounds + if transition.isAnimated { + transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion) + } else { + completion() + } + + //transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset)) + } + 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? + var firstResponderDisablesAutomaticKeyboardHandling = false for surface in self.surfaces { - if hasFirstResponder(surface.host) { + if let view = getFirstResponder(surface.host) { firstResponderView = surface.host + firstResponderDisablesAutomaticKeyboardHandling = view.disablesAutomaticKeyboardHandling 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 { + let horizontalTranslation = CATransform3DMakeTranslation(firstResponderDisablesAutomaticKeyboardHandling ? 0.0 : containerOrigin.x, 0.0, 0.0) + let currentTransform = keyboardWindow.layer.sublayerTransform + if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) { + //print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))") + keyboardWindow.layer.sublayerTransform = horizontalTranslation + } + if let tracingLayer = firstResponderView.layer as? CATracingLayer, !firstResponderDisablesAutomaticKeyboardHandling { if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer { previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil) } @@ -137,11 +105,4 @@ class KeyboardManager { self.previousFirstResponderView = firstResponderView } - - @objc func minimizeGesture(_ recognizer: UISwipeGestureRecognizer) { - if case .ended = recognizer.state { - self.minimized = !self.minimized - self.minimizedUpdated?() - } - } } diff --git a/Display/ListView.swift b/Display/ListView.swift index 464a291ca2..c978a5f14a 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -146,8 +146,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel private var bottomItemOverscrollBackground: ASDisplayNode? private var touchesPosition = CGPoint() - private var isTracking = false - private var isDeceleratingAfterTracking = false + public private(set) var isTracking = false + public private(set) var trackingOffset: CGFloat = 0.0 + public private(set) var beganTrackingAtTopOrigin = false + public private(set) var isDeceleratingAfterTracking = false private final var transactionQueue: ListViewTransactionQueue private final var transactionOffset: CGFloat = 0.0 @@ -447,6 +449,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.transactionOffset += -deltaY + if self.isTracking { + self.trackingOffset += -deltaY + } + self.enqueueUpdateVisibleItems() var useScrollDynamics = false @@ -612,7 +618,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var transition: ContainedViewLayoutTransition = .immediate if let updateSizeAndInsets = updateSizeAndInsets { - if updateSizeAndInsets.duration > DBL_EPSILON { + if !updateSizeAndInsets.duration.isZero { switch updateSizeAndInsets.curve { case let .Spring(duration): transition = .animated(duration: duration, curve: .spring) @@ -954,8 +960,8 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel return ListViewState(insets: self.insets, visibleSize: self.visibleSize, invisibleInset: self.invisibleInset, nodes: nodes, scrollPosition: nil, stationaryOffset: nil, stackFromBottom: self.stackFromBottom) } - public func transaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem? = nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in }) { - if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil && updateSizeAndInsets == nil { + public func transaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem? = nil, additionalScrollDistance: CGFloat = 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in }) { + if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil && updateSizeAndInsets == nil && additionalScrollDistance.isZero { completion(self.immediateDisplayedItemRange()) return } @@ -963,7 +969,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.transactionQueue.addTransaction({ [weak self] transactionCompletion in if let strongSelf = self { strongSelf.transactionOffset = 0.0 - strongSelf.deleteAndInsertItemsTransaction(deleteIndices: deleteIndices, insertIndicesAndItems: insertIndicesAndItems, updateIndicesAndItems: updateIndicesAndItems, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, updateOpaqueState: updateOpaqueState, completion: { [weak strongSelf] in + strongSelf.deleteAndInsertItemsTransaction(deleteIndices: deleteIndices, insertIndicesAndItems: insertIndicesAndItems, updateIndicesAndItems: updateIndicesAndItems, options: options, scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, updateOpaqueState: updateOpaqueState, completion: { [weak strongSelf] in completion(strongSelf?.immediateDisplayedItemRange() ?? ListViewDisplayedItemRange(loadedRange: nil, visibleRange: nil)) transactionCompletion() @@ -972,7 +978,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel }) } - private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, updateOpaqueState: Any?, completion: @escaping () -> Void) { + private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, updateOpaqueState: Any?, completion: @escaping () -> Void) { if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil { if let updateSizeAndInsets = updateSizeAndInsets , (self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets)) { self.visibleSize = updateSizeAndInsets.size @@ -1249,7 +1255,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel let beginReplay = { [weak self] in if let strongSelf = self { - strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { + strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { if options.contains(.PreferSynchronousDrawing) { let startTime = CACurrentMediaTime() self?.recursivelyEnsureDisplaySynchronously(true) @@ -1656,7 +1662,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } - private func replayOperations(animated: Bool, animateAlpha: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem: ListViewScrollToItem?, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { + private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { let timestamp = CACurrentMediaTime() if let updateOpaqueState = updateOpaqueState { @@ -1666,10 +1672,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() - snapshotView = self.view.snapshotView(afterScreenUpdates: false) } var previousApparentFrames: [(ListViewItemNode, CGRect)] = [] @@ -1913,6 +1921,13 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel break } } + } else if !additionalScrollDistance.isZero { + self.stopScrolling() + /*for itemNode in self.itemNodes { + var frame = itemNode.frame + frame.origin.y += additionalScrollDistance + itemNode.frame = frame + }*/ } self.insertNodesInBatches(nodes: [], completion: { @@ -1928,12 +1943,16 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.visibleSize = updateSizeAndInsets.size var offsetFix: CGFloat - if self.snapToBottomInsetUntilFirstInteraction { + if self.isTracking { + offsetFix = 0.0 + } else if self.snapToBottomInsetUntilFirstInteraction { offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom } else { offsetFix = updateSizeAndInsets.insets.top - self.insets.top } + offsetFix += additionalScrollDistance + self.insets = updateSizeAndInsets.insets self.visibleSize = updateSizeAndInsets.size @@ -1962,7 +1981,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel sizeAndInsetsOffset = offsetFix completeOffset += snapToBoundsOffset - if updateSizeAndInsets.duration > DBL_EPSILON { + if !updateSizeAndInsets.duration.isZero { let animation: CABasicAnimation switch updateSizeAndInsets.curve { case let .Spring(duration): @@ -2063,6 +2082,14 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if !snapToBoundsOffset.isZero { self.updateVisibleContentOffset() } + + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: snapshotView.frame.size) + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } } self.updateAccessoryNodes(animated: animated, currentTimestamp: timestamp) @@ -2586,7 +2613,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var updatedOperations = operations updatedState.removeInvisibleNodes(&updatedOperations) self.dispatchOnVSync { - self.replayOperations(animated: false, animateAlpha: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) + self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) } } } @@ -2759,6 +2786,14 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } override open func touchesBegan(_ touches: Set, with event: UIEvent?) { + let offset = self.visibleContentOffset() + switch offset { + case let .known(value) where value <= 10.0: + self.beganTrackingAtTopOrigin = true + default: + self.beganTrackingAtTopOrigin = false + } + self.touchesPosition = touches.first!.location(in: self.view) self.selectionTouchLocation = touches.first!.location(in: self.view) @@ -2915,7 +2950,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel switch recognizer.state { case .began: self.isTracking = true - break + self.trackingOffset = 0.0 case .changed: self.touchesPosition = recognizer.location(in: self.view) case .ended, .cancelled: diff --git a/Display/ListViewIntermediateState.swift b/Display/ListViewIntermediateState.swift index 47313cfed0..cf1fc977dc 100644 --- a/Display/ListViewIntermediateState.swift +++ b/Display/ListViewIntermediateState.swift @@ -121,6 +121,7 @@ public struct ListViewDeleteAndInsertOptions: OptionSet { public static let AnimateTopItemPosition = ListViewDeleteAndInsertOptions(rawValue: 32) public static let PreferSynchronousDrawing = ListViewDeleteAndInsertOptions(rawValue: 64) public static let PreferSynchronousResourceLoading = ListViewDeleteAndInsertOptions(rawValue: 128) + public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256) } public struct ListViewUpdateSizeAndInsets { diff --git a/Display/NativeWindowHostView.swift b/Display/NativeWindowHostView.swift index c502450939..dd1bd8009b 100644 --- a/Display/NativeWindowHostView.swift +++ b/Display/NativeWindowHostView.swift @@ -61,13 +61,22 @@ private final class NativeWindow: UIWindow, WindowHost { var presentController: ((ViewController, PresentationSurfaceLevel) -> Void)? var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? var presentNativeImpl: ((UIViewController) -> Void)? + var invalidateDeferScreenEdgeGestureImpl: (() -> Void)? + + private var frameTransition: ContainedViewLayoutTransition? override var frame: CGRect { get { return super.frame } set(value) { let sizeUpdated = super.frame.size != value.size - super.frame = value + if sizeUpdated, let transition = self.frameTransition, case let .animated(duration, curve) = transition { + let previousFrame = super.frame + super.frame = value + self.layer.animateFrame(from: previousFrame, to: value, duration: duration, timingFunction: curve.timingFunction) + } else { + super.frame = value + } if sizeUpdated { self.updateSize?(value.size) @@ -97,7 +106,11 @@ private final class NativeWindow: UIWindow, WindowHost { override func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) { self.updateIsUpdatingOrientationLayout?(true) + if !arg2.isZero { + self.frameTransition = .animated(duration: arg2, curve: .easeInOut) + } super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3) + self.frameTransition = nil self.updateIsUpdatingOrientationLayout?(false) self.updateToInterfaceOrientation?() @@ -114,6 +127,26 @@ private final class NativeWindow: UIWindow, WindowHost { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return self.hitTestImpl?(point, event) } + + override func insertSubview(_ view: UIView, at index: Int) { + super.insertSubview(view, at: index) + } + + override func addSubview(_ view: UIView) { + super.addSubview(view) + } + + override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) { + if let transitionClass = NSClassFromString("UITransitionView"), view.isKind(of: transitionClass) { + super.insertSubview(view, aboveSubview: self.subviews.last!) + } else { + super.insertSubview(view, aboveSubview: siblingSubview) + } + } + + func invalidateDeferScreenEdgeGestures() { + self.invalidateDeferScreenEdgeGestureImpl?() + } } public func nativeWindowHostView() -> WindowHostView { @@ -161,6 +194,10 @@ public func nativeWindowHostView() -> WindowHostView { return hostView?.hitTest?(point, event) } + window.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in + return hostView?.invalidateDeferScreenEdgeGesture?() + } + rootViewController.presentController = { [weak hostView] controller, level, animated, completion in if let strongSelf = hostView { strongSelf.present?(LegacyPresentedController(legacyController: controller, presentation: .custom), level) diff --git a/Display/NavigationBar.swift b/Display/NavigationBar.swift index 9e6818d00d..6249b98b6c 100644 --- a/Display/NavigationBar.swift +++ b/Display/NavigationBar.swift @@ -9,9 +9,9 @@ public final class NavigationBarTheme { context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) - context.translateBy(x: 0.0, y: 2.0) + context.translateBy(x: 0.0, y: -UIScreenPixel) - let _ = try? drawSvgPath(context, path: "M8.16012402,0.373030797 L0.635333572,7.39652821 L0.635333572,7.39652821 C-0.172148528,8.15021677 -0.215756811,9.41579564 0.537931744,10.2232777 C0.56927099,10.2568538 0.601757528,10.2893403 0.635333572,10.3206796 L8.16012402,17.344177 L8.16012402,17.344177 C8.69299787,17.8415514 9.51995274,17.8415514 10.0528266,17.344177 L10.0528266,17.344177 L10.0528266,17.344177 C10.5406633,16.8888394 10.567009,16.1242457 10.1116715,15.636409 C10.092738,15.6161242 10.0731114,15.5964976 10.0528266,15.5775641 L2.85430928,8.85860389 L10.0528266,2.13964366 L10.0528266,2.13964366 C10.5406633,1.68430612 10.567009,0.919712345 10.1116715,0.431875673 C10.092738,0.411590857 10.0731114,0.391964261 10.0528266,0.373030797 L10.0528266,0.373030797 L10.0528266,0.373030797 C9.51995274,-0.124343599 8.69299787,-0.124343599 8.16012402,0.373030797 Z ") + let _ = try? drawSvgPath(context, path: "M3.60751322,11.5 L11.5468531,3.56066017 C12.1326395,2.97487373 12.1326395,2.02512627 11.5468531,1.43933983 C10.9610666,0.853553391 10.0113191,0.853553391 9.42553271,1.43933983 L0.449102936,10.4157696 C-0.149700979,11.0145735 -0.149700979,11.9854265 0.449102936,12.5842304 L9.42553271,21.5606602 C10.0113191,22.1464466 10.9610666,22.1464466 11.5468531,21.5606602 C12.1326395,20.9748737 12.1326395,20.0251263 11.5468531,19.4393398 L3.60751322,11.5 Z ") }) } @@ -19,16 +19,22 @@ public final class NavigationBarTheme { public let primaryTextColor: UIColor public let backgroundColor: UIColor public let separatorColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeStrokeColor: UIColor + public let badgeTextColor: UIColor - public init(buttonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor) { + public init(buttonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.primaryTextColor = primaryTextColor self.backgroundColor = backgroundColor self.separatorColor = separatorColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeStrokeColor = badgeStrokeColor + self.badgeTextColor = badgeTextColor } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, separatorColor: color) + return NavigationBarTheme(buttonColor: self.buttonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } } @@ -509,7 +515,7 @@ open class NavigationBar: ASDisplayNode { self.titleNode = ASTextNode() self.backButtonNode = NavigationButtonNode() - self.badgeNode = NavigationBarBadgeNode(fillColor: .red, textColor: .white) + self.badgeNode = NavigationBarBadgeNode(fillColor: theme.badgeBackgroundColor, strokeColor: theme.badgeStrokeColor, textColor: theme.badgeTextColor) self.badgeNode.isUserInteractionEnabled = false self.badgeNode.isHidden = true self.backButtonArrow = ASImageNode() @@ -581,6 +587,8 @@ open class NavigationBar: ASDisplayNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.theme.primaryTextColor) } self.stripeNode.backgroundColor = self.theme.separatorColor + + self.badgeNode.updateTheme(fillColor: theme.badgeBackgroundColor, strokeColor: theme.badgeStrokeColor, textColor: theme.badgeTextColor) } } @@ -810,7 +818,7 @@ open class NavigationBar: ASDisplayNode { private func makeTransitionBadgeNode() -> ASDisplayNode? { if self.badgeNode.supernode != nil && !self.badgeNode.isHidden { - let node = NavigationBarBadgeNode(fillColor: .red, textColor: .white) + let node = NavigationBarBadgeNode(fillColor: self.theme.badgeBackgroundColor, strokeColor: self.theme.badgeStrokeColor, textColor: self.theme.badgeTextColor) node.text = self.badgeNode.text let nodeSize = node.measure(CGSize(width: 200.0, height: 100.0)) node.frame = CGRect(origin: CGPoint(), size: nodeSize) diff --git a/Display/NavigationBarBadge.swift b/Display/NavigationBarBadge.swift index f5864262ca..e52e565149 100644 --- a/Display/NavigationBarBadge.swift +++ b/Display/NavigationBarBadge.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit final class NavigationBarBadgeNode: ASDisplayNode { private var fillColor: UIColor + private var strokeColor: UIColor private var textColor: UIColor private let textNode: ASTextNode2 @@ -17,8 +18,9 @@ final class NavigationBarBadgeNode: ASDisplayNode { } } - init(fillColor: UIColor, textColor: UIColor) { + init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { self.fillColor = fillColor + self.strokeColor = strokeColor self.textColor = textColor self.textNode = ASTextNode2() @@ -29,7 +31,7 @@ final class NavigationBarBadgeNode: ASDisplayNode { self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor) + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) super.init() @@ -37,6 +39,14 @@ final class NavigationBarBadgeNode: ASDisplayNode { self.addSubnode(self.textNode) } + func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.textColor = textColor + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) + self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let badgeSize = self.textNode.measure(constrainedSize) let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) diff --git a/Display/NavigationController.swift b/Display/NavigationController.swift index 5d4d6fab2b..e0fedbdb29 100644 --- a/Display/NavigationController.swift +++ b/Display/NavigationController.swift @@ -75,7 +75,7 @@ open class NavigationController: UINavigationController, ContainableController, self.containerLayout = layout self.view.frame = CGRect(origin: self.view.frame.origin, size: layout.size) - let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight) + let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) if let topViewController = self.topViewController { if let topViewController = topViewController as? ContainableController { @@ -220,13 +220,16 @@ open class NavigationController: UINavigationController, ContainableController, } public func pushViewController(_ controller: ViewController) { - self.view.endEditing(true) - let appliedLayout = self.containerLayout.withUpdatedInputHeight(nil) + 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 + 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) + let containerLayout = strongSelf.containerLayout.withUpdatedInputHeight(controller.hasActiveInput ? strongSelf.containerLayout.inputHeight : nil) + if containerLayout != appliedLayout { + controller.containerLayoutUpdated(containerLayout, transition: .immediate) } strongSelf.pushViewController(controller, animated: true) } @@ -320,7 +323,11 @@ open class NavigationController: UINavigationController, ContainableController, if let controller = topViewController as? ContainableController { var layoutToApply = self.containerLayout - if !self.viewControllers.contains(where: { $0 === controller }) { + var hasActiveInput = false + if let controller = controller as? ViewController { + hasActiveInput = controller.hasActiveInput + } + if !hasActiveInput { layoutToApply = layoutToApply.withUpdatedInputHeight(nil) } controller.containerLayoutUpdated(layoutToApply, transition: .immediate) diff --git a/Display/NotificationCenterUtils.m b/Display/NotificationCenterUtils.m index d1b1aa9bff..a99f1b4b15 100644 --- a/Display/NotificationCenterUtils.m +++ b/Display/NotificationCenterUtils.m @@ -29,6 +29,7 @@ static NSMutableArray *notificationHandlers() { } } + //printf("***** %s\n", [aName cStringUsingEncoding:NSUTF8StringEncoding]); [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo]; } diff --git a/Display/PresentationContext.swift b/Display/PresentationContext.swift index fd2cb9e088..bf91c03433 100644 --- a/Display/PresentationContext.swift +++ b/Display/PresentationContext.swift @@ -37,7 +37,7 @@ final class PresentationContext { return self.view != nil && self.layout != nil } - private var controllers: [ViewController] = [] + private(set) var controllers: [ViewController] = [] private var presentationDisposables = DisposableSet() diff --git a/Display/StatusBarManager.swift b/Display/StatusBarManager.swift index 71e6d8f376..6f12a61d1e 100644 --- a/Display/StatusBarManager.swift +++ b/Display/StatusBarManager.swift @@ -79,13 +79,13 @@ class StatusBarManager { self.host = host } - func updateState(surfaces: [StatusBarSurface], forceInCallStatusBarText: String?, animated: Bool) { + func updateState(surfaces: [StatusBarSurface], forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool) { let previousSurfaces = self.surfaces self.surfaces = surfaces - self.updateSurfaces(previousSurfaces, forceInCallStatusBarText: forceInCallStatusBarText, animated: animated) + self.updateSurfaces(previousSurfaces, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: animated) } - private func updateSurfaces(_ previousSurfaces: [StatusBarSurface], forceInCallStatusBarText: String?, animated: Bool) { + private func updateSurfaces(_ previousSurfaces: [StatusBarSurface], forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool) { let statusBarFrame = self.host.statusBarFrame guard let statusBarView = self.host.statusBarView else { return @@ -215,7 +215,7 @@ class StatusBarManager { statusBar.updateState(statusBar: statusBarView, inCallText: forceInCallStatusBarText, animated: animated) } - if let globalStatusBar = globalStatusBar { + if let globalStatusBar = globalStatusBar, !forceHiddenBySystemWindows { let statusBarStyle: UIStatusBarStyle if forceInCallStatusBarText != nil { statusBarStyle = .lightContent diff --git a/Display/StatusBarProxyNode.swift b/Display/StatusBarProxyNode.swift index 83808959d6..2786088fe0 100644 --- a/Display/StatusBarProxyNode.swift +++ b/Display/StatusBarProxyNode.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import AsyncDisplayKit public enum StatusBarStyle { @@ -6,6 +7,28 @@ public enum StatusBarStyle { case White case Ignore case Hide + + public init(systemStyle: UIStatusBarStyle) { + switch systemStyle { + case .default: + self = .Black + case .lightContent: + self = .White + case .blackOpaque: + self = .Black + } + } + + public var systemStyle: UIStatusBarStyle { + switch self { + case .Black: + return .default + case .White: + return .lightContent + default: + return .default + } + } } private enum StatusBarItemType { diff --git a/Display/TabBarContollerNode.swift b/Display/TabBarContollerNode.swift index baf4d120db..0b7bed48e5 100644 --- a/Display/TabBarContollerNode.swift +++ b/Display/TabBarContollerNode.swift @@ -36,14 +36,17 @@ final class TabBarControllerNode: ASDisplayNode { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let update = { - self.tabBarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - layout.insets(options: []).bottom - 49.0), size: CGSize(width: layout.size.width, height: 49.0)) - self.tabBarNode.layout() + let tabBarHeight = 49.0 + layout.insets(options: []).bottom + self.tabBarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + if self.tabBarNode.isNodeLoaded { + self.tabBarNode.layout() + } } switch transition { case .immediate: update() - case let .animated(duration, curve): + case .animated: update() } } diff --git a/Display/TabBarController.swift b/Display/TabBarController.swift index 4434010aa0..daf9c043ee 100644 --- a/Display/TabBarController.swift +++ b/Display/TabBarController.swift @@ -10,15 +10,17 @@ public final class TabBarControllerTheme { public let tabBarTextColor: UIColor public let tabBarSelectedTextColor: UIColor public let tabBarBadgeBackgroundColor: UIColor + public let tabBarBadgeStrokeColor: UIColor public let tabBarBadgeTextColor: UIColor - public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeTextColor: UIColor) { + public init(backgroundColor: UIColor, tabBarBackgroundColor: UIColor, tabBarSeparatorColor: UIColor, tabBarTextColor: UIColor, tabBarSelectedTextColor: UIColor, tabBarBadgeBackgroundColor: UIColor, tabBarBadgeStrokeColor: UIColor, tabBarBadgeTextColor: UIColor) { self.backgroundColor = backgroundColor self.tabBarBackgroundColor = tabBarBackgroundColor self.tabBarSeparatorColor = tabBarSeparatorColor self.tabBarTextColor = tabBarTextColor self.tabBarSelectedTextColor = tabBarSelectedTextColor self.tabBarBadgeBackgroundColor = tabBarBadgeBackgroundColor + self.tabBarBadgeStrokeColor = tabBarBadgeStrokeColor self.tabBarBadgeTextColor = tabBarBadgeTextColor } } @@ -32,20 +34,16 @@ open class TabBarController: ViewController { } } - public var controllers: [ViewController] = [] { - didSet { - self.tabBarControllerNode.tabBarNode.tabBarItems = self.controllers.map({ $0.tabBarItem }) - - if oldValue.count == 0 && self.controllers.count != 0 { - self.updateSelectedIndex() - } - } - } + private var controllers: [ViewController] = [] - private var _selectedIndex: Int = 2 + private var _selectedIndex: Int? public var selectedIndex: Int { get { - return _selectedIndex + if let _selectedIndex = self._selectedIndex { + return _selectedIndex + } else { + return 0 + } } set(value) { let index = max(0, min(self.controllers.count - 1, value)) if _selectedIndex != index { @@ -122,8 +120,8 @@ open class TabBarController: ViewController { self.currentController = nil } - if self._selectedIndex < self.controllers.count { - self.currentController = self.controllers[self._selectedIndex] + if let _selectedIndex = self._selectedIndex, _selectedIndex < self.controllers.count { + self.currentController = self.controllers[_selectedIndex] } var displayNavigationBar = false @@ -150,6 +148,8 @@ open class TabBarController: ViewController { if self.displayNavigationBar != displayNavigationBar { self.setDisplayNavigationBar(displayNavigationBar) } + + self.tabBarControllerNode.containerLayoutUpdated(self.containerLayout, transition: .immediate) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -177,4 +177,22 @@ open class TabBarController: ViewController { currentController.viewDidDisappear(animated) } } + + public func setControllers(_ controllers: [ViewController], selectedIndex: Int?) { + var updatedSelectedIndex: Int? = selectedIndex + if updatedSelectedIndex == nil, let selectedIndex = self._selectedIndex, selectedIndex < self.controllers.count { + if let index = controllers.index(where: { $0 === self.controllers[selectedIndex] }) { + updatedSelectedIndex = index + } else { + updatedSelectedIndex = 0 + } + } + self.controllers = controllers + self.tabBarControllerNode.tabBarNode.tabBarItems = self.controllers.map({ $0.tabBarItem }) + + if let updatedSelectedIndex = updatedSelectedIndex { + self.selectedIndex = updatedSelectedIndex + self.updateSelectedIndex() + } + } } diff --git a/Display/TabBarNode.swift b/Display/TabBarNode.swift index fbfc1169bb..087e614ce7 100644 --- a/Display/TabBarNode.swift +++ b/Display/TabBarNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit private let separatorHeight: CGFloat = 1.0 / UIScreen.main.scale private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor) -> UIImage? { - let font = Font.regular(10.0) + let font = Font.medium(10.0) let titleSize = (title as NSString).boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], attributes: [NSAttributedStringKey.font: font], context: nil).size let imageSize: CGSize @@ -22,7 +22,7 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: context.fill(CGRect(origin: CGPoint(), size: size)) if let image = image { - let imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: 0.0), size: imageSize) + let imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: 1.0), size: imageSize) context.saveGState() context.translateBy(x: imageRect.midX, y: imageRect.midY) context.scaleBy(x: 1.0, y: -1.0) @@ -34,7 +34,7 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: } } - (title as NSString).draw(at: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 3.0), withAttributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: tintColor]) + (title as NSString).draw(at: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 2.0), withAttributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: tintColor]) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() @@ -150,7 +150,7 @@ class TabBarNode: ASDisplayNode { self.separatorNode.isOpaque = true self.separatorNode.isLayerBacked = true - self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, backgroundColor: nil)! + self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, strokeColor: theme.tabBarBadgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)! super.init() @@ -167,7 +167,12 @@ class TabBarNode: ASDisplayNode { self.separatorNode.backgroundColor = theme.tabBarSeparatorColor self.backgroundColor = theme.tabBarBackgroundColor - self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, backgroundColor: nil)! + self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, strokeColor: theme.tabBarBadgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)! + for container in self.tabBarNodeContainers { + if let attributedText = container.badgeTextNode.attributedText, !attributedText.string.isEmpty { + container.badgeTextNode.attributedText = NSAttributedString(string: attributedText.string, font: badgeFont, textColor: self.theme.tabBarBadgeTextColor) + } + } for i in 0 ..< self.tabBarItems.count { self.updateNodeImage(i) @@ -275,7 +280,7 @@ class TabBarNode: ASDisplayNode { if container.badgeValue != container.appliedBadgeValue { container.appliedBadgeValue = container.badgeValue if let badgeValue = container.badgeValue, !badgeValue.isEmpty { - container.badgeTextNode.attributedText = NSAttributedString(string: badgeValue, font: badgeFont, textColor: .white) + container.badgeTextNode.attributedText = NSAttributedString(string: badgeValue, font: badgeFont, textColor: self.theme.tabBarBadgeTextColor) container.badgeBackgroundNode.isHidden = false container.badgeTextNode.isHidden = false } else { @@ -287,7 +292,7 @@ class TabBarNode: ASDisplayNode { if !container.badgeBackgroundNode.isHidden { let badgeSize = container.badgeTextNode.measure(CGSize(width: 200.0, height: 100.0)) let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) - let backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.calculatedSize.width / 2.0) - 3.0 + node.calculatedSize.width - backgroundSize.width - 1.0, y: 2.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) - 3.0 + node.frame.width - backgroundSize.width - 1.0, y: 2.0), size: backgroundSize) container.badgeBackgroundNode.frame = backgroundFrame container.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: 3.0), size: badgeSize) } diff --git a/Display/TextAlertController.swift b/Display/TextAlertController.swift index 314b14833e..8bf98938ca 100644 --- a/Display/TextAlertController.swift +++ b/Display/TextAlertController.swift @@ -33,7 +33,8 @@ private final class TextAlertContentActionNode: HighlightableButtonNode { super.init() - self.setTitle(action.title, with: action.type == .defaultAction ? Font.medium(17.0) : Font.regular(17.0), with: UIColor(rgb: 0x007ee5), for: []) + self.titleNode.maximumNumberOfLines = 2 + self.setAttributedTitle(NSAttributedString(string: action.title, font: Font.regular(17.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center), for: []) self.highligthedChanged = { [weak self] value in if let strongSelf = self { diff --git a/Display/UIViewController+Navigation.h b/Display/UIViewController+Navigation.h index 1cbc548127..62e2c3d9b9 100644 --- a/Display/UIViewController+Navigation.h +++ b/Display/UIViewController+Navigation.h @@ -7,12 +7,17 @@ - (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller; - (void)navigation_setPresentingViewController:(UIViewController * _Nullable)presentingViewController; - (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:( UIViewController * _Nullable )rootController; +- (void)state_setNeedsStatusBarAppearanceUpdate:(void (^_Nullable)())block; @end @interface UIView (Navigation) @property (nonatomic) bool disablesInteractiveTransitionGestureRecognizer; +@property (nonatomic) bool disablesAutomaticKeyboardHandling; + +- (void)input_setInputAccessoryHeightProvider:(CGFloat (^_Nullable)())block; +- (CGFloat)input_getInputAccessoryHeight; @end diff --git a/Display/UIViewController+Navigation.m b/Display/UIViewController+Navigation.m index dade219ffc..77c3643a11 100644 --- a/Display/UIViewController+Navigation.m +++ b/Display/UIViewController+Navigation.m @@ -35,6 +35,9 @@ static const void *UIViewControllerNavigationControllerKey = &UIViewControllerNa static const void *UIViewControllerPresentingControllerKey = &UIViewControllerPresentingControllerKey; static const void *UIViewControllerPresentingProxyControllerKey = &UIViewControllerPresentingProxyControllerKey; static const void *disablesInteractiveTransitionGestureRecognizerKey = &disablesInteractiveTransitionGestureRecognizerKey; +static const void *disablesAutomaticKeyboardHandlingKey = &disablesAutomaticKeyboardHandlingKey; +static const void *setNeedsStatusBarAppearanceUpdateKey = &setNeedsStatusBarAppearanceUpdateKey; +static const void *inputAccessoryHeightProviderKey = &inputAccessoryHeightProviderKey; static bool notyfyingShiftState = false; @@ -96,6 +99,7 @@ static bool notyfyingShiftState = false; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(navigationController) newSelector:@selector(_65087dc8_navigationController)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentingViewController) newSelector:@selector(_65087dc8_presentingViewController)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)]; //[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIKeyboardImpl") currentSelector:@selector(notifyShiftState) withAnotherClass:[UIKeyboardImpl_65087dc8 class] newSelector:@selector(notifyShiftState)]; //[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIInputWindowController") currentSelector:@selector(updateViewConstraints) withAnotherClass:[UIInputWindowController_65087dc8 class] newSelector:@selector(updateViewConstraints)]; @@ -189,6 +193,19 @@ static bool notyfyingShiftState = false; [self _65087dc8_presentViewController:viewControllerToPresent animated:flag completion:completion]; } +- (void)_65087dc8_setNeedsStatusBarAppearanceUpdate { + [self _65087dc8_setNeedsStatusBarAppearanceUpdate]; + + void (^block)() = [self associatedObjectForKey:setNeedsStatusBarAppearanceUpdateKey]; + if (block) { + block(); + } +} + +- (void)state_setNeedsStatusBarAppearanceUpdate:(void (^_Nullable)())block { + [self setAssociatedObject:[block copy] forKey:setNeedsStatusBarAppearanceUpdateKey]; +} + @end @implementation UIView (Navigation) @@ -201,6 +218,26 @@ static bool notyfyingShiftState = false; [self setAssociatedObject:@(disablesInteractiveTransitionGestureRecognizer) forKey:disablesInteractiveTransitionGestureRecognizerKey]; } +- (bool)disablesAutomaticKeyboardHandling { + return [[self associatedObjectForKey:disablesAutomaticKeyboardHandlingKey] boolValue]; +} + +- (void)setDisablesAutomaticKeyboardHandling:(bool)disablesAutomaticKeyboardHandling { + [self setAssociatedObject:@(disablesAutomaticKeyboardHandling) forKey:disablesAutomaticKeyboardHandlingKey]; +} + +- (void)input_setInputAccessoryHeightProvider:(CGFloat (^_Nullable)())block { + [self setAssociatedObject:[block copy] forKey:inputAccessoryHeightProviderKey]; +} + +- (CGFloat)input_getInputAccessoryHeight { + CGFloat (^block)() = [self associatedObjectForKey:inputAccessoryHeightProviderKey]; + if (block) { + return block(); + } + return 0.0f; +} + @end static NSString *TGEncodeText(NSString *string, int key) diff --git a/Display/ViewController.swift b/Display/ViewController.swift index d5205cd68f..f4f88ae472 100644 --- a/Display/ViewController.swift +++ b/Display/ViewController.swift @@ -38,7 +38,14 @@ open class ViewControllerPresentationArguments { return self.supportedOrientations } - public final var deferScreenEdgeGestures: UIRectEdge = [] + public final var deferScreenEdgeGestures: UIRectEdge = [] { + didSet { + if self.deferScreenEdgeGestures != oldValue { + self.window?.invalidateDeferScreenEdgeGestures() + } + } + } + override open func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge { return .bottom } @@ -76,6 +83,8 @@ open class ViewControllerPresentationArguments { private weak var activeInputViewCandidate: UIResponder? private weak var activeInputView: UIResponder? + open var hasActiveInput: Bool = false + private var navigationBarOrigin: CGFloat = 0.0 public var navigationOffset: CGFloat = 0.0 { @@ -160,15 +169,22 @@ open class ViewControllerPresentationArguments { if !self.isViewLoaded { self.loadView() } - self.view.frame = CGRect(origin: self.view.frame.origin, size: layout.size) + transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) if let _ = layout.statusBarHeight { self.statusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0)) } let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 - var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, statusBarHeight - 20.0)), size: CGSize(width: layout.size.width, height: 64.0)) + let navigationBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0 + let navigationBarOffset: CGFloat + if statusBarHeight.isZero { + navigationBarOffset = -20.0 + } else { + navigationBarOffset = 0.0 + } + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: navigationBarHeight)) if layout.statusBarHeight == nil { - navigationBarFrame.size.height = 44.0 + navigationBarFrame.size.height = 64.0 } if !self.displayNavigationBar { diff --git a/Display/WindowContent.swift b/Display/WindowContent.swift index a515a931d7..8525275dc0 100644 --- a/Display/WindowContent.swift +++ b/Display/WindowContent.swift @@ -14,12 +14,14 @@ private class WindowRootViewController: UIViewController { } private struct WindowLayout: Equatable { - public let size: CGSize - public let metrics: LayoutMetrics - public let statusBarHeight: CGFloat? - public let forceInCallStatusBarText: String? - public let inputHeight: CGFloat? - public let inputMinimized: Bool + let size: CGSize + let metrics: LayoutMetrics + let statusBarHeight: CGFloat? + let forceInCallStatusBarText: String? + let inputHeight: CGFloat? + let safeInsets: UIEdgeInsets + let onScreenNavigationHeight: CGFloat? + let upperKeyboardInputPositionBound: CGFloat? static func ==(lhs: WindowLayout, rhs: WindowLayout) -> Bool { if !lhs.size.equalTo(rhs.size) { @@ -54,7 +56,15 @@ private struct WindowLayout: Equatable { return false } - if lhs.inputMinimized != rhs.inputMinimized { + if lhs.safeInsets != rhs.safeInsets { + return false + } + + if lhs.onScreenNavigationHeight != rhs.onScreenNavigationHeight { + return false + } + + if lhs.upperKeyboardInputPositionBound != rhs.upperKeyboardInputPositionBound { return false } @@ -81,44 +91,58 @@ private struct UpdatingLayout { mutating func update(size: CGSize, metrics: LayoutMetrics, forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) - self.layout = WindowLayout(size: size, metrics: metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, inputMinimized: self.layout.inputMinimized) + self.layout = WindowLayout(size: size, metrics: metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) } mutating func update(forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) - self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, inputMinimized: self.layout.inputMinimized) + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) } mutating func update(statusBarHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) - self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, inputMinimized: self.layout.inputMinimized) + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) } mutating func update(inputHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) - self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: inputHeight, inputMinimized: self.layout.inputMinimized) + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) } - mutating func update(inputMinimized: Bool, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + mutating func update(safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { self.update(transition: transition, override: overrideTransition) - self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, inputMinimized: inputMinimized) + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) + } + + mutating func update(onScreenNavigationHeight: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: self.layout.upperKeyboardInputPositionBound) + } + + mutating func update(upperKeyboardInputPositionBound: CGFloat?, transition: ContainedViewLayoutTransition, overrideTransition: Bool) { + self.update(transition: transition, override: overrideTransition) + + self.layout = WindowLayout(size: self.layout.size, metrics: self.layout.metrics, statusBarHeight: self.layout.statusBarHeight, forceInCallStatusBarText: self.layout.forceInCallStatusBarText, inputHeight: self.layout.inputHeight, safeInsets: self.layout.safeInsets, onScreenNavigationHeight: self.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: upperKeyboardInputPositionBound) } } private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3 private let statusBarHiddenInLandscape: Bool = UIDevice.current.userInterfaceIdiom == .phone -private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> ContainerViewLayout { - var inputHeight: CGFloat? = layout.inputHeight - if let inputHeightValue = inputHeight, layout.inputMinimized { - inputHeight = floor(0.85 * inputHeightValue) +private func inputHeightOffsetForLayout(_ layout: WindowLayout) -> CGFloat { + if let inputHeight = layout.inputHeight, let upperBound = layout.upperKeyboardInputPositionBound { + return max(0.0, upperBound - (layout.size.height - inputHeight)) } - + return 0.0 +} + +private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> ContainerViewLayout { let resolvedStatusBarHeight: CGFloat? if let statusBarHeight = layout.statusBarHeight { if layout.forceInCallStatusBarText != nil { @@ -130,7 +154,94 @@ private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> Container resolvedStatusBarHeight = nil } - return ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(), statusBarHeight: resolvedStatusBarHeight, inputHeight: inputHeight) + var updatedInputHeight = layout.inputHeight + if let inputHeight = updatedInputHeight, let _ = layout.upperKeyboardInputPositionBound { + updatedInputHeight = inputHeight - inputHeightOffsetForLayout(layout) + } + + var resolvedSafeInsets = layout.safeInsets + if layout.size.height.isEqual(to: 375.0) && layout.size.width.isEqual(to: 812.0) { + resolvedSafeInsets.left = 44.0 + 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) +} + +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +private func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView) -> Bool { + if view.disablesInteractiveTransitionGestureRecognizer { + return true + } + if let superview = view.superview { + return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview) + } + return false +} + +private let transitionClass: AnyClass? = NSClassFromString(encodeText("VJUsbotjujpoWjfx", -1)) +private let previewingClass: AnyClass? = NSClassFromString("UIVisualEffectView") +private let previewingActionGroupClass: AnyClass? = NSClassFromString("UIInterfaceActionGroupView") +private func checkIsPreviewingView(_ view: UIView) -> Bool { + if let transitionClass = transitionClass, view.isKind(of: transitionClass) { + for subview in view.subviews { + if let previewingClass = previewingClass, subview.isKind(of: previewingClass) { + return true + } + } + } + return false +} + +private func applyThemeToPreviewingView(_ view: UIView, accentColor: UIColor, darkBlur: Bool) { + if let previewingActionGroupClass = previewingActionGroupClass, view.isKind(of: previewingActionGroupClass) { + view.tintColor = accentColor + if darkBlur { + applyThemeToPreviewingEffectView(view) + } + return + } + + for subview in view.subviews { + applyThemeToPreviewingView(subview, accentColor: accentColor, darkBlur: darkBlur) + } +} + +private func applyThemeToPreviewingEffectView(_ view: UIView) { + if let previewingClass = previewingClass, view.isKind(of: previewingClass) { + if let view = view as? UIVisualEffectView { + view.effect = UIBlurEffect(style: .dark) + } + } + + for subview in view.subviews { + applyThemeToPreviewingEffectView(subview) + } +} + +private func getFirstResponderAndAccessoryHeight(_ view: UIView, _ accessoryHeight: CGFloat? = nil) -> (UIView?, CGFloat?) { + if view.isFirstResponder { + return (view, accessoryHeight) + } else { + var updatedAccessoryHeight = accessoryHeight + if let view = view as? WindowInputAccessoryHeightProvider { + updatedAccessoryHeight = view.getWindowInputAccessoryHeight() + } + for subview in view.subviews { + let (result, resultHeight) = getFirstResponderAndAccessoryHeight(subview, updatedAccessoryHeight) + if let result = result { + return (result, resultHeight) + } + } + return (nil, nil) + } } public final class WindowHostView { @@ -147,6 +258,7 @@ public final class WindowHostView { var updateToInterfaceOrientation: (() -> Void)? var isUpdatingOrientationLayout = false var hitTest: ((CGPoint, UIEvent?) -> UIView?)? + var invalidateDeferScreenEdgeGesture: (() -> Void)? init(view: UIView, isRotating: @escaping () -> Bool, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void) { self.view = view @@ -163,12 +275,23 @@ public struct WindowTracingTags { public protocol WindowHost { func present(_ controller: ViewController, on level: PresentationSurfaceLevel) + func invalidateDeferScreenEdgeGestures() } private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics { return LayoutMetrics(widthClass: .compact, heightClass: .compact) } +private final class KeyboardGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + public class Window1 { public let hostView: WindowHostView @@ -180,13 +303,21 @@ public class Window1 { private var windowLayout: WindowLayout private var updatingLayout: UpdatingLayout? + private var updatedContainerLayout: ContainerViewLayout? + private var upperKeyboardInputPositionBound: CGFloat? + private var cachedWindowSubviewCount: Int = 0 + private var cachedHasPreview: Bool = false private let presentationContext: PresentationContext private var tracingStatusBarsInvalidated = false + private var shouldUpdateDeferScreenEdgeGestures = false private var statusBarHidden = false + public var previewThemeAccentColor: UIColor = .blue + public var previewThemeDarkBlur: Bool = false + public private(set) var forceInCallStatusBarText: String? = nil public var inCallNavigate: (() -> Void)? { didSet { @@ -194,6 +325,10 @@ public class Window1 { } } + private let keyboardGestureRecognizerDelegate = KeyboardGestureRecognizerDelegate() + private var keyboardGestureBeginLocation: CGPoint? + private var keyboardGestureAccessoryHeight: CGFloat? + public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) { self.hostView = hostView @@ -209,14 +344,16 @@ public class Window1 { statusBarHeight = 20.0 } - let minimized: Bool - if let keyboardManager = self.keyboardManager { - minimized = keyboardManager.minimized - } else { - minimized = false + let boundsSize = self.hostView.view.bounds.size + + var onScreenNavigationHeight: CGFloat? + if (boundsSize.width.isEqual(to: 375.0) && boundsSize.height.isEqual(to: 812.0)) || boundsSize.height.isEqual(to: 375.0) && boundsSize.width.isEqual(to: 812.0) { + onScreenNavigationHeight = 20.0 } - self.windowLayout = WindowLayout(size: self.hostView.view.bounds.size, metrics: layoutMetricsForScreenSize(self.hostView.view.bounds.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, inputMinimized: minimized) + let safeInsets = UIEdgeInsets() + + self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(self.hostView.view.bounds.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil) self.presentationContext = PresentationContext() self.hostView.present = { [weak self] controller, level in @@ -247,12 +384,8 @@ public class Window1 { return self?.hitTest(point, with: event) } - 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.hostView.invalidateDeferScreenEdgeGesture = { [weak self] in + self?.invalidateDeferScreenEdgeGestures() } self.presentationContext.view = self.hostView.view @@ -287,6 +420,22 @@ public class Window1 { strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: .animated(duration: duration, curve: transitionCurve), overrideTransition: false) } } }) + + let recognizer = WindowPanRecognizer(target: self, action: #selector(self.panGesture(_:))) + recognizer.cancelsTouchesInView = false + recognizer.delaysTouchesBegan = false + recognizer.delaysTouchesEnded = false + recognizer.delegate = self.keyboardGestureRecognizerDelegate + recognizer.began = { [weak self] point in + self?.panGestureBegan(location: point) + } + recognizer.moved = { [weak self] point in + self?.panGestureMoved(location: point) + } + recognizer.ended = { [weak self] point, velocity in + self?.panGestureEnded(location: point, velocity: velocity) + } + self.hostView.view.addGestureRecognizer(recognizer) } public required init(coder aDecoder: NSCoder) { @@ -317,6 +466,11 @@ public class Window1 { self.hostView.view.setNeedsLayout() } + public func invalidateDeferScreenEdgeGestures() { + self.shouldUpdateDeferScreenEdgeGestures = true + self.hostView.view.setNeedsLayout() + } + public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for view in self.hostView.view.subviews.reversed() { if NSStringFromClass(type(of: view)) == "UITransitionView" { @@ -391,11 +545,25 @@ public class Window1 { } private func layoutSubviews() { - if self.tracingStatusBarsInvalidated, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { + var hasPreview = false + var updatedHasPreview = false + for subview in self.hostView.view.subviews { + if checkIsPreviewingView(subview) { + applyThemeToPreviewingView(subview, accentColor: self.previewThemeAccentColor, darkBlur: self.previewThemeDarkBlur) + hasPreview = true + break + } + } + if hasPreview != self.cachedHasPreview { + self.cachedHasPreview = hasPreview + updatedHasPreview = true + } + + if self.tracingStatusBarsInvalidated || updatedHasPreview, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { self.tracingStatusBarsInvalidated = false if self.statusBarHidden { - statusBarManager.updateState(surfaces: [], forceInCallStatusBarText: nil, animated: false) + statusBarManager.updateState(surfaces: [], forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, animated: false) } else { var statusBarSurfaces: [StatusBarSurface] = [] for layers in self.hostView.view.layer.traceableLayerSurfaces(withTag: WindowTracingTags.statusBar) { @@ -415,7 +583,8 @@ public class Window1 { animatedUpdate = true } } - statusBarManager.updateState(surfaces: statusBarSurfaces, forceInCallStatusBarText: self.forceInCallStatusBarText, animated: animatedUpdate) + self.cachedWindowSubviewCount = self.hostView.view.window?.subviews.count ?? 0 + statusBarManager.updateState(surfaces: statusBarSurfaces, forceInCallStatusBarText: self.forceInCallStatusBarText, forceHiddenBySystemWindows: hasPreview, animated: animatedUpdate) } var keyboardSurfaces: [KeyboardSurface] = [] @@ -429,7 +598,13 @@ public class Window1 { keyboardManager.surfaces = keyboardSurfaces self.hostView.updateSupportedInterfaceOrientations(self.presentationContext.combinedSupportedOrientations()) - self.hostView.updateDeferScreenEdgeGestures(self.presentationContext.combinedDeferScreenEdgeGestures()) + self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) + + self.shouldUpdateDeferScreenEdgeGestures = false + } else if self.shouldUpdateDeferScreenEdgeGestures { + self.shouldUpdateDeferScreenEdgeGestures = false + + self.hostView.updateDeferScreenEdgeGestures(self.collectScreenEdgeGestures()) } if !UIWindow.isDeviceRotating() { @@ -467,10 +642,16 @@ public class Window1 { private func updateLayout(_ update: (inout UpdatingLayout) -> ()) { if self.updatingLayout == nil { - self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) + var updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) + update(&updatingLayout) + if updatingLayout.layout != self.windowLayout { + self.updatingLayout = updatingLayout + self.hostView.view.setNeedsLayout() + } + } else { + update(&self.updatingLayout!) + self.hostView.view.setNeedsLayout() } - update(&self.updatingLayout!) - self.hostView.view.setNeedsLayout() } private func commitUpdatingLayout() { @@ -494,13 +675,33 @@ public class Window1 { self.tracingStatusBarsInvalidated = true self.hostView.view.setNeedsLayout() } - self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, inputMinimized: updatingLayout.layout.inputMinimized) + let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) + self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: updatingLayout.layout.onScreenNavigationHeight, upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound) - self._rootController?.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition) - self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition) + let childLayout = containedLayoutForWindowLayout(self.windowLayout) + let childLayoutUpdated = self.updatedContainerLayout != childLayout + self.updatedContainerLayout = childLayout - for controller in self.topLevelOverlayControllers { - controller.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: updatingLayout.transition) + if childLayoutUpdated { + self._rootController?.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + + for controller in self.topLevelOverlayControllers { + controller.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + } + } + + let updatedInputOffset = inputHeightOffsetForLayout(self.windowLayout) + if !previousInputOffset.isEqual(to: updatedInputOffset) { + let hide = updatingLayout.transition.isAnimated && updatingLayout.layout.upperKeyboardInputPositionBound == updatingLayout.layout.size.height + self.keyboardManager?.updateInteractiveInputOffset(updatedInputOffset, transition: updatingLayout.transition, completion: { [weak self] in + if let strongSelf = self, hide { + strongSelf.updateLayout { + $0.update(upperKeyboardInputPositionBound: nil, transition: .immediate, overrideTransition: false) + } + strongSelf.hostView.view.endEditing(true) + } + }) } } } @@ -513,4 +714,83 @@ public class Window1 { public func presentNative(_ controller: UIViewController) { } + + private func panGestureBegan(location: CGPoint) { + let keyboardGestureBeginLocation = location + let view = self.hostView.view + let (firstResponder, accessoryHeight) = getFirstResponderAndAccessoryHeight(view) + if let inputHeight = self.windowLayout.inputHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < self.windowLayout.size.height - inputHeight - (accessoryHeight ?? 0.0) { + var enableGesture = true + if let view = self.hostView.view.hitTest(location, with: nil) { + if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { + enableGesture = false + } + } + if enableGesture, let _ = firstResponder { + self.keyboardGestureBeginLocation = keyboardGestureBeginLocation + self.keyboardGestureAccessoryHeight = accessoryHeight + } + } + } + + private func panGestureMoved(location: CGPoint) { + if let keyboardGestureBeginLocation = self.keyboardGestureBeginLocation { + let currentLocation = location + let deltaY = keyboardGestureBeginLocation.y - location.y + if deltaY * deltaY >= 3.0 * 3.0 || self.windowLayout.upperKeyboardInputPositionBound != nil { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0), transition: .immediate, overrideTransition: false) + } + } + } + } + + private func panGestureEnded(location: CGPoint, velocity: CGPoint?) { + self.keyboardGestureBeginLocation = nil + let currentLocation = location + if let velocity = velocity, let inputHeight = self.windowLayout.inputHeight, velocity.y > 100.0 && currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) > self.windowLayout.size.height - inputHeight { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: self.windowLayout.size.height, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) + } + } else { + self.updateLayout { + $0.update(upperKeyboardInputPositionBound: nil, transition: .animated(duration: 0.25, curve: .spring), overrideTransition: false) + } + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.panGestureBegan(location: recognizer.location(in: recognizer.view)) + case .changed: + self.panGestureMoved(location: recognizer.location(in: recognizer.view)) + case .ended: + self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: recognizer.velocity(in: recognizer.view)) + case .cancelled: + self.panGestureEnded(location: recognizer.location(in: recognizer.view), velocity: nil) + default: + break + } + } + + private func collectScreenEdgeGestures() -> UIRectEdge { + var edges = self.presentationContext.combinedDeferScreenEdgeGestures() + + for controller in self.topLevelOverlayControllers { + if let controller = controller as? ViewController { + edges = edges.union(controller.deferScreenEdgeGestures) + } + } + + return edges + } + + public func forEachViewController(_ f: (ViewController) -> Bool) { + for controller in self.presentationContext.controllers { + if !f(controller) { + break + } + } + } } diff --git a/Display/WindowInputAccessoryHeightProvider.swift b/Display/WindowInputAccessoryHeightProvider.swift new file mode 100644 index 0000000000..ef51090cbc --- /dev/null +++ b/Display/WindowInputAccessoryHeightProvider.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit + +public protocol WindowInputAccessoryHeightProvider: class { + func getWindowInputAccessoryHeight() -> CGFloat +} diff --git a/Display/WindowPanRecognizer.swift b/Display/WindowPanRecognizer.swift new file mode 100644 index 0000000000..85efc21149 --- /dev/null +++ b/Display/WindowPanRecognizer.swift @@ -0,0 +1,80 @@ +import Foundation + +final class WindowPanRecognizer: UIGestureRecognizer { + var began: ((CGPoint) -> Void)? + var moved: ((CGPoint) -> Void)? + var ended: ((CGPoint, CGPoint?) -> Void)? + + private var previousPoints: [(CGPoint, Double)] = [] + + override func reset() { + super.reset() + + self.previousPoints.removeAll() + } + + private func addPoint(_ point: CGPoint) { + self.previousPoints.append((point, CACurrentMediaTime())) + if self.previousPoints.count > 6 { + self.previousPoints.removeFirst() + } + } + + private func estimateVerticalVelocity() -> CGFloat { + let timestamp = CACurrentMediaTime() + var sum: CGFloat = 0.0 + var count = 0 + if self.previousPoints.count > 1 { + for i in 1 ..< self.previousPoints.count { + if self.previousPoints[i].1 >= timestamp - 0.1 { + sum += (self.previousPoints[i].0.y - self.previousPoints[i - 1].0.y) / CGFloat(self.previousPoints[i].1 - self.previousPoints[i - 1].1) + count += 1 + } + } + } + + if count != 0 { + return sum / CGFloat(count * 5) + } else { + return 0.0 + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.began?(location) + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.moved?(location) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if let touch = touches.first { + let location = touch.location(in: self.view) + self.addPoint(location) + self.ended?(location, CGPoint(x: 0.0, y: self.estimateVerticalVelocity())) + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if let touch = touches.first { + self.ended?(touch.location(in: self.view), nil) + } + } +}