How can I implement the fullscreen image and dismiss image functionality of the Apple Photos? Additionally, how can I get the aspect ratio of an image to fit within a fullscreen view? Are they sending a UIImage to a new view controller?
My current method of fullscreening an image simply sets a UIImageView's frame equal to the superview's frame while turning the alphas of the UINavigationBar and UITabBar to 0. And to dismiss, I added a tap gesture recognizer that reverses the alphas and removes the UIImageView from the superview.
Here's my fullscreen and dismiss code
func fullscreen(forImage image: UIImage) {
let imageView = UIImageView(image: image)
imageView.frame = self.view.frame
imageView.backgroundColor = .black
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
self.navigationController?.navigationBar.alpha = 0
self.tabBarController?.tabBar.alpha = 0
let dismissTap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
imageView.addGestureRecognizer(dismissTap)
self.view.addSubview(imageView)
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
sender.view?.removeFromSuperview()
self.navigationController?.navigationBar.alpha = 1
self.tabBarController?.tabBar.alpha = 1
}
This could easily be achieved using Apple's QuickLook framework as it works great for many extensions and collections of images.
Edit: Most of the functionality you want is built into the QLPreviewController
let previewController = QLPreviewController()
previewController.dataSource = self
self.present(previewController, animated: true, completion: nil)
The data source is whatever class conforms to the QLPreviewControllerDataSource protocol
Here is a video guide from apple on how to achieve this easily
Edit: This part goes in the previewItemAt function
guard let url = Bundle.main.url(forResource: "imageName", withExtension: "jpg")
else {
fatalError("Could not load imageName.jpg")
}
return url as QLPreviewItem
Related
I am getting a delay on animation when popping a view controller.
The reason I am getting the delay is because I have a background image.
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundImage()
}
#IBAction func navigationButton(_ sender: Any) {
print("navigation button presssed")
self.navigationController?.popViewController(animated: true)
}
func setBackgroundImage() {
let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
backgroundImage.image = UIImage(named: "RewardsBackgroundONLY")
backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill
self.view.insertSubview(backgroundImage, at: 0)
}
How do I prevent the delay from happening while setting the background image.
I also tried popping the view controller on the main thread, still doesnt work.
Probably your image is too large and the content mode backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill cause this behavior.
you have two ways to resolve this problem.
change the UIView.ContentMode value and check if is ok for you. try scaleToFill or scaleAspectFit
resize image to fit on your app.
just set
clipsToBounds = true
it works for me
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).
I have an image slider where multiple images are added to a scrollview. Each is stored in an array of my custom data type (named Slide). I cycle through them like this:
for (index, slide) in slides.enumerated() {
let imageView = UIImageView()
imageView.image = UIImage(named: slide.image) //set the imageView with the imageName in this slide
imageView.contentMode = .scaleAspectFit
let xPos = self.view.frame.width * CGFloat(index) //calculate x depending upon slide index
imageView.frame = CGRect(x: xPos, y: -200, width: self.SVSlider.frame.width, height: self.SVSlider.frame.height)
SVSlider.contentSize.width = SVSlider.frame.width * CGFloat(index + 1) //add slide image to scrollview
SVSlider.addSubview(imageView)
//add tap function to slide
imageView.isUserInteractionEnabled = true
imageView.tag = index
let tapImage = UITapGestureRecognizer(target: self, action: #selector(slideAction(_:)))
imageView.addGestureRecognizer(tapImage)
}
Each slide image is attached to the method "slideAction", below:
#objc func slideAction(_ sender: UITapGestureRecognizer) {
let selName = slides[(sender.view?.tag)!].target
NSObject.perform(Selector(selName))
}
But the application throws an error when I click an image. By debugging I can see that the "tag" is correctly set to the slide index number. I want to perform the target action held in the slide object. (Currently the target is a String of the method name - ending with ':').
How can I get the "slideAction" to goto the method in the slide object?
Try this ,create target method in the custom class callMethodDirectly and call it
#objc func slideAction(_ sender: UITapGestureRecognizer) {
let selName = slides[(sender.view?.tag)!]
selName.callMethodDirectly()
}
class slide:NSObjcet
{
func callMethodDirectly()
{
// implement target code here
}
}
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!
In my app, I want the user to be able to be able to take a picture, be presented with the picture, and by tapping the photo a textfield can be added so that they can write on top of the image. This is exactly the same as the functionality of adding text to pictures in Snapchat.
As far as I can understand, the only way to be presented the image after having taken it and be able to edit it, is to set:
imagePicker.showsCameraControls = false
Make a custom overlay:
#IBAction func takePhoto(sender: UIButton) {
imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .Camera
imagePicker.showsCameraControls = false
imagePicker.allowsEditing = true
let overlayView = UIView(frame: CGRectMake(0, self.view.frame.width, self.view.frame.width, self.view.frame.height-self.view.frame.width))
overlayView.backgroundColor = UIColor.blackColor()
overlayView.alpha = 0.5
println(overlayView)
let snapButton = UIView(frame: CGRectMake(150, 160, 80, 80))
snapButton.layer.cornerRadius = 40
snapButton.userInteractionEnabled = true
snapButton.backgroundColor = UIColor.purpleColor()
overlayView.addSubview(snapButton)
overlayView.bringSubviewToFront(snapButton)
let recognizer = UITapGestureRecognizer(target: self, action:Selector("handleSnapTap:"))
recognizer.delegate = self
snapButton.addGestureRecognizer(recognizer)
let cancelButton = UIView(frame: CGRectMake(40, 40, 44, 44))
cancelButton.layer.cornerRadius = 22
cancelButton.userInteractionEnabled = true
cancelButton.backgroundColor = UIColor.redColor()
overlayView.addSubview(cancelButton)
overlayView.bringSubviewToFront(cancelButton)
let cancelRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleCancelTap:"))
cancelRecognizer.delegate = self
cancelButton.addGestureRecognizer(cancelRecognizer)
let changeCameraButton = UIView(frame: CGRectMake(165, 40, 44, 44))
changeCameraButton.layer.cornerRadius = 22
changeCameraButton.userInteractionEnabled = true
changeCameraButton.backgroundColor = UIColor.blueColor()
overlayView.addSubview(changeCameraButton)
overlayView.bringSubviewToFront(changeCameraButton)
let changeCameraRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleChangeCameraTap:"))
changeCameraRecognizer.delegate = self
changeCameraButton.addGestureRecognizer(changeCameraRecognizer)
let flashButton = UIView(frame: CGRectMake(300, 40, 44, 44))
flashButton.layer.cornerRadius = 22
flashButton.userInteractionEnabled = true
flashButton.backgroundColor = UIColor.yellowColor()
overlayView.addSubview(flashButton)
overlayView.bringSubviewToFront(flashButton)
let flashRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleFlashTap:"))
flashRecognizer.delegate = self
flashButton.addGestureRecognizer(flashRecognizer)
imagePicker.cameraOverlayView = overlayView
presentViewController(imagePicker, animated: true, completion: nil)
}
func handleSnapTap(recognizer: UITapGestureRecognizer) {
println("Take picture")
imagePicker.takePicture()
self.performSegueWithIdentifier("cameraToImageViewSegue", sender: self)
}
func handleCancelTap(recognizer: UITapGestureRecognizer) {
println("Cancel")
self.imagePicker.dismissViewControllerAnimated(true, completion: nil)
}
func handleChangeCameraTap(recognizer: UITapGestureRecognizer) {
if (hasChangedCamera == nil){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Front
hasChangedCamera = true
return
}
if (hasChangedCamera == true){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Rear
hasChangedCamera = false
return
}
if (hasChangedCamera! == false){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Front
hasChangedCamera = true
return
}
}
func handleFlashTap(recognizer: UITapGestureRecognizer) {
if (hasTurnedOnFlash == nil){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.On
hasTurnedOnFlash = true
return
}
if (hasTurnedOnFlash == true){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.Off
hasTurnedOnFlash = false
return
}
if (hasTurnedOnFlash == false){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.On
hasTurnedOnFlash = true
return
}
}
And finally present a new view controller in which the picked image is placed in a UIView, and edit it from there. My issue is how to segue directly from the UIImagePickerController to a new view controller. I have tried the following:
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
self.imagePicker.dismissViewControllerAnimated(false, completion: nil)
let vc = ModifyCameraImageViewController() //change this to your class name
self.presentViewController(vc, animated: false, completion: nil)
}
First off, this just leads to a black screen, but I'm sure there's a simple enough way around that. My main issue is the fact that the view controller from which the UIImagePickerController was presented briefly appears on the screen before the next view controller appears. This obviously does not look good. I also tried removing the dismissViewController function, as well as placing the presentViewController function above the dismissView controller function. Both of these attempts gave me the error message:
Warning: Attempt to present <xxx.ModifyCameraImageViewController: 0x145e3eb70> on <xxx.ViewController: 0x145d20a60> whose view is not in the window hierarchy!
Attempting to use performSegueWithIdentifier with a segue linking the underlying view and the next view controller gives the same error warning.
I have found the following similar question, but I am completely inept at Objective C, so I'm struggling to make any sense of it: Push a viewController from the UIImagePickerController camera view
So, can anyone help in regards to how to present a view controller directly from the UIImagePickerController?
Also, keep in mind that I'm doing this in order to be able to create a text overlay on the newly picked image (like in Snapchat), so if anyone has a more elegant solution to that, feel free to post it!
Thanks!
Ok, found a simple solution to my issue. Instead of presenting the imagePickerController from the underlying view controller when the takePicture button is pressed, and segueing to another view controller directly from there, use the takePicture button to segue to another view controller and present the imagePickerController from the viewDidLoad of the second view controller. The second view controller will then be presented when the imagePickerController is dismissed. This however requires the underlying view controller to look similar to the camera controls, and some playing around with animations for this to look natural.
let pickerController: Void = picker.dismissViewControllerAnimated(true) { _ in
UIImageView.image = info[UIImagePickerControllerOriginalImage] as? UIImage
self.performSegueWithIdentifier("segueIdentifier", sender: nil)
//Do what you want when picker is dismissed
}