ImageAnalysisInteraction in UIViewRepresentable not working correctly - swift

I have a simple UIViewRepresentable wrapper for the live text feature (ImageAnalysisInteraction). It was working without issues until I started updating the UIImage inside the updateUIView(...) function.
I have always been seeing this error in the console which originates from this view:
[api] -[CIImage initWithCVPixelBuffer:options:] failed because the buffer is nil.
When I change the image, it's updating correctly, but the selectableItemsHighlighted overlay stays the same and I can still select the text of the old image (even though it's no longer visible).
import UIKit
import SwiftUI
import VisionKit
#MainActor
struct LiveTextInteraction: UIViewRepresentable {
#Binding var image: UIImage
let interaction = ImageAnalysisInteraction()
let imageView = LiveTextImageView()
let analyzer = ImageAnalyzer()
let configuration = ImageAnalyzer.Configuration([.text])
func makeUIView(context: Context) -> UIImageView {
interaction.setSupplementaryInterfaceHidden(true, animated: true)
imageView.image = image
imageView.addInteraction(interaction)
imageView.contentMode = .scaleAspectFit
return imageView
}
func updateUIView(_ uiView: UIImageView, context: Context) {
Task {
uiView.image = image
do {
if let image = uiView.image {
let analysis = try await analyzer.analyze(image, configuration: configuration)
interaction.analysis = analysis;
interaction.preferredInteractionTypes = .textSelection
interaction.selectableItemsHighlighted = true
interaction.setContentsRectNeedsUpdate()
}
} catch {
// catch
}
}
}
}
class LiveTextImageView: UIImageView {
// Use intrinsicContentSize to change the default image size
// so that we can change the size in our SwiftUI View
override var intrinsicContentSize: CGSize {
.zero
}
}
What am I doing wrong here?

It looks like a bug. Try use dispatch
let highlighted = interaction.selectableItemsHighlighted
interaction.analysis = analysis // highlighted == false
if highlighted
{
DispatchQueue.main.async
{
interaction.selectableItemsHighlighted = highlighted
}
}
You don't need interaction.setContentsRectNeedsUpdate() if the interaction is added to a UIImageView

Related

PDFKit's scaleFactorForSizeToFit isn't working to set zoom in SwiftUI (UIViewRepresentable)

I'm working on an app that displays a PDF using PDFKit, and I need to be able to set the minimum zoom level - otherwise the user can just zoom out forever. I've tried to set minScaleFactor and maxScaleFactor, and because these turn off autoScales, I need to set the scaleFactor to pdfView.scaleFactorForSizeToFit. However, this setting doesn't result in the same beginning zoom as autoScales and despite changing the actual scaleFactor number, the beginning zoom doesn't change. This photo is with autoScales on:
[![image with autoscales on][1]][1]
and then what happens when I use the scaleFactorForSizeToFit:
[![image with scaleFactorForSizeToFit][2]][2]
To quote the apple documentation for scaleFactorForSizeToFit, this is the
"size to fit" scale factor that autoScales would use for scaling the current document and layout.
I've pasted my code below. Thank you for your help.
import PDFKit
import SwiftUI
import Combine
class DataLoader : ObservableObject {
#Published var data : Data?
var cancellable : AnyCancellable?
func loadUrl(url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let failureType):
print(failureType)
//handle potential errors here
case .finished:
break
}
}, receiveValue: { (data) in
self.data = data
})
}
}
struct PDFSwiftUIView : View {
#StateObject private var dataLoader = DataLoader()
var StringToBeLoaded: String
var body: some View {
VStack {
if let data = dataLoader.data {
PDFRepresentedView(data: data)
.navigationBarHidden(false)
} else {
CustomProgressView()
//.navigationBarHidden(true)
}
}.onAppear {
dataLoader.loadUrl(url: URL(string: StringToBeLoaded)!)
}
}
}
struct PDFRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let singlePage: Bool = false
func makeUIView(context _: UIViewRepresentableContext<PDFRepresentedView>) -> UIViewType {
let pdfView = PDFView()
// pdfView.autoScales = true
// pdfView.maxScaleFactor = 0.1
pdfView.minScaleFactor = 1
pdfView.scaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 10
if singlePage {
pdfView.displayMode = .singlePage
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context: UIViewRepresentableContext<PDFRepresentedView>) {
pdfView.document = PDFDocument(data: data)
}
func canZoomIn() -> Bool {
return false
}
}
struct ContentV_Previews: PreviewProvider {
static var previews: some View {
PDFSwiftUIView(StringToBeLoaded: "EXAMPLE_STRING")
.previewInterfaceOrientation(.portrait)
}
}
maybe it is to do with the sequence. This seems to work for me:
pdfView.scaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 10.0
pdfView.minScaleFactor = 1.0
pdfView.autoScales = true
I was eventually able to solve this. The following code is how I managed to solve it:
if let document = PDFDocument(data: data) {
pdfView.displayDirection = .vertical
pdfView.autoScales = true
pdfView.document = document
pdfView.setNeedsLayout()
pdfView.layoutIfNeeded()
pdfView.minScaleFactor = UIScreen.main.bounds.height * 0.00075
pdfView.maxScaleFactor = 5.0
}
For some reason, the pdfView.scaleFactorForSizeToFit doesn't work - it always returns 0. This might be an iOS 15 issue - I noticed in another answer that someone else had the same issue. In the code above, what I did was I just scaled the PDF to fit the screen by screen height. This allowed me to more or less "autoscale" on my own. The code above autoscales the PDF correctly and prevents the user from zooming out too far.
This last solution works but you are kind of hardcoding the scale factor. so it only works if the page height is always the same. And bear in mind that macOS does not have a UIScreen and even under iPadOS there can be several windows and the window can have a different height than the screen height with the new StageManager... this worked for me:
first, wrap your swiftUIView (in the above example PDFRepresentedView) in a geometry reader. pass the view height (proxy.size.height) into the PDFRepresentedView.
in makeUIView, set
pdfView.maxScaleFactor = some value > 1
pdfView.autoScales = true
in updateUiView, set:
let pdfView = uiView
if let pageHeight = pdfView.currentPage?.bounds(for: .mediaBox).height {
let scaleFactor:CGFloat = self.viewHeight / pageHeight
pdfView.minScaleFactor = scaleFactor
}
Since my app also support macOS, I have written an NSViewRepresentable in the same way.
Happy coding!

Sharing Screenshot of SwiftUI view causes crash

I am grabbing a screenshot of a sub-view in my SwiftUI View to immediately pass to a share sheet in order to share the image.
The view is of a set of questions from a text array rendered as a stack of cards. I am trying to get a screenshot of the question and make it share-able along with a link to the app (testing with a link to angry birds).
I have been able to capture the screenshot using basically Asperi's answer to the below question:
How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?
My share sheet launches, and I've been able to use the "Copy" feature to copy the image, so I know it's actually getting a screenshot, but whenever I click "Message" to send it to someone, or if I just leave the share sheet open, the app crashes.
The message says it's a memory issue, but doesn't give much description of the problem. Is there a good way to troubleshoot this sort of thing? I assume it must be something with how the screenshot is being saved in this case.
Here are my extensions of View and UIView to render the image:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
Here's an abbreviated version of my view - the button is about halfway down, and should call the private function at the bottom that renders the image from the View/UIView extensions, and sets the "questionScreenShot" variable to the rendered image, which is then presented in the share sheet.
struct TopicPage: View {
var currentTopic: Topic
#State private var currentQuestions: [String]
#State private var showShareSheet = false
#State var questionScreenShot: UIImage? = nil
var body: some View {
GeometryReader { geometry in
Button(action: {
self.questionScreenShot = render()
if self.questionScreenShot != nil {
self.showShareSheet = true
} else {
print("Did not set screenshot")
}
}) {
Text("Share Question").bold()
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [questionScreenShot!])
}
}
}
private func render() -> UIImage {
QuestionBox(currentQuestion: self.currentQuestions[0]).asImage()
}
}
I've found a solution that seems to be working here. I start the variable where the questionScreenShot gets stored as nil to start:
#State var questionScreenShot: UIImage? = nil
Then I just make sure to set it to 'render' when the view appears, which means it loads the UIImage so if the user clicks "Share Question" it will be ready to be loaded (I think there was an issue earlier where the UIImage wasn't getting loaded in time once the sharing was done).
It also sets that variable back to nil on disappear.
.onAppear {
self.currentQuestions = currentTopic.questions.shuffled()
self.featuredQuestion = currentQuestions.last!
self.questionScreenShot = render()
}
.onDisappear {
self.questionScreenShot = nil
self.featuredQuestion = nil
}

Displaying PHAssets (Photos) using PHCachingImageManager in SwiftUI app - IF statement in body View not working

I am trying to load a photo using PHCachingImageManager in my SwiftUI app. I am able to get the photo but unable to get my SwiftUI view to display it.
I have a view called PhotoCell as shown below. I pass the Binding<UIImage> to a function which uses PHCachingImageManager to load the asset. This works and returns a 90x120 image to the Binding's set function. Inside the set function you can see that hasLoadedImage is set to true.
The body of the View is composed of an if statement. This is never executed except for the first time when the value of hasLoadedImage is false. I have no idea what else to do. Just as a test I replaced the Image inside the if with a Text but even that does not display once hasLoadedImage is set to true. I've used if statements frequently in body getters.
I feel like I've overlooked something obvious.
struct PhotoCell: View {
#State var photoImage: UIImage = UIImage()
#State var hasLoadedImage: Bool = false
var photoAsset: PHAsset
var photoBinding: Binding<UIImage> {
Binding<UIImage>(
get: {
return self.photoImage
},
set: {(newValue) in
print("Loaded image \(newValue.size)") // this prints with the correct size
self.photoImage = newValue
self.hasLoadedImage = true // seems to have no effect
})
}
init(photoAsset: PHAsset) {
self.photoAsset = photoAsset
let options: PHImageRequestOptions = PHImageRequestOptions()
options.deliveryMode = .fastFormat
options.isNetworkAccessAllowed = true
options.resizeMode = .fast
PhotosManager.shared.loadAsset(asset: photoAsset, size: CGSize(width: 48, height: 48), options: options, image: self.photoBinding)
}
var body: some View {
if self.hasLoadedImage { // shouldn't this execute once the value is changed??
return AnyView(
Image(uiImage: self.photoImage)
.resizable()
.frame(width: 48, height: 48)
)
} else {
return AnyView(
Image(systemName: "goforward")
)
}
}
}
So out of frustration I came up with a work-around which is actually simpler than what I was trying to do. I constructed a UIViewRepresentable wrapper for UIImageView:
struct MyImage: UIViewRepresentable {
var photoAsset: PHAsset
func makeUIView(context: Context) -> UIImageView {
let uiView = UIImageView(image: UIImage(systemName: "goforward") ?? UIImage())
return uiView
}
func updateUIView(_ uiView: UIImageView, context: Context) {
let options: PHImageRequestOptions = PHImageRequestOptions()
options.deliveryMode = .fastFormat
options.isNetworkAccessAllowed = true
options.resizeMode = .fast
PhotosManager.shared.loadAsset(asset: photoAsset, into: uiView, options: options)
}
}
The loadAsset function is just this, if you want to test it, where imageManager is an instance of PHCachingImageManager. I would like to understand why my posted question is not working. I suspect it has something to do with the closure from requestImage and capturing self inappropriately but I just don't see it.
func loadAsset(asset: PHAsset, into imageView: UIImageView, options: PHImageRequestOptions?) {
let size = CGSize(width: 48, height: 48)
imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: options, resultHandler: { (result, info) in
if let result = result {
imageView.image = result
}
})
}

Is there a way to add an action to a Lottie Animated Switch in SwiftUI

Is there a way to access an 'onTapGesture' (or similar) for a lottie animated switch. I would like to change a property (isComplete) of a CoreData item when the switch is on. Below is my Lottie Button View. Many thanks.
import SwiftUI
import Lottie
struct LottieButton: UIViewRepresentable {
#State var task: Task
// Create a switch
let animationSwitch = AnimatedSwitch()
var filename = "checkmark"
func makeUIView(context: UIViewRepresentableContext<LottieButton>) -> UIView {
let view = UIView()
let animation = Animation.named(filename)
animationSwitch.animation = animation
animationSwitch.contentMode = .scaleAspectFit
animationSwitch.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationSwitch)
animationSwitch.clipsToBounds = false
animationSwitch.setProgressForState(fromProgress: 0, toProgress: 0.4, forOnState: true)
// fail attempt #1
// self.onTapGesture {
// self.task.isComplete.toggle()
// print(self.task.isComplete)
// }
// fail attempt #2
// if animationSwitch.isOn{
// task.isComplete.toggle()
// print(task.isComplete)
// action()
// }
NSLayoutConstraint.activate([
animationSwitch.heightAnchor.constraint(equalTo: view.heightAnchor),
animationSwitch.widthAnchor.constraint(equalTo: view.widthAnchor),
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieButton>) {
}
}

Can't change alpha of subview when cell is focused

I have a simple imageView added to a custom UICollectionViewCell. I initialize it when i declare it:
let likeIcon = UIImageView()
And then I set properties in my class' initializer:
likeIcon.image = UIImage(named: "heart_empty")!
likeIcon.alpha = 0.0
addSubview(likeIcon)
Nothing too crazy. I want the imageView to be hidden initially but then visible when the cell is clicked.
I have a simple method that I call when the cell is selected (it's not animated yet):
func toggleLikeButtonAnimated() {
likeIcon.frame = likeIconFrame()
likeIcon.alpha = 1.0
}
But the icon doesn't show.
If I comment out the initial likeIcon.alpha = 0.0 then the icon is visible selected or unselected, so it's there
toggleLikeButtonAnimated is definitely called
The frame is the correct frame
The only thing I can think of, since this is really strange, is that something with the focus engine is interfering with the alpha changing.
I have this code in the cell right now:
// MARK: -- Focus
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
coordinator.addCoordinatedAnimations({ () -> Void in
if self.focused {
self.focusItem()
} else {
self.unfocusItem()
}
}) { () -> Void in
}
}
func focusItem() {
self.overlay.alpha = 0.0
}
func unfocusItem() {
self.overlay.alpha = 0.6
}
The overlay is below the icon so it shouldn't interfere with it's visibility. So I tried this:
// MARK: -- Focus
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
coordinator.addCoordinatedAnimations({ () -> Void in
if self.focused {
self.focusItem()
} else {
self.unfocusItem()
}
}) { () -> Void in
}
}
func focusItem() {
self.overlay.alpha = 0.0
self.likeIcon.alpha = 1.0
}
func unfocusItem() {
self.overlay.alpha = 0.6
self.likeIcon.alpha = 0.0
}
The likeIcon animates in when the cell is focused and out when unfocused. But this is not what I want, and it seems like the animation of the focus engine is preventing my alpha change when selected.
Any ideas on how to fix?
According your description,you want the imageView to be hidden initially but then visible when the UICollectionViewCell is clicked, but it is did't work. When you set UICollectionViewDelegate,you must be call reloadData function,just like UITableView.Maybe you can try use this when you click cell.Solve you problem yet.