From decdb54c5ad90fd51513099a8e86132d256b9443 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Tue, 10 Sep 2019 17:22:18 +0400 Subject: [PATCH] Allow separate master and detail navigation --- .../ChatListUI/Sources/ChatContextMenus.swift | 2 +- .../Sources/ChatListController.swift | 3 + .../Sources/ContactsController.swift | 4 +- .../Sources/ContextActionsContainerNode.swift | 31 +- .../Sources/ContextContentSourceNode.swift | 4 +- .../ContextUI/Sources/ContextController.swift | 226 +++++-- .../Sources/ContextControllerSourceNode.swift | 8 +- .../ContextUI/Sources/ContextGesture.swift | 38 +- .../Display/ContainableController.swift | 1 + .../Display/Display/DisplayLinkAnimator.swift | 7 +- .../Display/NavigationController.swift | 637 ++++++++++++++---- .../Display/Display/ViewController.swift | 26 +- .../GalleryUI/Sources/GalleryController.swift | 10 +- .../TelegramUI/TelegramRootController.swift | 3 +- 14 files changed, 789 insertions(+), 211 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 50be5f7c80..6e6860de4f 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -114,7 +114,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, source: ChatC if let (group, index) = groupAndIndex { if !isSavedMessages { let isArchived = group == Namespaces.PeerGroup.archive - items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Archive : strings.ChatList_Context_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in + items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in if isArchived { let _ = (context.account.postbox.transaction { transaction -> Void in updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ce97d0e0b7..6eed3e288f 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -509,6 +509,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if let strongSelf = self { if let navigationController = strongSelf.navigationController as? NavigationController { let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId, controlsHistoryPreload: false, enableDebugActions: false) + chatListController.navigationPresentation = .master navigationController.pushViewController(chatListController) strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) } @@ -664,6 +665,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, switch item.content { case let .groupReference(groupReference): let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupReference.groupId, controlsHistoryPreload: false, enableDebugActions: false) + chatListController.navigationPresentation = .master let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupReference.groupId, chatListController: strongSelf), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case let .peer(peer): @@ -1094,6 +1096,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } case let .groupReference(groupId, _, _, _, _): let chatListController = ChatListControllerImpl(context: self.context, groupId: groupId, controlsHistoryPreload: false, enableDebugActions: false) + chatListController.navigationPresentation = .master chatListController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) return (chatListController, sourceRect) } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 17c4a536a9..1ef6b4a377 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -281,7 +281,8 @@ public class ContactsController: ViewController { } let presentPeersNearby = { let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context) - (strongSelf.navigationController as? NavigationController)?.replaceAllButRootController(controller, animated: true, completion: { [weak self] in + controller.navigationPresentation = .master + (strongSelf.navigationController as? NavigationController)?.pushViewController(controller, animated: true, completion: { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) } @@ -294,6 +295,7 @@ public class ContactsController: ViewController { default: let controller = PermissionController(context: strongSelf.context, splashScreen: false) controller.setState(.permission(.nearbyLocation(status: PermissionRequestStatus(accessType: status))), animated: false) + controller.navigationPresentation = .master controller.proceed = { result in if result { presentPeersNearby() diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 4aecca23d1..ad251153c8 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -10,6 +10,7 @@ private enum ContextItemNode { } final class ContextActionsContainerNode: ASDisplayNode { + private var effectView: UIVisualEffectView? private var itemNodes: [ContextItemNode] init(theme: PresentationTheme, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { @@ -51,8 +52,28 @@ final class ContextActionsContainerNode: ASDisplayNode { }) } - func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { - let minActionsWidth = min(constrainedWidth, max(250.0, floor(constrainedWidth / 3.0))) + func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + var minActionsWidth: CGFloat = 250.0 + switch widthClass { + case .compact: + minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0)) + if let effectView = self.effectView { + self.effectView = nil + effectView.removeFromSuperview() + } + case .regular: + if self.effectView == nil { + let effectView: UIVisualEffectView + if #available(iOS 10.0, *) { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + } else { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + } + self.effectView = effectView + self.view.insertSubview(effectView, at: 0) + } + } + minActionsWidth = min(minActionsWidth, constrainedWidth) let separatorHeight: CGFloat = 8.0 var maxWidth: CGFloat = 0.0 @@ -111,7 +132,11 @@ final class ContextActionsContainerNode: ASDisplayNode { } } - return CGSize(width: maxWidth, height: verticalOffset) + let size = CGSize(width: maxWidth, height: verticalOffset) + if let effectView = self.effectView { + transition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: size)) + } + return size } func updateTheme(theme: PresentationTheme) { diff --git a/submodules/ContextUI/Sources/ContextContentSourceNode.swift b/submodules/ContextUI/Sources/ContextContentSourceNode.swift index 80a93d9a56..edfbea80de 100644 --- a/submodules/ContextUI/Sources/ContextContentSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextContentSourceNode.swift @@ -27,9 +27,11 @@ public final class ContextExtractedContentNode: ASDisplayNode { } final class ContextControllerContentNode: ASDisplayNode { + let sourceNode: ASDisplayNode let controller: ViewController - init(controller: ViewController) { + init(sourceNode: ASDisplayNode, controller: ViewController) { + self.sourceNode = sourceNode self.controller = controller super.init() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 851071299c..331da8efd9 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -80,6 +80,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private var propertyAnimator: AnyObject? private var displayLinkAnimator: DisplayLinkAnimator? private let dimNode: ASDisplayNode + private let withoutBlurDimNode: ASDisplayNode private let dismissNode: ASDisplayNode private let clippingNode: ASDisplayNode @@ -132,6 +133,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.dimNode.backgroundColor = theme.contextMenu.dimColor self.dimNode.alpha = 0.0 + self.withoutBlurDimNode = ASDisplayNode() + self.withoutBlurDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + self.withoutBlurDimNode.alpha = 0.0 + self.dismissNode = ASDisplayNode() self.clippingNode = ASDisplayNode() @@ -164,8 +169,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.scrollNode.view.delegate = self - self.view.addSubview(self.effectView) self.addSubnode(self.dimNode) + self.addSubnode(self.withoutBlurDimNode) self.addSubnode(self.clippingNode) @@ -437,14 +442,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.originalProjectedContentViewFrame = (convertFrame(takenViewInfo.contentContainingNode.frame, from: parentSupernode.view, to: self.view), convertFrame(takenViewInfo.contentContainingNode.contentRect, from: takenViewInfo.contentContainingNode.view, to: self.view)) } case let .controller(source): - let contentParentNode = ContextControllerContentNode(controller: source.controller) - self.contentContainerNode.contentNode = .controller(contentParentNode) - self.contentContainerNode.clipsToBounds = true - self.contentContainerNode.cornerRadius = 14.0 - self.contentContainerNode.addSubnode(contentParentNode) - let transitionInfo = source.transitionInfo() if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() { + let contentParentNode = ContextControllerContentNode(sourceNode: sourceNode, controller: source.controller) + self.contentContainerNode.contentNode = .controller(contentParentNode) + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = 14.0 + self.contentContainerNode.addSubnode(contentParentNode) + let projectedFrame = convertFrame(sourceNodeRect, from: sourceNode.view, to: self.view) self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } @@ -483,8 +488,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) } - self.dimNode.alpha = 1.0 - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + if !self.dimNode.isHidden { + self.dimNode.alpha = 1.0 + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + } else { + self.withoutBlurDimNode.alpha = 1.0 + self.withoutBlurDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + } if #available(iOS 10.0, *) { if let propertyAnimator = self.propertyAnimator { @@ -662,7 +672,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) } - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedActionsNode = true intermediateCompletion() @@ -799,7 +814,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) } - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + if !self.dimNode.isHidden { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } else { + self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) + } self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedActionsNode = true intermediateCompletion() @@ -872,10 +891,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.reactionContextNode = nil reactionCompleted = true intermediateCompletion() - /*strongSelf.animateOut(result: .default, completion: { - reactionCompleted = true - intermediateCompletion() - })*/ }) } @@ -939,6 +954,31 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.withoutBlurDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + switch layout.metrics.widthClass { + case .compact: + if self.effectView.superview == nil { + self.view.insertSubview(self.effectView, at: 0) + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + } + self.effectView.effect = makeCustomZoomBlurEffect() + self.dimNode.alpha = 1.0 + } + self.dimNode.isHidden = false + self.withoutBlurDimNode.isHidden = true + case .regular: + if self.effectView.superview != nil { + self.effectView.removeFromSuperview() + self.withoutBlurDimNode.alpha = 1.0 + } + self.dimNode.isHidden = true + self.withoutBlurDimNode.isHidden = false + } transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -958,7 +998,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) let contentSize = originalProjectedContentViewFrame.1.size self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) @@ -1006,65 +1046,105 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } case let .controller(contentParentNode): - let contentActionsSpacing: CGFloat = actionsSideInset - let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + let projectedFrame = convertFrame(contentParentNode.sourceNode.bounds, from: contentParentNode.sourceNode.view, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - //contentParentNode.updateLayout(size: layout.size, transition: transition) - - let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero - let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - - let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) - let contentScale = (layout.size.width - actionsSideInset * 2.0) / layout.size.width - let contentUnscaledSize: CGSize - if !contentParentNode.controller.preferredContentSize.width.isZero { - contentUnscaledSize = contentParentNode.controller.preferredContentSize - } else { - let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset - contentUnscaledSize = CGSize(width: layout.size.width, height: max(400.0, proposedContentHeight)) - } - let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) - - self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) - - let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) - var originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize) - var originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) - if originalContentFrame.minY < topEdge { - let requiredOffset = topEdge - originalContentFrame.minY - let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) - let offset = min(requiredOffset, availableOffset) - originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) - originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) - } - - let contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) - - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) - if self.scrollNode.view.contentSize != scrollContentSize { - self.scrollNode.view.contentSize = scrollContentSize - } - - let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) - - let contentContainerFrame = originalContentFrame - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) - - if isInitialLayout { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) - let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) - if overflowOffset < 0.0 { - transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { + let contentActionsSpacing: CGFloat = actionsSideInset + let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) + + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) + let contentScale = (layout.size.width - actionsSideInset * 2.0) / layout.size.width + var contentUnscaledSize: CGSize + if case .compact = layout.metrics.widthClass { + let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset + contentUnscaledSize = CGSize(width: layout.size.width, height: max(400.0, proposedContentHeight)) + + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + contentUnscaledSize = preferredSize + } + } else { + let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset + contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) + + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + contentUnscaledSize = preferredSize + } + } + let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) + + self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + var originalActionsFrame: CGRect + var originalContentFrame: CGRect + var contentHeight: CGFloat + if case .compact = layout.metrics.widthClass { + originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize) + originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) + if originalContentFrame.minY < topEdge { + let requiredOffset = topEdge - originalContentFrame.minY + let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) + let offset = min(requiredOffset, availableOffset) + originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) + originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) + } + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + } else { + originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) + originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width) + originalContentFrame.origin.x = max(originalContentFrame.origin.x, actionsSideInset) + originalContentFrame.origin.y = min(originalContentFrame.origin.y, layout.size.height - layout.intrinsicInsets.bottom - actionsSideInset - contentSize.height) + originalContentFrame.origin.y = max(originalContentFrame.origin.y, contentTopInset) + if originalContentFrame.maxX <= layout.size.width - actionsSideInset - actionsSize.width - contentActionsSpacing { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + contentActionsSpacing, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.maxX > layout.size.width - actionsSideInset { + let offset = originalActionsFrame.maxX - (layout.size.width - actionsSideInset) + originalActionsFrame.origin.x -= offset + originalContentFrame.origin.x -= offset + } + } else { + originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentActionsSpacing - actionsSize.width, y: originalContentFrame.minY), size: actionsSize) + if originalActionsFrame.minX < actionsSideInset { + let offset = actionsSideInset - originalActionsFrame.minX + originalActionsFrame.origin.x += offset + originalContentFrame.origin.x += offset + } + } + contentHeight = layout.size.height + contentHeight = max(contentHeight, originalActionsFrame.maxY + actionsBottomInset) + contentHeight = max(contentHeight, originalContentFrame.maxY + actionsBottomInset) + } + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + + let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + + if let reactionContextNode = self.reactionContextNode { + let insets = layout.insets(options: [.statusBar]) + transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX, y: absoluteContentRect.minY), size: contentSize), transition: transition) } - } - - let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) - - if let reactionContextNode = self.reactionContextNode { - let insets = layout.insets(options: [.statusBar]) - transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX, y: absoluteContentRect.minY), size: contentSize), transition: transition) } } } diff --git a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift index 18137bbe91..0ff94a03f0 100644 --- a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift @@ -23,21 +23,17 @@ public final class ContextControllerSourceNode: ASDisplayNode { guard let strongSelf = self, !strongSelf.bounds.width.isZero else { return } - let minScale: CGFloat = (strongSelf.bounds.width - 20.0) / strongSelf.bounds.width + let minScale: CGFloat = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width let currentScale = 1.0 * (1.0 - progress) + minScale * progress switch update { case .update: strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0) case .begin: strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0) - /*let previousProgress: CGFloat = 0.0 - let previousScale = 1.0 * (1.0 - previousProgress) + minScale * previousProgress - strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0) - strongSelf.layer.animate(from: previousScale as NSNumber, to: currentScale as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, additive: false)*/ case let .ended(previousProgress): let previousScale = 1.0 * (1.0 - previousProgress) + minScale * previousProgress strongSelf.layer.sublayerTransform = CATransform3DMakeScale(currentScale, currentScale, 1.0) - strongSelf.layer.animate(from: previousScale as NSNumber, to: currentScale as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3) + strongSelf.layer.animateSpring(from: previousScale as NSNumber, to: currentScale as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 90.0) } } contextGesture.activated = { [weak self] gesture in diff --git a/submodules/ContextUI/Sources/ContextGesture.swift b/submodules/ContextUI/Sources/ContextGesture.swift index b99981c1ed..6450b1f77c 100644 --- a/submodules/ContextUI/Sources/ContextGesture.swift +++ b/submodules/ContextUI/Sources/ContextGesture.swift @@ -40,6 +40,7 @@ private func cancelParentGestures(view: UIView) { public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDelegate { private var currentProgress: CGFloat = 0.0 private var delayTimer: Timer? + private var animator: DisplayLinkAnimator? private var isValidated: Bool = false public var activationProgress: ((CGFloat, ContextGestureTransition) -> Void)? @@ -62,6 +63,8 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg self.isValidated = false self.externalUpdated = nil self.externalEnded = nil + self.animator?.invalidate() + self.animator = nil } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -80,6 +83,33 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg return } strongSelf.isValidated = true + if strongSelf.animator == nil { + strongSelf.animator = DisplayLinkAnimator(duration: 0.3, from: 0.0, to: 1.0, update: { value in + guard let strongSelf = self else { + return + } + if strongSelf.isValidated { + strongSelf.currentProgress = value + strongSelf.activationProgress?(value, .update) + } + }, completion: { + guard let strongSelf = self else { + return + } + switch strongSelf.state { + case .possible: + strongSelf.delayTimer?.invalidate() + strongSelf.animator?.invalidate() + strongSelf.activated?(strongSelf) + if let view = strongSelf.view?.superview { + cancelParentGestures(view: view) + } + strongSelf.state = .began + default: + break + } + }) + } strongSelf.activationProgress?(strongSelf.currentProgress, .begin) }, selector: #selector(TimerTargetWrapper.timerEvent), userInfo: nil, repeats: false) self.delayTimer = delayTimer @@ -91,7 +121,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg super.touchesMoved(touches, with: event) if let touch = touches.first { - if #available(iOS 9.0, *) { + /*if #available(iOS 9.0, *) { let maxForce: CGFloat = max(2.5, min(3.0, touch.maximumPossibleForce)) let progress = touch.force / maxForce self.currentProgress = progress @@ -111,7 +141,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg break } } - } + }*/ self.externalUpdated?(self.view, touch.location(in: self.view)) } @@ -131,6 +161,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg } self.delayTimer?.invalidate() + self.animator?.invalidate() self.state = .failed } @@ -145,6 +176,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg } self.delayTimer?.invalidate() + self.animator?.invalidate() self.state = .failed } @@ -154,6 +186,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg self.activationProgress?(0.0, .ended(self.currentProgress)) self.delayTimer?.invalidate() + self.animator?.invalidate() self.state = .failed } } @@ -163,6 +196,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg let previousProgress = self.currentProgress self.currentProgress = 0.0 self.delayTimer?.invalidate() + self.animator?.invalidate() self.isValidated = false self.activationProgress?(0.0, .ended(previousProgress)) } diff --git a/submodules/Display/Display/ContainableController.swift b/submodules/Display/Display/ContainableController.swift index 75265cf39f..3080367511 100644 --- a/submodules/Display/Display/ContainableController.swift +++ b/submodules/Display/Display/ContainableController.swift @@ -22,6 +22,7 @@ public protocol ContainableController: class { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) + func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? func viewWillAppear(_ animated: Bool) func viewWillDisappear(_ animated: Bool) diff --git a/submodules/Display/Display/DisplayLinkAnimator.swift b/submodules/Display/Display/DisplayLinkAnimator.swift index 40892dcc0e..a7cbd79858 100644 --- a/submodules/Display/Display/DisplayLinkAnimator.swift +++ b/submodules/Display/Display/DisplayLinkAnimator.swift @@ -44,7 +44,12 @@ public final class DisplayLinkAnimator { self.displayLink.invalidate() } - @objc func tick() { + public func invalidate() { + self.displayLink.isPaused = true + self.displayLink.invalidate() + } + + @objc private func tick() { if self.completed { return } diff --git a/submodules/Display/Display/NavigationController.swift b/submodules/Display/Display/NavigationController.swift index 7850239b47..4983d226c4 100644 --- a/submodules/Display/Display/NavigationController.swift +++ b/submodules/Display/Display/NavigationController.swift @@ -42,6 +42,7 @@ private final class NavigationControllerView: UITracingLayerView { var inTransition = false let sharedStatusBar: StatusBar + let masterContainerView: NavigationControllerContainerView let containerView: NavigationControllerContainerView let separatorView: UIView var navigationBackgroundView: UIView? @@ -52,12 +53,14 @@ private final class NavigationControllerView: UITracingLayerView { var topControllerNode: ASDisplayNode? override init(frame: CGRect) { + self.masterContainerView = NavigationControllerContainerView() self.containerView = NavigationControllerContainerView() self.separatorView = UIView() self.sharedStatusBar = StatusBar() super.init(frame: frame) + self.addSubview(self.masterContainerView) self.addSubview(self.containerView) } @@ -138,6 +141,7 @@ open class NavigationController: UINavigationController, ContainableController, private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + private var masterTransitionCoordinator: NavigationTransitionCoordinator? private var navigationTransitionCoordinator: NavigationTransitionCoordinator? private var currentPushDisposable = MetaDisposable() @@ -230,6 +234,10 @@ open class NavigationController: UINavigationController, ContainableController, private var previouslyLaidOutMasterController: UIViewController? private var previouslyLaidOutTopController: UIViewController? + open func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + return nil + } + private func layoutConfiguration(for layout: ContainerViewLayout) -> ControllerLayoutConfiguration { switch self.mode { case .single: @@ -275,7 +283,7 @@ open class NavigationController: UINavigationController, ContainableController, switch layoutConfiguration { case .masterDetail: - self.viewControllers.first?.view.clipsToBounds = true + self.controllerView.masterContainerView.clipsToBounds = true self.controllerView.containerView.clipsToBounds = true let masterData = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 0) firstControllerFrameAndLayout = masterData @@ -402,6 +410,7 @@ open class NavigationController: UINavigationController, ContainableController, }) } } + self.controllerView.masterContainerView.clipsToBounds = false self.controllerView.containerView.clipsToBounds = false lastControllerFrameAndLayout = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 1) transition.updateFrame(view: self.controllerView.separatorView, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)), completion: { [weak self] completed in @@ -410,6 +419,9 @@ open class NavigationController: UINavigationController, ContainableController, } }) } + if let firstControllerFrameAndLayout = firstControllerFrameAndLayout { + transition.updateFrame(view: self.controllerView.masterContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: firstControllerFrameAndLayout.0.size)) + } transition.updateFrame(view: self.controllerView.containerView, frame: CGRect(origin: CGPoint(x: firstControllerFrameAndLayout?.0.maxX ?? 0.0, y: 0.0), size: lastControllerFrameAndLayout.0.size)) switch layoutConfiguration { @@ -434,11 +446,28 @@ open class NavigationController: UINavigationController, ContainableController, } var controllersAndFrames: [(Bool, ControllerRecord, ContainerViewLayout)] = [] + var masterRange: ClosedRange? + if case .masterDetail = layoutConfiguration { + for i in 0 ..< self._viewControllers.count { + if let controller = self._viewControllers[i].controller as? ViewController { + switch controller.navigationPresentation { + case .default: + break + case .master: + if let masterRangeValue = masterRange { + masterRange = masterRangeValue.lowerBound ... i + } else { + masterRange = i ... i + } + } + } + } + } for i in 0 ..< self._viewControllers.count { if let controller = self._viewControllers[i].controller as? ViewController { if i == 0 { controller.navigationBar?.previousItem = nil - } else if case .masterDetail = layoutConfiguration, i == 1 { + } else if let masterRange = masterRange, i == masterRange.upperBound + 1 { controller.navigationBar?.previousItem = .close } else { controller.navigationBar?.previousItem = .item(self.viewControllers[i - 1].navigationItem) @@ -451,13 +480,25 @@ open class NavigationController: UINavigationController, ContainableController, } self.viewControllers[i].navigation_setNavigationController(self) - if i == 0, let (_, layout) = firstControllerFrameAndLayout { + if let (_, layout) = firstControllerFrameAndLayout, let controller = self._viewControllers[i].controller as? ViewController, case .master = controller.navigationPresentation { controllersAndFrames.append((true, self._viewControllers[i], layout)) } else if i == self._viewControllers.count - 1 { controllersAndFrames.append((false, self._viewControllers[i], lastControllerFrameAndLayout.1)) } } + while controllersAndFrames.count >= 2 { + if controllersAndFrames[0].0 { + if controllersAndFrames[1].0 { + controllersAndFrames.remove(at: 0) + } else { + break + } + } else { + break + } + } + var masterController: UIViewController? var appearingMasterController: ControllerRecord? var appearingDetailController: ControllerRecord? @@ -489,10 +530,10 @@ open class NavigationController: UINavigationController, ContainableController, } else { appearingDetailController = record } - } else if record.controller.view.superview !== (isMaster ? self.controllerView : self.controllerView.containerView) { + } else if record.controller.view.superview !== (isMaster ? self.controllerView.masterContainerView : self.controllerView.containerView) { record.controller.setIgnoreAppearanceMethodInvocations(true) if isMaster { - self.controllerView.insertSubview(record.controller.view, at: 0) + self.controllerView.masterContainerView.addSubview(record.controller.view) } else { self.controllerView.containerView.addSubview(record.controller.view) } @@ -518,8 +559,76 @@ open class NavigationController: UINavigationController, ContainableController, } var animatedAppearingDetailController = false + var animatedAppearingMasterController = false - if let previousController = self.previouslyLaidOutTopController, !controllersAndFrames.contains(where: { $0.1.controller === previousController }), previousController.view.superview != nil { + if let previousController = self.previouslyLaidOutMasterController, !controllersAndFrames.contains(where: { $0.1.controller === previousController }), previousController.view.superview != nil { + if transition.isAnimated, let record = appearingMasterController { + animatedAppearingMasterController = true + + previousController.viewWillDisappear(true) + record.controller.viewWillAppear(true) + record.controller.setIgnoreAppearanceMethodInvocations(true) + + if let controller = record.controller as? ViewController, !controller.hasActiveInput { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 0) + + let appliedLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil) + controller.containerLayoutUpdated(appliedLayout, transition: .immediate) + } + self.controllerView.masterContainerView.addSubview(record.controller.view) + record.controller.setIgnoreAppearanceMethodInvocations(false) + + if let _ = previousControllers.firstIndex(where: { $0.controller === record.controller }) { + let masterTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.masterContainerView, topView: previousController.view, topNavigationBar: (previousController as? ViewController)?.navigationBar, bottomView: record.controller.view, bottomNavigationBar: (record.controller as? ViewController)?.navigationBar) + self.masterTransitionCoordinator = masterTransitionCoordinator + + self.controllerView.inTransition = true + masterTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in + if let strongSelf = self { + strongSelf.masterTransitionCoordinator = nil + strongSelf.controllerView.inTransition = false + + record.controller.viewDidAppear(true) + + previousController.setIgnoreAppearanceMethodInvocations(true) + previousController.view.removeFromSuperview() + previousController.setIgnoreAppearanceMethodInvocations(false) + previousController.viewDidDisappear(true) + } + }) + } else { + if let index = self._viewControllers.firstIndex(where: { $0.controller === previousController }) { + self._viewControllers[index].transition = .appearance + } + let masterTransitionCoordinator = NavigationTransitionCoordinator(transition: .Push, container: self.controllerView.masterContainerView, topView: record.controller.view, topNavigationBar: (record.controller as? ViewController)?.navigationBar, bottomView: previousController.view, bottomNavigationBar: (previousController as? ViewController)?.navigationBar) + self.masterTransitionCoordinator = masterTransitionCoordinator + + self.controllerView.inTransition = true + masterTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in + if let strongSelf = self { + if let index = strongSelf._viewControllers.firstIndex(where: { $0.controller === previousController }) { + strongSelf._viewControllers[index].transition = .none + } + strongSelf.masterTransitionCoordinator = nil + strongSelf.controllerView.inTransition = false + + record.controller.viewDidAppear(true) + + previousController.setIgnoreAppearanceMethodInvocations(true) + previousController.view.removeFromSuperview() + previousController.setIgnoreAppearanceMethodInvocations(false) + previousController.viewDidDisappear(true) + } + }) + } + } else { + previousController.viewWillDisappear(false) + previousController.view.removeFromSuperview() + previousController.viewDidDisappear(false) + } + } + + if let previousController = self.previouslyLaidOutTopController, previousController !== self.previouslyLaidOutMasterController, !controllersAndFrames.contains(where: { $0.1.controller === previousController }), previousController.view.superview != nil { if transition.isAnimated, let record = appearingDetailController { animatedAppearingDetailController = true @@ -537,7 +646,6 @@ open class NavigationController: UINavigationController, ContainableController, record.controller.setIgnoreAppearanceMethodInvocations(false) if let _ = previousControllers.firstIndex(where: { $0.controller === record.controller }) { - //previousControllers[index].transition = .appearance let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: previousController.view, topNavigationBar: (previousController as? ViewController)?.navigationBar, bottomView: record.controller.view, bottomNavigationBar: (record.controller as? ViewController)?.navigationBar) self.navigationTransitionCoordinator = navigationTransitionCoordinator @@ -598,10 +706,10 @@ open class NavigationController: UINavigationController, ContainableController, } } - if let record = appearingMasterController, let firstControllerFrameAndLayout = firstControllerFrameAndLayout { + if !animatedAppearingMasterController, let record = appearingMasterController, let firstControllerFrameAndLayout = firstControllerFrameAndLayout { record.controller.viewWillAppear(false) record.controller.setIgnoreAppearanceMethodInvocations(true) - self.controllerView.insertSubview(record.controller.view, belowSubview: self.controllerView.containerView) + self.controllerView.masterContainerView.addSubview(record.controller.view) record.controller.setIgnoreAppearanceMethodInvocations(false) record.controller.viewDidAppear(false) if let controller = record.controller as? ViewController { @@ -782,127 +890,281 @@ open class NavigationController: UINavigationController, ContainableController, @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { - case .began: - guard let layout = self.validLayout else { + case .began: + guard let layout = self.validLayout else { + return + } + var beginMasterGesture = false + var beginDetailGesture = false + + let masterControllers: [ControllerRecord] + let detailControllers: [ControllerRecord] + + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return true + } else { + return false + } + } else { + return false + } + }) + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + + let locationInMaster = recognizer.location(in: self.controllerView.masterContainerView) + let locationInDetail = recognizer.location(in: self.controllerView.containerView) + + if self.controllerView.masterContainerView.bounds.contains(locationInMaster) { + beginMasterGesture = masterControllers.count >= 2 + } + if self.controllerView.containerView.bounds.contains(locationInDetail) { + beginDetailGesture = detailControllers.count >= 2 + } + case .single: + beginDetailGesture = self._viewControllers.count >= 2 + masterControllers = [] + detailControllers = self._viewControllers + } + + if beginMasterGesture { + guard self.masterTransitionCoordinator == nil else { return } + + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller + + if let topController = topController as? ViewController { + if !topController.attemptNavigation({ [weak self, weak topController] in + if let topController = topController { + self?.filterController(topController, animated: true) + } + }) { + return + } + } + + topController.viewWillDisappear(true) + let topView = topController.view! + if let bottomController = bottomController as? ViewController { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 0) + + let appliedLayout = controllerLayout.withUpdatedInputHeight(bottomController.hasActiveInput ? controllerLayout.inputHeight : nil) + bottomController.containerLayoutUpdated(appliedLayout, transition: .immediate) + } + bottomController.viewWillAppear(true) + let bottomView = bottomController.view! + + let masterTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.masterContainerView, topView: topView, topNavigationBar: (topController as? ViewController)?.navigationBar, bottomView: bottomView, bottomNavigationBar: (bottomController as? ViewController)?.navigationBar, didUpdateProgress: { [weak self, weak topController, weak bottomController] progress, transition in + if let strongSelf = self, let topController = topController, let bottomController = bottomController { + (bottomController as? ViewController)?.updateNavigationCustomData((topController as? ViewController)?.customData, progress: 1.0 - progress, transition: transition) + } + }) + if let bottomController = bottomController as? ViewController { + bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true) + } + self.masterTransitionCoordinator = masterTransitionCoordinator + } + if beginDetailGesture { guard self.navigationTransitionCoordinator == nil else { return } - let beginGesture: Bool - switch self.layoutConfiguration(for: layout) { - case .masterDetail: - let location = recognizer.location(in: self.controllerView.containerView) - if self.controllerView.containerView.bounds.contains(location) { - beginGesture = self._viewControllers.count >= 3 - } else { - beginGesture = false - } - case .single: - beginGesture = self._viewControllers.count >= 2 + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller + + if let topController = topController as? ViewController { + if !topController.attemptNavigation({ [weak self] in + let _ = self?.popViewController(animated: true) + }) { + return + } } - if beginGesture { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + topController.viewWillDisappear(true) + let topView = topController.view! + if let bottomController = bottomController as? ViewController { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 1) - if let topController = topController as? ViewController { - if !topController.attemptNavigation({ [weak self] in - let _ = self?.popViewController(animated: true) - }) { - return - } - } - - topController.viewWillDisappear(true) - let topView = topController.view! - if let bottomController = bottomController as? ViewController { - let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: self.viewControllers.count - 2) - - let appliedLayout = controllerLayout.withUpdatedInputHeight(bottomController.hasActiveInput ? controllerLayout.inputHeight : nil) - bottomController.containerLayoutUpdated(appliedLayout, transition: .immediate) - } - bottomController.viewWillAppear(true) - let bottomView = bottomController.view! - - let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: topView, topNavigationBar: (topController as? ViewController)?.navigationBar, bottomView: bottomView, bottomNavigationBar: (bottomController as? ViewController)?.navigationBar, didUpdateProgress: { [weak self] progress, transition in - if let strongSelf = self { - for i in 0 ..< strongSelf._viewControllers.count { - if let controller = strongSelf._viewControllers[i].controller as? ViewController { - if i < strongSelf._viewControllers.count - 1 { - controller.updateNavigationCustomData((strongSelf.viewControllers[i + 1] as? ViewController)?.customData, progress: 1.0 - progress, transition: transition) - } else { - controller.updateNavigationCustomData(nil, progress: 1.0 - progress, transition: transition) - } - } - } - } - }) - if let bottomController = bottomController as? ViewController { - bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true) - } - self.navigationTransitionCoordinator = navigationTransitionCoordinator + let appliedLayout = controllerLayout.withUpdatedInputHeight(bottomController.hasActiveInput ? controllerLayout.inputHeight : nil) + bottomController.containerLayoutUpdated(appliedLayout, transition: .immediate) } - case .changed: - if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + bottomController.viewWillAppear(true) + let bottomView = bottomController.view! + + let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: topView, topNavigationBar: (topController as? ViewController)?.navigationBar, bottomView: bottomView, bottomNavigationBar: (bottomController as? ViewController)?.navigationBar, didUpdateProgress: { [weak self, weak topController, weak bottomController] progress, transition in + if let strongSelf = self, let topController = topController, let bottomController = bottomController { + (bottomController as? ViewController)?.updateNavigationCustomData((topController as? ViewController)?.customData, progress: 1.0 - progress, transition: transition) + } + }) + if let bottomController = bottomController as? ViewController { + bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true) + } + self.navigationTransitionCoordinator = navigationTransitionCoordinator + } + case .changed: + if let layout = self.validLayout { + if let masterTransitionCoordinator = self.masterTransitionCoordinator, !masterTransitionCoordinator.animatingCompletion { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 0) let translation = recognizer.translation(in: self.view).x - let progress = max(0.0, min(1.0, translation / self.view.frame.width)) + let progress = max(0.0, min(1.0, translation / controllerLayout.size.width)) + masterTransitionCoordinator.progress = progress + } + if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 1) + let translation = recognizer.translation(in: self.view).x + let progress = max(0.0, min(1.0, translation / controllerLayout.size.width)) navigationTransitionCoordinator.progress = progress } - case .ended: - if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { - let velocity = recognizer.velocity(in: self.view).x - - if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 { - (self.view as! NavigationControllerView).inTransition = true - navigationTransitionCoordinator.animateCompletion(velocity, completion: { - (self.view as! NavigationControllerView).inTransition = false - - self.navigationTransitionCoordinator = nil - - if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController - - topController.setIgnoreAppearanceMethodInvocations(true) - bottomController.setIgnoreAppearanceMethodInvocations(true) - let _ = self.popViewController(animated: false) - topController.setIgnoreAppearanceMethodInvocations(false) - bottomController.setIgnoreAppearanceMethodInvocations(false) - - topController.viewDidDisappear(true) - bottomController.viewDidAppear(true) + } + case .ended: + if let masterTransitionCoordinator = self.masterTransitionCoordinator, !masterTransitionCoordinator.animatingCompletion { + let velocity = recognizer.velocity(in: self.view).x + + let masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController, case .master = controller.navigationPresentation { + return true + } + return false + }) + + if velocity > 1000 || masterTransitionCoordinator.progress > 0.2 { + (self.view as! NavigationControllerView).inTransition = true + masterTransitionCoordinator.animateCompletion(velocity, completion: { + (self.view as! NavigationControllerView).inTransition = false + self.masterTransitionCoordinator = nil + + let masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController, case .master = controller.navigationPresentation { + return true } + return false }) - } else { - if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + + if masterControllers.count >= 2 && self.masterTransitionCoordinator == nil { + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller - topController.viewWillAppear(true) - bottomController.viewWillDisappear(true) + topController.setIgnoreAppearanceMethodInvocations(true) + bottomController.setIgnoreAppearanceMethodInvocations(true) + if let topController = topController as? ViewController { + self.filterController(topController, animated: false) + } + topController.setIgnoreAppearanceMethodInvocations(false) + bottomController.setIgnoreAppearanceMethodInvocations(false) + + topController.viewDidDisappear(true) + bottomController.viewDidAppear(true) + } + }) + } else { + if masterControllers.count >= 2 && masterControllers == nil { + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller + + topController.viewWillAppear(true) + bottomController.viewWillDisappear(true) + } + + (self.view as! NavigationControllerView).inTransition = true + masterTransitionCoordinator.animateCancel({ + (self.view as! NavigationControllerView).inTransition = false + self.masterTransitionCoordinator = nil + + let masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController, case .master = controller.navigationPresentation { + return true + } + return false + }) + + if masterControllers.count >= 2 && self.masterTransitionCoordinator == nil { + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller + + topController.viewDidAppear(true) + bottomController.viewDidDisappear(true) + } + }) + } + } + if let layout = self.validLayout, let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + let velocity = recognizer.velocity(in: self.view).x + + let detailControllers: [ControllerRecord] + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + case .single: + detailControllers = self._viewControllers + } + + if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 { + (self.view as! NavigationControllerView).inTransition = true + navigationTransitionCoordinator.animateCompletion(velocity, completion: { + (self.view as! NavigationControllerView).inTransition = false + + let detailControllers: [ControllerRecord] + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + case .single: + detailControllers = self._viewControllers } - (self.view as! NavigationControllerView).inTransition = true - navigationTransitionCoordinator.animateCancel({ - (self.view as! NavigationControllerView).inTransition = false - self.navigationTransitionCoordinator = nil + self.navigationTransitionCoordinator = nil + + if detailControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller - if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController - - topController.viewDidAppear(true) - bottomController.viewDidDisappear(true) - } - }) - } - } - case .cancelled: - if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { - if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + topController.setIgnoreAppearanceMethodInvocations(true) + bottomController.setIgnoreAppearanceMethodInvocations(true) + self.filterController(topController as! ViewController, animated: false) + topController.setIgnoreAppearanceMethodInvocations(false) + bottomController.setIgnoreAppearanceMethodInvocations(false) + + topController.viewDidDisappear(true) + bottomController.viewDidAppear(true) + } + }) + } else { + if detailControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller topController.viewWillAppear(true) bottomController.viewWillDisappear(true) @@ -913,17 +1175,133 @@ open class NavigationController: UINavigationController, ContainableController, (self.view as! NavigationControllerView).inTransition = false self.navigationTransitionCoordinator = nil - if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil { - let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController - let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController + let detailControllers: [ControllerRecord] + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + case .single: + detailControllers = self._viewControllers + } + + if detailControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller topController.viewDidAppear(true) bottomController.viewDidDisappear(true) } }) } - default: - break + } + case .cancelled: + if let masterTransitionCoordinator = self.masterTransitionCoordinator, !masterTransitionCoordinator.animatingCompletion { + let masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController, case .master = controller.navigationPresentation { + return true + } + return false + }) + + if masterControllers.count >= 2 && self.masterTransitionCoordinator == nil { + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller + + topController.viewWillAppear(true) + bottomController.viewWillDisappear(true) + } + + (self.view as! NavigationControllerView).inTransition = true + masterTransitionCoordinator.animateCancel({ + (self.view as! NavigationControllerView).inTransition = false + self.masterTransitionCoordinator = nil + + let masterControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController, case .master = controller.navigationPresentation { + return true + } + return false + }) + + if masterControllers.count >= 2 && self.masterTransitionCoordinator == nil { + let topController = masterControllers[masterControllers.count - 1].controller + let bottomController = masterControllers[masterControllers.count - 2].controller + + topController.viewDidAppear(true) + bottomController.viewDidDisappear(true) + } + }) + } + if let layout = self.validLayout, let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion { + let detailControllers: [ControllerRecord] + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + case .single: + detailControllers = self._viewControllers + } + + if detailControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller + + topController.viewWillAppear(true) + bottomController.viewWillDisappear(true) + } + + (self.view as! NavigationControllerView).inTransition = true + navigationTransitionCoordinator.animateCancel({ + (self.view as! NavigationControllerView).inTransition = false + self.navigationTransitionCoordinator = nil + + let detailControllers: [ControllerRecord] + switch self.layoutConfiguration(for: layout) { + case .masterDetail: + detailControllers = self._viewControllers.filter({ record in + if let controller = record.controller as? ViewController { + if case .master = controller.navigationPresentation { + return false + } else { + return true + } + } else { + return true + } + }) + case .single: + detailControllers = self._viewControllers + } + + if detailControllers.count >= 2 && self.navigationTransitionCoordinator == nil { + let topController = detailControllers[detailControllers.count - 1].controller + let bottomController = detailControllers[detailControllers.count - 2].controller + + topController.viewDidAppear(true) + bottomController.viewDidDisappear(true) + } + }) + } + default: + break } } @@ -985,7 +1363,30 @@ open class NavigationController: UINavigationController, ContainableController, self.currentPushDisposable.set(nil) var controllers = self.viewControllers - controllers.append(viewController) + if let controller = viewController as? ViewController { + switch controller.navigationPresentation { + case .default: + controllers.append(viewController) + case .master: + var i = 0 + loop: while i < controllers.count { + if let currentController = controllers[i] as? ViewController { + switch currentController.navigationPresentation { + case .master: + break + case .default: + break loop + } + } else { + break loop + } + i += 1 + } + controllers.insert(viewController, at: i) + } + } else { + controllers.append(viewController) + } self.setViewControllers(controllers, animated: animated) } diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift index 923a40f8e8..c3764f6e03 100644 --- a/submodules/Display/Display/ViewController.swift +++ b/submodules/Display/Display/ViewController.swift @@ -55,6 +55,11 @@ open class ViewControllerPresentationArguments { } } +public enum ViewControllerNavigationPresentation { + case `default` + case master +} + @objc open class ViewController: UIViewController, ContainableController { private var validLayout: ContainerViewLayout? public var currentlyAppliedLayout: ContainerViewLayout? { @@ -117,6 +122,8 @@ open class ViewControllerPresentationArguments { return self.preferNavigationUIHidden } + open var navigationPresentation: ViewControllerNavigationPresentation = .default + public var presentationArguments: Any? public var tabBarItemDebugTapAction: (() -> Void)? @@ -236,6 +243,10 @@ open class ViewControllerPresentationArguments { return true } + open func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + return nil + } + private func updateScrollToTopView() { if self.scrollToTop != nil { if let displayNode = self._displayNode , self.scrollToTopView == nil { @@ -267,9 +278,20 @@ open class ViewControllerPresentationArguments { self.navigationBar?.backPressed = { [weak self] in if let strongSelf = self, strongSelf.attemptNavigation({ - self?.navigationController?.popViewController(animated: true) + guard let strongSelf = self else { + return + } + if let navigationController = strongSelf.navigationController as? NavigationController { + navigationController.filterController(strongSelf, animated: true) + } else { + strongSelf.navigationController?.popViewController(animated: true) + } }) { - strongSelf.navigationController?.popViewController(animated: true) + if let navigationController = strongSelf.navigationController as? NavigationController { + navigationController.filterController(strongSelf, animated: true) + } else { + strongSelf.navigationController?.popViewController(animated: true) + } } } self.navigationBar?.requestContainerLayout = { [weak self] transition in diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index bcea3d7da7..a5b146553a 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -945,6 +945,14 @@ public class GalleryController: ViewController { self.accountInUseDisposable.set(nil) } + override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { + return itemSize.aspectFitted(layout.size) + } else { + return nil + } + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -955,7 +963,7 @@ public class GalleryController: ViewController { self.adjustedForInitialPreviewingLayout = true self.galleryNode.setControlsHidden(true, animated: false) if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { - self.preferredContentSize = itemSize.aspectFitted(self.view.bounds.size) + self.preferredContentSize = itemSize.aspectFitted(layout.size) self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) } } diff --git a/submodules/TelegramUI/TelegramUI/TelegramRootController.swift b/submodules/TelegramUI/TelegramUI/TelegramRootController.swift index 50cff99aea..c0de067a68 100644 --- a/submodules/TelegramUI/TelegramUI/TelegramRootController.swift +++ b/submodules/TelegramUI/TelegramUI/TelegramRootController.swift @@ -63,8 +63,6 @@ public final class TelegramRootController: NavigationController { if previousTheme !== presentationData.theme { strongSelf.rootTabController?.updateTheme(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData), theme: TabBarControllerTheme(rootControllerTheme: presentationData.theme)) strongSelf.rootTabController?.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style - - } } }) @@ -81,6 +79,7 @@ public final class TelegramRootController: NavigationController { public func addRootControllers(showCallsTab: Bool) { let tabBarController = TabBarController(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), theme: TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) + tabBarController.navigationPresentation = .master let chatListController = self.context.sharedContext.makeChatListController(context: self.context, groupId: .root, controlsHistoryPreload: true, hideNetworkActivityStatus: false, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild) if let sharedContext = self.context.sharedContext as? SharedAccountContextImpl { chatListController.tabBarItem.badgeValue = sharedContext.switchingData.chatListBadge