Content of scrollview clear when taking screenshot - ios

I have a scrollview and i need a screenshot of the complete content of that scrollview, i search and try many solutions on stackoverflow but that provide only the screenshot of the total height of the viewcontroller.
For example if the height of the scrollview is 1096 and Viewcontroller height is 532 , it screenshot the content according to the Viewcontroller height
I found only this code useful on stackoverflow and it return the complete height of the scrollview but it clear the content of scrollview in the Viewcontroller. Any help how to retain the content of scrollview or any better solution to take a complete screenshot in swift. Thankyou
func getImageOfScrollView()->UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.mainScreen().scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset;
let savedFrame = self.scrollView.frame;
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPointZero;
// set frame to content size
self.scrollView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height);
// remove background
self.scrollView.backgroundColor = UIColor.clearColor()
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.renderInContext(UIGraphicsGetCurrentContext())
// and get image
image = UIGraphicsGetImageFromCurrentImageContext();
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset;
self.scrollView.frame = savedFrame;
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}

Using this as I guide, I think you can snapshot any view using:
func snapShot(view:UIView) -> UIImage
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Though with a scroll view this will only return an image of what's visible in the scroll view's frame, so we can create a helper function:
func snapShotScrollView(scrollView:UIScrollView) -> UIImage
{
let bounds = scrollView.bounds
scrollView.bounds.size = scrollView.contentSize
let image = snapShot(scrollView)
scrollView.bounds = bounds
return image
}
This will briefly adjust our scroll view's bounds to it's content size.
I don't know how performant this code is, but this works for me in a swift playground. For example, running this code:
let scrollView = UIScrollView(frame:CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
scrollView.backgroundColor = UIColor.whiteColor()
scrollView.contentSize = CGSize(width: 600.0, height: 600.0)
let blueView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
blueView.backgroundColor = UIColor.blueColor()
let redView = UIView(frame: CGRect(x: 200.0, y: 200.0, width: 400.0, height: 600.0))
redView.backgroundColor = UIColor.redColor()
scrollView.addSubview(blueView)
scrollView.addSubview(redView)
let image = snapShotScrollView(scrollView)
Results in
where as calling snapShot by itself only returns a blue square. After the snapshot the scroll view is the original size with the original content.

Related

Blend UIView (Overlay) with app background

I would like to blend a UIView with my app's background, using a special blend mode (in my case, the Overlay mode). However, the view to blend is contained in a complex hierarchy of views.
Blending a view with its direct siblings can be achieved using view.layer.compositingFilter = "overlayBlendMode", but the view won't blend with non-siblings views, like the app background.
To recreate the problem, I made the following playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let parentView = UIView()
parentView.backgroundColor = .purple
// Child view
let childView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
childView.layer.borderColor = UIColor.orange.cgColor
childView.layer.borderWidth = 3
parentView.addSubview(childView)
// Child child view
let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
childChildView.backgroundColor = .white
childChildView.layer.compositingFilter = "overlayBlendMode"
childView.addSubview(childChildView)
self.view = parentView
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
We can see here that the child child view, in white, is not blended:
Whereas the view should appear blended like this (the border should not change color):
To create the second picture, I applied the compositing filter on the childView instead of the childChildView, which will blend all the other subviews — therefore it's not what I want. I just want this specific view to be blended.
Note: this view is supposed to move, because it's inside a UIScrollView.
EDIT: More complex example with image background and scrollviews
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let parentView = UIView()
// Background image
let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
backgroundImageView.frame = UIScreen.main.bounds
parentView.addSubview(backgroundImageView)
// Page view (horizontal scrollview)
let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
pageView.contentSize = CGSize(width: 600, height: 200)
pageView.flashScrollIndicators()
pageView.layer.borderColor = UIColor.orange.cgColor
pageView.layer.borderWidth = 3
parentView.addSubview(pageView)
// Child view (vertical scrollview)
let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
childView.contentSize = CGSize(width: 100, height: 300)
childView.layer.borderColor = UIColor.green.cgColor
childView.layer.borderWidth = 3
pageView.addSubview(childView)
// Child child view
let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
childChildView.backgroundColor = .white
childChildView.layer.compositingFilter = "overlayBlendMode"
childView.addSubview(childChildView)
self.view = parentView
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
UPDATE 2:
I've tried several ways including adding layers or creating custom image filters that use the background image as input image but none of these solutions got the desired result. The main problem was always the view hierarchy.
I may have found a solution by using a generated image of the views or the actual background image as the content background of the childView once the childChildView is being created but before being displayed. I've changed your example code a bit to add a scroll view and background image in the parentView. See if this works for you / is your desired result:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let parentView = UIView()
parentView.backgroundColor = .purple
let imageName = "image.jpg"
let image = UIImage(named: imageName)
let imageWidth = Int((image?.size.width)!)
let imageheight = Int((image?.size.height)!)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: 50, y: 50, width: imageWidth , height: imageheight)
parentView.addSubview(imageView)
// Child view as UIScrollView
let childView = UIScrollView(frame: CGRect(x: 55, y: 55, width: imageWidth - 10, height: imageheight - 10 ))
childView.contentSize = CGSize(width: imageWidth - 10, height: 5000)
childView.layer.borderColor = UIColor.orange.cgColor
childView.flashScrollIndicators()
childView.layer.borderWidth = 10
parentView.addSubview(childView)
// ChildChild view
let childChildView = UIView(frame: CGRect(x: 15, y: 100, width: 85, height: imageheight - 180))
childChildView.layer.compositingFilter = "overlayBlendMode"
childChildView.backgroundColor = .white
//Creating a static image of the background views BEFORE adding the childChildView.
let format = UIGraphicsImageRendererFormat()
format.scale = 1
format.preferredRange = .standard ///color profile
///Change the imageView to the parentView size of the app. Not available if not set in the playground.
let renderer = UIGraphicsImageRenderer(size: imageView.bounds.size, format: format)
let imageBG = renderer.image { context in
///This draws all subviews of the parentView one after the other.
///Because the background image is not a parent of our current view, otherwise childView.drawHierachy would have been enough
for subview in parentView.subviews {
///Skip specific views or view classes you don't want to be added to the image. or if you only need the parentView itself rendered remove the for in loop.
subview.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
}
}
//Adding the static background image. This could simply also be the actual image: UIImage if no other views are supposed to be used.
childView.layer.contents = imageBG.cgImage
childView.addSubview(childChildView)
self.view = parentView
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
It results in the following:
UPDATE:
The colors in the images are misleading, as you could assume a normal transparency effect would be the same. But the overlayBlendMode is quite different as Coconuts has pointed out. I assume the issue is that the compositingFilter only works with the view below, even if this view is transparent.
I tried finding a workaround by using a mask that cuts out a square of the size of the childchild from the childview. But this also didn't work as the mask is also applied to all subviews. The only way I got it to work is by making the childchildview a sibling of childview instead, or a direct subview of the background view. But not sure if this will be possible in the complex view hierarchy mentioned by Coconuts.
// Sibling view with adjusted x and y
let childChildView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
childChildView.backgroundColor = .white
childChildView.layer.compositingFilter = "overlayBlendMode"
parentView.addSubview(childChildView)
MISC:
To only get the visual result of the sample images, not actually using the overlayBlendMode filter as asked by Coconut.
If you only need to blend the color you could change the alpha value of the color.
// Child child view
let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
childChildView.backgroundColor = UIColor(white: 1, alpha: 0.5)
//childChildView.layer.compositingFilter = "overlayBlendMode"
childView.addSubview(childChildView)
Or try this:
// Child child view
let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
childChildView.backgroundColor = .white
childChildView.layer.opacity = 0.5
childView.addSubview(childChildView)
ADDITIONAL ATTEMPTS WHEN HAVING SEVERAL SCROLL VIEWS:
This is an attempt to solve the from Coconut added more complicated view hierarchy with multiple scroll views. The performance needs to be improved or the part that adjusts the background image of the background image layer needs to run in sync when the app is updating (redrawing) its views. At the moment it's lagging behind a bit.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let parentView = UIView()
// Background image
let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
backgroundImageView.frame = UIScreen.main.bounds
parentView.addSubview(backgroundImageView)
// Page view (horizontal scrollview)
let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
pageView.contentSize = CGSize(width: 600, height: 200)
pageView.flashScrollIndicators()
pageView.layer.borderColor = UIColor.yellow.cgColor
pageView.layer.borderWidth = 3
pageView.clipsToBounds = true
parentView.addSubview(pageView)
// Child view (vertical scrollview)
let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
childView.contentSize = CGSize(width: 100, height: 300)
childView.layer.borderColor = UIColor.red.cgColor
childView.layer.borderWidth = 3
pageView.addSubview(childView)
// Child child view
let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50))
//Child child view foreground sublayer
let childChildFrontLayer = CALayer()
childChildFrontLayer.frame = childChildView.frame.offsetBy(dx: -75, dy: -50)
childChildFrontLayer.backgroundColor = UIColor.white.cgColor
childChildFrontLayer.compositingFilter = "overlayBlendMode"
//Child child view background sublayer
let childChildBackLayer = CALayer()
childChildBackLayer.contents = UIImage(named: "image.jpg")?.cgImage
var absolutFrame = parentView.convert(childChildView.frame, from: childView)
childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)
childChildView.layer.addSublayer(childChildBackLayer)
childChildView.layer.addSublayer(childChildFrontLayer)
childView.addSubview(childChildView)
//Checking for any scrolling. Is slightly faster then the scollview delegate methods but might cause main thread checker warning.
DispatchQueue.global(qos: .userInteractive).async {
while true {
if pageView.isDragging || pageView.isTracking || pageView.isDecelerating || childView.isDragging || childView.isTracking || childView.isDecelerating {
absolutFrame = parentView.convert(childChildView.frame, from: childView)
DispatchQueue.main.async {
childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)
}
}
}
}
self.view = parentView
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Is the issue related to the child view background being clear and therefore 'blending' to give the white colour. Could set the child view background colour to be equal to the app background and then then just blend the childView within the childChildView?

Getting a full screenshot of a UIScrollView in iOS 13

I am using Swift 5 and iOS 13. I am trying to take a screenshot of the entire scrollview image and print it, but the bottom appears in white. How can i solve this problem? I showed and explained everything in the picture below.
I am also checked this topic
My extension code like this:
extension UIScrollView {
func screenshot() -> UIImage {
let savedContentOffset = contentOffset
let savedFrame = frame
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0)
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
contentOffset = savedContentOffset
frame = savedFrame
return image ?? UIImage()
}
}
Here is the code snippet that worked for me.
extension UIView {
func snapshot(scrollView: UIScrollView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, false, UIScreen.main.scale)
layer.render(in: UIGraphicsGetCurrentContext()!)
let savedContentOffset = scrollView.contentOffset
let savedFrame = frame
scrollView.contentOffset = CGPoint.zero
frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
frame = savedFrame
UIGraphicsEndImageContext()
return image
}
}
And when you call it:
view.snapshot(scrollView: scrollView)
It's seems like iOS 13 bug but I found the solution in this answer.
The solution is I am removing scrollview from superview and then adding in with its old constraints after taking the screenshot. Yeah, it's stupid but it just works that way :(
scrollView.removeFromSuperview()
let print = scrollView.screenshot()
mainView.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.top.equalTo(topView.snp.bottom)
make.left.bottom.right.equalToSuperview()
}

scrolling between invisible rects

Take a look at this picture :
Imagine a scroll view, where the pink rects are images each on a scroller page.
Imagine that the blue rects are invisible rects different in size on each page.
Imagine that the yellow rect is an image that is static = not on the scroll view.
I would like to scroll between 2 and 3, where the yellow stay at the same position, BUT it disappear when the blue rects moving. (you see it only inside)
In rect 3 you can see what happen to the yellow while scrolling .
How should I put my layers on the scroller to create such an effect ?
So based on comments, it look like you need a scrollview with simple views.
This views can be with overlay that has a hole inside, take a look at code for making layer with hole:
func layerWith(img: UIImage) -> CALayer {
let overlay = UIView(frame: CGRect(x: 0, y: 0,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height))
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = overlay.bounds
let myImage = UIImage(named: "star")?.cgImage
maskLayer.frame = myView.bounds
maskLayer.contents = myImage
// Create the frame.
let radius: CGFloat = 150.0
let rect = CGRect(x: overlay.frame.midX - radius,
y: overlay.frame.midY - radius,
width: 2 * radius,
height: 2 * radius)
// Create the path.
let path = UIBezierPath(rect: overlay.bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd
// Append the rect to the path so that it is subtracted.
path.append(UIBezierPath(rect: rect))
maskLayer.path = path.cgPath
// Set the mask of the view.
overlay.layer.mask = maskLayer
// Add the view so it is visible.
return overlay
}
And some code to add subviews into scrollview:
var i: CGFloat = 0
//or width that you want
let width = UIApplication.shared.keyWindow!.frame.width
for item in items {
let frame = CGRect(x: width * i, y: 0, width: width, height: scrollView.frame.height)
let v = UIView(frame: frame)
//here you can add layer from code above to this view before adding it to scrollview
v.layer.addSublayer(layerWith(img:your image goes here))
self.scrollView.addSubview(v)
i += 1
}

How to make a screenshot of all the content of a Scrollview?

I want to create a screenshot of a UIScrollView which should contain the whole content of the scroll view, even that content, which is currently not visible to the user. For that I tried the following two methods:
func snapShot(view:UIView) -> UIImage {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0);
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true);
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
func snapShotScrollView(scrollView:UIScrollView) -> UIImage {
let bounds = scrollView.bounds;
scrollView.bounds.size = scrollView.contentSize;
let image = snapShot(scrollView);
scrollView.bounds = bounds;
return image;
}
But the resulting image is still just showing those view elements inside the scroll view which are currently visible to the user. But I want to see all views.
How can I do that?
EDIT
I also tried:
func snapshot() -> UIImage? {
var image: UIImage?
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame;
scrollView.contentOffset = CGPoint.zero;
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height);
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
image = UIGraphicsGetImageFromCurrentImageContext();
scrollView.contentOffset = savedContentOffset;
scrollView.frame = savedFrame;
UIGraphicsEndImageContext();
return image
}
Edit 2
My UIScrollView is placed inside a UIView and does contain a UIStackView. The View is designed as a popover view so that it looks like a dialogue is popping up. The code sample from my first edit is working in a blank UIViewController with only one UIScrollView but not in the mentioned constellation.
To Swift from this answer, adding a test ViewController:
class ScreenShotTestViewController: UIViewController {
var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: CGRect(origin: CGPoint.zero, size: view.frame.size))
scrollView.contentSize = CGSize(width: view.frame.size.width, height: view.frame.size.height * 2)
scrollView.backgroundColor = UIColor.yellow
view.addSubview(scrollView)
let label = UILabel(frame: CGRect(x: 0.0, y: view.frame.size.height * 1.5, width: view.frame.size.width, height: 44.0))
label.text = "Hello World!"
scrollView.addSubview(label)
let screenShot = snapshot()
}
func snapshot() -> UIImage?
{
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
scrollView.contentOffset = CGPoint.zero
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
}
Results in an image of the complete content, including subviews:

Why does NSLog show screen width and screen height as 0?

So I wrote the following piece of code, which displays the red square in the middle, as I intended to be. However, when I "NSLog"ed the output of the screen size of the device onto the console, I found that the value is 0. Why is that?
let container = UIView()
let redSquare = UIView()
let blueSquare = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let screenSize : CGSize = UIScreen.mainScreen().applicationFrame.size
let screenWidth = screenSize.width
let screenHeight = screenSize.height
NSLog("screenWidth = %f, screenHeight = %f", screenWidth, screenHeight);
self.container.frame = CGRect(x: 50, y: 50, width: screenWidth-100, height: screenWidth-100)
self.view.addSubview(container)
// set red square frame up
// we want the blue square to have the same position as redSquare
// so lets just reuse blueSquare.frame
self.redSquare.frame = CGRect(x: 0, y: 0, width: screenWidth-100, height: screenWidth-100)
self.blueSquare.frame = redSquare.frame
// set background colors
self.redSquare.backgroundColor = UIColor.redColor()
self.blueSquare.backgroundColor = UIColor.blueColor()
// for now just add the redSquare
// we'll add blueSquare as part of the transition animation
self.container.addSubview(self.redSquare)
}

Resources