diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index aeccfbe154..1c641e2b50 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */; }; D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE11D9049F60066BF65 /* GridItemNode.swift */; }; D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE31D904A000066BF65 /* GridItem.swift */; }; + D01EA41B203227BA00B4B0B5 /* ViewControllerPreviewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */; }; D02383801DDF7916004018B6 /* LegacyPresentedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */; }; D02383821DDF798E004018B6 /* LegacyPresentedControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */; }; D02383861DE0E3B4004018B6 /* ListViewIntermediateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */; }; @@ -51,6 +52,16 @@ D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.swift */; }; D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */; }; D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */; }; + D03AA4D5202C793E0056C405 /* PeekController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D4202C793E0056C405 /* PeekController.swift */; }; + D03AA4D7202C79600056C405 /* PeekControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D6202C79600056C405 /* PeekControllerNode.swift */; }; + D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */; }; + D03AA4DB202DA6D60056C405 /* PeekControllerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */; }; + D03AA4DD202DB1840056C405 /* PeekControllerGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */; }; + D03AA4E1202DD4490056C405 /* PeekControllerMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */; }; + D03AA4E3202DD52E0056C405 /* PeekControllerMenuItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */; }; + D03AA4E9202E02070056C405 /* ListViewReorderingItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */; }; + D03AA4EB202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */; }; + D03AA5162030C5F80056C405 /* ListViewTempItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */; }; D03B0E701D6331FB00955575 /* StatusBarHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B0E6F1D6331FB00955575 /* StatusBarHost.swift */; }; D03BCCEB1C72AE590097A291 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03BCCEA1C72AE590097A291 /* Theme.swift */; }; D03E7DE41C96A90100C07816 /* NavigationShadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D03E7DE31C96A90100C07816 /* NavigationShadow@2x.png */; }; @@ -71,7 +82,6 @@ D05CC2731B69316F00E235A3 /* DisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2721B69316F00E235A3 /* DisplayTests.swift */; }; D05CC2A01B69326400E235A3 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC29F1B69326400E235A3 /* NavigationController.swift */; }; D05CC2A21B69326C00E235A3 /* WindowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2A11B69326C00E235A3 /* WindowContent.swift */; }; - D05CC2E31B69552C00E235A3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2E21B69552C00E235A3 /* ViewController.swift */; }; D05CC2E71B69555800E235A3 /* CAAnimationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2E41B69555800E235A3 /* CAAnimationUtils.swift */; }; D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D05CC2EA1B69558A00E235A3 /* RuntimeUtils.m */; }; D05CC2ED1B69558A00E235A3 /* RuntimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D05CC2EB1B69558A00E235A3 /* RuntimeUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -111,6 +121,8 @@ D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */; }; D08E90471D243C2F00533158 /* HighlightTrackingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */; }; D096A4501EA64F580000A7AE /* ActionSheetCheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */; }; + D0A1346220336C350059716A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1346120336C350059716A /* ViewController.swift */; }; + D0A134642034DE580059716A /* TabBarTapRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A134632034DE580059716A /* TabBarTapRecognizer.swift */; }; D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A749941E3A9E7B00AD786E /* SwitchNode.swift */; }; D0AA840E1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */; }; D0AA84101FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */; }; @@ -195,6 +207,7 @@ D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNodeScroller.swift; sourceTree = ""; }; D01E2BE11D9049F60066BF65 /* GridItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItemNode.swift; sourceTree = ""; }; D01E2BE31D904A000066BF65 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; + D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerPreviewing.swift; sourceTree = ""; }; D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyPresentedController.swift; sourceTree = ""; }; D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyPresentedControllerNode.swift; sourceTree = ""; }; D02383851DE0E3B4004018B6 /* ListViewIntermediateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewIntermediateState.swift; sourceTree = ""; }; @@ -204,6 +217,16 @@ D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = ""; }; D03725C21D6DF7A6007FC290 /* ContextMenuAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuAction.swift; sourceTree = ""; }; D03725C41D6DF8B9007FC290 /* ContextMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuController.swift; sourceTree = ""; }; + D03AA4D4202C793E0056C405 /* PeekController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekController.swift; sourceTree = ""; }; + D03AA4D6202C79600056C405 /* PeekControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerNode.swift; sourceTree = ""; }; + D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalOverlayPresentationContext.swift; sourceTree = ""; }; + D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerContent.swift; sourceTree = ""; }; + D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerGestureRecognizer.swift; sourceTree = ""; }; + D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerMenuNode.swift; sourceTree = ""; }; + D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeekControllerMenuItemNode.swift; sourceTree = ""; }; + D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewReorderingItemNode.swift; sourceTree = ""; }; + D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewReorderingGestureRecognizer.swift; sourceTree = ""; }; + D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewTempItemNode.swift; sourceTree = ""; }; D03B0E6F1D6331FB00955575 /* StatusBarHost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarHost.swift; sourceTree = ""; }; D03BCCEA1C72AE590097A291 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; D03E7DE31C96A90100C07816 /* NavigationShadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "NavigationShadow@2x.png"; sourceTree = ""; }; @@ -227,7 +250,6 @@ D05CC2741B69316F00E235A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D05CC29F1B69326400E235A3 /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; D05CC2A11B69326C00E235A3 /* WindowContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowContent.swift; sourceTree = ""; }; - D05CC2E21B69552C00E235A3 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; D05CC2E41B69555800E235A3 /* CAAnimationUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAAnimationUtils.swift; sourceTree = ""; }; D05CC2EA1B69558A00E235A3 /* RuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RuntimeUtils.m; sourceTree = ""; }; D05CC2EB1B69558A00E235A3 /* RuntimeUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RuntimeUtils.h; sourceTree = ""; }; @@ -267,6 +289,8 @@ D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroup.swift; sourceTree = ""; }; D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightTrackingButton.swift; sourceTree = ""; }; D096A44F1EA64F580000A7AE /* ActionSheetCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetCheckboxItem.swift; sourceTree = ""; }; + D0A1346120336C350059716A /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + D0A134632034DE580059716A /* TabBarTapRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTapRecognizer.swift; sourceTree = ""; }; D0A749941E3A9E7B00AD786E /* SwitchNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchNode.swift; sourceTree = ""; }; D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewFloatingHeaderNode.swift; sourceTree = ""; }; D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewOverscrollBackgroundNode.swift; sourceTree = ""; }; @@ -381,6 +405,7 @@ D0BB4EB91F96DCC60036D9DE /* WindowInputAccessoryHeightProvider.swift */, D0CB788F1F9822F8004AB79B /* WindowPanRecognizer.swift */, D053DAD02018ECF900993D32 /* WindowCoveringView.swift */, + D03AA4D8202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift */, ); name = Window; sourceTree = ""; @@ -391,6 +416,7 @@ D0DC48531BF93D8A00F672FD /* TabBarController.swift */, D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */, D0DC48551BF945DD00F672FD /* TabBarNode.swift */, + D0A134632034DE580059716A /* TabBarTapRecognizer.swift */, ); name = "Tab Bar"; sourceTree = ""; @@ -458,6 +484,19 @@ name = "Context Menu"; sourceTree = ""; }; + D03AA4D3202C790D0056C405 /* Peek */ = { + isa = PBXGroup; + children = ( + D03AA4D4202C793E0056C405 /* PeekController.swift */, + D03AA4D6202C79600056C405 /* PeekControllerNode.swift */, + D03AA4DA202DA6D60056C405 /* PeekControllerContent.swift */, + D03AA4DC202DB1840056C405 /* PeekControllerGestureRecognizer.swift */, + D03AA4E0202DD4490056C405 /* PeekControllerMenuNode.swift */, + D03AA4E2202DD52E0056C405 /* PeekControllerMenuItemNode.swift */, + ); + name = Peek; + sourceTree = ""; + }; D03BCCE91C72AE4B0097A291 /* Theme */ = { isa = PBXGroup; children = ( @@ -637,11 +676,12 @@ D081229B1D19A9F0005F7395 /* Controllers */ = { isa = PBXGroup; children = ( + D0A1346120336C350059716A /* ViewController.swift */, D081229C1D19AA1C005F7395 /* ContainerViewLayout.swift */, D015F7511D1AE08D00E269B5 /* ContainableController.swift */, D05BE4AA1D1F25E3002BD72C /* PresentationContext.swift */, - D05CC2E21B69552C00E235A3 /* ViewController.swift */, D08CAA7A1ED73C990000FDA8 /* ViewControllerTracingNode.swift */, + D01EA41A203227BA00B4B0B5 /* ViewControllerPreviewing.swift */, D023837F1DDF7916004018B6 /* LegacyPresentedController.swift */, D02383811DDF798E004018B6 /* LegacyPresentedControllerNode.swift */, D081229A1D19A9EB005F7395 /* Navigation */, @@ -650,6 +690,7 @@ D03725BF1D6DF57B007FC290 /* Context Menu */, D00701962029CAC4006B9E34 /* Tooltip */, D0DA444A1E4DCA36005FDCA7 /* Alert */, + D03AA4D3202C790D0056C405 /* Peek */, ); name = Controllers; sourceTree = ""; @@ -671,6 +712,9 @@ D0F7AB361DCFF6F8009AD9A1 /* ListViewItemHeader.swift */, D0AA840D1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift */, D0AA840F1FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift */, + D03AA4E8202E02070056C405 /* ListViewReorderingItemNode.swift */, + D03AA4EA202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift */, + D03AA5152030C5F80056C405 /* ListViewTempItemNode.swift */, ); name = "List Node"; sourceTree = ""; @@ -906,6 +950,7 @@ buildActionMask = 2147483647; files = ( D08E903C1D2417E000533158 /* ActionSheetButtonItem.swift in Sources */, + D03AA5162030C5F80056C405 /* ListViewTempItemNode.swift in Sources */, D087BFB51F75181D003FD209 /* ChildWindowHostView.swift in Sources */, D0E49C881B83A3580099E553 /* ImageCache.swift in Sources */, D0078A681C92B21400DF6D92 /* StatusBar.swift in Sources */, @@ -917,7 +962,6 @@ D05CC31F1B695A9600E235A3 /* NavigationControllerProxy.m in Sources */, D05CC3031B69568600E235A3 /* NotificationCenterUtils.m in Sources */, D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */, - D05CC2E31B69552C00E235A3 /* ViewController.swift in Sources */, D05BE4AB1D1F25E3002BD72C /* PresentationContext.swift in Sources */, D0C2DFCA1CC4431D0044FF83 /* ListViewItem.swift in Sources */, D05CC2A01B69326400E235A3 /* NavigationController.swift in Sources */, @@ -926,6 +970,7 @@ D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */, D05CC3251B695B0700E235A3 /* NavigationBarProxy.m in Sources */, D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */, + D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */, D0F8C3932014FB7C00236FC5 /* ListView.swift in Sources */, D03E7DE61C96B96E00C07816 /* NavigationBarTransitionContainer.swift in Sources */, D0C85DD01D1C082E00124894 /* ActionSheetItemGroupsContainerNode.swift in Sources */, @@ -939,6 +984,7 @@ D0DA44501E4DCBDE005FDCA7 /* AlertContentNode.swift in Sources */, D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */, D00701982029CAD6006B9E34 /* TooltipController.swift in Sources */, + D01EA41B203227BA00B4B0B5 /* ViewControllerPreviewing.swift in Sources */, D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */, D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */, D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */, @@ -951,6 +997,7 @@ D0AE3D4D1D25C816001CCE13 /* NavigationBarTransitionState.swift in Sources */, D0C85DD21D1C08AE00124894 /* ActionSheetItemNode.swift in Sources */, D0DC48561BF945DD00F672FD /* TabBarNode.swift in Sources */, + D03AA4D5202C793E0056C405 /* PeekController.swift in Sources */, D05CC31A1B695A9600E235A3 /* NavigationButtonNode.swift in Sources */, D05CC2E71B69555800E235A3 /* CAAnimationUtils.swift in Sources */, D05CC31B1B695A9600E235A3 /* NavigationTitleNode.swift in Sources */, @@ -963,12 +1010,15 @@ D0C2DFCE1CC4431D0044FF83 /* ListViewAccessoryItem.swift in Sources */, D0CE8CE91F6FC7EC00AA2DB0 /* NavigationBarTitleView.swift in Sources */, D0C12A1A1F3375B400B3F66D /* NavigationBarTitleTransitionNode.swift in Sources */, + D03AA4E1202DD4490056C405 /* PeekControllerMenuNode.swift in Sources */, D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */, + D03AA4D7202C79600056C405 /* PeekControllerNode.swift in Sources */, D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */, D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */, D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */, D01847661FFA72E000075256 /* ContainedViewLayoutTransition.swift in Sources */, D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */, + D03AA4DB202DA6D60056C405 /* PeekControllerContent.swift in Sources */, D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */, D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */, D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */, @@ -977,20 +1027,26 @@ D0FF9B301E7196F6000C66DB /* KeyboardManager.swift in Sources */, D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */, D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */, + D0A1346220336C350059716A /* ViewController.swift in Sources */, D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */, + D03AA4E9202E02070056C405 /* ListViewReorderingItemNode.swift in Sources */, D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */, D0E35A031DE473B900BC6096 /* HighlightableButton.swift in Sources */, D0CD12161CCFEB4E000DE7BC /* ScrollToTopProxyView.swift in Sources */, D0AA840E1FEBFB72005C6E91 /* ListViewFloatingHeaderNode.swift in Sources */, + D03AA4EB202E02B10056C405 /* ListViewReorderingGestureRecognizer.swift in Sources */, D0C2DFCD1CC4431D0044FF83 /* ListViewTransactionQueue.swift in Sources */, D0AA84101FED2887005C6E91 /* ListViewOverscrollBackgroundNode.swift in Sources */, D02383821DDF798E004018B6 /* LegacyPresentedControllerNode.swift in Sources */, D05CC2FC1B6955D000E235A3 /* UIKitUtils.m in Sources */, D0C2DFC61CC4431D0044FF83 /* ASTransformLayerNode.swift in Sources */, + D03AA4DD202DB1840056C405 /* PeekControllerGestureRecognizer.swift in Sources */, D007019A2029CAE2006B9E34 /* TooltipControllerNode.swift in Sources */, D05CC3291B69750D00E235A3 /* InteractiveTransitionGestureRecognizer.swift in Sources */, D077B8E91F4637040046D27A /* NavigationBarBadge.swift in Sources */, D0CE67921F7DA11700FFB557 /* ActionSheetTheme.swift in Sources */, + D0A134642034DE580059716A /* TabBarTapRecognizer.swift in Sources */, + D03AA4E3202DD52E0056C405 /* PeekControllerMenuItemNode.swift in Sources */, D0C0D2901C997110001D2851 /* FBAnimationPerformanceTracker.mm in Sources */, D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */, D036574B1E71C44D00BB1EE4 /* MinimizeKeyboardGestureRecognizer.swift in Sources */, diff --git a/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index 4b2e9b0571..250c9b5f47 100644 --- a/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Display.xcscheme orderHint - 11 + 7 DisplayMac.xcscheme @@ -17,7 +17,7 @@ DisplayTests.xcscheme orderHint - 12 + 9 SuppressBuildableAutocreation diff --git a/Display/CATracingLayer.m b/Display/CATracingLayer.m index bc8d2668bb..65389bdfd5 100644 --- a/Display/CATracingLayer.m +++ b/Display/CATracingLayer.m @@ -367,6 +367,14 @@ static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull @implementation UITracingLayerView +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)autoresizingMask { + [super setAutoresizingMask:0]; +} + + (Class)layerClass { return [CATracingLayer class]; } diff --git a/Display/ContainerViewLayout.swift b/Display/ContainerViewLayout.swift index 4680a6c64a..50c384972a 100644 --- a/Display/ContainerViewLayout.swift +++ b/Display/ContainerViewLayout.swift @@ -45,25 +45,17 @@ public struct ContainerViewLayout: Equatable { public let safeInsets: UIEdgeInsets public let statusBarHeight: CGFloat? public let inputHeight: CGFloat? + public let standardInputHeight: CGFloat public let inputHeightIsInteractivellyChanging: Bool - public init() { - self.size = CGSize() - self.metrics = LayoutMetrics() - self.intrinsicInsets = UIEdgeInsets() - self.safeInsets = UIEdgeInsets() - self.statusBarHeight = nil - self.inputHeight = nil - self.inputHeightIsInteractivellyChanging = false - } - - public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, inputHeightIsInteractivellyChanging: Bool) { + public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool) { self.size = size self.metrics = metrics self.intrinsicInsets = intrinsicInsets self.safeInsets = safeInsets self.statusBarHeight = statusBarHeight self.inputHeight = inputHeight + self.standardInputHeight = standardInputHeight self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging } @@ -79,15 +71,15 @@ public struct ContainerViewLayout: Equatable { } public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout { - return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) + return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) } public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { - return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) + return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) } public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout { - return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) + return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging) } public static func ==(lhs: ContainerViewLayout, rhs: ContainerViewLayout) -> Bool { @@ -131,6 +123,10 @@ public struct ContainerViewLayout: Equatable { return false } + if !lhs.standardInputHeight.isEqual(to: rhs.standardInputHeight) { + return false + } + if lhs.inputHeightIsInteractivellyChanging != rhs.inputHeightIsInteractivellyChanging { return false } diff --git a/Display/GlobalOverlayPresentationContext.swift b/Display/GlobalOverlayPresentationContext.swift new file mode 100644 index 0000000000..95239f270a --- /dev/null +++ b/Display/GlobalOverlayPresentationContext.swift @@ -0,0 +1,160 @@ +import Foundation +import AsyncDisplayKit +import SwiftSignalKit + +final class GlobalOverlayPresentationContext { + private let statusBarHost: StatusBarHost? + + private var controllers: [ViewController] = [] + + private var presentationDisposables = DisposableSet() + private var layout: ContainerViewLayout? + + private var ready: Bool { + return self.currentPresentationView() != nil && self.layout != nil + } + + init(statusBarHost: StatusBarHost?) { + self.statusBarHost = statusBarHost + } + + private func currentPresentationView() -> UIView? { + if let statusBarHost = self.statusBarHost { + if let keyboardWindow = statusBarHost.keyboardWindow { + return keyboardWindow + } else { + return statusBarHost.statusBarWindow + } + } + return nil + } + + func present(_ controller: ViewController) { + let controllerReady = controller.ready.get() + |> filter({ $0 }) + |> take(1) + |> deliverOnMainQueue + |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true)) + + if let _ = self.currentPresentationView(), let initialLayout = self.layout { + controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size) + controller.containerLayoutUpdated(initialLayout, transition: .immediate) + + self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in + if let strongSelf = self { + if strongSelf.controllers.contains(where: { $0 === controller }) { + return + } + + strongSelf.controllers.append(controller) + if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout { + controller.navigation_setDismiss({ [weak controller] in + if let strongSelf = self, let controller = controller { + strongSelf.dismiss(controller) + } + }, rootController: nil) + controller.setIgnoreAppearanceMethodInvocations(true) + if layout != initialLayout { + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + view.addSubview(controller.view) + controller.containerLayoutUpdated(layout, transition: .immediate) + } else { + view.addSubview(controller.view) + } + controller.setIgnoreAppearanceMethodInvocations(false) + view.layer.invalidateUpTheTree() + controller.viewWillAppear(false) + controller.viewDidAppear(false) + } + } + })) + } else { + self.controllers.append(controller) + } + } + + deinit { + self.presentationDisposables.dispose() + } + + private func dismiss(_ controller: ViewController) { + if let index = self.controllers.index(where: { $0 === controller }) { + self.controllers.remove(at: index) + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + } + } + + public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let wasReady = self.ready + self.layout = layout + + if wasReady != self.ready { + self.readyChanged(wasReady: wasReady) + } else if self.ready { + for controller in self.controllers { + controller.containerLayoutUpdated(layout, transition: transition) + } + } + } + + private func readyChanged(wasReady: Bool) { + if !wasReady { + self.addViews() + } else { + self.removeViews() + } + } + + private func addViews() { + if let view = self.currentPresentationView(), let layout = self.layout { + for controller in self.controllers { + controller.viewWillAppear(false) + view.addSubview(controller.view) + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + controller.containerLayoutUpdated(layout, transition: .immediate) + controller.viewDidAppear(false) + } + } + } + + private func removeViews() { + for controller in self.controllers { + controller.viewWillDisappear(false) + controller.view.removeFromSuperview() + controller.viewDidDisappear(false) + } + } + + func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for controller in self.controllers.reversed() { + if controller.isViewLoaded { + if let result = controller.view.hitTest(point, with: event) { + return result + } + } + } + return nil + } + + func combinedSupportedOrientations() -> UIInterfaceOrientationMask { + var mask: UIInterfaceOrientationMask = .all + + for controller in self.controllers { + mask = mask.intersection(controller.supportedInterfaceOrientations) + } + + return mask + } + + func combinedDeferScreenEdgeGestures() -> UIRectEdge { + var edges: UIRectEdge = [] + + for controller in self.controllers { + edges = edges.union(controller.deferScreenEdgeGestures) + } + + return edges + } +} diff --git a/Display/GridNode.swift b/Display/GridNode.swift index 4d36d1e715..e9368db585 100644 --- a/Display/GridNode.swift +++ b/Display/GridNode.swift @@ -102,34 +102,6 @@ public struct GridNodeUpdateLayout { } } -/*private func binarySearch(_ inputArr: [GridNodePresentationItem], searchItem: CGFloat) -> Int? { - if inputArr.isEmpty { - return nil - } - - var lowerPosition = inputArr[0].frame.origin.y + inputArr[0].frame.size.height - var upperPosition = inputArr[inputArr.count - 1].frame.origin.y - - if lowerPosition > upperPosition { - return nil - } - - while (true) { - let currentPosition = (lowerIndex + upperIndex) / 2 - if (inputArr[currentIndex] == searchItem) { - return currentIndex - } else if (lowerIndex > upperIndex) { - return nil - } else { - if (inputArr[currentIndex] > searchItem) { - upperIndex = currentIndex - 1 - } else { - lowerIndex = currentIndex + 1 - } - } - } -}*/ - public enum GridNodeStationaryItems { case none case all @@ -263,6 +235,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public var visibleItemsUpdated: ((GridNodeVisibleItems) -> Void)? public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)? + public var scrollingCompleted: (() -> Void)? public final var floatingSections = false @@ -397,11 +370,13 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.updateItemNodeVisibilititesAndScrolling() + self.scrollingCompleted?() } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.updateItemNodeVisibilititesAndScrolling() + self.scrollingCompleted?() } public func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/Display/ListView.swift b/Display/ListView.swift index 5d12a1c7bf..4ede1d78bd 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -1,10 +1,6 @@ -#if os(macOS) -import SwiftSignalKitMac -#else import UIKit import AsyncDisplayKit import SwiftSignalKit -#endif private let useBackgroundDeallocation = false @@ -48,7 +44,6 @@ final class ListViewBackingView: UIView { override func setNeedsDisplay() { } - #if os(iOS) override func touchesBegan(_ touches: Set, with event: UIEvent?) { self.target?.touchesBegan(touches, with: event) } @@ -73,7 +68,6 @@ final class ListViewBackingView: UIView { } return super.hitTest(point, with: event) } - #endif } private final class ListViewTimerProxy: NSObject { @@ -197,6 +191,8 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public final var beganInteractiveDragging: () -> Void = { } + public final var reorderItem: (Int, Int, Any?) -> Void = { _, _, _ in } + private final var animations: [ListViewAnimation] = [] private final var actionsForVSync: [() -> ()] = [] private final var inVSync = false @@ -211,6 +207,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel private var selectionLongTapDelayTimer: Foundation.Timer? private var flashNodesDelayTimer: Foundation.Timer? private var highlightedItemIndex: Int? + private var reorderNode: ListViewReorderingItemNode? private let waitingForNodesDisposable = MetaDisposable() @@ -266,13 +263,37 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel trackingRecognizer.delegate = self self.view.addGestureRecognizer(trackingRecognizer) + self.view.addGestureRecognizer(ListViewReorderingGestureRecognizer(shouldBegin: { [weak self] point in + if let strongSelf = self { + if let index = strongSelf.itemIndexAtPoint(point) { + for i in 0 ..< strongSelf.itemNodes.count { + if strongSelf.itemNodes[i].index == index { + let itemNode = strongSelf.itemNodes[i] + let itemNodeFrame = itemNode.frame + let itemNodeBounds = itemNode.bounds + if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) { + strongSelf.beginReordering(itemNode: itemNode) + return true + } + break + } + } + } + } + return false + }, ended: { [weak self] in + self?.endReordering() + }, moved: { [weak self] offset in + self?.updateReordering(offset: offset) + })) + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) - #if os(iOS) + if #available(iOS 10.0, *) { self.displayLink.preferredFramesPerSecond = 60 } - #endif + self.displayLink.isPaused = true } @@ -334,6 +355,86 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } + private func beginReordering(itemNode: ListViewItemNode) { + if let reorderNode = self.reorderNode { + reorderNode.removeFromSupernode() + } + let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) + self.reorderNode = reorderNode + self.addSubnode(reorderNode) + itemNode.isHidden = true + } + + private func endReordering() { + if let reorderNode = self.reorderNode { + self.reorderNode = nil + if let itemNode = reorderNode.itemNode, itemNode.supernode == self { + self.view.bringSubview(toFront: itemNode.view) + reorderNode.animateCompletion(completion: { [weak itemNode, weak reorderNode] in + //itemNode?.isHidden = false + reorderNode?.removeFromSupernode() + }) + self.setNeedsAnimations() + } else { + reorderNode.removeFromSupernode() + } + } + } + + private func updateReordering(offset: CGFloat) { + if let reorderNode = self.reorderNode { + reorderNode.updateOffset(offset: offset) + self.checkItemReordering() + } + } + + private func checkItemReordering() { + if let reorderNode = self.reorderNode, let reorderItemNode = reorderNode.itemNode, let reorderItemIndex = reorderItemNode.index, reorderItemNode.supernode == self { + guard let verticalTopOffset = reorderNode.currentOffset() else { + return + } + let verticalOffset = verticalTopOffset + var closestIndex: (Int, CGFloat)? + for i in 0 ..< self.itemNodes.count { + if let itemNodeIndex = self.itemNodes[i].index, itemNodeIndex != reorderItemIndex { + let itemOffset = self.itemNodes[i].frame.midY + let deltaOffset = itemOffset - verticalOffset + if let (_, closestOffset) = closestIndex { + if abs(deltaOffset) < abs(closestOffset) { + closestIndex = (itemNodeIndex, deltaOffset) + } + } else { + closestIndex = (itemNodeIndex, deltaOffset) + } + } + } + if let (closestIndexValue, offset) = closestIndex { + //print("closest \(closestIndexValue) offset \(offset)") + var toIndex: Int + if offset > 0 { + toIndex = closestIndexValue + if toIndex > reorderItemIndex { + toIndex -= 1 + } + } else { + toIndex = closestIndexValue + 1 + if toIndex > reorderItemIndex { + toIndex -= 1 + } + } + if toIndex != reorderItemNode.index { + if reorderNode.currentState?.0 != reorderItemIndex || reorderNode.currentState?.1 != toIndex { + reorderNode.currentState = (reorderItemIndex, toIndex) + //print("reorder \(reorderItemIndex) to \(toIndex) offset \(offset)") + self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState) + } + } + } + + self.setNeedsAnimations() + } + } + private func resetHeaderItemsFlashTimer(start: Bool) { if let flashNodesDelayTimer = self.flashNodesDelayTimer { flashNodesDelayTimer.invalidate() @@ -1524,7 +1625,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var offsetHeight = node.apparentHeight var takenAnimation = false - if let _ = previousFrame , animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 { + if let _ = previousFrame, animated && node.index != nil && nodeIndex != self.itemNodes.count - 1 { let nextNode = self.itemNodes[nodeIndex + 1] if nextNode.index == nil && nextNode.subnodes.isEmpty { let nextHeight = nextNode.apparentHeight @@ -1560,8 +1661,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } if node.index == nil { - node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) - node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + if node.animationForKey("height") == nil || !(node is ListViewTempItemNode) { + node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) { + node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) } else if animated { if takenAnimation { @@ -1656,7 +1761,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } - private func lowestHeaderNode() -> ASDisplayNode? { + private func lowestNodeToInsertBelow() -> ASDisplayNode? { + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + //return itemNode + } var lowestHeaderNode: ASDisplayNode? var lowestHeaderNodeIndex: Int? for (_, headerNode) in self.itemHeaderNodes { @@ -1709,6 +1817,18 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { + /*if true { + print("----------") + for itemNode in self.itemNodes { + var anim = "" + if let animation = itemNode.animationForKey("apparentHeight") { + anim = "\(animation.from)->\(animation.to)" + } + print("\(itemNode.index) \(itemNode.apparentFrame.height) \(anim)") + } + print("----------") + }*/ + let timestamp = CACurrentMediaTime() let listInsets = updateSizeAndInsets?.insets ?? self.insets @@ -1718,14 +1838,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } var previousTopItemVerticalOrigin: CGFloat? - var previousBottomItemMaxY: CGFloat? var snapshotView: UIView? if animateCrossfade { snapshotView = self.view.snapshotView(afterScreenUpdates: false) } if animateTopItemVerticalOrigin { previousTopItemVerticalOrigin = self.topItemVerticalOrigin() - previousBottomItemMaxY = self.bottomItemMaxY() } var previousApparentFrames: [(ListViewItemNode, CGRect)] = [] @@ -1740,7 +1858,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } - let lowestHeaderNode = self.lowestHeaderNode() + let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() for operation in operations { switch operation { @@ -1763,8 +1881,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp, listInsets: listInsets) if let _ = updatedPreviousFrame { - if let lowestHeaderNode = lowestHeaderNode { - self.insertSubnode(node, belowSubnode: lowestHeaderNode) + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + self.insertSubnode(node, belowSubnode: itemNode) + } else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow) } else { self.addSubnode(node) } @@ -1776,8 +1896,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.insertSubnode(node, at: 0) } } else { - if let lowestHeaderNode = lowestHeaderNode { - self.insertSubnode(node, belowSubnode: lowestHeaderNode) + if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { + self.insertSubnode(node, belowSubnode: itemNode) + } else if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(node, belowSubnode: lowestNodeToInsertBelow) } else { self.addSubnode(node) } @@ -1797,7 +1919,9 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if let height = height, let previousLayout = previousLayout { if takenPreviousNodes.contains(referenceNode) { - self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: ListViewItemNode(layerBacked: true), layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets) + let tempNode = ListViewTempItemNode(layerBacked: true) + //referenceNode.copyHeightAndApparentHeightAnimations(to: tempNode) + self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: tempNode, layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets) } else { referenceNode.index = nil self.insertNodeAtIndex(animated: false, animateAlpha: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: referenceNode, layout: previousLayout, apply: { return (nil, {}) }, timestamp: timestamp, listInsets: listInsets) @@ -2212,11 +2336,11 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) if let offset = offset , abs(offset) > CGFloat.ulpOfOne { - let lowestHeaderNode = self.lowestHeaderNode() + let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() for itemNode in temporaryPreviousNodes { itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: offset) - if let lowestHeaderNode = lowestHeaderNode { - self.insertSubnode(itemNode, belowSubnode: lowestHeaderNode) + if let lowestNodeToInsertBelow = lowestNodeToInsertBelow { + self.insertSubnode(itemNode, belowSubnode: lowestNodeToInsertBelow) } else { self.addSubnode(itemNode) } @@ -2714,7 +2838,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel private func immediateDisplayedItemRange() -> ListViewDisplayedItemRange { var loadedRange: ListViewItemRange? - var visibleRange: ListViewItemRange? + var visibleRange: ListViewVisibleItemRange? if self.itemNodes.count != 0 { var firstIndex: (nodeIndex: Int, index: Int)? var lastIndex: (nodeIndex: Int, index: Int)? @@ -2736,12 +2860,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel i -= 1 } if let firstIndex = firstIndex, let lastIndex = lastIndex { - var firstVisibleIndex: Int? + var firstVisibleIndex: (Int, Bool)? for i in firstIndex.nodeIndex ... lastIndex.nodeIndex { if let index = self.itemNodes[i].index { let frame = self.itemNodes[i].apparentFrame if frame.maxY >= self.insets.top && frame.minY < self.visibleSize.height + self.insets.bottom { - firstVisibleIndex = index + firstVisibleIndex = (index, frame.minY >= self.insets.top) break } } @@ -2760,7 +2884,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } if let lastVisibleIndex = lastVisibleIndex { - visibleRange = ListViewItemRange(firstIndex: firstVisibleIndex, lastIndex: lastVisibleIndex) + visibleRange = ListViewVisibleItemRange(firstIndex: firstVisibleIndex.0, firstIndexFullyVisible: firstVisibleIndex.1, lastIndex: lastVisibleIndex) } } @@ -2807,6 +2931,20 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var offsetRanges = OffsetRanges() + if let reorderOffset = self.reorderNode?.currentOffset(), !self.itemNodes.isEmpty { + if reorderOffset < self.insets.top + 10.0 { + if self.itemNodes[0].apparentFrame.minY < self.insets.top { + continueAnimations = true + offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: 6.0) + } + } else if reorderOffset > self.visibleSize.height - self.insets.bottom - 10.0 { + if self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY > self.visibleSize.height - self.insets.bottom { + continueAnimations = true + offsetRanges.offset(IndexRange(first: 0, last: Int.max), offset: -6.0) + } + } + } + var requestUpdateVisibleItems = false var index = 0 while index < self.itemNodes.count { @@ -2871,9 +3009,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if requestUpdateVisibleItems { self.enqueueUpdateVisibleItems() } + + self.checkItemReordering() } - #if os(iOS) override open func touchesBegan(_ touches: Set, with event: UIEvent?) { let touchesPosition = touches.first!.location(in: self.view) @@ -2963,7 +3102,6 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.updateScroller(transition: .immediate) } - #endif public func clearHighlightAnimated(_ animated: Bool) { if let highlightedItemIndex = self.highlightedItemIndex { @@ -3016,8 +3154,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } return nil } - - #if os(iOS) + override open func touchesMoved(_ touches: Set, with event: UIEvent?) { if let selectionTouchLocation = self.selectionTouchLocation { let location = touches.first!.location(in: self.view) @@ -3099,7 +3236,6 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel break } } - #endif public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true @@ -3111,8 +3247,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel completion() } } - - #if os(iOS) + fileprivate func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> Bool { if self.limitHitTestToNodes { var foundHit = false @@ -3128,5 +3263,4 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } return true } - #endif } diff --git a/Display/ListViewAnimation.swift b/Display/ListViewAnimation.swift index cf0ea9c09e..db07f932cf 100644 --- a/Display/ListViewAnimation.swift +++ b/Display/ListViewAnimation.swift @@ -124,6 +124,19 @@ public final class ListViewAnimation { self.completed = completed } + init(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) { + self.from = copying.from + self.to = copying.to + self.duration = copying.duration + self.curve = copying.curve + self.startTime = copying.startTime + self.interpolator = copying.interpolator + self.update = { progress, value in + update(progress, value as! T) + } + self.completed = completed + } + public func completeAt(_ timestamp: Double) -> Bool { if timestamp >= self.startTime + self.duration { self.completed(true) diff --git a/Display/ListViewIntermediateState.swift b/Display/ListViewIntermediateState.swift index 8cf6609a95..92412b4d6b 100644 --- a/Display/ListViewIntermediateState.swift +++ b/Display/ListViewIntermediateState.swift @@ -145,15 +145,25 @@ public struct ListViewUpdateSizeAndInsets { public struct ListViewItemRange: Equatable { public let firstIndex: Int public let lastIndex: Int + + public static func ==(lhs: ListViewItemRange, rhs: ListViewItemRange) -> Bool { + return lhs.firstIndex == rhs.firstIndex && lhs.lastIndex == rhs.lastIndex + } } -public func ==(lhs: ListViewItemRange, rhs: ListViewItemRange) -> Bool { - return lhs.firstIndex == rhs.firstIndex && lhs.lastIndex == rhs.lastIndex +public struct ListViewVisibleItemRange: Equatable { + public let firstIndex: Int + public let firstIndexFullyVisible: Bool + public let lastIndex: Int + + public static func ==(lhs: ListViewVisibleItemRange, rhs: ListViewVisibleItemRange) -> Bool { + return lhs.firstIndex == rhs.firstIndex && lhs.firstIndexFullyVisible == rhs.firstIndexFullyVisible && lhs.lastIndex == rhs.lastIndex + } } public struct ListViewDisplayedItemRange: Equatable { public let loadedRange: ListViewItemRange? - public let visibleRange: ListViewItemRange? + public let visibleRange: ListViewVisibleItemRange? } public func ==(lhs: ListViewDisplayedItemRange, rhs: ListViewDisplayedItemRange) -> Bool { diff --git a/Display/ListViewItemNode.swift b/Display/ListViewItemNode.swift index e353a36b26..1bae95320d 100644 --- a/Display/ListViewItemNode.swift +++ b/Display/ListViewItemNode.swift @@ -430,6 +430,27 @@ open class ListViewItemNode: ASDisplayNode { self.setAnimationForKey("height", animation: animation) } + func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) { + if let animation = self.animationForKey("apparentHeight") { + let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in + if let strongSelf = otherNode { + let frame = strongSelf.frame + strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue)) + } + }) + otherNode.setAnimationForKey("height", animation: updatedAnimation) + } + + if let animation = self.animationForKey("apparentHeight") { + let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in + if let strongSelf = otherNode { + strongSelf.apparentHeight = currentValue + } + }) + otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation) + } + } + public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { @@ -484,6 +505,10 @@ open class ListViewItemNode: ASDisplayNode { open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { } + open func isReorderable(at point: CGPoint) -> Bool { + return false + } + open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { } diff --git a/Display/ListViewReorderingGestureRecognizer.swift b/Display/ListViewReorderingGestureRecognizer.swift new file mode 100644 index 0000000000..fa631b4d6f --- /dev/null +++ b/Display/ListViewReorderingGestureRecognizer.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit + +final class ListViewReorderingGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> Bool + private let ended: () -> Void + private let moved: (CGFloat) -> Void + + private var initialLocation: CGPoint? + + init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { + self.shouldBegin = shouldBegin + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + } + + override func reset() { + super.reset() + + self.initialLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.state == .possible { + if let location = touches.first?.location(in: self.view), self.shouldBegin(location) { + self.initialLocation = location + self.state = .began + } else { + self.state = .failed + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if self.state == .began || self.state == .changed { + self.ended() + self.state = .failed + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = location.y - initialLocation.y + self.moved(offset) + } + } +} diff --git a/Display/ListViewReorderingItemNode.swift b/Display/ListViewReorderingItemNode.swift new file mode 100644 index 0000000000..e1abb43424 --- /dev/null +++ b/Display/ListViewReorderingItemNode.swift @@ -0,0 +1,48 @@ +import Foundation +import AsyncDisplayKit + +final class ListViewReorderingItemNode: ASDisplayNode { + weak var itemNode: ListViewItemNode? + + var currentState: (Int, Int)? + + private let copyView: UIView? + private let initialLocation: CGPoint + + init(itemNode: ListViewItemNode, initialLocation: CGPoint) { + self.itemNode = itemNode + self.copyView = itemNode.view.snapshotContentTree() + self.initialLocation = initialLocation + + super.init() + + if let copyView = self.copyView { + self.view.addSubview(copyView) + copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size) + } + } + + func updateOffset(offset: CGFloat) { + if let copyView = self.copyView { + copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size) + } + } + + func currentOffset() -> CGFloat? { + if let copyView = self.copyView { + return copyView.center.y + } + return nil + } + + func animateCompletion(completion: @escaping () -> Void) { + if let copyView = self.copyView, let itemNode = self.itemNode { + itemNode.isHidden = false + itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY + itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime()) + completion() + } else { + completion() + } + } +} diff --git a/Display/ListViewTempItemNode.swift b/Display/ListViewTempItemNode.swift new file mode 100644 index 0000000000..0a6ebc9e18 --- /dev/null +++ b/Display/ListViewTempItemNode.swift @@ -0,0 +1,5 @@ +import Foundation + +final class ListViewTempItemNode: ListViewItemNode { + +} diff --git a/Display/NativeWindowHostView.swift b/Display/NativeWindowHostView.swift index cd0288008b..6668b9f3d3 100644 --- a/Display/NativeWindowHostView.swift +++ b/Display/NativeWindowHostView.swift @@ -11,6 +11,7 @@ private let defaultOrientations: UIInterfaceOrientationMask = { private class WindowRootViewController: UIViewController { var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)? + var orientations: UIInterfaceOrientationMask = defaultOrientations { didSet { if oldValue != self.orientations { @@ -73,6 +74,7 @@ private final class NativeWindow: UIWindow, WindowHost { var updateIsUpdatingOrientationLayout: ((Bool) -> Void)? var updateToInterfaceOrientation: (() -> Void)? var presentController: ((ViewController, PresentationSurfaceLevel) -> Void)? + var presentControllerInGlobalOverlay: ((_ controller: ViewController) -> Void)? var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? var presentNativeImpl: ((UIViewController) -> Void)? var invalidateDeferScreenEdgeGestureImpl: (() -> Void)? @@ -150,6 +152,10 @@ private final class NativeWindow: UIWindow, WindowHost { self.presentController?(controller, level) } + func presentInGlobalOverlay(_ controller: ViewController) { + self.presentControllerInGlobalOverlay?(controller) + } + func presentNative(_ controller: UIViewController) { self.presentNativeImpl?(controller) } @@ -226,6 +232,10 @@ public func nativeWindowHostView() -> WindowHostView { hostView?.present?(controller, level) } + window.presentControllerInGlobalOverlay = { [weak hostView] controller in + hostView?.presentInGlobalOverlay?(controller) + } + window.presentNativeImpl = { [weak hostView] controller in hostView?.presentNative?(controller) } diff --git a/Display/NavigationController.swift b/Display/NavigationController.swift index 531ccdc445..6cf72c003d 100644 --- a/Display/NavigationController.swift +++ b/Display/NavigationController.swift @@ -21,7 +21,7 @@ private class NavigationControllerView: UIView { open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate { public private(set) weak var overlayPresentingController: ViewController? - private var containerLayout = ContainerViewLayout() + private var validLayout: ContainerViewLayout? private var navigationTransitionCoordinator: NavigationTransitionCoordinator? @@ -72,10 +72,10 @@ open class NavigationController: UINavigationController, ContainableController, if !self.isViewLoaded { self.loadView() } - self.containerLayout = layout + self.validLayout = layout transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) - let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) + let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) if let topViewController = self.topViewController { if let topViewController = topViewController as? ContainableController { @@ -101,6 +101,7 @@ open class NavigationController: UINavigationController, ContainableController, open override func loadView() { self.view = NavigationControllerView() self.view.clipsToBounds = true + self.view.autoresizingMask = [] if #available(iOSApplicationExtension 11.0, *) { self.navigationBar.prefersLargeTitles = false @@ -224,17 +225,21 @@ open class NavigationController: UINavigationController, ContainableController, if !controller.hasActiveInput { self.view.endEditing(true) } - let appliedLayout = self.containerLayout.withUpdatedInputHeight(controller.hasActiveInput ? self.containerLayout.inputHeight : nil) - controller.containerLayoutUpdated(appliedLayout, transition: .immediate) - self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in - if let strongSelf = self { - let containerLayout = strongSelf.containerLayout.withUpdatedInputHeight(controller.hasActiveInput ? strongSelf.containerLayout.inputHeight : nil) - if containerLayout != appliedLayout { - controller.containerLayoutUpdated(containerLayout, transition: .immediate) + if let validLayout = self.validLayout { + let appliedLayout = validLayout.withUpdatedInputHeight(controller.hasActiveInput ? validLayout.inputHeight : nil) + controller.containerLayoutUpdated(appliedLayout, transition: .immediate) + self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in + if let strongSelf = self, let validLayout = strongSelf.validLayout { + let containerLayout = validLayout.withUpdatedInputHeight(controller.hasActiveInput ? validLayout.inputHeight : nil) + if containerLayout != appliedLayout { + controller.containerLayoutUpdated(containerLayout, transition: .immediate) + } + strongSelf.pushViewController(controller, animated: true) } - strongSelf.pushViewController(controller, animated: true) - } - })) + })) + } else { + self.pushViewController(controller, animated: false) + } } open override func pushViewController(_ viewController: UIViewController, animated: Bool) { @@ -247,7 +252,9 @@ open class NavigationController: UINavigationController, ContainableController, public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { self.view.endEditing(true) - controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) + if let validLayout = self.validLayout { + controller.containerLayoutUpdated(validLayout, transition: .immediate) + } self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { ready?.set(true) @@ -261,7 +268,9 @@ open class NavigationController: UINavigationController, ContainableController, public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { self.view.endEditing(true) - controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) + if let validLayout = self.validLayout { + controller.containerLayoutUpdated(validLayout, transition: .immediate) + } self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { ready?.set(true) @@ -323,15 +332,17 @@ open class NavigationController: UINavigationController, ContainableController, let topViewController = viewControllers[viewControllers.count - 1] as UIViewController if let controller = topViewController as? ContainableController { - var layoutToApply = self.containerLayout - var hasActiveInput = false - if let controller = controller as? ViewController { - hasActiveInput = controller.hasActiveInput + if let validLayout = self.validLayout { + var layoutToApply = validLayout + var hasActiveInput = false + if let controller = controller as? ViewController { + hasActiveInput = controller.hasActiveInput + } + if !hasActiveInput { + layoutToApply = layoutToApply.withUpdatedInputHeight(nil) + } + controller.containerLayoutUpdated(layoutToApply, transition: .immediate) } - if !hasActiveInput { - layoutToApply = layoutToApply.withUpdatedInputHeight(nil) - } - controller.containerLayoutUpdated(layoutToApply, transition: .immediate) } else { topViewController.view.frame = CGRect(origin: CGPoint(), size: self.view.bounds.size) } @@ -452,7 +463,9 @@ open class NavigationController: UINavigationController, ContainableController, self._presentedViewController = controller self.view.endEditing(true) - controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) + if let validLayout = self.validLayout { + controller.containerLayoutUpdated(validLayout, transition: .immediate) + } var ready: Signal = .single(true) diff --git a/Display/PeekController.swift b/Display/PeekController.swift new file mode 100644 index 0000000000..fb968c77aa --- /dev/null +++ b/Display/PeekController.swift @@ -0,0 +1,81 @@ +import Foundation +import AsyncDisplayKit + +public final class PeekControllerTheme { + public let isDark: Bool + public let menuBackgroundColor: UIColor + public let menuItemHighligtedColor: UIColor + public let menuItemSeparatorColor: UIColor + public let accentColor: UIColor + public let destructiveColor: UIColor + + public init(isDark: Bool, menuBackgroundColor: UIColor, menuItemHighligtedColor: UIColor, menuItemSeparatorColor: UIColor, accentColor: UIColor, destructiveColor: UIColor) { + self.isDark = isDark + self.menuBackgroundColor = menuBackgroundColor + self.menuItemHighligtedColor = menuItemHighligtedColor + self.menuItemSeparatorColor = menuItemSeparatorColor + self.accentColor = accentColor + self.destructiveColor = destructiveColor + } +} + +public final class PeekController: ViewController { + private var controllerNode: PeekControllerNode { + return self.displayNode as! PeekControllerNode + } + + private let theme: PeekControllerTheme + private let content: PeekControllerContent + private let sourceNode: () -> ASDisplayNode? + + private var animatedIn = false + + public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { + self.theme = theme + self.content = content + self.sourceNode = sourceNode + + super.init(navigationBarTheme: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in + self?.dismiss() + }) + self.displayNodeDidLoad() + } + + private func getSourceRect() -> CGRect { + if let sourceNode = self.sourceNode() { + return sourceNode.view.convert(sourceNode.bounds, to: self.view) + } else { + let size = self.displayNode.bounds.size + return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0)) + } + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn(from: self.getSourceRect()) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } +} diff --git a/Display/PeekControllerContent.swift b/Display/PeekControllerContent.swift new file mode 100644 index 0000000000..871b3cb18e --- /dev/null +++ b/Display/PeekControllerContent.swift @@ -0,0 +1,23 @@ +import Foundation +import AsyncDisplayKit + +public enum PeekControllerContentPresentation { + case contained + case freeform +} + +public enum PeerkControllerMenuActivation { + case drag + case press +} + +public protocol PeekControllerContent { + func presentation() -> PeekControllerContentPresentation + func menuActivation() -> PeerkControllerMenuActivation + func menuItems() -> [PeekControllerMenuItem] + func node() -> PeekControllerContentNode & ASDisplayNode +} + +public protocol PeekControllerContentNode { + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize +} diff --git a/Display/PeekControllerGestureRecognizer.swift b/Display/PeekControllerGestureRecognizer.swift new file mode 100644 index 0000000000..2a4d69e3ec --- /dev/null +++ b/Display/PeekControllerGestureRecognizer.swift @@ -0,0 +1,228 @@ +import Foundation +import UIKit +import SwiftSignalKit + +private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { + if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { + return true + } + for subview in view.subviews { + let subviewPoint = view.convert(point, to: subview) + if traceDeceleratingScrollView(subview, at: subviewPoint) { + return true + } + } + return false +} + +public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { + private let contentAtPoint: (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? + private let present: (PeekControllerContent, ASDisplayNode) -> PeekController? + + private var tapLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var pressTimer: SwiftSignalKit.Timer? + + private let candidateContentDisposable = MetaDisposable() + private var candidateContent: (ASDisplayNode, PeekControllerContent)? + + private var menuActivation: PeerkControllerMenuActivation? + private weak var presentedController: PeekController? + + public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> PeekController?) { + self.contentAtPoint = contentAtPoint + self.present = present + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.pressTimer?.invalidate() + self.candidateContentDisposable.dispose() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func startPressTimer() { + self.pressTimer?.invalidate() + let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.pressTimerFired() + }, queue: Queue.mainQueue()) + self.pressTimer = pressTimer + pressTimer.start() + } + + private func stopLongTapTimer() { + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func stopPressTimer() { + self.pressTimer?.invalidate() + self.pressTimer = nil + } + + override public func reset() { + super.reset() + + self.stopLongTapTimer() + self.stopPressTimer() + self.tapLocation = nil + self.candidateContent = nil + self.menuActivation = nil + self.presentedController = nil + } + + private func longTapTimerFired() { + guard let _ = self.tapLocation, let (sourceNode, content) = self.candidateContent else { + return + } + + self.state = .began + + if let presentedController = self.present(content, sourceNode) { + self.menuActivation = content.menuActivation() + self.presentedController = presentedController + + switch content.menuActivation() { + case .drag: + break + case .press: + if #available(iOSApplicationExtension 9.0, *) { + if presentedController.traitCollection.forceTouchCapability != .available { + self.startPressTimer() + } + } else { + self.startPressTimer() + } + } + } + } + + private func pressTimerFired() { + if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation { + if let presentedController = self.presentedController { + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + } + self.menuActivation = nil + self.presentedController = nil + self.state = .ended + } + } + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let view = self.view, let tapLocation = touches.first?.location(in: view) { + if traceDeceleratingScrollView(view, at: tapLocation) { + self.candidateContent = nil + self.state = .failed + } else { + if let contentSignal = self.contentAtPoint(tapLocation) { + self.candidateContentDisposable.set((contentSignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch strongSelf.state { + case .possible, .changed: + if let (sourceNode, content) = result { + strongSelf.tapLocation = tapLocation + strongSelf.candidateContent = (sourceNode, content) + strongSelf.menuActivation = content.menuActivation() + strongSelf.startLongTapTimer() + } else { + strongSelf.state = .failed + } + default: + break + } + } + })) + } else { + self.state = .failed + } + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + let velocity = self.velocity(in: self.view) + + if let presentedController = self.presentedController, presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.endDraggingWithVelocity(velocity.y) + self.presentedController = nil + self.menuActivation = nil + } + + self.tapLocation = nil + self.candidateContent = nil + self.state = .failed + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.tapLocation = nil + self.candidateContent = nil + self.state = .failed + + if let presentedController = self.presentedController { + self.menuActivation = nil + self.presentedController = nil + presentedController.dismiss() + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let touch = touches.first, let initialTapLocation = self.tapLocation, let menuActivation = self.menuActivation { + let touchLocation = touch.location(in: self.view) + if let presentedController = self.presentedController { + switch menuActivation { + case .drag: + var offset = touchLocation.y - initialTapLocation.y + let delta = abs(offset) + let factor: CGFloat = 60.0 + offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) + + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) + } + case .press: + if #available(iOSApplicationExtension 9.0, *) { + if touch.force >= 2.5 { + if presentedController.isNodeLoaded { + (presentedController.displayNode as? PeekControllerNode)?.activateMenu() + self.menuActivation = nil + self.presentedController = nil + self.state = .ended + } + } + } + break + } + } else { + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.tapLocation = nil + self.candidateContent = nil + self.state = .failed + } + } + } + } +} diff --git a/Display/PeekControllerMenuItemNode.swift b/Display/PeekControllerMenuItemNode.swift new file mode 100644 index 0000000000..324004fbe9 --- /dev/null +++ b/Display/PeekControllerMenuItemNode.swift @@ -0,0 +1,91 @@ +import Foundation +import AsyncDisplayKit + +public enum PeekControllerMenuItemColor { + case accent + case destructive +} + +public struct PeekControllerMenuItem { + public let title: String + public let color: PeekControllerMenuItemColor + public let action: () -> Void + + public init(title: String, color: PeekControllerMenuItemColor, action: @escaping () -> Void) { + self.title = title + self.color = color + self.action = action + } +} + +final class PeekControllerMenuItemNode: HighlightTrackingButtonNode { + private let item: PeekControllerMenuItem + private let activatedAction: () -> Void + + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let textNode: ASTextNode + + init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) { + self.item = item + self.activatedAction = activatedAction + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.menuItemSeparatorColor + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.textNode = ASTextNode() + self.textNode.isLayerBacked = true + self.textNode.displaysAsynchronously = false + + let textColor: UIColor + switch item.color { + case .accent: + textColor = theme.accentColor + case .destructive: + textColor = theme.destructiveColor + } + self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(20.0), textColor: textColor) + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.view.superview?.bringSubview(toFront: strongSelf.view) + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let height: CGFloat = 57.0 + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) + + let textSize = self.textNode.measure(CGSize(width: width - 10.0, height: height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((height - textSize.height) / 2.0)), size: textSize)) + + return height + } + + @objc func buttonPressed() { + self.activatedAction() + self.item.action() + } +} diff --git a/Display/PeekControllerMenuNode.swift b/Display/PeekControllerMenuNode.swift new file mode 100644 index 0000000000..a89587d5ea --- /dev/null +++ b/Display/PeekControllerMenuNode.swift @@ -0,0 +1,30 @@ +import Foundation +import AsyncDisplayKit + +final class PeekControllerMenuNode: ASDisplayNode { + private let itemNodes: [PeekControllerMenuItemNode] + + init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) { + self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) } + + super.init() + + self.backgroundColor = theme.menuBackgroundColor + self.cornerRadius = 16.0 + self.clipsToBounds = true + + for itemNode in self.itemNodes { + self.addSubnode(itemNode) + } + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var verticalOffset: CGFloat = 0.0 + for itemNode in self.itemNodes { + let itemHeight = itemNode.updateLayout(width: width, transition: transition) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight))) + verticalOffset += itemHeight + } + return verticalOffset - UIScreenPixel + } +} diff --git a/Display/PeekControllerNode.swift b/Display/PeekControllerNode.swift new file mode 100644 index 0000000000..2ca82a1fad --- /dev/null +++ b/Display/PeekControllerNode.swift @@ -0,0 +1,206 @@ +import Foundation +import AsyncDisplayKit + +final class PeekControllerNode: ViewControllerTracingNode { + private let requestDismiss: () -> Void + + private let theme: PeekControllerTheme + + private let blurView: UIView + private let dimNode: ASDisplayNode + private let containerBackgroundNode: ASImageNode + private let containerNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + private var containerOffset: CGFloat = 0.0 + + private let content: PeekControllerContent + private let contentNode: PeekControllerContentNode & ASDisplayNode + + private let menuNode: PeekControllerMenuNode? + private var displayingMenu = false + + init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { + self.theme = theme + self.requestDismiss = requestDismiss + + self.dimNode = ASDisplayNode() + self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light)) + self.blurView.isUserInteractionEnabled = false + + switch content.menuActivation() { + case .drag: + self.dimNode.backgroundColor = nil + self.blurView.alpha = 1.0 + case .press: + self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5) + self.blurView.alpha = 0.0 + } + + self.containerBackgroundNode = ASImageNode() + self.containerBackgroundNode.isLayerBacked = true + self.containerBackgroundNode.displayWithoutProcessing = true + self.containerBackgroundNode.displaysAsynchronously = false + + self.containerNode = ASDisplayNode() + self.containerNode.clipsToBounds = true + self.containerNode.cornerRadius = 16.0 + + self.content = content + self.contentNode = content.node() + + var activatedActionImpl: (() -> Void)? + let menuItems = content.menuItems() + if menuItems.isEmpty { + self.menuNode = nil + } else { + self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: { + activatedActionImpl?() + }) + } + + super.init() + + self.addSubnode(self.dimNode) + self.view.addSubview(self.blurView) + self.containerNode.addSubnode(self.contentNode) + self.addSubnode(self.containerNode) + + if let menuNode = self.menuNode { + self.addSubnode(menuNode) + } + + activatedActionImpl = { [weak self] in + self?.requestDismiss() + } + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let layoutInsets = layout.insets(options: []) + let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) + + var menuSize: CGSize? + + let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: transition) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + + var containerFrame: CGRect + switch self.content.presentation() { + case .contained: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: self.containerOffset + floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + case .freeform: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: self.containerOffset + floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize) + } + + if let menuNode = self.menuNode { + let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0 + let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition) + menuSize = CGSize(width: menuWidth, height: menuHeight) + + if self.displayingMenu { + containerFrame.origin.y = min(containerFrame.origin.y, layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height) + + transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0) + } + } + + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + if let menuNode = self.menuNode, let menuSize = menuSize { + let menuY: CGFloat + if self.displayingMenu { + menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height) + } else { + menuY = layout.size.height + 14.0 + } + + transition.updateFrame(node: menuNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize)) + } + } + + func animateIn(from rect: CGRect) { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) + + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + func animateOut(to rect: CGRect, completion: @escaping () -> Void) { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, force: true, completion: { _ in + completion() + }) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + if let menuNode = self.menuNode { + menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false) + } + } + + @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.requestDismiss() + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + break + case .cancelled, .ended: + break + default: + break + } + } + + func applyDraggingOffset(_ offset: CGFloat) { + self.containerOffset = min(0.0, offset) + if self.containerOffset < -25.0 { + //self.displayingMenu = true + } else { + //self.displayingMenu = false + } + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .immediate) + } + } + + func activateMenu() { + if let layout = self.validLayout { + self.displayingMenu = true + self.containerOffset = 0.0 + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) + } + } + + func endDraggingWithVelocity(_ velocity: CGFloat) { + if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 { + if let layout = self.validLayout { + self.displayingMenu = true + self.containerOffset = 0.0 + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) + } + } else { + self.requestDismiss() + } + } +} diff --git a/Display/StatusBarManager.swift b/Display/StatusBarManager.swift index 4ab5f9483e..5546e940e2 100644 --- a/Display/StatusBarManager.swift +++ b/Display/StatusBarManager.swift @@ -226,15 +226,15 @@ class StatusBarManager { self.host.statusBarStyle = statusBarStyle } if let statusBarWindow = self.host.statusBarWindow { - statusBarWindow.alpha = globalStatusBar.1 + statusBarView.alpha = globalStatusBar.1 var statusBarBounds = statusBarWindow.bounds if !statusBarBounds.origin.y.isEqual(to: globalStatusBar.2) { statusBarBounds.origin.y = globalStatusBar.2 statusBarWindow.bounds = statusBarBounds } } - } else if let statusBarWindow = self.host.statusBarWindow { - statusBarWindow.alpha = 0.0 + } else { + statusBarView.alpha = 0.0 } } } diff --git a/Display/StatusBarProxyNode.swift b/Display/StatusBarProxyNode.swift index b6750b07fb..791332e55e 100644 --- a/Display/StatusBarProxyNode.swift +++ b/Display/StatusBarProxyNode.swift @@ -34,6 +34,7 @@ public enum StatusBarStyle { private enum StatusBarItemType { case Generic case Battery + case Activity } func makeStatusBarProxy(_ statusBarStyle: StatusBarStyle, statusBar: UIView) -> StatusBarProxyNode { @@ -90,7 +91,22 @@ private class StatusBarItemNode: ASDisplayNode { UIGraphicsPopContext() } } - let type: StatusBarItemType = self.targetView.checkIsKind(of: batteryItemClass!) ? .Battery : .Generic + //dumpViews(self.targetView) + var type: StatusBarItemType = self.targetView.checkIsKind(of: batteryItemClass!) ? .Battery : .Generic + if case .Generic = type { + var hasActivityBackground = false + var hasText = false + for subview in self.targetView.subviews { + if let stringClass = stringClass, subview.checkIsKind(of: stringClass) { + hasText = true + } else if let activityClass = activityClass, subview.checkIsKind(of: activityClass) { + hasActivityBackground = true + } + } + if hasActivityBackground && hasText { + type = .Activity + } + } tintStatusBarItem(context, type: type, style: statusBarStyle) self.contents = context.generateImage()?.cgImage @@ -229,6 +245,8 @@ private func tintStatusBarItem(_ context: DrawingContext, type: StatusBarItemTyp } } } + case .Activity: + break case .Generic: var pixel = context.bytes.assumingMemoryBound(to: UInt32.self) let end = context.bytes.advanced(by: context.length).assumingMemoryBound(to: UInt32.self) @@ -259,7 +277,15 @@ private func tintStatusBarItem(_ context: DrawingContext, type: StatusBarItemTyp } } -private let batteryItemClass: AnyClass? = { () -> AnyClass? in +private let foregroundClass: AnyClass? = { + var nameString = "StatusBar" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ForegroundView" + } + return NSClassFromString("_UI" + nameString) +}() + +private let batteryItemClass: AnyClass? = { var nameString = "StatusBarBattery" if CFAbsoluteTimeGetCurrent() > 0 { nameString += "ItemView" @@ -267,6 +293,22 @@ private let batteryItemClass: AnyClass? = { () -> AnyClass? in return NSClassFromString("UI" + nameString) }() +private let activityClass: AnyClass? = { + var nameString = "StatusBarBackground" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "ActivityView" + } + return NSClassFromString("_UI" + nameString) +}() + +private let stringClass: AnyClass? = { + var nameString = "StatusBar" + if CFAbsoluteTimeGetCurrent() > 0 { + nameString += "StringView" + } + return NSClassFromString("_UI" + nameString) +}() + private class StatusBarProxyNodeTimerTarget: NSObject { let action: () -> Void @@ -327,7 +369,15 @@ class StatusBarProxyNode: ASDisplayNode { self.clipsToBounds = true //self.backgroundColor = UIColor.blueColor().colorWithAlphaComponent(0.2) + var rootView: UIView = statusBar for subview in statusBar.subviews { + if let foregroundClass = foregroundClass, subview.checkIsKind(of: foregroundClass) { + rootView = subview + break + } + } + + for subview in rootView.subviews { let itemNode = StatusBarItemNode(statusBarStyle: statusBarStyle, targetView: subview) self.itemNodes.append(itemNode) self.addSubnode(itemNode) @@ -343,10 +393,20 @@ class StatusBarProxyNode: ASDisplayNode { private func updateItems() { let statusBar = self.statusBar + var rootView: UIView = statusBar + for subview in statusBar.subviews { + if let foregroundClass = foregroundClass, subview.checkIsKind(of: foregroundClass) { + rootView = subview + break + } + } + + //dumpViews(self.statusBar) + var i = 0 while i < self.itemNodes.count { var found = false - for subview in statusBar.subviews { + for subview in rootView.subviews { if self.itemNodes[i].targetView == subview { found = true break @@ -362,7 +422,7 @@ class StatusBarProxyNode: ASDisplayNode { } } - for subview in statusBar.subviews { + for subview in rootView.subviews { var found = false for itemNode in self.itemNodes { if itemNode.targetView == subview { diff --git a/Display/TabBarController.swift b/Display/TabBarController.swift index daf9c043ee..5f1b1434db 100644 --- a/Display/TabBarController.swift +++ b/Display/TabBarController.swift @@ -26,7 +26,7 @@ public final class TabBarControllerTheme { } open class TabBarController: ViewController { - private var containerLayout = ContainerViewLayout() + private var validLayout: ContainerViewLayout? private var tabBarControllerNode: TabBarControllerNode { get { @@ -88,10 +88,32 @@ open class TabBarController: ViewController { } } + private var debugTapCounter: (Double, Int) = (0.0, 0) + override open func loadDisplayNode() { self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index in if let strongSelf = self { - strongSelf.controllers[index].containerLayoutUpdated(strongSelf.containerLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate) + if strongSelf.selectedIndex == index { + let timestamp = CACurrentMediaTime() + if strongSelf.debugTapCounter.0 < timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 = 0 + } + + if strongSelf.debugTapCounter.0 >= timestamp - 0.4 { + strongSelf.debugTapCounter.0 = timestamp + strongSelf.debugTapCounter.1 += 1 + } + + if strongSelf.debugTapCounter.1 >= 10 { + strongSelf.debugTapCounter.1 = 0 + + strongSelf.controllers[index].tabBarItemDebugTapAction?() + } + } + if let validLayout = strongSelf.validLayout { + strongSelf.controllers[index].containerLayoutUpdated(validLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate) + } strongSelf.pendingControllerDisposable.set((strongSelf.controllers[index].ready.get() |> deliverOnMainQueue).start(next: { _ in if let strongSelf = self { strongSelf.selectedIndex = index @@ -127,7 +149,9 @@ open class TabBarController: ViewController { var displayNavigationBar = false if let currentController = self.currentController { currentController.willMove(toParentViewController: self) - currentController.containerLayoutUpdated(self.containerLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate) + if let validLayout = self.validLayout { + currentController.containerLayoutUpdated(validLayout.addedInsets(insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 49.0, right: 0.0)), transition: .immediate) + } self.tabBarControllerNode.currentControllerView = currentController.view currentController.navigationBar?.isHidden = true self.addChildViewController(currentController) @@ -149,13 +173,15 @@ open class TabBarController: ViewController { self.setDisplayNavigationBar(displayNavigationBar) } - self.tabBarControllerNode.containerLayoutUpdated(self.containerLayout, transition: .immediate) + if let validLayout = self.validLayout { + self.tabBarControllerNode.containerLayoutUpdated(validLayout, transition: .immediate) + } } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.containerLayout = layout + self.validLayout = layout self.tabBarControllerNode.containerLayoutUpdated(layout, transition: transition) diff --git a/Display/TabBarNode.swift b/Display/TabBarNode.swift index 1b19c3152a..36e91edae6 100644 --- a/Display/TabBarNode.swift +++ b/Display/TabBarNode.swift @@ -197,6 +197,16 @@ class TabBarNode: ASDisplayNode { self.addSubnode(self.separatorNode) } + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(TabBarTapRecognizer(tap: { [weak self] point in + if let strongSelf = self { + strongSelf.tapped(at: point) + } + })) + } + func updateTheme(_ theme: TabBarControllerTheme) { if self.theme !== theme { self.theme = theme @@ -354,11 +364,8 @@ class TabBarNode: ASDisplayNode { } } - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - - if let touch = touches.first, let bottomInset = self.validLayout?.3 { - let location = touch.location(in: self.view) + private func tapped(at location: CGPoint) { + if let bottomInset = self.validLayout?.3 { if location.y > self.bounds.size.height - bottomInset { return } diff --git a/Display/TabBarTapRecognizer.swift b/Display/TabBarTapRecognizer.swift new file mode 100644 index 0000000000..4cbe02b777 --- /dev/null +++ b/Display/TabBarTapRecognizer.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit + +final class TabBarTapRecognizer: UIGestureRecognizer { + private let tap: (CGPoint) -> Void + + private var initialLocation: CGPoint? + + init(tap: @escaping (CGPoint) -> Void) { + self.tap = tap + + super.init(target: nil, action: nil) + } + + override func reset() { + super.reset() + + self.initialLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.initialLocation == nil { + self.initialLocation = touches.first?.location(in: self.view) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if let initialLocation = self.initialLocation { + self.initialLocation = nil + self.tap(initialLocation) + self.state = .ended + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + let deltaX = initialLocation.x - location.x + let deltaY = initialLocation.y - location.y + if deltaX * deltaX + deltaY * deltaY > 4.0 { + self.initialLocation = nil + self.state = .failed + } + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + self.state = .failed + } +} diff --git a/Display/UIKitUtils.swift b/Display/UIKitUtils.swift index 1fb1150a23..9299e63fee 100644 --- a/Display/UIKitUtils.swift +++ b/Display/UIKitUtils.swift @@ -134,7 +134,7 @@ public extension UIImage { private func makeSubtreeSnapshot(layer: CALayer) -> UIView? { let view = UIView() - //view.layer.isHidden = layer.isHidden + view.layer.isHidden = layer.isHidden view.layer.opacity = layer.opacity view.layer.contents = layer.contents view.layer.contentsRect = layer.contentsRect @@ -143,6 +143,7 @@ private func makeSubtreeSnapshot(layer: CALayer) -> UIView? { view.layer.contentsGravity = layer.contentsGravity view.layer.masksToBounds = layer.masksToBounds view.layer.cornerRadius = layer.cornerRadius + view.layer.backgroundColor = layer.backgroundColor if let sublayers = layer.sublayers { for sublayer in sublayers { let subtree = makeSubtreeSnapshot(layer: sublayer) @@ -160,7 +161,7 @@ private func makeSubtreeSnapshot(layer: CALayer) -> UIView? { private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { let view = CALayer() - //view.layer.isHidden = layer.isHidden + view.isHidden = layer.isHidden view.opacity = layer.opacity view.contents = layer.contents view.contentsRect = layer.contentsRect @@ -169,6 +170,7 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { view.contentsGravity = layer.contentsGravity view.masksToBounds = layer.masksToBounds view.cornerRadius = layer.cornerRadius + view.backgroundColor = layer.backgroundColor if let sublayers = layer.sublayers { for sublayer in sublayers { let subtree = makeLayerSubtreeSnapshot(layer: sublayer) @@ -185,24 +187,40 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { } public extension UIView { - public func snapshotContentTree() -> UIView? { - if let snapshot = makeSubtreeSnapshot(layer: self.layer) { + public func snapshotContentTree(unhide: Bool = false) -> UIView? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeSubtreeSnapshot(layer: self.layer) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { snapshot.frame = self.frame return snapshot - } else { - return nil } + + return nil } } public extension CALayer { - public func snapshotContentTree() -> CALayer? { - if let snapshot = makeLayerSubtreeSnapshot(layer: self) { + public func snapshotContentTree(unhide: Bool = false) -> CALayer? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeLayerSubtreeSnapshot(layer: self) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { snapshot.frame = self.frame return snapshot - } else { - return nil } + + return nil } } diff --git a/Display/UIViewController+Navigation.h b/Display/UIViewController+Navigation.h index 316fc4bca2..5926a69e3e 100644 --- a/Display/UIViewController+Navigation.h +++ b/Display/UIViewController+Navigation.h @@ -7,6 +7,7 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { @interface UIViewController (Navigation) +- (BOOL)isPresentedInPreviewingContext; - (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations; - (BOOL)ignoreAppearanceMethodInvocations; - (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller; diff --git a/Display/UIViewController+Navigation.m b/Display/UIViewController+Navigation.m index 085c7be66d..332d4fc0f1 100644 --- a/Display/UIViewController+Navigation.m +++ b/Display/UIViewController+Navigation.m @@ -106,6 +106,10 @@ static bool notyfyingShiftState = false; }); } +- (BOOL)isPresentedInPreviewingContext { + return ![self.presentingViewController isKindOfClass:[UIViewControllerPresentingProxy class]]; +} + - (void)setIgnoreAppearanceMethodInvocations:(BOOL)ignoreAppearanceMethodInvocations { [self setAssociatedObject:@(ignoreAppearanceMethodInvocations) forKey:UIViewControllerIgnoreAppearanceMethodInvocationsKey]; diff --git a/Display/ViewController.swift b/Display/ViewController.swift index c00c6f1ba1..4c12b141c1 100644 --- a/Display/ViewController.swift +++ b/Display/ViewController.swift @@ -30,7 +30,7 @@ open class ViewControllerPresentationArguments { } @objc open class ViewController: UIViewController, ContainableController { - private var containerLayout = ContainerViewLayout() + private var validLayout: ContainerViewLayout? private let presentationContext: PresentationContext public final var supportedOrientations: UIInterfaceOrientationMask = .all @@ -60,6 +60,8 @@ open class ViewControllerPresentationArguments { public private(set) var presentationArguments: Any? + public var tabBarItemDebugTapAction: (() -> Void)? + private var _displayNode: ASDisplayNode? public final var displayNode: ASDisplayNode { get { @@ -86,6 +88,8 @@ open class ViewControllerPresentationArguments { public let statusBar: StatusBar public let navigationBar: NavigationBar? + private var previewingContext: Any? + public var displayNavigationBar = true private weak var activeInputViewCandidate: UIResponder? @@ -172,7 +176,7 @@ open class ViewControllerPresentationArguments { } open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.containerLayout = layout + self.validLayout = layout if !self.isViewLoaded { self.loadView() @@ -221,6 +225,7 @@ open class ViewControllerPresentationArguments { self.displayNode.addSubnode(navigationBar) } } + self.view.autoresizingMask = [] self.view.addSubview(self.statusBar.view) self.presentationContext.view = self.view } @@ -238,8 +243,8 @@ open class ViewControllerPresentationArguments { } public func requestLayout(transition: ContainedViewLayoutTransition) { - if self.isViewLoaded { - self.containerLayoutUpdated(self.containerLayout, transition: transition) + if self.isViewLoaded, let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) } } @@ -293,6 +298,11 @@ open class ViewControllerPresentationArguments { } } + public func presentInGlobalOverlay(_ controller: ViewController, with arguments: Any? = nil) { + controller.presentationArguments = arguments + self.window?.presentInGlobalOverlay(controller) + } + open override func viewWillDisappear(_ animated: Bool) { self.activeInputViewCandidate = findCurrentResponder(self.view) @@ -313,4 +323,27 @@ open class ViewControllerPresentationArguments { open func dismiss(completion: (() -> Void)? = nil) { } + + @available(iOSApplicationExtension 9.0, *) + open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) { + if self.traitCollection.forceTouchCapability == .available { + let _ = super.registerForPreviewing(with: delegate, sourceView: sourceView) + } else if !onlyNative { + if self.previewingContext == nil { + let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in + self?.presentInGlobalOverlay(c, with: a) + }) + self.previewingContext = previewingContext + } + } + } + + @available(iOSApplicationExtension 9.0, *) + open override func unregisterForPreviewing(withContext previewing: UIViewControllerPreviewing) { + if self.previewingContext != nil { + self.previewingContext = nil + } else { + super.unregisterForPreviewing(withContext: previewing) + } + } } diff --git a/Display/ViewControllerPreviewing.swift b/Display/ViewControllerPreviewing.swift new file mode 100644 index 0000000000..137384e6dc --- /dev/null +++ b/Display/ViewControllerPreviewing.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import SwiftSignalKit + +@available(iOSApplicationExtension 9.0, *) +private final class ViewControllerPeekContent: PeekControllerContent { + private let controller: ViewController + private let menu: [PeekControllerMenuItem] + + init(controller: ViewController) { + self.controller = controller + var menu: [PeekControllerMenuItem] = [] + for item in controller.previewActionItems { + menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] in + if let controller = controller, let item = item as? UIPreviewAction { + item.handler(item, controller) + } + })) + } + self.menu = menu + } + + func presentation() -> PeekControllerContentPresentation { + return .contained + } + + func menuActivation() -> PeerkControllerMenuActivation { + return .drag + } + + func menuItems() -> [PeekControllerMenuItem] { + return self.menu + } + + func node() -> PeekControllerContentNode & ASDisplayNode { + return ViewControllerPeekContentNode(controller: self.controller) + } +} + +private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode { + private let controller: ViewController + private var hasValidLayout = false + + init(controller: ViewController) { + self.controller = controller + + super.init() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + if !self.hasValidLayout { + self.hasValidLayout = true + self.controller.view.frame = CGRect(origin: CGPoint(), size: size) + self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + self.controller.setIgnoreAppearanceMethodInvocations(true) + self.view.addSubview(self.controller.view) + self.controller.setIgnoreAppearanceMethodInvocations(false) + self.controller.viewWillAppear(false) + self.controller.viewDidAppear(false) + } else { + self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: transition) + } + + return size + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.view + } + return nil + } +} + +@available(iOSApplicationExtension 9.0, *) +final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing { + weak var delegateImpl: UIViewControllerPreviewingDelegate? + var delegate: UIViewControllerPreviewingDelegate { + return self.delegateImpl! + } + let recognizer: PeekControllerGestureRecognizer + var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer { + return self.recognizer + } + let sourceView: UIView + let node: ASDisplayNode + + var sourceRect: CGRect = CGRect() + + init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void) { + self.delegateImpl = delegate + self.sourceView = sourceView + self.node = node + var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)? + self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in + return contentAtPointImpl?(point) + }, present: { content, sourceNode in + let controller = PeekController(theme: theme, content: content, sourceNode: { + return sourceNode + }) + present(controller, nil) + return controller + }) + + node.view.addGestureRecognizer(self.recognizer) + + super.init() + + contentAtPointImpl = { [weak self] point in + if let strongSelf = self, let delegate = strongSelf.delegateImpl { + if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController { + return .single((strongSelf.node, ViewControllerPeekContent(controller: controller))) + } + } + return nil + } + } +} diff --git a/Display/WindowContent.swift b/Display/WindowContent.swift index b2b1f9cdee..a085e496ac 100644 --- a/Display/WindowContent.swift +++ b/Display/WindowContent.swift @@ -162,7 +162,26 @@ private func containedLayoutForWindowLayout(_ layout: WindowLayout) -> Container resolvedSafeInsets.right = 44.0 } - return ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 00, right: 0.0), safeInsets: resolvedSafeInsets, statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil) + var standardInputHeight: CGFloat = 216.0 + var predictiveHeight: CGFloat = 42.0 + + if layout.size.width.isEqual(to: 320.0) || layout.size.width.isEqual(to: 375.0) { + standardInputHeight = 216.0 + predictiveHeight = 42.0 + } else if layout.size.width.isEqual(to: 414.0) { + standardInputHeight = 226.0 + predictiveHeight = 42.0 + } else if layout.size.width.isEqual(to: 480.0) || layout.size.width.isEqual(to: 568.0) || layout.size.width.isEqual(to: 667.0) || layout.size.width.isEqual(to: 736.0) { + standardInputHeight = 162.0 + predictiveHeight = 38.0 + } else if layout.size.width.isEqual(to: 768.0) || layout.size.width.isEqual(to: 1024.0) { + standardInputHeight = 264.0 + predictiveHeight = 42.0 + } + + standardInputHeight += predictiveHeight + + return ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 00, right: 0.0), safeInsets: resolvedSafeInsets, statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, standardInputHeight: standardInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil) } private func encodeText(_ string: String, _ key: Int) -> String { @@ -250,6 +269,7 @@ public final class WindowHostView { let updatePreferNavigationUIHidden: (Bool) -> Void var present: ((ViewController, PresentationSurfaceLevel) -> Void)? + var presentInGlobalOverlay: ((_ controller: ViewController) -> Void)? var presentNative: ((UIViewController) -> Void)? var updateSize: ((CGSize) -> Void)? var layoutSubviews: (() -> Void)? @@ -276,6 +296,7 @@ public struct WindowTracingTags { public protocol WindowHost { func present(_ controller: ViewController, on level: PresentationSurfaceLevel) + func presentInGlobalOverlay(_ controller: ViewController) func invalidateDeferScreenEdgeGestures() func invalidatePreferNavigationUIHidden() func cancelInteractiveKeyboardGestures() @@ -324,6 +345,7 @@ public class Window1 { private var cachedHasPreview: Bool = false private let presentationContext: PresentationContext + private let overlayPresentationContext: GlobalOverlayPresentationContext private var tracingStatusBarsInvalidated = false private var shouldUpdateDeferScreenEdgeGestures = false @@ -372,11 +394,16 @@ public class Window1 { self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsetsForScreenSize(boundsSize), onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil) self.presentationContext = PresentationContext() + self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost) self.hostView.present = { [weak self] controller, level in self?.present(controller, on: level) } + self.hostView.presentInGlobalOverlay = { [weak self] controller in + self?.presentInGlobalOverlay(controller) + } + self.hostView.presentNative = { [weak self] controller in self?.presentNative(controller) } @@ -415,6 +442,7 @@ public class Window1 { self.presentationContext.view = self.hostView.view self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate) + self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout), transition: .immediate) self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil, queue: OperationQueue.main, using: { [weak self] notification in if let strongSelf = self { @@ -570,6 +598,10 @@ public class Window1 { } } + if let result = self.overlayPresentationContext.hitTest(point, with: event) { + return result + } + for controller in self._topLevelOverlayControllers.reversed() { if let result = controller.view.hitTest(point, with: event) { return result @@ -802,6 +834,7 @@ public class Window1 { if childLayoutUpdated { self._rootController?.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) + self.overlayPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) for controller in self.topLevelOverlayControllers { controller.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) @@ -833,6 +866,10 @@ public class Window1 { self.presentationContext.present(controller, on: level) } + public func presentInGlobalOverlay(_ controller: ViewController) { + self.overlayPresentationContext.present(controller) + } + public func presentNative(_ controller: UIViewController) { }