UIImagePickerController cropping image rect is not correct - ios

I have a UIViewController that holds the image picker:
let picker = UIImagePickerController()
and I call the image picker like that:
private func showCamera() {
picker.allowsEditing = true
picker.sourceType = .camera
picker.cameraCaptureMode = .photo
picker.modalPresentationStyle = .fullScreen
present(picker, animated: true, completion: nil)
}
when I'm done I get a delegate callback like that:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
DispatchQueue.main.async {
if let croppedImage = info[UIImagePickerControllerEditedImage] as? UIImage {
self.imageView.contentMode = .scaleAspectFill
self.imageView.image = croppedImage
self.dismiss(animated:true, completion: nil)
}
}
}
and I get the cropping UI after I took the image and in the video you can see the behaviour:
https://youtu.be/OaJnsjrlwF8
As you can see, I can not scroll the zoomed rect to the bottom or top. This behaviour is reproducible on iOS 10/11 on multiple devices.
Is there any way to get this right with UIImagePickerController?

No, this component has been bugged for quite some time now. Not only this one with positioning but also the cropped rect is usually incorrect (off by some 20px vertically).
It seems Apple has no interest in fixing it and you should create your own. It is not too much of work. Start by creating a screen that accepts and displays image on scroll view. Then ensure zoom and pan is working (maybe even rotation) which should be all done pretty quickly.
Then the cropping part occurs which is actually done quickest by using view snapshot:
The following will create an image from image view:
func snapshotImageFor(view: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
view.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Then in your view controller you can do this little trick:
func createSnapshot(inFrame rect: CGRect) -> UIImage? {
let temporaryView = UIView(frame: rect) // This is a view from which the snapshot will occure
temporaryView.clipsToBounds = true
view.addSubview(temporaryView) // We want to put it into hierarchy
guard let viewToSnap = scrollViewContainer else { return nil } // We want to use the superview of the scrollview because using scroll view directly may have some issues.
let originalImageViewFrame = viewToSnap.frame // Preserve previous frame
guard let originalImageViewSuperview = viewToSnap.superview else { return nil } // Preserve previous superview
guard let index = originalImageViewSuperview.subviews.index(of: viewToSnap) else { return nil } // Preserve view hierarchy index
// Now change the frame and put it on the new view
viewToSnap.frame = originalImageViewSuperview.convert(originalImageViewFrame, to: temporaryView)
temporaryView.addSubview(viewToSnap)
// Create snapshot
let croppedImage = snapshotImageFor(view: temporaryView)
// Put everything back the way it was
viewToSnap.frame = originalImageViewFrame // Reset frame
originalImageViewSuperview.insertSubview(viewToSnap, at: index) // Reset superview
temporaryView.removeFromSuperview() // Remove the temporary view
self.croppedImage = croppedImage
return croppedImage
}
There are some downsides to this procedures like doing everything on main thread but for your specific procedure this should not be a problem at all.
You might at some point want some control of the image output size. You can do that easiest by modifying snapshot image to include custom scale:
func snapshotImageFor(view: UIView, scale: CGFloat = 0.0) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, scale)
Then you would for instance call snapshotImageFor(view: view, scale: expectedWidth/view.bounds.width).

Related

How to mimic an actual 'Hardware Screenshot'? (AVCaptureSessionPreview not showing...)

I need to capture the entire contents of my app's screen (screenshot) with a UIbutton on screen. There are labels/static images/etc. in addition to a live preview box for what the camera is showing(which is a sublayer of the main view).
I've already tried every version of UIGraphicsGetImageFromCurrentImageContext and view.drawHierarchy() methods (posted here and other places) for capturing a screenshot programmatically but no matter what I try, the AVCaptureVideoPreviewLayer I have in the middle of the screen NEVER shows up.
Does anyone know how to mimic the code when a user presses the two hardware buttons to initiate a screenshot? When I do that, the resulting picture DOES have the entire contents of the screen! If I try any other programmatic method, the PreviewLayer is always blank.
Here's an example of one of the methods I've used:
Create extension -
extension UIView {
func screenShot () -> UIImage? {
let scale = UIScreen.main.scale
let bounds = self.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, true, scale)
if let _ = UIGraphicsGetCurrentContext() {
self.drawHierarchy(in: bounds, afterScreenUpdates: true)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot
}
return nil
}
}
Then, function to save the image-
func saveImage(screenshot: UIImage) {
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
}
Finally, run the functions-
guard let screenshot = self.view.screenShot() else {return}
saveImage(screenshot: screenshot)
I know many people have different variations of this but nothing I try will include the PreviewLayer (which is a sublayer of the view).

UIView take a screenshot when it is not visible

I have a UIView that can be drawn like a finger paint application, but sometimes it is not visible. I want to be able to take a screenshot of it when it is not visible. Also, I want a screenshot where it is visible, but I don't want any subviews. I just want the UIView itself. This is the method I have tried:
func shapshot() -> UIImage? {
UIGraphicsBeginImageContext(self.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
self.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if image == nil {
return nil
}
return image
}
func snapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, self.isOpaque, UIScreen.main.scale)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
To get view rendered as UIImage, you could introduce a very simple protocol and extend UIView with it.
protocol Renderable {
var render: UIImage { get }
}
extension UIView: Renderable {
var render: UIImage {
UIGraphicsImageRenderer(bounds: bounds).image { context in
layer.render(in: context.cgContext)
}
}
}
and now it's super easy to get the image of any view
let image: UIImage = someView.render
then if you plan to share it or save it, you probably want to convert it to Data
let data: Data? = image.pngData()
I am not sure what you mean with the "when it is not visible" but this should work as long as the view is in the view hierarchy and it's properly laid out. I have been using this method in many apps for sharing stuff and it never failed me.
And of course there is no need for a protocol, feel free to use only the render computed property. It's just a matter of preference.
Documentation:
UIGraphicsImageRenderer, image(actions:)

Can you convert uiview that is not currently being displayed to uiimage

I have found numerous examples of converting UIView to UIImage, and they work perfectly for once the view has been laid out etc. Even in my view controller with many rows, some of which are net yet displaying on the screen, I can do the conversion. Unfortunately, for some of the views that are too far down in the table (and hence have not yet been "drawn"), doing the conversion produces a blank UIImage.
I've tried calling setNeedsDisplay and layoutIfNeeded, but these don't work. I've even tried to automatically scroll through the table, but perhaps I'm not doing in a way (using threads) that ensures that the scroll happens first, allowing the views to update, before the conversion takes place. I suspect this can't be done because I have found various questions asking this, and none have found a solution. Alternatively, can I just redraw my entire view in a UIImage, not requiring a UIView?
From Paul Hudson's website
Using any UIView that is not showing on the screen (say a row in a UITableview that is way down below the bottom of the screen.
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
You don't have to have a view in a window/on-screen to be able to render it into an image. I've done exactly this in PixelTest:
extension UIView {
/// Creates an image from the view's contents, using its layer.
///
/// - Returns: An image, or nil if an image couldn't be created.
func image() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
layer.render(in: context)
context.restoreGState()
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image
}
}
This will render a view's layer into an image as it currently looks if it was to be rendered on-screen. That is to say, if the view hasn't been laid out yet, then it won't look the way you expect. PixelTest does this by force-laying out the view beforehand when verifying a view for snapshot testing.
You can also accomplish this using UIGraphicsImageRenderer.
extension UIView {
func image() -> UIImage {
let imageRenderer = UIGraphicsImageRenderer(bounds: bounds)
if let format = imageRenderer.format as? UIGraphicsImageRendererFormat {
format.opaque = true
}
let image = imageRenderer.image { context in
return layer.render(in: context.cgContext)
}
return image
}
}

snapshotViewAfterScreenUpdates returns blank image on iPhone but not on iPad

I'm doing a custom view controller transition in which the presented view controller, detailVC, is scaled down when dismissed.
The procedure I chose is the following:
snapshot detailVC.view, add it to the transition context's containerView
hide detailVC.view
scale down the snapshot.
Here's the code:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let detailVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? DetailViewController
containerView.addSubview(detailVC.view)
detailVC.view.frame = containerView.frame
let detailVCSnapshot = detailVC.view.snapshotViewAfterScreenUpdates(true)
containerView.addSubview(detailVCSnapshot)
detailVC.view.hidden = true
...
}
Bizarrely, this works well on the iPad, but not on iPhone. On iPhone, detailVCSnapshot is completely transparent.
Other answers (1, 2) recommend ensuring that the view has been drawn, but this is indeed the case as detailVC is the currently visible view controller!
Any thoughts?
I found a workaround involving using UIGraphicsGetImageFromCurrentImageContext on iPhone:
let detailVCSnapshot = isIphone ? UIImageView(image: detailVC.view.snapshotImage) : detailVC.view.snapshotViewAfterScreenUpdates(true)
private extension UIView {
var snapshotImage: UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0)
layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Any explanations or alternatives would be welcome!

Activity Indicator for Swift only covering part of the screen

I'm trying to do the activity indicator with a background that covers the entire screen. My problem is that when I run the app, it only covers a portion of the screen. I am not sure what I am doing wrong here.
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {
self.dismissViewControllerAnimated(true) { () -> Void in
self.activityIndicator = UIActivityIndicatorView(frame: self.view.frame)
self.activityIndicator.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
self.activityIndicator.center = self.view.center
self.activityIndicator.hidesWhenStopped = true
self.activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
self.view.addSubview(self.activityIndicator)
self.activityIndicator.startAnimating()
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
print("save the data on to core data and onto Parse, then segue to the new input controller")
let file = PFFile(data: UIImageJPEGRepresentation(image, 1.0)!)
let insurance = PFObject(className: "Insurance")
insurance["userId"] = PFUser.currentUser()?.objectId
insurance["imageFile"] = file as PFFile
insurance.saveInBackgroundWithBlock({ (success, error ) -> Void in
self.activityIndicator.stopAnimating()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
if (success) {
self.performSegueWithIdentifier("segueToDetails", sender: self)
}else{
self.displayAlert("Problems Savings", message: "There was an error with saving the data")
}
})
}
}
Are there any suggestions?
Change this line
self.activityIndicator = UIActivityIndicatorView(frame: self.view.frame)
to
self.activityIndicator = UIActivityIndicatorView(frame: self.view.bounds)
Judging from your screenshot, the frame of self.view is something like (0, 64, /* some width */, /* some height */). The y origin is 64 points down the screen. You copy this frame and give the activity indicator that same frame. This would would if you were adding the activity indicator to a view whose y origin is at 0 points. Since the view you add it to is already 64 points down the screen, adding the activity indicator with that frame will make the activity indicators real y in terms of the phone screen 128 points down the screen. I got this number from adding the y origin of the view and the y origin of the activity indicator.
What I did to solve this problem is pushing the loading indicator on topmost controller:
private var topMostController: UIViewController? {
var presentedVC = UIApplication.sharedApplication().keyWindow?.rootViewController
while let pVC = presentedVC?.presentedViewController {
presentedVC = pVC
}
return presentedVC
}
You might want to try this, a project of mine: https://github.com/goktugyil/EZLoadingActivity

Resources