diff --git a/FlipcashTests/Chat/ChatMediaViewerTests.swift b/FlipcashTests/Chat/ChatMediaViewerTests.swift index 0ff072047..83ad24e63 100644 --- a/FlipcashTests/Chat/ChatMediaViewerTests.swift +++ b/FlipcashTests/Chat/ChatMediaViewerTests.swift @@ -130,6 +130,28 @@ struct ChatMediaViewerTests { #expect(shared == nil) } + @Test("A tap hides the close and share buttons, and a second tap brings them back") + func tapTogglesControls() throws { + let viewer = ChatMediaViewerController(request: try #require(request(message(media()), localImage: Self.localImage))) { _ in } + viewer.loadViewIfNeeded() + + viewer.setControlsHidden(true, animated: false) + #expect(viewer.areControlsHidden) + for button in [viewer.closeButton, viewer.shareButton] { + #expect(button.alpha == 0) + #expect(!button.isUserInteractionEnabled) + #expect(button.accessibilityElementsHidden) + } + + viewer.setControlsHidden(false, animated: false) + #expect(!viewer.areControlsHidden) + for button in [viewer.closeButton, viewer.shareButton] { + #expect(button.alpha == 1) + #expect(button.isUserInteractionEnabled) + #expect(!button.accessibilityElementsHidden) + } + } + // MARK: - Transcript wiring private func transcript(_ message: ChatMessage, localImage: UIImage? = nil) -> (ChatViewController, ChatMediaCell) { diff --git a/FlipcashUI/Sources/FlipcashUI/Chat/ChatMediaViewerController.swift b/FlipcashUI/Sources/FlipcashUI/Chat/ChatMediaViewerController.swift index c78363446..d94f11233 100644 --- a/FlipcashUI/Sources/FlipcashUI/Chat/ChatMediaViewerController.swift +++ b/FlipcashUI/Sources/FlipcashUI/Chat/ChatMediaViewerController.swift @@ -52,7 +52,8 @@ public struct ChatMediaViewerRequest { } /// A full-screen photo, zoomed in from the tapped transcript row: pinch or double-tap to zoom, swipe -/// down or close to put it back, and share once the photo itself has loaded. +/// down or close to put it back, tap to hide or show the buttons, and share once the photo itself +/// has loaded. public final class ChatMediaViewerController: UIViewController, UIScrollViewDelegate { private static let maximumZoomScale: CGFloat = 4 @@ -64,7 +65,10 @@ public final class ChatMediaViewerController: UIViewController, UIScrollViewDele let scrollView = UIScrollView() let imageView = UIImageView() let shareButton = UIButton(configuration: .plain()) - private let closeButton = UIButton(configuration: .plain()) + let closeButton = UIButton(configuration: .plain()) + + /// Whether a single tap has put the close and share buttons away. + private(set) var areControlsHidden = false /// The photo itself, once drawn — never the placeholder, so share never hands out a BlurHash. private var loadedImage: UIImage? { @@ -113,6 +117,10 @@ public final class ChatMediaViewerController: UIViewController, UIScrollViewDele doubleTap.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(doubleTap) + let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapped)) + singleTap.require(toFail: doubleTap) + scrollView.addGestureRecognizer(singleTap) + configure(closeButton, symbol: "xmark", label: "Close", action: #selector(closeTapped)) configure(shareButton, symbol: "square.and.arrow.up", label: "Share", action: #selector(shareTapped)) @@ -199,6 +207,31 @@ public final class ChatMediaViewerController: UIViewController, UIScrollViewDele scrollView.zoom(to: CGRect(x: point.x - size.width / 2, y: point.y - size.height / 2, width: size.width, height: size.height), animated: true) } + // MARK: - Controls + + @objc private func singleTapped() { + setControlsHidden(!areControlsHidden, animated: true) + } + + /// Fades the close and share buttons out or back in; hidden buttons take no touches and drop out + /// of VoiceOver. + func setControlsHidden(_ hidden: Bool, animated: Bool) { + areControlsHidden = hidden + for button in [closeButton, shareButton] { + button.isUserInteractionEnabled = !hidden + button.accessibilityElementsHidden = hidden + } + let fade = { [closeButton, shareButton] in + closeButton.alpha = hidden ? 0 : 1 + shareButton.alpha = hidden ? 0 : 1 + } + if animated { + UIView.animate(withDuration: 0.2, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: fade) + } else { + fade() + } + } + // MARK: - Actions @objc private func closeTapped() {