diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3d774ba0e..c3019010a 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -341,7 +341,7 @@ extension SceneCoordinator { case .custom(let transitioningDelegate): viewController.modalPresentationStyle = .custom viewController.transitioningDelegate = transitioningDelegate - // viewController.modalPresentationCapturesStatusBarAppearance = true + viewController.modalPresentationCapturesStatusBarAppearance = true (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) case .customPush(let animated): diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageView.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageView.swift index 05f2ce70f..8432ddade 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageView.swift @@ -9,6 +9,7 @@ import os.log import func AVFoundation.AVMakeRect import UIKit import FLAnimatedImage +import VisionKit final class MediaPreviewImageView: UIScrollView { @@ -28,9 +29,21 @@ final class MediaPreviewImageView: UIScrollView { tapGestureRecognizer.numberOfTapsRequired = 2 return tapGestureRecognizer }() - + private var containerFrame: CGRect? - + + private var _interaction: UIInteraction? = { + if #available(iOS 16.0, *) { + return ImageAnalysisInteraction() + } else { + return nil + } + }() + @available(iOS 16.0, *) + var liveTextInteraction: ImageAnalysisInteraction { + _interaction as! ImageAnalysisInteraction + } + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -55,10 +68,13 @@ extension MediaPreviewImageView { maximumZoomScale = 4.0 addSubview(imageView) - + doubleTapGestureRecognizer.addTarget(self, action: #selector(MediaPreviewImageView.doubleTapGestureRecognizerHandler(_:))) imageView.addGestureRecognizer(doubleTapGestureRecognizer) - + if #available(iOS 16.0, *) { + imageView.addInteraction(liveTextInteraction) + } + delegate = self } @@ -129,6 +145,22 @@ extension MediaPreviewImageView { centerScrollViewContents() contentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top) + + if #available(iOS 16.0, *) { + Task.detached(priority: .userInitiated) { + do { + let analysis = try await ImageAnalyzer.shared.analyze(image, configuration: ImageAnalyzer.Configuration([.text, .machineReadableCode])) + await MainActor.run { + self.liveTextInteraction.analysis = analysis + self.liveTextInteraction.preferredInteractionTypes = .automatic + } + } catch { + await MainActor.run { + self.liveTextInteraction.preferredInteractionTypes = [] + } + } + } + } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: setup image for container %s", ((#file as NSString).lastPathComponent), #line, #function, container.frame.debugDescription) } diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift index 127c4c0c0..513110d29 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift @@ -11,6 +11,7 @@ import Combine import MastodonAsset import MastodonLocalization import FLAnimatedImage +import VisionKit protocol MediaPreviewImageViewControllerDelegate: AnyObject { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) @@ -31,7 +32,7 @@ final class MediaPreviewImageViewController: UIViewController { let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer let longPressGestureRecognizer = UILongPressGestureRecognizer() - + deinit { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) previewImageView.imageView.af.cancelImageRequest() @@ -42,7 +43,10 @@ extension MediaPreviewImageViewController { override func viewDidLoad() { super.viewDidLoad() - + + if #available(iOS 16.0, *) { + previewImageView.liveTextInteraction.delegate = self + } previewImageView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(previewImageView) NSLayoutConstraint.activate([ @@ -53,7 +57,9 @@ extension MediaPreviewImageViewController { ]) tapGestureRecognizer.addTarget(self, action: #selector(MediaPreviewImageViewController.tapGestureRecognizerHandler(_:))) + tapGestureRecognizer.delegate = self longPressGestureRecognizer.addTarget(self, action: #selector(MediaPreviewImageViewController.longPressGestureRecognizerHandler(_:))) + longPressGestureRecognizer.delegate = self tapGestureRecognizer.require(toFail: previewImageView.doubleTapGestureRecognizer) tapGestureRecognizer.require(toFail: longPressGestureRecognizer) previewImageView.addGestureRecognizer(tapGestureRecognizer) @@ -105,10 +111,54 @@ extension MediaPreviewImageViewController { } +extension MediaPreviewImageViewController: MediaPreviewPage { + func setShowingChrome(_ showingChrome: Bool) { + if #available(iOS 16.0, *) { + UIView.animate(withDuration: 0.3) { + self.previewImageView.liveTextInteraction.setSupplementaryInterfaceHidden(!showingChrome, animated: true) + } + } + } +} + +// MARK: - ImageAnalysisInteractionDelegate +@available(iOS 16.0, *) +extension MediaPreviewImageViewController: ImageAnalysisInteractionDelegate { + func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? { + self + } +} + +// MARK: - UIGestureRecognizerDelegate +extension MediaPreviewImageViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if #available(iOS 16.0, *) { + let location = touch.location(in: previewImageView.imageView) + // for tap gestures, only items that can be tapped are relevant + if gestureRecognizer is UITapGestureRecognizer { + return !previewImageView.liveTextInteraction.hasSupplementaryInterface(at: location) + && !previewImageView.liveTextInteraction.hasDataDetector(at: location) + } else { + // for long press, block out everything + return !previewImageView.liveTextInteraction.hasInteractiveItem(at: location) + } + } else { + return true + } + } +} + // MARK: - UIContextMenuInteractionDelegate extension MediaPreviewImageViewController: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + if #available(iOS 16.0, *) { + if previewImageView.liveTextInteraction.hasInteractiveItem(at: previewImageView.imageView.convert(location, from: previewImageView)) { + return nil + } + } + let previewProvider: UIContextMenuContentPreviewProvider = { () -> UIViewController? in return nil diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index 5dede76df..dee6c00ea 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -132,6 +132,17 @@ extension MediaPreviewViewController { } } .store(in: &disposeBag) + + viewModel.$showingChrome + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [weak self] showingChrome in + UIView.animate(withDuration: 0.3) { + self?.setNeedsStatusBarAppearanceUpdate() + self?.closeButtonBackground.alpha = showingChrome ? 1 : 0 + } + } + .store(in: &disposeBag) // viewModel.$isPoping // .receive(on: DispatchQueue.main) @@ -148,6 +159,14 @@ extension MediaPreviewViewController { } +extension MediaPreviewViewController { + + override var prefersStatusBarHidden: Bool { + !viewModel.showingChrome + } + +} + extension MediaPreviewViewController { @objc private func closeButtonPressed(_ sender: UIButton) { @@ -234,8 +253,11 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { let location = tapGestureRecognizer.location(in: viewController.previewImageView.imageView) let isContainsTap = viewController.previewImageView.imageView.frame.contains(location) - guard !isContainsTap else { return } - dismiss(animated: true, completion: nil) + if isContainsTap { + self.viewModel.showingChrome.toggle() + } else { + dismiss(animated: true, completion: nil) + } } func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer) { diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index c43d24945..3f60b19e5 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -12,6 +12,10 @@ import CoreDataStack import Pageboy import MastodonCore +protocol MediaPreviewPage: UIViewController { + func setShowingChrome(_ showingChrome: Bool) +} + final class MediaPreviewViewModel: NSObject { weak var mediaPreviewImageViewControllerDelegate: MediaPreviewImageViewControllerDelegate? @@ -22,9 +26,12 @@ final class MediaPreviewViewModel: NSObject { let transitionItem: MediaPreviewTransitionItem @Published var currentPage: Int + @Published var showingChrome = true // output let viewControllers: [UIViewController] + + private var disposeBag: Set = [] init( context: AppContext, @@ -34,7 +41,7 @@ final class MediaPreviewViewModel: NSObject { self.context = context self.item = item var currentPage = 0 - var viewControllers: [UIViewController] = [] + var viewControllers: [MediaPreviewPage] = [] switch item { case .attachment(let previewContext): currentPage = previewContext.initialIndex @@ -106,6 +113,14 @@ final class MediaPreviewViewModel: NSObject { self.currentPage = currentPage self.transitionItem = transitionItem super.init() + + for viewController in viewControllers { + self.$showingChrome + .sink { [weak viewController] showingChrome in + viewController?.setShowingChrome(showingChrome) + } + .store(in: &disposeBag) + } } } diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index 254b9b2c5..e924f38d4 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -107,6 +107,12 @@ extension MediaPreviewVideoViewController { // } //} +extension MediaPreviewVideoViewController: MediaPreviewPage { + func setShowingChrome(_ showingChrome: Bool) { + // TODO: does this do anything? + } +} + // MARK: - AVPlayerViewControllerDelegate extension MediaPreviewVideoViewController: AVPlayerViewControllerDelegate { diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift index 80df8938e..88441f9ad 100644 --- a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift +++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift @@ -14,7 +14,7 @@ class AdaptiveStatusBarStyleNavigationController: UINavigationController { // Make status bar style adaptive for child view controller // SeeAlso: `modalPresentationCapturesStatusBarAppearance` override var childForStatusBarStyle: UIViewController? { - visibleViewController + topViewController } } diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 8219859a2..ed0503502 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -131,6 +131,12 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } return animator } + + if let toVC = transitionContext.viewController(forKey: .to) { + animator.addCompletion { _ in + toVC.setNeedsStatusBarAppearanceUpdate() + } + } // update close button UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { diff --git a/MastodonSDK/Sources/MastodonExtension/ImageAnalyzer.swift b/MastodonSDK/Sources/MastodonExtension/ImageAnalyzer.swift new file mode 100644 index 000000000..cf8a0f437 --- /dev/null +++ b/MastodonSDK/Sources/MastodonExtension/ImageAnalyzer.swift @@ -0,0 +1,13 @@ +// +// ImageAnalyzer.swift +// +// +// Created by Jed Fox on 2022-11-14. +// + +import VisionKit + +@available(iOS 16.0, *) +extension ImageAnalyzer { + public static let shared = ImageAnalyzer() +}