Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support iOS 16 Live Text #332

Merged
merged 3 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions Agrume/Agrume.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public final class Agrume: UIViewController {
private var images: [AgrumeImage]!
private let startIndex: Int
private let dismissal: Dismissal
private let enableLiveText: Bool

private var overlayView: AgrumeOverlayView?
private weak var dataSource: AgrumeDataSource?
Expand Down Expand Up @@ -63,9 +64,10 @@ public final class Agrume: UIViewController {
/// - background: The background configuration
/// - dismissal: The dismiss configuration
/// - overlayView: View to overlay the image (does not display with 'button' dismissals)
/// - enableLiveText: Enables Live Text interaction, iOS 16 only
public convenience init(image: UIImage, background: Background = .colored(.black),
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil) {
self.init(images: [image], background: background, dismissal: dismissal, overlayView: overlayView)
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
self.init(images: [image], background: background, dismissal: dismissal, overlayView: overlayView, enableLiveText: enableLiveText)
}

/// Initialize with a single image url
Expand All @@ -75,9 +77,10 @@ public final class Agrume: UIViewController {
/// - background: The background configuration
/// - dismissal: The dismiss configuration
/// - overlayView: View to overlay the image (does not display with 'button' dismissals)
/// - enableLiveText: Enables Live Text interaction, iOS 16 only
public convenience init(url: URL, background: Background = .colored(.black), dismissal: Dismissal = .withPan(.standard),
overlayView: AgrumeOverlayView? = nil) {
self.init(urls: [url], background: background, dismissal: dismissal, overlayView: overlayView)
overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
self.init(urls: [url], background: background, dismissal: dismissal, overlayView: overlayView, enableLiveText: enableLiveText)
}

/// Initialize with a data source
Expand All @@ -88,10 +91,11 @@ public final class Agrume: UIViewController {
/// - background: The background configuration
/// - dismissal: The dismiss configuration
/// - overlayView: View to overlay the image (does not display with 'button' dismissals)
/// - enableLiveText: Enables Live Text interaction, iOS 16 only
public convenience init(dataSource: AgrumeDataSource, startIndex: Int = 0, background: Background = .colored(.black),
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil) {
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
self.init(images: nil, dataSource: dataSource, startIndex: startIndex, background: background, dismissal: dismissal,
overlayView: overlayView)
overlayView: overlayView, enableLiveText: enableLiveText)
}

/// Initialize with an array of images
Expand All @@ -102,9 +106,10 @@ public final class Agrume: UIViewController {
/// - background: The background configuration
/// - dismissal: The dismiss configuration
/// - overlayView: View to overlay the image (does not display with 'button' dismissals)
/// - enableLiveText: Enables Live Text interaction, iOS 16 only
public convenience init(images: [UIImage], startIndex: Int = 0, background: Background = .colored(.black),
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil) {
self.init(images: images, urls: nil, startIndex: startIndex, background: background, dismissal: dismissal, overlayView: overlayView)
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
self.init(images: images, urls: nil, startIndex: startIndex, background: background, dismissal: dismissal, overlayView: overlayView, enableLiveText: enableLiveText)
}

/// Initialize with an array of image urls
Expand All @@ -115,13 +120,14 @@ public final class Agrume: UIViewController {
/// - background: The background configuration
/// - dismissal: The dismiss configuration
/// - overlayView: View to overlay the image (does not display with 'button' dismissals)
/// - enableLiveText: Enables Live Text interaction, iOS 16 only
public convenience init(urls: [URL], startIndex: Int = 0, background: Background = .colored(.black),
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil) {
self.init(images: nil, urls: urls, startIndex: startIndex, background: background, dismissal: dismissal, overlayView: overlayView)
dismissal: Dismissal = .withPan(.standard), overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
self.init(images: nil, urls: urls, startIndex: startIndex, background: background, dismissal: dismissal, overlayView: overlayView, enableLiveText: enableLiveText)
}

private init(images: [UIImage]? = nil, urls: [URL]? = nil, dataSource: AgrumeDataSource? = nil, startIndex: Int,
background: Background, dismissal: Dismissal, overlayView: AgrumeOverlayView? = nil) {
background: Background, dismissal: Dismissal, overlayView: AgrumeOverlayView? = nil, enableLiveText: Bool = false) {
switch (images, urls) {
case (let images?, nil):
self.images = images.map { AgrumeImage(image: $0) }
Expand All @@ -135,6 +141,7 @@ public final class Agrume: UIViewController {
self.currentIndex = startIndex
self.background = background
self.dismissal = dismissal
self.enableLiveText = enableLiveText
super.init(nibName: nil, bundle: nil)

self.overlayView = overlayView
Expand Down Expand Up @@ -468,6 +475,7 @@ extension Agrume: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: AgrumeCell = collectionView.dequeue(indexPath: indexPath)

cell.enableLiveText = enableLiveText
cell.tapBehavior = tapBehavior
switch dismissal {
case .withPan(let physics), .withPanAndButton(let physics, _):
Expand Down
36 changes: 36 additions & 0 deletions Agrume/AgrumeCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import SwiftyGif
import UIKit
import VisionKit

protocol AgrumeCellDelegate: AnyObject {

Expand Down Expand Up @@ -60,12 +61,24 @@ final class AgrumeCell: UICollectionViewCell {
// if set to true, it means we are updating image on the same cell, so we want to reserve the zoom level & position
var updatingImageOnSameCell = false

// enables Live Text analysis & interaction
var enableLiveText = false

var image: UIImage? {
didSet {
if image?.imageData != nil, let image = image {
imageView.setGifImage(image)
} else {
imageView.image = image
if enableLiveText {
if #available(iOS 16, *) {
if let image = image {
Task {
await analyzeImage(image)
}
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this expression and get rid of some of the nesting:

Suggested change
if enableLiveText {
if #available(iOS 16, *) {
if let image = image {
Task {
await analyzeImage(image)
}
}
}
}
if #available(iOS 16, *), enableLiveText, let image = image {
Task {
await analyzeImage(image)
}
}

}
if !updatingImageOnSameCell {
updateScrollViewAndImageViewForCurrentMetrics()
Expand Down Expand Up @@ -482,4 +495,27 @@ extension AgrumeCell: UIScrollViewDelegate {
dismiss()
}
}

@available(iOS 16, *)
private func analyzeImage(_ image: UIImage) async {
guard ImageAnalyzer.isSupported else {
return
}

let analyzer = ImageAnalyzer()
let interaction = await MainActor.run {
let interaction = ImageAnalysisInteraction()
imageView.addInteraction(interaction)
return interaction
}
let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
do {
let analysis = try await analyzer.analyze(image, configuration: configuration)
await MainActor.run {
interaction.analysis = analysis
interaction.preferredInteractionTypes = .automatic
}
} catch {
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we changed the method to not be async and instead encapsulate that inside of the method itself. Then we don't need the await MainActor.run dance to construct the interaction. We could then also make use of Task { @MainActor in

Another alternative would be to keep the method async but annotate it to run on the MainActor:

@MainActor
private func analyzeImage(_ image: UIImage) async {
  
}
Suggested change
private func analyzeImage(_ image: UIImage) async {
guard ImageAnalyzer.isSupported else {
return
}
let analyzer = ImageAnalyzer()
let interaction = await MainActor.run {
let interaction = ImageAnalysisInteraction()
imageView.addInteraction(interaction)
return interaction
}
let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
do {
let analysis = try await analyzer.analyze(image, configuration: configuration)
await MainActor.run {
interaction.analysis = analysis
interaction.preferredInteractionTypes = .automatic
}
} catch {
}
}
private func analyzeImage(_ image: UIImage) {
guard ImageAnalyzer.isSupported else {
return
}
let interaction = ImageAnalysisInteraction()
imageView.addInteraction(interaction)
let analyzer = ImageAnalyzer()
let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
Task { @MainActor in
do {
let analysis = try await analyzer.analyze(image, configuration: configuration)
interaction.analysis = analysis
interaction.preferredInteractionTypes = .automatic
} catch {
print(error.localizedDescription)
}
}
}

}
4 changes: 4 additions & 0 deletions Example/Agrume Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
39B9D7C228DE0B500016BE7F /* LiveTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */; };
39CA658926EFFC5700A5A910 /* URLUpdatedToImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */; };
771DA7342179EF1800541206 /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 771DA7332179EF1800541206 /* SwiftyGif.framework */; };
9464AFE923C692C7006ADEBD /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9464AFE823C692C7006ADEBD /* OverlayView.swift */; };
Expand Down Expand Up @@ -80,6 +81,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTextViewController.swift; sourceTree = "<group>"; };
39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUpdatedToImageViewController.swift; sourceTree = "<group>"; };
771DA7332179EF1800541206 /* SwiftyGif.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftyGif.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9464AFE823C692C7006ADEBD /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -181,6 +183,7 @@
E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */,
F2D9598D1B1A133800073772 /* SingleImageViewController.swift */,
F2D959901B1A140200073772 /* SingleURLViewController.swift */,
39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */,
39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */,
F2A520181B130C7E00924912 /* Supporting Files */,
);
Expand Down Expand Up @@ -362,6 +365,7 @@
F2D9598E1B1A133800073772 /* SingleImageViewController.swift in Sources */,
F2539BD420F2418900062C80 /* CustomCloseButtonViewController.swift in Sources */,
F2A5201B1B130C7E00924912 /* AppDelegate.swift in Sources */,
39B9D7C228DE0B500016BE7F /* LiveTextViewController.swift in Sources */,
9464AFE923C692C7006ADEBD /* OverlayView.swift in Sources */,
F2D959971B1A199F00073772 /* MultipleURLsCollectionViewController.swift in Sources */,
F224A73227832DD900A8F5ED /* SwiftUIExampleViewController.swift in Sources */,
Expand Down
49 changes: 49 additions & 0 deletions Example/Agrume Example/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,26 @@
<segue destination="cCQ-GJ-dUD" kind="show" id="r46-tO-UZQ"/>
</connections>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="6Zp-bu-jIU" style="IBUITableViewCellStyleDefault" id="Hfv-Wz-qVQ">
<rect key="frame" x="0.0" y="578" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Hfv-Wz-qVQ" id="KQk-qO-EJ6">
<rect key="frame" x="0.0" y="0.0" width="348.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="With Live Text" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6Zp-bu-jIU">
<rect key="frame" x="16" y="0.0" width="324.5" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="qLg-q5-gAS" kind="show" id="5bd-8h-ouw"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
Expand Down Expand Up @@ -733,6 +753,35 @@
</objects>
<point key="canvasLocation" x="2001" y="2266"/>
</scene>
<!--Live Text Image View Controller-->
<scene sceneID="1WM-Xw-5br">
<objects>
<viewController title="Live Text Image View Controller" id="qLg-q5-gAS" customClass="LiveTextViewController" customModule="Agrume_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="6HJ-JL-fid"/>
<viewControllerLayoutGuide type="bottom" id="hXH-Vp-sjT"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Vfa-Ml-qsJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dFR-Lm-f5v">
<rect key="frame" x="146" y="318" width="83" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Open Image"/>
<connections>
<action selector="openImage:" destination="qLg-q5-gAS" eventType="touchUpInside" id="gRd-G8-Zjs"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="Hrj-fJ-cCk"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="0eg-SZ-Rgn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1281" y="2956"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "textAndQR.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions Example/Agrume Example/LiveTextViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// LiveTextViewController.swift
//

import Agrume
import UIKit
import VisionKit

final class LiveTextViewController: UIViewController {
@IBAction private func openImage(_ sender: Any) {
if #available(iOS 16, *) {
if ImageAnalyzer.isSupported {
let agrume = Agrume(
image: UIImage(named: "TextAndQR")!,
enableLiveText: true
)
agrume.show(from: self)
return
}
}

let alert = UIAlertController(title: "Not supported on this device", message: "Live Text is available for devices with iOS 16 (or above) and A12 (or above) Bionic chip (iPhone XS and later, physical device only)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
}
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ agrume.onLongPress = helper.makeSaveToLibraryLongPressGesture

You can customise the look and functionality of the image views. To do so, you need create a class that inherits from `AgrumeOverlayView: UIView`. As this is nothing more than a regular `UIView` you can do anything you want with it like add a custom toolbar or buttons to it. The example app shows a detailed example of how this can be achieved.

### Live Text Support

Agrume supports Live Text introduced since iOS 16. This allows user to interact with texts and QR codes in the image. It is available for iOS 16 or newer, on devices with A12 Bionic Chip (iPhone XS) or newer.

```swift
let agrume = Agrume(image: UIImage(named: "…")!, enableLiveText: true)
```

### Lifecycle

`Agrume` offers the following lifecycle closures that you can optionally set:
Expand Down