Merge pull request #592 from j-f1/live-text
Add Live Text support to images
This commit is contained in:
commit
c6b2f730d2
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<AnyCancellable> = []
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -107,6 +107,12 @@ extension MediaPreviewVideoViewController {
|
|||
// }
|
||||
//}
|
||||
|
||||
extension MediaPreviewVideoViewController: MediaPreviewPage {
|
||||
func setShowingChrome(_ showingChrome: Bool) {
|
||||
// TODO: does this do anything?
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
extension MediaPreviewVideoViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class AdaptiveStatusBarStyleNavigationController: UINavigationController {
|
|||
// Make status bar style adaptive for child view controller
|
||||
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
visibleViewController
|
||||
topViewController
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue