From c04ff1cc24ef27757b9f2b4e960756bac06b204d Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Wed, 31 Jan 2024 14:14:15 +0000 Subject: [PATCH] prevent a gap appearing behind the keyboard when it goes away and refactor the code a little --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/MainView.swift | 168 +++++--------------------- DuckDuckGo/MainViewController.swift | 6 +- DuckDuckGo/MainViewCoordinator.swift | 147 ++++++++++++++++++++++ DuckDuckGo/SwipeTabsCoordinator.swift | 43 +++---- 5 files changed, 207 insertions(+), 161 deletions(-) create mode 100644 DuckDuckGo/MainViewCoordinator.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6731e36728..ebf64e9d50 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -497,6 +497,7 @@ 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DB12EA2A1FE2A4000A4A72 /* LockScreenWidgets.swift */; }; 85DB12ED2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */; }; 85DDE0402AC6FF65006ABCA2 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DDE03F2AC6FF65006ABCA2 /* MainView.swift */; }; + 85DE681A2B6A8BB000DED4FE /* MainViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DE68192B6A8BB000DED4FE /* MainViewCoordinator.swift */; }; 85DF714624F7FE6100C89288 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F143C2E41E4A4CD400CFDE3A /* Core.framework */; }; 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DFEDEC24C7CCA500973FE7 /* AppWidthObserver.swift */; }; 85DFEDEF24C7EA3B00973FE7 /* SmallOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DFEDEE24C7EA3B00973FE7 /* SmallOmniBarState.swift */; }; @@ -1590,6 +1591,7 @@ 85DB12EA2A1FE2A4000A4A72 /* LockScreenWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenWidgets.swift; sourceTree = ""; }; 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+AppDeepLinks.swift"; sourceTree = ""; }; 85DDE03F2AC6FF65006ABCA2 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 85DE68192B6A8BB000DED4FE /* MainViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewCoordinator.swift; sourceTree = ""; }; 85DFEDEC24C7CCA500973FE7 /* AppWidthObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppWidthObserver.swift; sourceTree = ""; }; 85DFEDEE24C7EA3B00973FE7 /* SmallOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallOmniBarState.swift; sourceTree = ""; }; 85DFEDF024C7EEA400973FE7 /* LargeOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeOmniBarState.swift; sourceTree = ""; }; @@ -5432,6 +5434,7 @@ 851DFD86212C39D300D95F20 /* TabSwitcherButton.swift */, CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */, 85B9814D2B5EB618009AC9A6 /* SwipeTabsCoordinator.swift */, + 85DE68192B6A8BB000DED4FE /* MainViewCoordinator.swift */, ); name = Main; sourceTree = ""; @@ -6708,6 +6711,7 @@ 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, 857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */, 311BD1AF2836BB4200AEF6C1 /* AutofillItemsLockedView.swift in Sources */, + 85DE681A2B6A8BB000DED4FE /* MainViewCoordinator.swift in Sources */, 0290472A29E867800008FE3C /* AppTPTrackerDetailView.swift in Sources */, F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */, 85449EF523FDA02800512AAF /* KeyboardSettingsViewController.swift in Sources */, diff --git a/DuckDuckGo/MainView.swift b/DuckDuckGo/MainView.swift index 98c1eeb464..25881d444b 100644 --- a/DuckDuckGo/MainView.swift +++ b/DuckDuckGo/MainView.swift @@ -19,9 +19,6 @@ import UIKit -// swiftlint:disable file_length -// swiftlint:disable line_length - class MainViewFactory { private let coordinator: MainViewCoordinator @@ -63,6 +60,7 @@ extension MainViewFactory { createOmniBar() createToolbar() createNavigationBarContainer() + createNavigationBarCollectionView() createProgressView() } @@ -76,24 +74,38 @@ extension MainViewFactory { coordinator.omniBar.translatesAutoresizingMaskIntoConstraints = false } - final class NavigationBarContainer: UICollectionView { + final class NavigationBarCollectionView: UICollectionView { var hitTestInsets = UIEdgeInsets.zero override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - var extendedBounds = bounds.inset(by: hitTestInsets) - return extendedBounds.contains(point) + return bounds.inset(by: hitTestInsets).contains(point) + } + + // Don't allow the use to drag the scrollbar or the UI will glitch. + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + if view == self.subviews.first(where: { $0 is UIImageView }) { + return nil + } + return view } } - private func createNavigationBarContainer() { + private func createNavigationBarCollectionView() { // Layout is replaced elsewhere, but required to construct the view. - coordinator.navigationBarContainer = NavigationBarContainer(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - coordinator.navigationBarContainer.decelerationRate = .fast + coordinator.navigationBarCollectionView = NavigationBarCollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) // scrollview subclasses change the default to true, but we need this for the separator on the omnibar - coordinator.navigationBarContainer.clipsToBounds = false + coordinator.navigationBarCollectionView.clipsToBounds = false + coordinator.navigationBarCollectionView.translatesAutoresizingMaskIntoConstraints = false + coordinator.navigationBarContainer.addSubview(coordinator.navigationBarCollectionView) + } + + final class NavigationBarContainer: UIView { } + private func createNavigationBarContainer() { + coordinator.navigationBarContainer = NavigationBarContainer() superview.addSubview(coordinator.navigationBarContainer) } @@ -199,19 +211,26 @@ extension MainViewFactory { coordinator.constraints.progressBarTop, ]) } - + private func constrainNavigationBarContainer() { let navigationBarContainer = coordinator.navigationBarContainer! let toolbar = coordinator.toolbar! + let navigationBarCollectionView = coordinator.navigationBarCollectionView! coordinator.constraints.navigationBarContainerTop = navigationBarContainer.constrainView(superview.safeAreaLayoutGuide, by: .top) coordinator.constraints.navigationBarContainerBottom = navigationBarContainer.constrainView(toolbar, by: .bottom, to: .top) - + coordinator.constraints.navigationBarCollectionViewBottom = navigationBarCollectionView.constrainView(navigationBarContainer, by: .bottom, relatedBy: .greaterThanOrEqual) + NSLayoutConstraint.activate([ coordinator.constraints.navigationBarContainerTop, navigationBarContainer.constrainView(superview, by: .leading), navigationBarContainer.constrainView(superview, by: .trailing), - navigationBarContainer.constrainAttribute(.height, to: 52), + navigationBarContainer.constrainAttribute(.height, to: 52, relatedBy: .greaterThanOrEqual), + navigationBarCollectionView.constrainAttribute(.height, to: 52), + navigationBarCollectionView.constrainView(navigationBarContainer, by: .top), + navigationBarCollectionView.constrainView(navigationBarContainer, by: .leading), + navigationBarCollectionView.constrainView(navigationBarContainer, by: .trailing), + coordinator.constraints.navigationBarCollectionViewBottom ]) } @@ -322,126 +341,3 @@ extension MainViewFactory { } } - -class MainViewCoordinator { - - let superview: UIView - - var contentContainer: UIView! - var lastToolbarButton: UIBarButtonItem! - var logo: UIImageView! - var logoContainer: UIView! - var logoText: UIImageView! - var navigationBarContainer: MainViewFactory.NavigationBarContainer! - var notificationBarContainer: UIView! - var omniBar: OmniBar! - var progress: ProgressView! - var statusBackground: UIView! - var suggestionTrayContainer: UIView! - var tabBarContainer: UIView! - var toolbar: UIToolbar! - var toolbarBackButton: UIBarButtonItem! - var toolbarFireButton: UIBarButtonItem! - var toolbarForwardButton: UIBarButtonItem! - var toolbarTabSwitcherButton: UIBarButtonItem! - - let constraints = Constraints() - - // The default after creating the hiearchy is top - var addressBarPosition: AddressBarPosition = .top - - fileprivate init(superview: UIView) { - self.superview = superview - } - - func decorateWithTheme(_ theme: Theme) { - superview.backgroundColor = theme.mainViewBackgroundColor - logoText.tintColor = theme.ddgTextTintColor - omniBar.decorate(with: theme) - } - - func showToolbarSeparator() { - toolbar.setShadowImage(nil, forToolbarPosition: .any) - } - - func hideToolbarSeparator() { - self.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any) - } - - class Constraints { - - var navigationBarContainerTop: NSLayoutConstraint! - var navigationBarContainerBottom: NSLayoutConstraint! - var toolbarBottom: NSLayoutConstraint! - var contentContainerTop: NSLayoutConstraint! - var tabBarContainerTop: NSLayoutConstraint! - var notificationContainerTopToNavigationBar: NSLayoutConstraint! - var notificationContainerTopToStatusBackground: NSLayoutConstraint! - var notificationContainerHeight: NSLayoutConstraint! - var progressBarTop: NSLayoutConstraint! - var progressBarBottom: NSLayoutConstraint! - var statusBackgroundToNavigationBarContainerBottom: NSLayoutConstraint! - var statusBackgroundBottomToSafeAreaTop: NSLayoutConstraint! - var contentContainerBottomToToolbarTop: NSLayoutConstraint! - var contentContainerBottomToNavigationBarContainerTop: NSLayoutConstraint! - - } - - func moveAddressBarToPosition(_ position: AddressBarPosition) { - guard position != addressBarPosition else { return } - switch position { - case .top: - setAddressBarBottomActive(false) - setAddressBarTopActive(true) - - case .bottom: - setAddressBarTopActive(false) - setAddressBarBottomActive(true) - } - - addressBarPosition = position - } - - func hideNavigationBarWithBottomPosition() { - guard addressBarPosition.isBottom else { - return - } - - // Hiding the container won't suffice as it still defines the contentContainer.bottomY through constraints - navigationBarContainer.isHidden = true - - constraints.contentContainerBottomToNavigationBarContainerTop.isActive = false - constraints.contentContainerBottomToToolbarTop.isActive = true - } - - func showNavigationBarWithBottomPosition() { - guard addressBarPosition.isBottom else { - return - } - - constraints.contentContainerBottomToToolbarTop.isActive = false - constraints.contentContainerBottomToNavigationBarContainerTop.isActive = true - - navigationBarContainer.isHidden = false - } - - func setAddressBarTopActive(_ active: Bool) { - constraints.contentContainerBottomToToolbarTop.isActive = active - constraints.navigationBarContainerTop.isActive = active - constraints.progressBarTop.isActive = active - constraints.notificationContainerTopToNavigationBar.isActive = active - constraints.statusBackgroundToNavigationBarContainerBottom.isActive = active - } - - func setAddressBarBottomActive(_ active: Bool) { - constraints.contentContainerBottomToNavigationBarContainerTop.isActive = active - constraints.progressBarBottom.isActive = active - constraints.navigationBarContainerBottom.isActive = active - constraints.notificationContainerTopToStatusBackground.isActive = active - constraints.statusBackgroundBottomToSafeAreaTop.isActive = active - } - -} - -// swiftlint:enable line_length -// swiftlint:enable file_length diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 786f047c61..3b12916955 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -483,6 +483,7 @@ class MainViewController: UIViewController { swipeTabsCoordinator?.addressBarPositionChanged(isTop: true) viewCoordinator.omniBar.moveSeparatorToBottom() viewCoordinator.showToolbarSeparator() + viewCoordinator.constraints.navigationBarContainerBottom.isActive = false case .bottom: swipeTabsCoordinator?.addressBarPositionChanged(isTop: false) @@ -543,7 +544,7 @@ class MainViewController: UIViewController { if self.appSettings.currentAddressBarPosition.isBottom { let navBarOffset = min(0, self.toolbarHeight - intersection.height) - self.viewCoordinator.constraints.navigationBarContainerBottom.constant = navBarOffset + self.viewCoordinator.constraints.navigationBarCollectionViewBottom.constant = navBarOffset UIView.animate(withDuration: duration, delay: 0, options: animationCurve) { self.viewCoordinator.navigationBarContainer.superview?.layoutIfNeeded() } @@ -1089,7 +1090,6 @@ class MainViewController: UIViewController { viewCoordinator.omniBar.enterPadState() swipeTabsCoordinator?.isEnabled = false - // disableSwipeTabs() } private func applySmallWidth() { @@ -1098,8 +1098,6 @@ class MainViewController: UIViewController { viewCoordinator.omniBar.enterPhoneState() swipeTabsCoordinator?.isEnabled = featureFlagger.isFeatureOn(.swipeTabs) - - // enableSwipeTabs() } @discardableResult diff --git a/DuckDuckGo/MainViewCoordinator.swift b/DuckDuckGo/MainViewCoordinator.swift new file mode 100644 index 0000000000..9906ef7767 --- /dev/null +++ b/DuckDuckGo/MainViewCoordinator.swift @@ -0,0 +1,147 @@ +// +// MainViewCoordinator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class MainViewCoordinator { + + let superview: UIView + + var contentContainer: UIView! + var lastToolbarButton: UIBarButtonItem! + var logo: UIImageView! + var logoContainer: UIView! + var logoText: UIImageView! + var navigationBarContainer: UIView! + var navigationBarCollectionView: MainViewFactory.NavigationBarCollectionView! + var notificationBarContainer: UIView! + var omniBar: OmniBar! + var progress: ProgressView! + var statusBackground: UIView! + var suggestionTrayContainer: UIView! + var tabBarContainer: UIView! + var toolbar: UIToolbar! + var toolbarBackButton: UIBarButtonItem! + var toolbarFireButton: UIBarButtonItem! + var toolbarForwardButton: UIBarButtonItem! + var toolbarTabSwitcherButton: UIBarButtonItem! + + let constraints = Constraints() + + // The default after creating the hiearchy is top + var addressBarPosition: AddressBarPosition = .top + + /// STOP - why are you instanciating this? + init(superview: UIView) { + self.superview = superview + } + + func showToolbarSeparator() { + toolbar.setShadowImage(nil, forToolbarPosition: .any) + } + + func hideToolbarSeparator() { + self.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any) + } + + class Constraints { + + var navigationBarContainerTop: NSLayoutConstraint! + var navigationBarContainerBottom: NSLayoutConstraint! + var navigationBarCollectionViewBottom: NSLayoutConstraint! + var toolbarBottom: NSLayoutConstraint! + var contentContainerTop: NSLayoutConstraint! + var tabBarContainerTop: NSLayoutConstraint! + var notificationContainerTopToNavigationBar: NSLayoutConstraint! + var notificationContainerTopToStatusBackground: NSLayoutConstraint! + var notificationContainerHeight: NSLayoutConstraint! + var progressBarTop: NSLayoutConstraint! + var progressBarBottom: NSLayoutConstraint! + var statusBackgroundToNavigationBarContainerBottom: NSLayoutConstraint! + var statusBackgroundBottomToSafeAreaTop: NSLayoutConstraint! + var contentContainerBottomToToolbarTop: NSLayoutConstraint! + var contentContainerBottomToNavigationBarContainerTop: NSLayoutConstraint! + + } + + func moveAddressBarToPosition(_ position: AddressBarPosition) { + guard position != addressBarPosition else { return } + switch position { + case .top: + setAddressBarBottomActive(false) + setAddressBarTopActive(true) + + case .bottom: + setAddressBarTopActive(false) + setAddressBarBottomActive(true) + } + + addressBarPosition = position + } + + func hideNavigationBarWithBottomPosition() { + guard addressBarPosition.isBottom else { + return + } + + // Hiding the container won't suffice as it still defines the contentContainer.bottomY through constraints + navigationBarContainer.isHidden = true + + constraints.contentContainerBottomToNavigationBarContainerTop.isActive = false + constraints.contentContainerBottomToToolbarTop.isActive = true + } + + func showNavigationBarWithBottomPosition() { + guard addressBarPosition.isBottom else { + return + } + + constraints.contentContainerBottomToToolbarTop.isActive = false + constraints.contentContainerBottomToNavigationBarContainerTop.isActive = true + + navigationBarContainer.isHidden = false + } + + func setAddressBarTopActive(_ active: Bool) { + constraints.contentContainerBottomToToolbarTop.isActive = active + constraints.navigationBarContainerTop.isActive = active + constraints.progressBarTop.isActive = active + constraints.notificationContainerTopToNavigationBar.isActive = active + constraints.statusBackgroundToNavigationBarContainerBottom.isActive = active + } + + func setAddressBarBottomActive(_ active: Bool) { + constraints.contentContainerBottomToNavigationBarContainerTop.isActive = active + constraints.progressBarBottom.isActive = active + constraints.navigationBarContainerBottom.isActive = active + constraints.notificationContainerTopToStatusBackground.isActive = active + constraints.statusBackgroundBottomToSafeAreaTop.isActive = active + } + +} + +extension MainViewCoordinator: Themable { + + func decorate(with theme: Theme) { + superview.backgroundColor = theme.mainViewBackgroundColor + logoText.tintColor = theme.ddgTextTintColor + omniBar.decorate(with: theme) + } + +} diff --git a/DuckDuckGo/SwipeTabsCoordinator.swift b/DuckDuckGo/SwipeTabsCoordinator.swift index 3deb4832f1..ebe0b93c60 100644 --- a/DuckDuckGo/SwipeTabsCoordinator.swift +++ b/DuckDuckGo/SwipeTabsCoordinator.swift @@ -19,12 +19,6 @@ import UIKit -// TODO handle launching on home screen tab - -// TODO slide the logo when in homescreen view? - -// TODO fix gap behind when keyboard shown - class SwipeTabsCoordinator: NSObject { static let tabGap: CGFloat = 10 @@ -47,10 +41,14 @@ class SwipeTabsCoordinator: NSObject { var isEnabled = false { didSet { - coordinator.navigationBarContainer.reloadData() + collectionView.reloadData() } } + var collectionView: MainViewFactory.NavigationBarCollectionView { + coordinator.navigationBarCollectionView + } + init(coordinator: MainViewCoordinator, tabPreviewsSource: TabPreviewsSource, appSettings: AppSettings, @@ -63,12 +61,15 @@ class SwipeTabsCoordinator: NSObject { self.selectTab = selectTab self.onSwipeStarted = onSwipeStarted - - coordinator.navigationBarContainer.register(OmniBarCell.self, forCellWithReuseIdentifier: "omnibar") - coordinator.navigationBarContainer.isPagingEnabled = true - + super.init() + collectionView.register(OmniBarCell.self, forCellWithReuseIdentifier: "omnibar") + collectionView.isPagingEnabled = true + collectionView.delegate = self + collectionView.dataSource = self + collectionView.decelerationRate = .fast + updateLayout() } @@ -90,7 +91,7 @@ class SwipeTabsCoordinator: NSObject { weak var currentView: UIView? private func updateLayout() { - let layout = coordinator.navigationBarContainer.collectionViewLayout as? UICollectionViewFlowLayout + let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout layout?.scrollDirection = .horizontal layout?.itemSize = CGSize(width: coordinator.superview.frame.size.width, height: coordinator.omniBar.frame.height) layout?.minimumLineSpacing = 0 @@ -104,9 +105,9 @@ class SwipeTabsCoordinator: NSObject { print("***", #function, animated) DispatchQueue.main.async { let indexPath = IndexPath(row: self.tabsModel.currentIndex, section: 0) - self.coordinator.navigationBarContainer.scrollToItem(at: indexPath, - at: .centeredHorizontally, - animated: animated) + self.collectionView.scrollToItem(at: indexPath, + at: .centeredHorizontally, + animated: animated) } } } @@ -220,14 +221,14 @@ extension SwipeTabsCoordinator: UICollectionViewDelegate { } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - print("***", #function, coordinator.navigationBarContainer.indexPathsForVisibleItems) + print("***", #function, coordinator.navigationBarCollectionView.indexPathsForVisibleItems) - let point = CGPoint(x: coordinator.navigationBarContainer.bounds.midX, - y: coordinator.navigationBarContainer.bounds.midY) - let index = coordinator.navigationBarContainer.indexPathForItem(at: point)?.row + let point = CGPoint(x: coordinator.navigationBarCollectionView.bounds.midX, + y: coordinator.navigationBarCollectionView.bounds.midY) + let index = coordinator.navigationBarCollectionView.indexPathForItem(at: point)?.row assert(index != nil) feedbackGenerator.selectionChanged() - selectTab(index ?? coordinator.navigationBarContainer.indexPathsForVisibleItems[0].row) + selectTab(index ?? coordinator.navigationBarCollectionView.indexPathsForVisibleItems[0].row) cleanUpViews() @@ -270,7 +271,7 @@ extension SwipeTabsCoordinator { let scrollToItem = self.tabsModel == nil self.tabsModel = tabsModel - coordinator.navigationBarContainer.reloadData() + coordinator.navigationBarCollectionView.reloadData() updateLayout() if scrollToItem {