From 33fbfb202d712a39b75af10643fbbc6df3ad4c45 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:08:30 -0500 Subject: [PATCH 1/5] Remove BottomSheetViewController usage from JetpackBrandingCoordinator --- .../JetpackBrandingCoordinator.swift | 8 +-- .../Branding/Overlay/JetpackOverlayView.swift | 58 ++++++++----------- .../JetpackOverlayViewController.swift | 14 ----- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift index dd59b175b96c..1227814ac5c9 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift @@ -4,7 +4,7 @@ import WordPressUI /// A class containing convenience methods for the the Jetpack branding experience class JetpackBrandingCoordinator { - static func presentOverlay(from viewController: UIViewController, redirectAction: (() -> Void)? = nil) { + static func presentOverlay(from presentingViewController: UIViewController, redirectAction: (() -> Void)? = nil) { let action = redirectAction ?? { // Try to export WordPress data to a shared location before redirecting the user. @@ -13,9 +13,9 @@ class JetpackBrandingCoordinator { } } - let jetpackOverlayViewController = JetpackOverlayViewController(viewFactory: makeJetpackOverlayView, redirectAction: action) - let bottomSheet = BottomSheetViewController(childViewController: jetpackOverlayViewController, customHeaderSpacing: 0) - bottomSheet.show(from: viewController) + let jetpackOverlayVC = JetpackOverlayViewController(viewFactory: makeJetpackOverlayView, redirectAction: action) + jetpackOverlayVC.sheetPresentationController?.detents = [.medium()] + presentingViewController.present(jetpackOverlayVC, animated: true) } static func makeJetpackOverlayView(redirectAction: (() -> Void)? = nil) -> UIView { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift index 7a44c82bc646..5729457435c5 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift @@ -1,7 +1,8 @@ import Lottie import UIKit +import WordPressUI -class JetpackOverlayView: UIView { +final class JetpackOverlayView: UIView { private var buttonAction: (() -> Void)? @@ -38,7 +39,7 @@ class JetpackOverlayView: UIView { }() private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [animationContainerView, titleLabel, descriptionLabel, getJetpackButton]) + let stackView = UIStackView(arrangedSubviews: [animationContainerView, titleLabel, descriptionLabel, SpacerView(minHeight: 8), getJetpackButton]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.alignment = .leading @@ -85,13 +86,11 @@ class JetpackOverlayView: UIView { }() private lazy var getJetpackButton: UIButton = { - let button = UIButton() - button.backgroundColor = UIAppColor.jetpackGreen(.shade40) - button.setTitle(TextContent.buttonTitle, for: .normal) - button.titleLabel?.adjustsFontSizeToFitWidth = true + var configuration = UIButton.Configuration.primary() + configuration.title = TextContent.buttonTitle + + let button = UIButton(configuration: configuration, primaryAction: nil) button.titleLabel?.adjustsFontForContentSizeCategory = true - button.layer.cornerRadius = Metrics.tryJetpackButtonCornerRadius - button.layer.cornerCurve = .continuous return button }() @@ -137,24 +136,16 @@ class JetpackOverlayView: UIView { private func configureConstraints() { animationContainerView.pinSubviewToAllEdges(animationView) - let stackViewTrailingConstraint = stackView.trailingAnchor.constraint(equalTo: trailingAnchor, - constant: -Metrics.edgeMargins.right) - stackViewTrailingConstraint.priority = Metrics.veryHighPriority - let stackViewBottomConstraint = stackView.bottomAnchor.constraint(lessThanOrEqualTo: safeBottomAnchor, - constant: -Metrics.edgeMargins.bottom) - stackViewBottomConstraint.priority = Metrics.veryHighPriority - NSLayoutConstraint.activate([ dismissButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.dismissButtonTrailingPadding), dismissButton.topAnchor.constraint(equalTo: topAnchor, constant: Metrics.dismissButtonTopPadding), dismissButton.heightAnchor.constraint(equalToConstant: Metrics.dismissButtonSize), dismissButton.widthAnchor.constraint(equalToConstant: Metrics.dismissButtonSize), stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.edgeMargins.left), - stackViewTrailingConstraint, + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.edgeMargins.right).withPriority(999), stackView.topAnchor.constraint(equalTo: dismissButton.bottomAnchor), - stackViewBottomConstraint, + stackView.bottomAnchor.constraint(lessThanOrEqualTo: safeBottomAnchor, constant: -Metrics.edgeMargins.bottom).withPriority(999), - getJetpackButton.heightAnchor.constraint(equalToConstant: Metrics.tryJetpackButtonHeight), getJetpackButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) } @@ -174,7 +165,7 @@ private extension JetpackOverlayView { static let imageToTitleSpacing: CGFloat = 24 static let titleToDescriptionSpacing: CGFloat = 10 static let descriptionToButtonSpacing: CGFloat = 40 - static let edgeMargins = UIEdgeInsets(top: 46, left: 30, bottom: 20, right: 30) + static let edgeMargins = UIEdgeInsets(top: 46, left: 20, bottom: 10, right: 20) // dismiss button static let dismissButtonTopPadding: CGFloat = 10 // takes into account the gripper static let dismissButtonTrailingPadding: CGFloat = 20 @@ -202,24 +193,25 @@ private extension JetpackOverlayView { let font = UIFont(descriptor: fontDescriptor, size: min(fontDescriptor.pointSize, maximumFontSize)) return UIFontMetrics.default.scaledFont(for: font, maximumPointSize: maximumFontSize) } - // "Try Jetpack" button - static let tryJetpackButtonHeight: CGFloat = 44 - static let tryJetpackButtonCornerRadius: CGFloat = 6 - // constraints - static let veryHighPriority = UILayoutPriority(rawValue: 999) } enum TextContent { - static let title = NSLocalizedString("jetpack.branding.overlay.title", - value: "WordPress is better with Jetpack", - comment: "Title of the Jetpack powered overlay.") + static let title = NSLocalizedString( + "jetpack.branding.overlay.title", + value: "WordPress is better with Jetpack", + comment: "Title of the Jetpack powered overlay." + ) - static let description = NSLocalizedString("jetpack.branding.overlay.description", - value: "The new Jetpack app has Stats, Reader, Notifications, and more that make your WordPress better.", - comment: "Description of the Jetpack powered overlay.") + static let description = NSLocalizedString( + "jetpack.branding.overlay.description", + value: "The new Jetpack app has Stats, Reader, Notifications, and more that make your WordPress better.", + comment: "Description of the Jetpack powered overlay." + ) - static let buttonTitle = NSLocalizedString("jetpack.branding.overlay.button.title", - value: "Try the new Jetpack app", - comment: "Button title of the Jetpack powered overlay.") + static let buttonTitle = NSLocalizedString( + "jetpack.branding.overlay.button.title", + value: "Try the new Jetpack app", + comment: "Button title of the Jetpack powered overlay." + ) } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift index ff9c853a6cc4..e61f94333462 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift @@ -37,17 +37,3 @@ class JetpackOverlayViewController: UIViewController { view.setNeedsLayout() } } - -extension JetpackOverlayViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - .intrinsicHeight - } - - var allowsUserTransition: Bool { - false - } - - var compactWidth: DrawerWidth { - .maxWidth - } -} From 39b66d1e997841ebd746082cc8dfa5458aff9aca Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:08:47 -0500 Subject: [PATCH 2/5] Remove ottomSheetViewControllerTests --- .../BottomSheetViewControllerTests.swift | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift diff --git a/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift b/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift deleted file mode 100644 index 3cfc8457fc1f..000000000000 --- a/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -import XCTest - -@testable import WordPressUI - -class BottomSheetViewControllerTests: XCTestCase { - - /// - Add the given ViewController as a child View Controller - /// - func testAddTheGivenViewControllerAsAChildViewController() { - let viewController = BottomSheetPresentableViewController() - let bottomSheet = BottomSheetViewController(childViewController: viewController) - - bottomSheet.viewDidLoad() - - XCTAssertTrue(bottomSheet.children.contains(viewController)) - } - - /// - Add the given ViewController view to the subviews of the Bottom Sheet - /// - func testAddGivenVCViewToTheBottomSheetSubviews() { - let viewController = BottomSheetPresentableViewController() - let bottomSheet = BottomSheetViewController(childViewController: viewController) - - bottomSheet.viewDidLoad() - - XCTAssertTrue(bottomSheet.view.subviews.flatMap { $0.subviews }.contains(viewController.view)) - } -} - -private class BottomSheetPresentableViewController: UIViewController, DrawerPresentable { - var initialHeight: CGFloat = 0 -} From 4ee01f8a354b601d80b8c093f2a96e7feb190ca1 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:10:41 -0500 Subject: [PATCH 3/5] Remove BottomSheetViewController --- .../BottomSheetViewController.swift | 276 ------------------ 1 file changed, 276 deletions(-) delete mode 100644 Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift diff --git a/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift b/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift deleted file mode 100644 index 4d5d1a000a70..000000000000 --- a/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift +++ /dev/null @@ -1,276 +0,0 @@ -import UIKit - -public class BottomSheetViewController: UIViewController { - public enum Constants { - static let gripHeight: CGFloat = 5 - static let cornerRadius: CGFloat = 8 - static let buttonSpacing: CGFloat = 8 - static let minimumWidth: CGFloat = 300 - - /// The height of the space above the bottom sheet content, including the grip view and space around it. - /// - public static let additionalContentTopMargin: CGFloat = BottomSheetViewController.Constants.gripHeight - + BottomSheetViewController.Constants.Header.spacing - + BottomSheetViewController.Constants.Stack.insets.top - - enum Header { - static let spacing: CGFloat = 16 - static let insets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18) - } - - enum Button { - static let height: CGFloat = 54 - static let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 35) - static let titleInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) - } - - enum Stack { - static let insets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0) - } - } - - private var customHeaderSpacing: CGFloat? - - public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return childViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations - } - - /// Additional safe are insets for regular horizontal size class - public var additionalSafeAreaInsetsRegular: UIEdgeInsets = .zero - - private weak var childViewController: DrawerPresentableViewController? - - public init(childViewController: DrawerPresentableViewController, - customHeaderSpacing: CGFloat? = nil) { - self.childViewController = childViewController - self.customHeaderSpacing = customHeaderSpacing - super.init(nibName: nil, bundle: nil) - } - - /// Presents the bottom sheet given an optional anchor and arrow directions for the popover on iPad. - /// If no anchors are provided, on iPad it will present a form sheet. - /// - Parameters: - /// - presenting: the view controller that presents the bottom sheet. - /// - sourceView: optional anchor view for the popover on iPad. - /// - sourceBarButtonItem: optional anchor bar button item for the popover on iPad. If non-nil, `sourceView` and `arrowDirections` are not used. - /// - arrowDirections: optional arrow directions for the popover on iPad. - public func show(from presenting: UIViewController, - sourceView: UIView? = nil, - sourceBarButtonItem: UIBarButtonItem? = nil, - arrowDirections: UIPopoverArrowDirection = .any) { - if UIDevice.isPad() { - - // If the anchor views are not set, or the user is using a larger text option - // we'll display the content in a sheet - if (sourceBarButtonItem == nil && sourceView == nil) || - traitCollection.preferredContentSizeCategory.isAccessibilityCategory { - modalPresentationStyle = .formSheet - } else { - modalPresentationStyle = .popover - - if let sourceBarButtonItem { - popoverPresentationController?.barButtonItem = sourceBarButtonItem - } else { - popoverPresentationController?.permittedArrowDirections = arrowDirections - popoverPresentationController?.sourceView = sourceView - popoverPresentationController?.sourceRect = sourceView?.bounds ?? .zero - } - - popoverPresentationController?.delegate = self - popoverPresentationController?.backgroundColor = view.backgroundColor - } - - } else { - transitioningDelegate = self - modalPresentationStyle = .custom - } - presenting.present(self, animated: true) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private lazy var gripButton: UIButton = { - let button = GripButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget( - self, - action: #selector(buttonPressed), - for: .touchUpInside - ) - button.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for button to dismiss a bottom sheet") - return button - }() - - private var stackView: UIStackView! - - private var defaultBrackgroundColor: UIColor { - return .systemBackground - } - - @objc func buttonPressed() { - dismiss(animated: true, completion: nil) - } - - override public func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) - - view.clipsToBounds = true - view.layer.cornerRadius = Constants.cornerRadius - view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] - view.backgroundColor = childViewController?.view.backgroundColor ?? defaultBrackgroundColor - - NSLayoutConstraint.activate([ - gripButton.heightAnchor.constraint(equalToConstant: Constants.gripHeight) - ]) - - guard let childViewController else { - return - } - - addChild(childViewController) - - stackView = UIStackView(arrangedSubviews: [ - gripButton, - childViewController.view - ]) - - stackView.setCustomSpacing(customHeaderSpacing ?? Constants.Header.spacing, after: gripButton) - - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - - refreshForTraits() - - view.addSubview(stackView) - view.pinSubviewToSafeArea(stackView, insets: Constants.Stack.insets) - - childViewController.didMove(toParent: self) - } - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - refreshForTraits() - } - - override public var preferredContentSize: CGSize { - set { - childViewController?.view.layoutIfNeeded() - - childViewController?.preferredContentSize = newValue - // Continue to make the assignment via super so preferredContentSizeDidChange is called on iPad popovers, resizing them as needed. - super.preferredContentSize = computePreferredContentSize() - } - get { - return computePreferredContentSize() - } - } - - func computePreferredContentSize() -> CGSize { - return (childViewController?.preferredContentSize ?? super.preferredContentSize) - } - - public override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - super.preferredContentSizeDidChange(forChildContentContainer: container) - // Update our preferred size in response to a child updating theres. - // While this leads to a recursive call, the sizes are the same preventing a loop. - // The assignment is needed in order for iPad popovers to correctly resize. - preferredContentSize = container.preferredContentSize - } - - override public func accessibilityPerformEscape() -> Bool { - dismiss(animated: true, completion: nil) - return true - } - - private func refreshForTraits() { - if presentingViewController?.traitCollection.horizontalSizeClass == .regular && presentingViewController?.traitCollection.verticalSizeClass != .compact { - gripButton.isHidden = true - additionalSafeAreaInsets = additionalSafeAreaInsetsRegular - } else { - gripButton.isHidden = false - additionalSafeAreaInsets = .zero - } - } - - @objc func keyboardWillShow(_ notification: NSNotification) { - guard childViewController?.presentedViewController == nil else { - return - } - - self.presentedVC?.transition(to: .expanded) - } - - @objc func keyboardWillHide(_ notification: NSNotification) { - guard childViewController?.presentedViewController == nil else { - return - } - - self.presentedVC?.transition(to: .collapsed) - } -} - -extension BottomSheetViewController: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - return BottomSheetAnimationController(transitionType: .presenting) - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - - handleDismiss() - - return BottomSheetAnimationController(transitionType: .dismissing) - } - - public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - return DrawerPresentationController(presentedViewController: presented, presenting: presenting) - } -} - -// MARK: - DrawerDelegate -extension BottomSheetViewController: DrawerPresentable { - public var allowsUserTransition: Bool { - return childViewController?.allowsUserTransition ?? true - } - - public var allowsTapToDismiss: Bool { - childViewController?.allowsTapToDismiss ?? true - } - - public var allowsDragToDismiss: Bool { - childViewController?.allowsDragToDismiss ?? true - } - - public var compactWidth: DrawerWidth { - childViewController?.compactWidth ?? .percentage(0.66) - } - - public var expandedHeight: DrawerHeight { - return childViewController?.expandedHeight ?? .maxHeight - } - - public var collapsedHeight: DrawerHeight { - return childViewController?.collapsedHeight ?? .contentHeight(200) - } - - public var scrollableView: UIScrollView? { - return childViewController?.scrollableView - } - - public func handleDismiss() { - if let childViewController { - childViewController.handleDismiss() - } - } -} - -extension BottomSheetViewController: UIPopoverPresentationControllerDelegate { - public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - handleDismiss() - } -} From 541f780aace87b673218e108fbba7166a6d98aa0 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:13:23 -0500 Subject: [PATCH 4/5] Remove DrawerPresentationController --- .../DrawerPresentationController.swift | 656 ------------------ .../Post/PostTagPickerViewController.swift | 20 - ...blishingSocialAccountsViewController.swift | 22 - 3 files changed, 698 deletions(-) delete mode 100644 Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift diff --git a/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift b/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift deleted file mode 100644 index 00af0a747449..000000000000 --- a/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift +++ /dev/null @@ -1,656 +0,0 @@ -import UIKit - -public enum DrawerPosition { - case expanded - case collapsed - case closed - case hidden -} - -public enum DrawerHeight { - // The maximum height for the screen - case maxHeight - - // Height is based on the specified margin from the top of the screen - case topMargin(CGFloat) - - // Height will be equal to the the content height value. A height of 0 will use the calculated height. - case contentHeight(CGFloat) - - // Height in the hidden state will be equal the screens height - case hidden - - // Calculate the intrisinc content based on the View Controller - case intrinsicHeight -} - -public enum DrawerWidth { - // Fills the whole screen width - case maxWidth - - // When in compact mode, fills a percentage of the screen - case percentage(CGFloat) - - // Width will be equal to the the content height value - case contentWidth(CGFloat) -} - -public protocol DrawerPresentable: AnyObject { - /// The height of the drawer when it's in the expanded position - var expandedHeight: DrawerHeight { get } - - /// The height of the drawer when it's in the collapsed position - var collapsedHeight: DrawerHeight { get } - - /// The width of the Drawer in compact screen - var compactWidth: DrawerWidth { get } - - /// Whether or not the user is allowed to swipe to switch between the expanded and collapsed position - var allowsUserTransition: Bool { get } - - /// Whether or not the user is allowed to drag to dismiss the drawer - var allowsDragToDismiss: Bool { get } - - /// Whether or not the user is allowed to tap outside the view to dismiss the drawer - var allowsTapToDismiss: Bool { get } - - /// A scroll view that should have its insets adjusted when the drawer is expanded/collapsed - var scrollableView: UIScrollView? { get } - - func handleDismiss() -} - -private enum Constants { - static let transitionDuration: TimeInterval = 0.5 - - static let flickVelocity: CGFloat = 300 - static let bounceAmount: CGFloat = 0.01 - - enum Defaults { - static let expandedHeight: DrawerHeight = .topMargin(20) - static let collapsedHeight: DrawerHeight = .contentHeight(0) - static let compactWidth: DrawerWidth = .percentage(0.66) - - static let allowsUserTransition: Bool = true - static let allowsTapToDismiss: Bool = true - static let allowsDragToDismiss: Bool = true - } -} - -public typealias DrawerPresentableViewController = DrawerPresentable & UIViewController - -public extension DrawerPresentable where Self: UIViewController { - // Default values - var allowsUserTransition: Bool { - return Constants.Defaults.allowsUserTransition - } - - var expandedHeight: DrawerHeight { - return Constants.Defaults.expandedHeight - } - - var collapsedHeight: DrawerHeight { - return Constants.Defaults.collapsedHeight - } - - var compactWidth: DrawerWidth { - return Constants.Defaults.compactWidth - } - - var scrollableView: UIScrollView? { - return nil - } - - var allowsDragToDismiss: Bool { - return Constants.Defaults.allowsDragToDismiss - } - - var allowsTapToDismiss: Bool { - return Constants.Defaults.allowsTapToDismiss - } - - // Helpers - - /// Try to determine the correct DrawerPresentationController to use - - /// Returns the `DrawerPresentationController` for a view controller if there is one - /// This tries to determine the correct one to use in the following order: - /// - The view controller - /// - The navController - /// - The navController parentViewController - /// - The views parentViewController - var presentedVC: DrawerPresentationController? { - let presentationController = self.presentationController as? DrawerPresentationController - let navigationPresentationController = navigationController?.presentationController as? DrawerPresentationController - let navParentPresetationController = navigationController?.parent?.presentationController as? DrawerPresentationController - let parentPresentationController = parent?.presentationController as? DrawerPresentationController - - return presentationController ?? navigationPresentationController ?? navParentPresetationController ?? parentPresentationController - } - - func handleDismiss() { } -} - -public class DrawerPresentationController: FancyAlertPresentationController { - override public var frameOfPresentedViewInContainerView: CGRect { - guard let containerView = self.containerView else { - return .zero - } - - var frame = containerView.frame - let y = collapsedYPosition - var width: CGFloat = containerView.bounds.width - (containerView.safeAreaInsets.left + containerView.safeAreaInsets.right) - - frame.origin.y = y - - /// If we're in a compact vertical size class, constrain the width a bit more so it doesn't get overly wide. - if let widthForCompactSizeClass = presentableViewController?.compactWidth, - traitCollection.verticalSizeClass == .compact { - - switch widthForCompactSizeClass { - case .percentage(let percentage): - width = width * percentage - case .contentWidth(let givenWidth): - width = givenWidth - case .maxWidth: - break - } - } - frame.size.width = width - - /// If we constrain the width, this centers the view by applying the appropriate insets based on width - frame.origin.x = ((containerView.bounds.width - width) / 2) - - return frame - } - - override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - coordinator.animate(alongsideTransition: { _ in - self.presentedView?.frame = self.frameOfPresentedViewInContainerView - self.transition(to: self.currentPosition) - }, completion: nil) - super.viewWillTransition(to: size, with: coordinator) - } - - /// Returns the current position of the drawer - public var currentPosition: DrawerPosition = .collapsed - - /// Returns the Y position of the drawer - public var yPosition: CGFloat? { - return presentedView?.frame.origin.y - } - - /// Animates between the drawer positions - /// - Parameter position: The position to animate to - public func transition(to position: DrawerPosition) { - currentPosition = position - - if position == .closed { - dismiss() - return - } - - var margin: CGFloat = 0 - - switch position { - case .expanded: - margin = expandedYPosition - - case .collapsed: - margin = collapsedYPosition - - case .hidden: - margin = hiddenYPosition - - default: - margin = 0 - } - - setTopMargin(margin) - } - - @objc func dismiss() { - presentedViewController.dismiss(animated: true, completion: nil) - } - - public override func presentationTransitionWillBegin() { - super.presentationTransitionWillBegin() - - configureScrollViewInsets() - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - transition(to: currentPosition) - } - - public override func presentationTransitionDidEnd(_ completed: Bool) { - super.presentationTransitionDidEnd(completed) - - configureScrollViewInsets() - } - - // MARK: - Internal Positions - // Helpers to calculate the Y positions for the drawer positions - - private var closedPosition: CGFloat { - guard let presentedView = self.presentedView else { - return 0 - } - - return presentedView.bounds.height - } - - private var collapsedYPosition: CGFloat { - let height = presentableViewController?.collapsedHeight ?? Constants.Defaults.collapsedHeight - - return topMargin(with: height) - } - - private var expandedYPosition: CGFloat { - let height = presentableViewController?.expandedHeight ?? Constants.Defaults.expandedHeight - - return topMargin(with: height) - } - - private var hiddenYPosition: CGFloat { - return topMargin(with: .hidden) - } - - /// Calculates the Y position for the view based on a DrawerHeight enum - /// - Parameter drawerHeight: The drawer height to calculate - private func topMargin(with drawerHeight: DrawerHeight) -> CGFloat { - var topMargin: CGFloat - - switch drawerHeight { - case .contentHeight(let height): - topMargin = calculatedTopMargin(for: height) - - case .topMargin(let margin): - topMargin = safeAreaInsets.top + margin - - case .maxHeight: - topMargin = safeAreaInsets.top - - case .intrinsicHeight: - // Force a layout to make sure we get the correct size from the views - presentedViewController.view.layoutIfNeeded() - - let height = presentedViewController.preferredContentSize.height - topMargin = calculatedTopMargin(for: height) - - case .hidden: - topMargin = UIScreen.main.bounds.height - } - - return topMargin - } - - // MARK: - Gestures - private lazy var tapGestureRecognizer: UITapGestureRecognizer = { - let gesture = UITapGestureRecognizer(target: self, action: #selector(self.dismiss(_:))) - gesture.delegate = self - return gesture - }() - - private lazy var panGestureRecognizer: UIPanGestureRecognizer = { - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.pan(_:))) - panGesture.delegate = self - return panGesture - }() - - override public func containerViewWillLayoutSubviews() { - super.containerViewWillLayoutSubviews() - - addGestures() - observe(scrollView: presentableViewController?.scrollableView) - } - - /// Represents whether the view is animating to a new position - private var isPresentedViewAnimating = false - - /// Whether or not the presented view is anchored to the top of the screen - private var isPresentedViewAnchored: Bool { - if !isPresentedViewAnimating - && (presentedView?.frame.origin.y.rounded() ?? 0) <= expandedYPosition.rounded() { - return true - } - - return false - } - - private var dragStartPoint: CGPoint? - - /// Stores the current `contentOffset.y` for `presentableViewController.scrollableView` - /// See `haltScrolling` and `trackScrolling` for more information. - private var scrollViewYOffset: CGFloat = 0.0 - - /// An observer of the content offset for `presentableViewController.scrollableView` - private var scrollObserver: NSKeyValueObservation? - - deinit { - scrollObserver?.invalidate() - } -} - -// MARK: - Dragging -private extension DrawerPresentationController { - - private func addGestures() { - guard - let presentedView = self.presentedView, - let containerView = self.containerView - else { return } - - presentedView.addGestureRecognizer(panGestureRecognizer) - containerView.addGestureRecognizer(tapGestureRecognizer) - } - - /// Dismiss action for the tap gesture - /// Will prevent dismissal if the `allowsTapToDismiss` is false - /// - Parameter gesture: The tap gesture - @objc func dismiss(_ gesture: UIPanGestureRecognizer) { - let canDismiss = presentableViewController?.allowsTapToDismiss ?? Constants.Defaults.allowsTapToDismiss - - guard canDismiss else { - return - } - - dismiss() - } - - @objc func pan(_ gesture: UIPanGestureRecognizer) { - guard let presentedView = self.presentedView else { return } - - let isScrolling = presentableViewController?.scrollableView?.isScrolling == true - - guard (presentableViewController?.scrollableView?.contentOffset.y ?? 0) <= 0 || isScrolling == false else { return } - - /// Ignore the animation once panning begins so we can immediately interact - isPresentedViewAnimating = false - - let translation = gesture.translation(in: presentedView) - let allowsUserTransition = presentableViewController?.allowsUserTransition ?? Constants.Defaults.allowsUserTransition - let allowDragToDismiss = presentableViewController?.allowsDragToDismiss ?? Constants.Defaults.allowsDragToDismiss - - switch gesture.state { - case .began: - dragStartPoint = presentedView.frame.origin - - case .changed: - let startY = dragStartPoint?.y ?? 0 - var yTranslation = translation.y - - /// Slows the deceleration rate - if isScrolling && presentedView.frame.origin.y < expandedYPosition { - yTranslation /= 2.0 - } - - if !allowsUserTransition || !allowDragToDismiss { - let maxBounce: CGFloat = (startY * Constants.bounceAmount) - - if yTranslation < 0 { - yTranslation = max(yTranslation, maxBounce * -1) - } else { - if !allowDragToDismiss { - yTranslation = min(yTranslation, maxBounce) - } - } - } - - let maxY = topMargin(with: .maxHeight) - var yPosition = startY + yTranslation - if isScrolling { - /// During scrolling, ensure yPosition doesn't extend past the expanded position - yPosition = max(yPosition, expandedYPosition) - } - - let newMargin = max(yPosition, maxY) - setTopMargin(newMargin, animated: false) - - case .ended: - /// Helper closure to prevent user transition/dismiss - let transition: (DrawerPosition) -> Void = { pos in - if allowsUserTransition || pos == .closed && allowDragToDismiss { - self.transition(to: pos) - } else { - // Reset to the original position - self.transition(to: self.currentPosition) - } - } - - let velocity = gesture.velocity(in: presentedView).y - let startY = dragStartPoint?.y ?? 0 - - let currentPosition = (startY + translation.y) - let position = closestPosition(for: currentPosition) - - // Determine how to handle flicking of the view - if (abs(velocity) - Constants.flickVelocity) > 0 { - // Flick up - if velocity < 0 { - transition(.expanded) - } else { - if position == .expanded { - transition(.collapsed) - } else { - transition(.closed) - } - } - - return - } - - transition(position) - - dragStartPoint = nil - - default: - return - } - } -} - -// MARK: - Scrolling -private extension DrawerPresentationController { - - /// Adds an observer for the scroll view's content offset. - /// Track scrolling without overriding the `scrollView` delegate - /// - Parameter scrollView: The scroll view whose content offset will be tracked. - func observe(scrollView: UIScrollView?) { - scrollObserver?.invalidate() - scrollObserver = scrollView?.observe(\.contentOffset, options: .old) { [weak self] scrollView, change in - - /// In case there are two containerViews in the same presentation - guard self?.containerView != nil - else { return } - - self?.didPan(on: scrollView, change: change) - } - } - - /// Handles scroll view content offset changes - /// - Parameters: - /// - scrollView: The scroll view whose content offset is changing. - /// - change: The change representing the old and new content offsets. - func didPan(on scrollView: UIScrollView, change: NSKeyValueObservedChange) { - - guard - !presentedViewController.isBeingDismissed, - !presentedViewController.isBeingPresented - else { return } - - if !isPresentedViewAnchored && scrollView.contentOffset.y > 0 { - - /// Halts scrolling when scrolling down from expanded or up from compact - haltScrolling(scrollView) - - } else if scrollView.isScrolling { - - if isPresentedViewAnchored { - /// Allow normal scrolling (with tracking) - trackScrolling(scrollView) - } else { - /// Halts scrolling when panning down from expanded - haltScrolling(scrollView) - } - - } else { - /// Allow normal scrolling (with tracking) - trackScrolling(scrollView) - } - } - - /// Stops scrolling behavior on `scrollView` and anchors to `scrollViewYOffset`. - /// - Parameter scrollView: The scroll view to stop and anchor anchor - private func haltScrolling(_ scrollView: UIScrollView) { - // Only halt the scrolling if we haven't halted it before - guard scrollView.showsVerticalScrollIndicator else { - return - } - - scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewYOffset), animated: false) - scrollView.showsVerticalScrollIndicator = false - } - - /// Tracks and saves the y offset of `scrollView` in `scrollViewYOffset`. - /// Used later by `haltScrolling` to adjust the scroll view to `scrollViewYOffset` to give the appearance of the sticking position. - /// - Parameter scrollView: The scroll view to track. - private func trackScrolling(_ scrollView: UIScrollView) { - scrollViewYOffset = max(scrollView.contentOffset.y, 0) - scrollView.showsVerticalScrollIndicator = true - } -} - -private extension UIScrollView { - /// A flag to determine if a scroll view is scrolling - var isScrolling: Bool { - return isDragging && !isDecelerating || isTracking - } -} - -extension DrawerPresentationController: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - - guard tapGestureRecognizer == gestureRecognizer else { return true } - - /// Shouldn't happen; should always have container & presented view when tapped - guard - let containerView, - let presentedView, - currentPosition != .hidden - else { - return false - } - - let touchPoint = touch.location(in: containerView) - let isInPresentedView = presentedView.frame.contains(touchPoint) - - /// Do not accept the touch if inside of the presented view - return (gestureRecognizer == tapGestureRecognizer) && isInPresentedView == false - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return otherGestureRecognizer.view == presentableViewController?.scrollableView - } -} - -// MARK: - Private: Helpers -private extension DrawerPresentationController { - - private func configureScrollViewInsets() { - guard - let scrollView = presentableViewController?.scrollableView, - let presentedView = self.presentedView, - let presentingView = presentingViewController.view - else { return } - - let bottom = presentingView.safeAreaInsets.bottom - let margin = presentedView.frame.origin.y + bottom - - scrollView.contentInset.bottom = margin - } - - private var presentableViewController: DrawerPresentable? { - return presentedViewController as? DrawerPresentable - } - - private func calculatedTopMargin(for height: CGFloat) -> CGFloat { - guard let containerView = self.containerView else { - return 0 - } - - let bounds = containerView.bounds - let margin = bounds.maxY - (safeAreaInsets.bottom + ((height > 0) ? height : (bounds.height * 0.5))) - - // Limit the max height - return max(margin, safeAreaInsets.top) - } - - private func setTopMargin(_ margin: CGFloat, animated: Bool = true) { - guard let presentedView = self.presentedView else { - return - } - - var frame = presentedView.frame - frame.origin.y = margin - - let animations = { - presentedView.frame = frame - - self.configureScrollViewInsets() - } - - if animated { - animate(animations) - } else { - animations() - } - } - - private var safeAreaInsets: UIEdgeInsets { - guard let rootViewController = self.rootViewController else { - return .zero - } - - return rootViewController.view.safeAreaInsets - } - - func closestPosition(for yPosition: CGFloat) -> DrawerPosition { - let positions = [closedPosition, collapsedYPosition, expandedYPosition] - let closestVal = positions.min(by: { abs(yPosition - $0) < abs(yPosition - $1) }) ?? yPosition - - var returnPosition: DrawerPosition = .closed - - if closestVal == expandedYPosition { - returnPosition = .expanded - } else if closestVal == collapsedYPosition { - returnPosition = .collapsed - } - - return returnPosition - } - - private func animate(_ animations: @escaping () -> Void) { - isPresentedViewAnimating = true - UIView.animate(withDuration: Constants.transitionDuration, - delay: 0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0, - options: [.curveEaseInOut, .allowUserInteraction], - animations: animations) { [weak self] _ in - self?.isPresentedViewAnimating = false - } - } - - private var rootViewController: UIViewController? { - guard let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication - else { return nil } - - return application.keyWindow?.rootViewController - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift index 4a7910392a37..b28e4b320d24 100644 --- a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift @@ -120,8 +120,6 @@ class PostTagPickerViewController: UIViewController { loadTags() tableView.contentInset.bottom += descriptionLabel.frame.height + 20 - - updateTableViewBottomInset() } override func viewWillDisappear(_ animated: Bool) { @@ -145,14 +143,6 @@ class PostTagPickerViewController: UIViewController { fileprivate func reloadTableData() { tableView.reloadData() } - - fileprivate func updateTableViewBottomInset() { - guard !UIDevice.isPad() else { - return - } - - tableView.contentInset.bottom += presentedVC?.yPosition ?? 0 - } } // MARK: - Tags Loading @@ -449,13 +439,3 @@ extension WPStyleGuide { cell.backgroundColor = .secondarySystemGroupedBackground } } - -extension PostTagPickerViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .contentHeight(300) - } - - var scrollableView: UIScrollView? { - return tableView - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift index b0d233fc80ea..a0e22c05034e 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift @@ -114,17 +114,6 @@ class PrepublishingSocialAccountsViewController: UITableViewController { tableView.tableHeaderView = UIView(frame: .init(x: 0, y: 0, width: 0, height: Constants.tableTopPadding)) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // when the vertical size class changes, ensure that we are displaying the max drawer height on compact size - // or revert to collapsed mode otherwise. - if let previousVerticalSizeClass = previousTraitCollection?.verticalSizeClass, - previousVerticalSizeClass != traitCollection.verticalSizeClass { - presentedVC?.transition(to: traitCollection.verticalSizeClass == .compact ? .expanded : .collapsed) - } - } - deinit { // only call the delegate method if the user has made some changes. if hasChanges { @@ -383,17 +372,6 @@ private extension PrepublishingSocialAccountsViewController { } -extension PrepublishingSocialAccountsViewController: DrawerPresentable { - - var collapsedHeight: DrawerHeight { - .intrinsicHeight - } - - var scrollableView: UIScrollView? { - tableView - } -} - private extension PrepublishingAutoSharingModel { var enabledConnectionsCount: Int { services.flatMap { $0.connections }.filter { $0.enabled }.count From 29a61c190690d80e2c21e19c64aa7212cd311191 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:26:40 -0500 Subject: [PATCH 5/5] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index c8f88ab3e1e1..99da70923bec 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ * [*] Fix transitions in Blogging Reminders flow, improve accessibility, add close buttons [#23931] * [*] Fix an issue with compliance popover not dismissing for self-hosted site [#23932] * [*] Fix dynamic type support in the compliance popover [#23932] +* [*] Improve transisions and interactive dismiss gestures for sheets [#23933] 25.6 -----