I need convert the contents of an UICollectionView into an UIIImage. But, the image only contains the visible portion of the UICollectionView and leaves out the parts which are yet to be rendered. Is there any that I can achieve it ?
I am using the following extension
func toImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
return renderer.image { ctx in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
}
Is there any that I can achieve it ?
No, at least not by a simple screen capture. The offscreen cells do not even exist, so there is nothing for you to capture.
There are two reasons why your full Collection View is not rendered as an Image.
You call drawHierarchy in self.bounds, which will only capture the content in the specified bounds
The rest of the collection view will not be rendered as the rest of the cells will be off screen.
Related
I have the following View extension function which creates a UIImage of the SwiftUI view by putting it in a UIHostingController then using a UIGraphicsImageRenderer to render the view:
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self.ignoresSafeArea())
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let format = UIGraphicsImageRendererFormat()
format.scale = 1
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
return renderer.image { _ in
view?.drawHierarchy(
in: controller.view.bounds,
afterScreenUpdates: true
)
}
}
}
This works but it blocks the main thread completely such that any loading spinners I display on screen don't spin. How can I stop that, or is there another way of creating a snapshot that I could use that doesn't block the main thread?
You can't run UI on background, but as help states about drawHierarchy:
Use this method when you want to apply a graphical effect, such as a blur, to a view snapshot. This method is not as fast as the snapshotView(afterScreenUpdates:) method.
Since you are not applying any graphical effect, you could use snapshotView, which as help again states:
This method very efficiently captures the current rendered appearance of a view and uses it to build a new snapshot view. You can use the returned view as a visual stand-in for the current view in your app. For example, you might use a snapshot view for animations where updating a large view hierarchy might be expensive. Because the content is captured from the already rendered content, this method reflects the current visual appearance of the view and is not updated to reflect animations that are scheduled or in progress. However, calling this method is faster than trying to render the contents of the current view into a bitmap image yourself.
I have recently moved a bunch of UIImageViews into nested Stack Views (Horizontal and Vertical). Whenever the user presses the play button it animates, then starts a timer. Once the timer reaches 0 it is supposed to flip each card consecutively and hide the image itself. This was working until I added them to a Stack View. I've read that the stack views are buggy when it comes to this sort of thing, but any attempt at fixing it hasn't worked thus far. I'm at an impasse, can anyone point me in the right direction?
Issue Occurs In Here I Think
//INFO: Disable cards while viewing.
for card in cardsPhone
{
card.isUserInteractionEnabled = false
flipCard(sender: card)
}
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 5) {
for card in self.cardsPhone
{
DispatchQueue.main.async {
//THIS IS THE ANIMATION CALL THAT MAKES VIEWS DISAPPEAR.
self.flipCard(sender: card)
}
usleep(20000)
}
DispatchQueue.main.async{
//INFO: Enable cards after hidden.
for card in self.cardsPhone
{
card.isUserInteractionEnabled = true
}
//INFO: Enable play button after cards are hidden to prevent crashing layout.
self.playButtonView.isUserInteractionEnabled = true
}
}
Flip Animation Controller
func flipCard(sender: UIImageView)
{
playButtonView.isUserInteractionEnabled = false
//INFO: If there is no UIImage, assign the appropriate image from the array.
if sender.image == nil
{
sender.image = cardsImages[sender.tag]
UIView.transition(with: sender, duration: 0.3, options: .transitionFlipFromLeft, animations: nil, completion: nil)
}
//INFO: If there is an image, remove it and replace with no Image.
else
{
sender.image = nil
UIView.transition(with: sender, duration: 0.3, options: .transitionFlipFromRight, animations: nil, completion: nil)
}
}
Update
I've discovered that if I replace the image on the UIImageView then that is actually what is causing it to disappear. So the issue is related to the code above in the else statement where sender.image = UIImage(). It doesn't matter if I replace with a different image or not, the moment the image is changed it disappears.
I've read that the stack views are buggy when it comes to this sort of thing
Well, that's not so. They are in fact quite sophisticated and consistent, and their behavior is well defined.
What is a stack view? It's a builder of autolayout constraints. And that is all it is. Putting views inside a stack view means "Please configure these views with equal spacing" (or whatever your setting is for the stack view) "using autolayout constraints that you impose."
Okay, but how does the stack view do that? Autolayout requires four pieces of information, x and y position, and width and height. The stack view will supply the position, but what about the size (width and height) of its views? Well, one way to tell it is to give your views internal constraints (if they don't conflict with what else the stack view will do). But in the absence of that, it has to use the intrinsic size of its views. And the intrinsic size of an image view under autolayout is the size of the image it contains.
So everything was fine when you had image views with images that were the same size. They all had the same intrinsic size and now the stack view could space them out. But then you took away an image view's image. Now it has no image. So it has no intrinsic size. So the stack view just removes it from the row (or column or whatever it is).
Your workaround is actually quite a good one, namely, to make sure that the image view always has an image; if it is to look blank, you just give it a blank image. The image looks like the background, but it is still an image with an actual size, and so it gives the image view a size and the image view continues to hold its place.
However another solution would have been simply to give the image view a width constraint the same size as the card image's width.
Solution
I've discovered that changing an image on a UIImageView while it's inside of a stack causes it to completely disappear. This is a weird Xcode but but the solution I found was to create a custom background on the fly to get rid of the image and only have the background color. This was done with the following code snippet. It's a bit messy, but it gets the job done.
func setColorImage(viewToModify: UIImageView, colorToSet: UIColor)
{
let rect = CGRect(origin: .zero, size: viewToModify.layer.bounds.size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
colorToSet.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard (image?.cgImage) != nil else { return }
viewToModify.image = image
viewToModify.backgroundColor = colorToSet
viewToModify.isUserInteractionEnabled = false
}
Essentially this code creates a new cgRect, sets the color, creates a new image from that color, then applies it back to the UIImageView.
I want to get a photo thumbnail for my collection view cell, this is how i did it:
imageManager.requestImageForAsset(asset as! PHAsset, targetSize:CGSizeMake(80, 80), contentMode: .AspectFit, options: nil, resultHandler: {(result, info)->Void in
cell.setThumbnail(result!)
}
})
This will get a thumbnail that is stretched, I want a square cropped thumbnail that keeps the photo's original ratio like iPhone's Photo app did.
Any ideas? Please respond in swift, because I am new to ios programming, and I start with swift, thanks.
If you are working with Interface Builder where you can set the properties on your UIImageView, take a look at these two properties:
The view mode will affect whether it looks stretched or if it fits. Sometimes, though, even though it technically fits, it may still overflow the bounds of the cell rect if your image is too big. In that case, you can enable "Clip Subviews" which will make sure the image does not display outside the bounds of the image view rect.
If you are not using Interface Builder, both of these properties can be set on the cell in code instead, i.e.:
// This is assuming your UIImageView inside your cell is
// called 'thumbnail'. You could put this code inside of your
// .setThumbnail() function instead--though it's not clear
// what all that is doing from you code snippet.
cell.thumbnail.clipsToBounds = true
cell.thumbnail.contentMode = .ScaleAspectFill
The various content modes you can choose from are defined like this in UIKit:
public enum UIViewContentMode : Int {
case ScaleToFill
case ScaleAspectFit // contents scaled to fit with fixed aspect. remainder is transparent
case ScaleAspectFill // contents scaled to fill with fixed aspect. some portion of content may be clipped.
case Redraw // redraw on bounds change (calls -setNeedsDisplay)
case Center // contents remain same size. positioned adjusted.
case Top
case Bottom
case Left
case Right
case TopLeft
case TopRight
case BottomLeft
case BottomRight
}
This is how I take a screenshot of my view:
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
However, in the view, there is a UIVisualEffectsView which I'd like to exclude from the screenshot.
I tried hiding the UIVisualEffectsView just before taking the screenshot and un-hiding it afterwards but I don't want the user to see that process. (which he does if I simply hide the view because the iPad is too slow and it looks like the screen is flickering...)
Any ideas? Thanks in advance!
I would take advantage of the snapshotViewAfterScreenUpdates() method
This method very efficiently captures the current rendered appearance of a view and uses it to build a new snapshot view. You can use the returned view as a visual stand-in for the current view in your app.
Therefore you can use it in order to display an overlay UIView of the complete un-altered view hierarchy to the user while rendering a version of the hierarchy with your changes underneath it.
The only caveat is that if you're capturing a view controller's hierarchy, you'll have to create a 'content view' subview in order to prevent the overlay view from being rendered in your screenshot where you make the changes to the hierarchy. You'll then want to add your view hierarchy that you want to render to this 'content view'.
So your view hierarchy will want to look something like this:
UIView // <- Your view
overlayView // <- Only present when a screenshot is being taken
contentView // <- The view that gets rendered in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
Although, if you are able to add the overlayView to the view's superview - instead of to the view itself – you don't need to mess about with the hierarchy at all. For example:
overlayView // <- Only present when a screenshot is being taken
UIView // <- Your view – You can render this in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
otherViews // <- The rest of your hierarchy
Something like this should achieve the desired result:
// get a snapshot view of your content
let overlayView = contentView.snapshotViewAfterScreenUpdates(true)
// add it over your view
view.addSubview(overlayView)
// do changes to the view heirarchy
viewToHide.hidden = true
// begin image context
UIGraphicsBeginImageContextWithOptions(contentView.frame.size, false, 0.0)
// render heirarchy
contentView.drawViewHierarchyInRect(contentView.bounds, afterScreenUpdates: true)
// get image and end context
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// reverse changes to the view heirarchy
viewToHide.hidden = false
// remove the overlay view
overlayView.removeFromSuperview()
Maybe the simplest solution would be not to include your unwanted view into the hierarchy of the taken as screenshot view. You could simply put it on top of it.
Oh, actually, just following code (swift 4) can work. That is, create a context, and add the item you want in the snapshot. No need to add view in real screen.
Note: currentView is the view which snapshot based on, commonly, it will be view of whole screen. And addViews are the views you want to add.
func takeSnapShot(currentView: UIView , addViews: [UIView], hideViews: [UIView]) -> UIImage {
for hideView in hideViews {
hideView.isHidden = true
}
UIGraphicsBeginImageContextWithOptions(currentView.frame.size, false, 0.0)
currentView.drawHierarchy(in: currentView.bounds, afterScreenUpdates: true)
for addView in addViews{
addView.drawHierarchy(in: addView.frame, afterScreenUpdates: true)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
for hideView in hideViews {
hideView.isHidden = false
}
return image!
}
Actually there is an easy way to do it.
For example we have a share button and View in storyboard and process with it in sample below code.
#IBAction func share(_ sender: Any) {
self.hideView.isHidden = true
let bounds = UIScreen.main.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0.0)
self.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//Add Activity view controller to use it
self.hideView.isHidden = false
}
I used it for hiding Ads if the screenshot taken with my share button and it works for me.
afterScreenUpdates: true works here for hide to view while you click the button. It hides for millisecond hideView and took screenshot then it appears back to your view like it was never hidden.
*This is my first answer and hope it helps someone.
Thanks.
This is how I take a screenshot of my view:
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
However, in the view, there is a UIVisualEffectsView which I'd like to exclude from the screenshot.
I tried hiding the UIVisualEffectsView just before taking the screenshot and un-hiding it afterwards but I don't want the user to see that process. (which he does if I simply hide the view because the iPad is too slow and it looks like the screen is flickering...)
Any ideas? Thanks in advance!
I would take advantage of the snapshotViewAfterScreenUpdates() method
This method very efficiently captures the current rendered appearance of a view and uses it to build a new snapshot view. You can use the returned view as a visual stand-in for the current view in your app.
Therefore you can use it in order to display an overlay UIView of the complete un-altered view hierarchy to the user while rendering a version of the hierarchy with your changes underneath it.
The only caveat is that if you're capturing a view controller's hierarchy, you'll have to create a 'content view' subview in order to prevent the overlay view from being rendered in your screenshot where you make the changes to the hierarchy. You'll then want to add your view hierarchy that you want to render to this 'content view'.
So your view hierarchy will want to look something like this:
UIView // <- Your view
overlayView // <- Only present when a screenshot is being taken
contentView // <- The view that gets rendered in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
Although, if you are able to add the overlayView to the view's superview - instead of to the view itself – you don't need to mess about with the hierarchy at all. For example:
overlayView // <- Only present when a screenshot is being taken
UIView // <- Your view – You can render this in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
otherViews // <- The rest of your hierarchy
Something like this should achieve the desired result:
// get a snapshot view of your content
let overlayView = contentView.snapshotViewAfterScreenUpdates(true)
// add it over your view
view.addSubview(overlayView)
// do changes to the view heirarchy
viewToHide.hidden = true
// begin image context
UIGraphicsBeginImageContextWithOptions(contentView.frame.size, false, 0.0)
// render heirarchy
contentView.drawViewHierarchyInRect(contentView.bounds, afterScreenUpdates: true)
// get image and end context
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// reverse changes to the view heirarchy
viewToHide.hidden = false
// remove the overlay view
overlayView.removeFromSuperview()
Maybe the simplest solution would be not to include your unwanted view into the hierarchy of the taken as screenshot view. You could simply put it on top of it.
Oh, actually, just following code (swift 4) can work. That is, create a context, and add the item you want in the snapshot. No need to add view in real screen.
Note: currentView is the view which snapshot based on, commonly, it will be view of whole screen. And addViews are the views you want to add.
func takeSnapShot(currentView: UIView , addViews: [UIView], hideViews: [UIView]) -> UIImage {
for hideView in hideViews {
hideView.isHidden = true
}
UIGraphicsBeginImageContextWithOptions(currentView.frame.size, false, 0.0)
currentView.drawHierarchy(in: currentView.bounds, afterScreenUpdates: true)
for addView in addViews{
addView.drawHierarchy(in: addView.frame, afterScreenUpdates: true)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
for hideView in hideViews {
hideView.isHidden = false
}
return image!
}
Actually there is an easy way to do it.
For example we have a share button and View in storyboard and process with it in sample below code.
#IBAction func share(_ sender: Any) {
self.hideView.isHidden = true
let bounds = UIScreen.main.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0.0)
self.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//Add Activity view controller to use it
self.hideView.isHidden = false
}
I used it for hiding Ads if the screenshot taken with my share button and it works for me.
afterScreenUpdates: true works here for hide to view while you click the button. It hides for millisecond hideView and took screenshot then it appears back to your view like it was never hidden.
*This is my first answer and hope it helps someone.
Thanks.