Merge pull request #592 from j-f1/live-text

Add Live Text support to images
This commit is contained in:
CMK 2022-11-22 13:14:38 +08:00 committed by GitHub
commit c6b2f730d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 155 additions and 11 deletions

View File

@ -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):

View File

@ -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)
}

View File

@ -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

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -107,6 +107,12 @@ extension MediaPreviewVideoViewController {
// }
//}
extension MediaPreviewVideoViewController: MediaPreviewPage {
func setShowingChrome(_ showingChrome: Bool) {
// TODO: does this do anything?
}
}
// MARK: - AVPlayerViewControllerDelegate
extension MediaPreviewVideoViewController: AVPlayerViewControllerDelegate {

View File

@ -14,7 +14,7 @@ class AdaptiveStatusBarStyleNavigationController: UINavigationController {
// Make status bar style adaptive for child view controller
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
override var childForStatusBarStyle: UIViewController? {
visibleViewController
topViewController
}
}

View File

@ -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]) {

View File

@ -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()
}