diff --git a/Agrume/Agrume.swift b/Agrume/Agrume.swift index 9a0f397..ee538a3 100644 --- a/Agrume/Agrume.swift +++ b/Agrume/Agrume.swift @@ -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? @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) } @@ -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 @@ -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, _): diff --git a/Agrume/AgrumeCell.swift b/Agrume/AgrumeCell.swift index ac5b15c..2028ff7 100644 --- a/Agrume/AgrumeCell.swift +++ b/Agrume/AgrumeCell.swift @@ -4,6 +4,7 @@ import SwiftyGif import UIKit +import VisionKit protocol AgrumeCellDelegate: AnyObject { @@ -60,12 +61,18 @@ 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 #available(iOS 16, *), enableLiveText, let image = image { + analyzeImage(image) + } } if !updatingImageOnSameCell { updateScrollViewAndImageViewForCurrentMetrics() @@ -482,4 +489,26 @@ extension AgrumeCell: UIScrollViewDelegate { dismiss() } } + + @available(iOS 16, *) + 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) + } + } + } } diff --git a/Example/Agrume Example.xcodeproj/project.pbxproj b/Example/Agrume Example.xcodeproj/project.pbxproj index 845bb4b..2afb952 100644 --- a/Example/Agrume Example.xcodeproj/project.pbxproj +++ b/Example/Agrume Example.xcodeproj/project.pbxproj @@ -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 */; }; @@ -80,6 +81,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTextViewController.swift; sourceTree = ""; }; 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUpdatedToImageViewController.swift; sourceTree = ""; }; 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 = ""; }; @@ -181,6 +183,7 @@ E77809E21D17821400CC60F1 /* SingleImageModalViewController.swift */, F2D9598D1B1A133800073772 /* SingleImageViewController.swift */, F2D959901B1A140200073772 /* SingleURLViewController.swift */, + 39B9D7C128DE0B500016BE7F /* LiveTextViewController.swift */, 39CA658826EFFC5700A5A910 /* URLUpdatedToImageViewController.swift */, F2A520181B130C7E00924912 /* Supporting Files */, ); @@ -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 */, diff --git a/Example/Agrume Example/Base.lproj/Main.storyboard b/Example/Agrume Example/Base.lproj/Main.storyboard index 2ccd148..9ab91d4 100644 --- a/Example/Agrume Example/Base.lproj/Main.storyboard +++ b/Example/Agrume Example/Base.lproj/Main.storyboard @@ -259,6 +259,26 @@ + + + + + + + + + + + + + + @@ -733,6 +753,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/Contents.json b/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/Contents.json new file mode 100644 index 0000000..1877e43 --- /dev/null +++ b/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/Contents.json @@ -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 + } +} diff --git a/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png b/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png new file mode 100644 index 0000000..cbf1b36 Binary files /dev/null and b/Example/Agrume Example/Images.xcassets/TextAndQR.imageset/textAndQR.png differ diff --git a/Example/Agrume Example/LiveTextViewController.swift b/Example/Agrume Example/LiveTextViewController.swift new file mode 100644 index 0000000..471684f --- /dev/null +++ b/Example/Agrume Example/LiveTextViewController.swift @@ -0,0 +1,24 @@ +// +// LiveTextViewController.swift +// + +import Agrume +import UIKit +import VisionKit + +final class LiveTextViewController: UIViewController { + @IBAction private func openImage(_ sender: Any) { + if #available(iOS 16, *), 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) + } +} diff --git a/README.md b/README.md index 1a0ea03..eb38375 100644 --- a/README.md +++ b/README.md @@ -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: