diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentWrapperGalleryContentViewController.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentWrapperGalleryContentViewController.swift index 6ad00d69..02ead3c4 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentWrapperGalleryContentViewController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentWrapperGalleryContentViewController.swift @@ -44,6 +44,10 @@ class AttachmentWrapperGalleryContentViewController: UIViewController, GalleryCo false } + var showBelowSafeArea: Bool { + false + } + init(draftAttachment: DraftAttachment, wrapped: any GalleryContentViewController) { self.draftAttachment = draftAttachment self.wrapped = wrapped @@ -76,16 +80,6 @@ class AttachmentWrapperGalleryContentViewController: UIViewController, GalleryCo if !visible { editDescriptionViewController.textView?.resignFirstResponder() } - if #available(iOS 16.0, macCatalyst 17.0, *), - let wrapped = wrapped as? ImageGalleryContentViewController, - let interaction = wrapped.analysisInteraction { - if visible { - let bottom = editDescriptionViewController.view.bounds.height - editDescriptionViewController.view.keyboardLayoutGuide.layoutFrame.height - interaction.supplementaryInterfaceContentInsets = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0) - } else { - interaction.supplementaryInterfaceContentInsets = .zero - } - } } func galleryContentDidAppear() { @@ -155,7 +149,9 @@ private class EditAttachmentDescriptionViewController: UIViewController { ]) if let wrapped { + addChild(wrapped) stack.addArrangedSubview(wrapped.view) + wrapped.didMove(toParent: self) } textView = UITextView() diff --git a/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift index a992ed9a..e24895af 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift @@ -25,7 +25,7 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi private static let analyzer = ImageAnalyzer() private var _analysisInteraction: AnyObject? @available(iOS 16.0, macCatalyst 17.0, *) - public var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction } + private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction } public init(image: UIImage, caption: String?, gifController: GIFController?) { self.caption = caption diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift index e88b5528..a7af7034 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift @@ -18,6 +18,7 @@ public protocol GalleryContentViewController: UIViewController { var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get } var presentationAnimation: GalleryContentPresentationAnimation { get } var hideControlsOnZoom: Bool { get } + var showBelowSafeArea: Bool { get } func shouldHideControls() -> Bool func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) @@ -47,6 +48,10 @@ public extension GalleryContentViewController { true } + var showBelowSafeArea: Bool { + true + } + func shouldHideControls() -> Bool { true } diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift index a9567752..a0c3a10a 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift @@ -45,6 +45,7 @@ class GalleryItemViewController: UIViewController { private(set) var scrollAndZoomEnabled = true private var scrollViewSizeForLastZoomScaleUpdate: CGSize? + private var skipScrollViewZoomUpdate = false var showShareButton: Bool = true { didSet { @@ -83,6 +84,7 @@ class GalleryItemViewController: UIViewController { // We calculate zoom/position ignoring the safe area, so content insets need to not incorporate it either. // Otherwise, content that fills the screen (extending into the safe area) may still end up scrollable // (this is readily observable with tall images on a landscape iPad). + // Even if the content is not being shown below the safe area, we still set this to make the calculations more consistent/straightforward. scrollView.contentInsetAdjustmentBehavior = .never view.addSubview(scrollView) @@ -229,19 +231,16 @@ class GalleryItemViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - if #available(iOS 17.0, *) { - view.keyboardLayoutGuide.usesBottomSafeArea = false - } } @objc private func keyboardWillUpdate() { - updateZoomScale(resetZoom: true) + updateScrollView(resetZoom: true) } override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() - updateZoomScale(resetZoom: false) + updateScrollView(resetZoom: false) // Ensure the transform is correct if the controls are hidden setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false) @@ -255,7 +254,7 @@ class GalleryItemViewController: UIViewController { // This might also fix an issue on macOS (Designed for iPad) where the content isn't placed correctly. See #446 if scrollViewSizeForLastZoomScaleUpdate != scrollView.bounds.size { scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size - updateZoomScale(resetZoom: true) + updateScrollView(resetZoom: true) } centerContent() // Ensure the transform is correct if the controls are hidden and their size changed. @@ -293,7 +292,7 @@ class GalleryItemViewController: UIViewController { contentViewLeadingConstraint!.isActive = true contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor) contentViewTopConstraint!.isActive = true - updateZoomScale(resetZoom: true) + updateScrollView(resetZoom: true) } else { // If the content was previously added, deactivate the old constraints. contentViewLeadingConstraint?.isActive = false @@ -332,6 +331,13 @@ class GalleryItemViewController: UIViewController { topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height) bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height) content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) + + if !content.showBelowSafeArea { + skipScrollViewZoomUpdate = true + updateScrollView(resetZoom: abs(scrollView.zoomScale - scrollView.minimumZoomScale) < 0.01) + skipScrollViewZoomUpdate = false + scrollView.layoutIfNeeded() + } } if animated { let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters()) @@ -344,7 +350,7 @@ class GalleryItemViewController: UIViewController { setNeedsUpdateOfHomeIndicatorAutoHidden() } - func updateZoomScale(resetZoom: Bool) { + func updateScrollView(resetZoom: Bool) { scrollView.contentSize = content.contentSize guard scrollAndZoomEnabled else { @@ -358,17 +364,7 @@ class GalleryItemViewController: UIViewController { return } - // Post-iOS 17, we can ask the keyboard layout guide to ignore the bottom safe area. - // Pre, we have to do that ourselves. - let keyboardHeight: CGFloat - if #available(iOS 17.0, *) { - keyboardHeight = view.keyboardLayoutGuide.layoutFrame.height - } else { - let bottomSafeArea = view.bounds.height - view.safeAreaLayoutGuide.layoutFrame.maxY - let rawKeyboardHeight = view.keyboardLayoutGuide.layoutFrame.height - keyboardHeight = abs(rawKeyboardHeight - bottomSafeArea) < 1 ? 0 : rawKeyboardHeight - } - let heightScale = (view.bounds.height - keyboardHeight) / content.contentSize.height + let heightScale = availableHeightForContent() / content.contentSize.height let widthScale = view.bounds.width / content.contentSize.width let minScale = min(widthScale, heightScale) let maxScale = minScale >= 1 ? minScale + 2 : 2 @@ -381,6 +377,20 @@ class GalleryItemViewController: UIViewController { scrollView.zoomScale = max(minScale, min(maxScale, scrollView.zoomScale)) } + let bottomInset: CGFloat + let bottomIndicatorInset: CGFloat + if !content.showBelowSafeArea, + controlsVisible, + let bottomControlsView { + bottomInset = bottomControlsView.bounds.height + view.safeAreaInsets.top + bottomIndicatorInset = bottomControlsView.safeAreaLayoutGuide.layoutFrame.height + } else { + bottomInset = 0 + bottomIndicatorInset = 0 + } + scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0) + scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: bottomIndicatorInset, right: 0) + centerContent() } @@ -389,15 +399,29 @@ class GalleryItemViewController: UIViewController { return } + let additionalYOffset = content.showBelowSafeArea ? 0 : view.safeAreaInsets.top // Note: use frame for the content.view, because that's in the coordinate space of the scroll view // which means it's already been scaled by the zoom factor. - let yOffset = max(0, (view.bounds.height - view.keyboardLayoutGuide.layoutFrame.height - content.view.frame.height) / 2) - contentViewTopConstraint!.constant = yOffset + let yOffset = max(0, (availableHeightForContent() - content.view.frame.height) / 2) + contentViewTopConstraint!.constant = yOffset + additionalYOffset let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2) contentViewLeadingConstraint!.constant = xOffset } + private func availableHeightForContent() -> CGFloat { + var availableHeight: CGFloat + if content.showBelowSafeArea { + availableHeight = view.bounds.height + } else { + availableHeight = view.safeAreaLayoutGuide.layoutFrame.height + if controlsVisible { + availableHeight -= bottomControlsView?.safeAreaLayoutGuide.layoutFrame.height ?? 0 + } + } + return availableHeight + } + private func updateShareButton() { shareButton.isEnabled = !content.activityItemsForSharing.isEmpty } @@ -580,14 +604,14 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer { } func galleryContentChanged() { - updateZoomScale(resetZoom: true) + updateScrollView(resetZoom: true) updateShareButton() updateCaptionTextView() } func disableGalleryScrollAndZoom() { scrollAndZoomEnabled = false - updateZoomScale(resetZoom: true) + updateScrollView(resetZoom: true) scrollView.isScrollEnabled = false // Make sure the content is re-added with the correct constraints if content.parent == self { @@ -610,6 +634,10 @@ extension GalleryItemViewController: UIScrollViewDelegate { } func scrollViewDidZoom(_ scrollView: UIScrollView) { + guard !skipScrollViewZoomUpdate else { + return + } + if scrollView.zoomScale <= scrollView.minimumZoomScale { setControlsVisible(true, animated: true, dueToUserInteraction: true) } else { diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift index e508a6f8..1854601c 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift @@ -56,7 +56,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated container.layoutIfNeeded() // Make sure the zoom scale is updated before getting the content view frame, since it needs to take into account the correct transform. - itemViewController.updateZoomScale(resetZoom: true) + itemViewController.updateScrollView(resetZoom: true) let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView) let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)