Inset gallery content to safe area when editing attachment description

This commit is contained in:
Shadowfacts 2025-02-05 11:17:00 -05:00
parent 86e1403230
commit 64c377c663
5 changed files with 64 additions and 35 deletions

View File

@ -44,6 +44,10 @@ class AttachmentWrapperGalleryContentViewController: UIViewController, GalleryCo
false false
} }
var showBelowSafeArea: Bool {
false
}
init(draftAttachment: DraftAttachment, wrapped: any GalleryContentViewController) { init(draftAttachment: DraftAttachment, wrapped: any GalleryContentViewController) {
self.draftAttachment = draftAttachment self.draftAttachment = draftAttachment
self.wrapped = wrapped self.wrapped = wrapped
@ -76,16 +80,6 @@ class AttachmentWrapperGalleryContentViewController: UIViewController, GalleryCo
if !visible { if !visible {
editDescriptionViewController.textView?.resignFirstResponder() 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() { func galleryContentDidAppear() {
@ -155,7 +149,9 @@ private class EditAttachmentDescriptionViewController: UIViewController {
]) ])
if let wrapped { if let wrapped {
addChild(wrapped)
stack.addArrangedSubview(wrapped.view) stack.addArrangedSubview(wrapped.view)
wrapped.didMove(toParent: self)
} }
textView = UITextView() textView = UITextView()

View File

@ -25,7 +25,7 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
private static let analyzer = ImageAnalyzer() private static let analyzer = ImageAnalyzer()
private var _analysisInteraction: AnyObject? private var _analysisInteraction: AnyObject?
@available(iOS 16.0, macCatalyst 17.0, *) @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?) { public init(image: UIImage, caption: String?, gifController: GIFController?) {
self.caption = caption self.caption = caption

View File

@ -18,6 +18,7 @@ public protocol GalleryContentViewController: UIViewController {
var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get } var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get }
var presentationAnimation: GalleryContentPresentationAnimation { get } var presentationAnimation: GalleryContentPresentationAnimation { get }
var hideControlsOnZoom: Bool { get } var hideControlsOnZoom: Bool { get }
var showBelowSafeArea: Bool { get }
func shouldHideControls() -> Bool func shouldHideControls() -> Bool
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
@ -47,6 +48,10 @@ public extension GalleryContentViewController {
true true
} }
var showBelowSafeArea: Bool {
true
}
func shouldHideControls() -> Bool { func shouldHideControls() -> Bool {
true true
} }

View File

@ -45,6 +45,7 @@ class GalleryItemViewController: UIViewController {
private(set) var scrollAndZoomEnabled = true private(set) var scrollAndZoomEnabled = true
private var scrollViewSizeForLastZoomScaleUpdate: CGSize? private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
private var skipScrollViewZoomUpdate = false
var showShareButton: Bool = true { var showShareButton: Bool = true {
didSet { 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. // 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 // 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). // (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 scrollView.contentInsetAdjustmentBehavior = .never
view.addSubview(scrollView) 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.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillChangeFrameNotification, 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() { @objc private func keyboardWillUpdate() {
updateZoomScale(resetZoom: true) updateScrollView(resetZoom: true)
} }
override func viewSafeAreaInsetsDidChange() { override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange() super.viewSafeAreaInsetsDidChange()
updateZoomScale(resetZoom: false) updateScrollView(resetZoom: false)
// Ensure the transform is correct if the controls are hidden // Ensure the transform is correct if the controls are hidden
setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false) 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 // 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 { if scrollViewSizeForLastZoomScaleUpdate != scrollView.bounds.size {
scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size
updateZoomScale(resetZoom: true) updateScrollView(resetZoom: true)
} }
centerContent() centerContent()
// Ensure the transform is correct if the controls are hidden and their size changed. // Ensure the transform is correct if the controls are hidden and their size changed.
@ -293,7 +292,7 @@ class GalleryItemViewController: UIViewController {
contentViewLeadingConstraint!.isActive = true contentViewLeadingConstraint!.isActive = true
contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor) contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor)
contentViewTopConstraint!.isActive = true contentViewTopConstraint!.isActive = true
updateZoomScale(resetZoom: true) updateScrollView(resetZoom: true)
} else { } else {
// If the content was previously added, deactivate the old constraints. // If the content was previously added, deactivate the old constraints.
contentViewLeadingConstraint?.isActive = false contentViewLeadingConstraint?.isActive = false
@ -332,6 +331,13 @@ class GalleryItemViewController: UIViewController {
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height) topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height) bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) 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 { if animated {
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters()) let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
@ -344,7 +350,7 @@ class GalleryItemViewController: UIViewController {
setNeedsUpdateOfHomeIndicatorAutoHidden() setNeedsUpdateOfHomeIndicatorAutoHidden()
} }
func updateZoomScale(resetZoom: Bool) { func updateScrollView(resetZoom: Bool) {
scrollView.contentSize = content.contentSize scrollView.contentSize = content.contentSize
guard scrollAndZoomEnabled else { guard scrollAndZoomEnabled else {
@ -358,17 +364,7 @@ class GalleryItemViewController: UIViewController {
return return
} }
// Post-iOS 17, we can ask the keyboard layout guide to ignore the bottom safe area. let heightScale = availableHeightForContent() / content.contentSize.height
// 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 widthScale = view.bounds.width / content.contentSize.width let widthScale = view.bounds.width / content.contentSize.width
let minScale = min(widthScale, heightScale) let minScale = min(widthScale, heightScale)
let maxScale = minScale >= 1 ? minScale + 2 : 2 let maxScale = minScale >= 1 ? minScale + 2 : 2
@ -381,6 +377,20 @@ class GalleryItemViewController: UIViewController {
scrollView.zoomScale = max(minScale, min(maxScale, scrollView.zoomScale)) 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() centerContent()
} }
@ -389,15 +399,29 @@ class GalleryItemViewController: UIViewController {
return 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 // 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. // 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) let yOffset = max(0, (availableHeightForContent() - content.view.frame.height) / 2)
contentViewTopConstraint!.constant = yOffset contentViewTopConstraint!.constant = yOffset + additionalYOffset
let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2) let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2)
contentViewLeadingConstraint!.constant = xOffset 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() { private func updateShareButton() {
shareButton.isEnabled = !content.activityItemsForSharing.isEmpty shareButton.isEnabled = !content.activityItemsForSharing.isEmpty
} }
@ -580,14 +604,14 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
} }
func galleryContentChanged() { func galleryContentChanged() {
updateZoomScale(resetZoom: true) updateScrollView(resetZoom: true)
updateShareButton() updateShareButton()
updateCaptionTextView() updateCaptionTextView()
} }
func disableGalleryScrollAndZoom() { func disableGalleryScrollAndZoom() {
scrollAndZoomEnabled = false scrollAndZoomEnabled = false
updateZoomScale(resetZoom: true) updateScrollView(resetZoom: true)
scrollView.isScrollEnabled = false scrollView.isScrollEnabled = false
// Make sure the content is re-added with the correct constraints // Make sure the content is re-added with the correct constraints
if content.parent == self { if content.parent == self {
@ -610,6 +634,10 @@ extension GalleryItemViewController: UIScrollViewDelegate {
} }
func scrollViewDidZoom(_ scrollView: UIScrollView) { func scrollViewDidZoom(_ scrollView: UIScrollView) {
guard !skipScrollViewZoomUpdate else {
return
}
if scrollView.zoomScale <= scrollView.minimumZoomScale { if scrollView.zoomScale <= scrollView.minimumZoomScale {
setControlsVisible(true, animated: true, dueToUserInteraction: true) setControlsVisible(true, animated: true, dueToUserInteraction: true)
} else { } else {

View File

@ -56,7 +56,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
container.layoutIfNeeded() 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. // 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 sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view) let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)