How to capture a full page screenshot of WKWebview? - ios

I can capture all content screenshot of UIWebView by adjust frame of UIScrollView of UIWebView. However, I can't capture all content screenshot of WKWebView by the same method.
The method I used for capture UIWebView as follow:
backup frame and superview of webview.scrollView
create a new container view as webview.scrollView's superview
adjust frame of new container view and webview.scrollView. frame is same to webview.scrollView.contentSize
draw by renderInContext or drawViewHierarchyInRect
However, this method will capture a white screenshot of WKWebview. It doesn't work!
I had print all level of WKWebView, then I found a UIView(WKContentView)'s size is same to contentView, you can found this view by this level:
WKWebView
WKScrollView
WKContentView(size is same to contentView)
I also had try to capture by WKContentView, then I found only visible view could be captured.
Anyway, Anyone could tell me how to capture a full page content screenshot of WKWebView?

Please refer to this answer
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)

Swift 3, Xcode 8
func takeScreenshot() -> UIImage? {
let currentSize = webView.frame.size
let currentOffset = webView.scrollView.contentOffset
webView.frame.size = webView.scrollView.contentSize
webView.scrollView.setContentOffset(CGPoint.zero, animated: false)
let rect = CGRect(x: 0, y: 0, width: webView.bounds.size.width, height: webView.bounds.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
webView.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
webView.frame.size = currentSize
webView.scrollView.setContentOffset(currentOffset, animated: false)
return image
}

Swift 3, Xcode 9:
I created an extension to achieve this. Hope this helps you.
extension WKWebView{
private func stageWebViewForScreenshot() {
let _scrollView = self.scrollView
let pageSize = _scrollView.contentSize;
let currentOffset = _scrollView.contentOffset
let horizontalLimit = CGFloat(ceil(pageSize.width/_scrollView.frame.size.width))
let verticalLimit = CGFloat(ceil(pageSize.height/_scrollView.frame.size.height))
for i in stride(from: 0, to: verticalLimit, by: 1.0) {
for j in stride(from: 0, to: horizontalLimit, by: 1.0) {
_scrollView.scrollRectToVisible(CGRect(x: _scrollView.frame.size.width * j, y: _scrollView.frame.size.height * i, width: _scrollView.frame.size.width, height: _scrollView.frame.size.height), animated: true)
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 1.0))
}
}
_scrollView.setContentOffset(currentOffset, animated: false)
}
func fullLengthScreenshot(_ completionBlock: ((UIImage) -> Void)?) {
// First stage the web view so that all resources are downloaded.
stageWebViewForScreenshot()
let _scrollView = self.scrollView
// Save the current bounds
let tmp = self.bounds
let tmpFrame = self.frame
let currentOffset = _scrollView.contentOffset
// Leave main thread alone for some time to let WKWebview render its contents / run its JS to load stuffs.
mainDispatchAfter(2.0) {
// Re evaluate the size of the webview
let pageSize = _scrollView.contentSize
UIGraphicsBeginImageContext(pageSize)
self.bounds = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y, width: pageSize.width, height: pageSize.height)
self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: pageSize.width, height: pageSize.height)
// Wait few seconds until the resources are loaded
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 0.5))
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
// reset Frame of view to origin
self.bounds = tmp
self.frame = tmpFrame
_scrollView.setContentOffset(currentOffset, animated: false)
completionBlock?(image)
}
}
}

The key here is to let capture after webkit be allowed time to render after it is given a new frame. I tried didFinishLoading calback and WKWebView's wkWebView.takeSnapshot, but did not work.
So I introduced an a delay for the screenshot:
func createImage(webView: WKWebView, completion: #escaping (UIImage?) -> ()) {
// save the original size to restore
let originalFrame = webView.frame
let originalConstraints = webView.constraints
let originalScrollViewOffset = webView.scrollView.contentOffset
let newSize = webView.scrollView.contentSize
// remove any constraints for the web view, and set the size
// to be size of the content size (will be restored later)
webView.removeConstraints(originalConstraints)
webView.translatesAutoresizingMaskIntoConstraints = true
webView.frame = CGRect(origin: .zero, size: newSize)
webView.scrollView.contentOffset = .zero
// wait for a while for the webview to render in the newly set frame
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
defer {
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
if let context = UIGraphicsGetCurrentContext() {
// render the scroll view's layer
webView.scrollView.layer.render(in: context)
// restore the original state
webView.frame = originalFrame
webView.translatesAutoresizingMaskIntoConstraints = false
webView.addConstraints(originalConstraints)
webView.scrollView.contentOffset = originalScrollViewOffset
if let image = UIGraphicsGetImageFromCurrentImageContext() {
completion(image)
} else {
completion(nil)
}
}
}
}
Result:

- xcode 10 & swift 4.2 -
Use this;
// this is the button which takes the screenshot
#IBAction func snapShot(_ sender: Any) {
captureScreenshot()
}
// this is the function which is the handle screenshot functionality.
func captureScreenshot(){
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
// Creates UIImage of same size as view
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
layer.render(in: UIGraphicsGetCurrentContext()!)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// THIS IS TO SAVE SCREENSHOT TO PHOTOS
UIImageWriteToSavedPhotosAlbum(screenshot!, nil, nil, nil)
}
and you must add these keys in your info.plist;
key value = "Privacy - Photo Library Additions Usage Description" ,
key value = Privacy - Photo Library Usage Description

Here is an iOS 11+ example for snapshotting the WKWebView without the need of any delays to render the content.
The key is to make sure that you dont receive a blank snapshot. Initially we had issues with the output image being blank and having to introduce a delay to have the web content in the output image. I see the solution with the delay in many posts and I would recommend to not use it because it is an unnecessarily unreliable and unperformant solution. The important solution for us was to set the rect of WKSnapshotConfiguration and to use the JavaScript functions to wait for the rendering and also receiving the correct width and height.
Setting up the WKWebView:
// Important: You can set a width here which will also be reflected in the width of the snapshot later
let webView = WKWebView(frame: .zero)
webView.configuration.dataDetectorTypes = []
webView.navigationDelegate = self
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.loadHTMLString(htmlString, baseURL: Bundle.main.resourceURL)
Capturing the snapshot:
func webView(
_ webView: WKWebView,
didFinish navigation: WKNavigation!
) {
// Wait for the page to be rendered completely and get the final size from javascript
webView.evaluateJavaScript("document.readyState", completionHandler: { [weak self] (readyState, readyStateError) in
webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: {(contentWidth, widthError) in
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (contentHeight, heightError) in
guard readyState != nil,
let contentHeight = contentHeight as? CGFloat,
let contentWidth = contentWidth as? CGFloat else {
[Potential error handling...]
return
}
let rect = CGRect(
x: 0,
y: 0,
width: contentWidth,
height: contentHeight
)
let configuration = WKSnapshotConfiguration()
configuration.rect = rect
if #available(iOS 13.0, *) {
configuration.afterScreenUpdates = true
}
webView.takeSnapshot(with: configuration) { (snapshotImage, error) in
guard let snapshotImage = snapshotImage else {
[Potential error handling...]
}
[Do something with the image]
}
})
})
})
}

UPDATE: Not sure why Jason is sticking this answer all over the net. IT DOES NOT solve the problem of HOW to Screenshot the Full WKWebView content... including that off the screen.
Try this:
func snapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.webView.bounds.size, true, 0);
self.webView.drawViewHierarchyInRect(self.webView.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(snapshotImage, nil, nil, nil)
return snapshotImage
}
The image will automatically save into the iOS Camera Roll (by UIImageWriteToSavedPhotosAlbum()).

Related

Make a pdf document by Image using PDFKit in iOS 16

while we make a pdf using an image file at that we add an image into PDFPage. at that time PDFDocument did not show the original image content in PDFView in iOS 16. I checked the same code which worked properly in iOS 15. For both test cases, I used New Xcode 14, and as well as previously uploaded TestFlight build also worked properly with iOS 15 or below but did not work with iOS 16. So I request to the apple developer who worked on PDFkit please check this case. I may be sure that there is an issue with iOS 16 and I hope apple provides a new iOS 16 sub-version release as soon as possible with includes this bug fix. here is the sample code that I used:-
let images = [UIImage]()
let pdfDocument = PDFDocument()
for (index,image) in images.enumerated(){
// Create a PDF page instance from your image
let pdfPage = PDFPage(image: image)
// Insert the PDF page into your document
pdfDocument.insert(pdfPage!, at: index)
}
Original Image:
PDF preview in PDFView:
My guess is there's an issue with the default color space in PDFKit: RGB vs CMYK
Here's a workaround:
class func imageToPDF(_ image:UIImage) -> Data {
let data = NSMutableData()
let bounds = CGRect(origin: CGPoint.zero, size: image.size)
UIGraphicsBeginPDFContextToData(data, bounds, nil)
UIGraphicsBeginPDFPage()
image.draw(at: CGPoint.zero)
UIGraphicsEndPDFContext()
return data as Data
}
You'll have to alter it for multiple pages in your instance
Try to remove alpha channel from the image before inserting it into PDFPage. Below is an example how to remove alpha channel from UIImage. Tested with iOS 16.1.
extension UIImage {
func removeAlpha() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.opaque = true
format.scale = scale
return UIGraphicsImageRenderer(size: size, format: format).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
view.document = PDFDocument()
// {{ REMOVE ALPHA BEFORE INSERTING TO PDF
guard let page = PDFPage(image: image.removeAlpha()) else { return view }
// }}
view.document?.insert(page, at: 0)
view.pageBreakMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
view.minScaleFactor = 1
view.maxScaleFactor = 10
view.autoScales = true
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
}
}

Cropping AVCapturePhoto to overlay rectangle displayed on screen

I am trying to take a picture of a thin piece of metal, cropped to the outline displayed on the screen. I have seen almost every other post on here, but nothing has got it for me yet. This image will then be used for analysis by a library. I can get some cropping to happen, but never to the rectangle displayed. I have tried rotating the image before cropping, and calculating the rect based on the rectangle on screen.
Here is my capture code. PreviewView is the container, videoLayer is for the AVCapture video.
// Photo capture delegate
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let imgData = photo.fileDataRepresentation(), let uiImg = UIImage(data: imgData), let cgImg = uiImg.cgImage else {
return
}
print("Original image size: ", uiImg.size, "\nCGHeight: ", cgImg.height, " width: ", cgImg.width)
print("Orientation: ", uiImg.imageOrientation.rawValue)
guard let img = cropImage(image: uiImg) else {
return
}
showImage(image: img)
}
func cropImage(image: UIImage) -> UIImage? {
print("Image size before crop: ", image.size)
//Get the croppedRect from function below
let croppedRect = calculateRect(image: image)
guard let imgRet = image.cgImage?.cropping(to: croppedRect) else {
return nil
}
return UIImage(cgImage: imgRet)
}
func calculateRect(image: UIImage) -> CGRect {
let originalSize: CGSize
let visibleLayerFrame = self.rectangleView.bounds
// Calculate the rect from the rectangleview to translate to the image
let metaRect = (self.videoLayer.metadataOutputRectConverted(fromLayerRect: visibleLayerFrame))
print("MetaRect: ", metaRect)
// check orientation
if (image.imageOrientation == UIImage.Orientation.left || image.imageOrientation == UIImage.Orientation.right) {
originalSize = CGSize(width: image.size.height, height: image.size.width)
} else {
originalSize = image.size
}
let cropRect: CGRect = CGRect(x: metaRect.origin.x * originalSize.width, y: metaRect.origin.y * originalSize.height, width: metaRect.size.width * originalSize.width, height: metaRect.size.height * originalSize.height).integral
print("Calculated Rect: ", cropRect)
return cropRect
}
func showImage(image: UIImage) {
if takenImage != nil {
takenImage = nil
}
takenImage = UIImageView(image: image)
takenImage.frame = CGRect(x: 10, y: 50, width: 400, height: 1080)
takenImage.contentMode = .scaleAspectFit
print("Cropped Image Size: ", image.size)
self.previewView.addSubview(takenImage)
}
And here is along the line of what I keep getting.
What am I screwing up?
I managed to solve the issue for my use case.
private func cropToPreviewLayer(from originalImage: UIImage, toSizeOf rect: CGRect) -> UIImage? {
guard let cgImage = originalImage.cgImage else { return nil }
// This previewLayer is the AVCaptureVideoPreviewLayer which the resizeAspectFill and videoOrientation portrait has been set.
let outputRect = previewLayer.metadataOutputRectConverted(fromLayerRect: rect)
let width = CGFloat(cgImage.width)
let height = CGFloat(cgImage.height)
let cropRect = CGRect(x: (outputRect.origin.x * width), y: (outputRect.origin.y * height), width: (outputRect.size.width * width), height: (outputRect.size.height * height))
if let croppedCGImage = cgImage.cropping(to: cropRect) {
return UIImage(cgImage: croppedCGImage, scale: 1.0, orientation: originalImage.imageOrientation)
}
return nil
}
usage of the piece of code for my case:
let rect = CGRect(x: 25, y: 150, width: 325, height: 230)
let croppedImage = self.cropToPreviewLayer(from: image, toSizeOf: rect)
self.imageView.image = croppedImage
The world of UIKit has the TOP LEFT corner as 0,0.
The 0,0 point in the AVFoundation world is the BOTTOM LEFT corner.
So you have to translate by rotating 90 degrees.
That's why your image is bonkers.
Also remember that because of the origin translation the following rules apply:
X is actually up and down
Y is actually left and right
width and height are swapped
Also be aware that the UIImageView content mode setting WILL impact how your image scales. You might want to use .scaleAspectFill and NOT AspectFit if you really want to see how your image looks in the UIView.
I used this code snippet to see what was behind the curtain:
// figure out how to cut/crop this
let realImageRect = AVMakeRect(aspectRatio: image.size, insideRect: (self.cameraPreview?.frame)!)
NSLog("real image rectangle = \(realImageRect.debugDescription)")
The 'cameraPreview' reference above is the control you're using for your AV Capture Session.
Good luck!

Take a screenshot with background image - iOS

I want to take a screenshot. In this screenshot, I want the text and image in it. However, I am having an issue because when I take a screenshot, I only see the text but not the image.
I think the problem is that clearContainerView only contains the text but not the image. I can't put the image inside of clearContainerView because I want the image to stretch the entire screen... and I want the text centered between the title and tab bar (as shown with green square above).
My code and pictures are below:
This is my current layout in Storyboard:
This is what I want a screenshot of:
This is the screenshot that I get:
This is my code:
#IBOutlet weak var clearContainerView: UIView!
#IBAction func takeScreenshotTapped(_ sender: UIButton) {
let screenshot = clearContainerView.screenshot()
print(screenshot)
}
extension UIView {
func screenshot() -> UIImage {
let image = UIGraphicsImageRenderer(size: bounds.size).image { _ in
drawHierarchy(in: CGRect(origin: .zero, size: bounds.size), afterScreenUpdates: true)
}
return image
}
}
Any suggestions on how to do this?
You can use the following method on your controller view to get the portion of clearContainerView which will be a snapshot view. Then you can use that view object and take a screenshot of it.
resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets:
You have to pass the rect which will is your clearContainerView frame. You can pass zero insets in case you don't want any stretchable content. It return a view object which will contain your imageView portion + your complete clearContainerView. Then you can use the returned view and take its screen shot.
I tried with the following.
My original view.
The screenshot
Use this extension.
//USAGE
let image = self.view.snapshot(of:self.<your view>.frame)
Here "your view" should be the base view from the hierarchy or your can simply use
let image = self.view.snapshot(of:self.view.frame)
Extension
// UIView screenshot
extension UIView {
/// Create snapshot
///
/// - parameter rect: The `CGRect` of the portion of the view to return. If `nil` (or omitted),
/// return snapshot of the whole view.
///
/// - returns: Returns `UIImage` of the specified portion of the view.
func snapshot(of rect: CGRect? = nil) -> UIImage? {
// snapshot entire view
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
drawHierarchy(in: bounds, afterScreenUpdates: true)
let wholeImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// if no `rect` provided, return image of whole view
guard let image = wholeImage, let rect = rect else { return wholeImage }
// otherwise, grab specified `rect` of image
let scale = image.scale
let scaledRect = CGRect(x: rect.origin.x * scale, y: rect.origin.y * scale, width: rect.size.width * scale, height: rect.size.height * scale)
guard let cgImage = image.cgImage?.cropping(to: scaledRect) else { return nil }
return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
}
}
Main Code:
let frame = containerView.frame
let x: CGFloat = 0
let y = frame.minY.pointsToPixel()
let width = frame.width.pointsToPixel()
let height = frame.height.pointsToPixel()
let rect = CGRect(x: x, y: y, width: width, height: height)
let image = cropImage(image: view.screenshot(), toRect: rect)
extension UIView {
func screenshot() -> UIImage {
let image = UIGraphicsImageRenderer(size: bounds.size).image { _ in
drawHierarchy(in: CGRect(origin: .zero, size: bounds.size), afterScreenUpdates: true)
}
return image
}
}
public extension CGFloat {
func pointsToPixel() -> CGFloat {
return self * UIScreen.main.scale
}
}
output:
After Screenshot:
What I've done: take a screenshot of the whole view with your method and then crop the image by converting CGPoints to pixels.
You can use the code given below to capture the screenshot. It will capture the whole window not the particular view. But take care of one thing that if you don't want your "Title", "Button" and "Tab Bar" in the screenshot then you need to hide them before the UIGraphicsBeginImageContextWithOptions and show them again after UIGraphicsEndImageContext.
func takeScreenshot() -> UIImage? {
var screenshotImage: UIImage?
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
// Hide your title label, button and tab bar here
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
guard let context = UIGraphicsGetCurrentContext() else {return nil}
layer.render(in:context)
screenshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Unhide your title label, button and tab bar here
return screenshotImage
}
#IBAction func takeScreenshotTapped(_ sender: UIButton) {
let screenshot = takeScreenshot()
print(screenshot)
}
Everything is fine. I have made a demo for your expected output.
You just need to change your view hierarchy like this:
As pr you say the image if outside of clearContainerView
Code
class VC: UIViewController {
#IBOutlet weak var mainVw: UIView!
#IBOutlet weak var clearContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func takeImage(_ sender: Any) {
if let VC_1 = self.storyboard?.instantiateViewController(withIdentifier: "VC1") as? VC1{
VC_1.img = mainVw.screenshot()
self.present(VC_1, animated: false, completion: nil)
}
}
}
Output :
Try this!
Helper Function
struct UIGraphicsDrawImageHelper {
static func drawImage(from image: UIImageView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(image.bounds.size, image.isOpaque, 0.0)
image.drawHierarchy(in: image.bounds, afterScreenUpdates: false)
let renderImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return renderImage
}
}
calling it
guard let image = UIGraphicsDrawImageHelper.drawImage(from: qrcodeImageView) else { return }
ImageView (Clear Container View) is the view that you want to snapshot your screen. you can change it to uiview or whatever is view.
Hope this help.

I don't know how to send screenshot to Instagram application in Swift

I am trying to implement image sharing to Instagram app using Swift.
I do know how to take screenshot but I am unable to get how to share the screenshot to Instagram application.
Here is my snapshot code:
_ = UIScreen.main
if UIApplication.shared.keyWindow != nil {
let top: CGFloat = 101
let bottom: CGFloat = 96
// The size of the cropped image
let size = CGSize(width: view.frame.size.width, height: view.frame.size.height - top - bottom)
// Start the context
UIGraphicsBeginImageContext(size)
// we are going to use context in a couple of places
let context = UIGraphicsGetCurrentContext()!
// Transform the context so that anything drawn into it is displaced "top" pixels up
// Something drawn at coordinate (0, 0) will now be drawn at (0, -top)
// This will result in the "top" pixels being cut off
// The bottom pixels are cut off because the size of the of the context
context.translateBy(x: 0, y: 0)
// Draw the view into the context (this is the snapshot)
UIGraphicsBeginImageContextWithOptions(size,view.isOpaque, 0)
self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
// End the context (this is required to not leak resources)
UIGraphicsEndImageContext()
Swift 3.x code
You can take screenshot by passing the whichever view you like:
func getScreenshot(viewToSnapShot:UIView) -> UIImage {
UIGraphicsBeginImageContext(viewToSnapShot.frame.size)
viewToSnapShot.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
and grab the documentDirectory path too. I made a simple function that will return the document directory path.
func documentsDirectory() -> String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
}
Now you can share the image on instagram using the UIDocumentInteractionController
In your ViewController import this:
import Social
//make UIDocumentInteractionController object
var docController:UIDocumentInteractionController!
And then call this function whenever you want to post screenshot/image on instagram
// On calling this function, will open your document controller
func postImageToInstagram() {
let instagram = URL(string: "instagram://app")!
if UIApplication.shared.canOpenURL(instagram) {
//By Passing self.view, I am actually taking the screenshot
let imageToBePosted = getScreenshot(viewToSnapShot: self.view)
let imageURL = URL(fileURLWithPath: documentsDirectory())
let fullPath = imageURL.appendingPathComponent("instagram.igo")
do {
try UIImagePNGRepresentation(imageToBePosted)!.write(to: fullPath)
let rect = CGRect(x: 0, y: 0, width: 0, height: 0)
let igImageHookFile = URL(string: "file://\(fullPath)")
docController = UIDocumentInteractionController(url: igImageHookFile!)
docController.uti = "com.instagram.photo"
docController.presentOpenInMenu(from: rect, in: self.view, animated: true)
} catch {
print(error)
}
} else {
print("Instagram not installed")
}
}
Tested on Xcode 8.1, iOS 10.1 device. Worked Fine.

How to convert a UIView to an image

I want to convert a UIView to an image and save it in my app. Can someone please tell me how to take screenshot of a view or convert it to an image and what is the best way to save it in an app (Not camera roll)? Here is the code for the view:
var overView = UIView(frame: CGRectMake(0, 0, self.view.frame.width/1.3, self.view.frame.height/1.3))
overView.center = CGPointMake(CGRectGetMidX(self.view.bounds),
CGRectGetMidY(self.view.bounds)-self.view.frame.height/16);
overView.backgroundColor = UIColor.whiteColor()
self.view.addSubview(overView)
self.view.bringSubviewToFront(overView)
An extension on UIView should do the trick.
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
Apple discourages using UIGraphicsBeginImageContext starting iOS 10 with the introduction of the P3 color gamut. UIGraphicsBeginImageContext is sRGB and 32-bit only. They introduced the new UIGraphicsImageRenderer API that is fully color managed, block-based, has subclasses for PDFs and images, and automatically manages the context lifetime. Check out WWDC16 session 205 for more details (image rendering begins around the 11:50 mark)
To be sure that it works on every device, use #available with a fallback to earlier versions of iOS:
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(self.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}
you can use extension
extension UIImage {
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: image!.cgImage!)
}
}
Convert your UIView to image by drawViewHierarchyInRect:afterScreenUpdates: which is many times faster than renderInContext
Important note: do not call this function from viewDidLoad or viewWillAppear , make sure you are capturing a view after it is it displayed /loaded fully
Obj C
UIGraphicsBeginImageContextWithOptions(myView.bounds.size, myView.opaque, 0.0f);
[myView drawViewHierarchyInRect:myView.bounds afterScreenUpdates:NO];
UIImage *snapshotImageFromMyView = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
myImageView.image = snapshotImageFromMyView;
Save the edited image Photo album
UIImageWriteToSavedPhotosAlbum(snapshotImageFromMyView, nil,nil, nil);
Swift 3/4
UIGraphicsBeginImageContextWithOptions(myView.bounds.size, myView.isOpaque, 0.0)
myView.drawHierarchy(in: myView.bounds, afterScreenUpdates: false)
let snapshotImageFromMyView = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
print(snapshotImageFromMyView)
myImageView.image = snapshotImageFromMyView
Super easy generalization with extension , iOS11 , swift3/4
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
Use :
//myView is completly loaded/visible , calling this code after only after viewDidAppear is call
imgVV.image = UIImage.init(view: myView)
// Simple image object
let img = UIImage.init(view: myView)
On iOS 10:
extension UIImage {
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
Best practice as of iOS 10 and Swift 3
while still supporting iOS 9 and earlier
still works as of iOS 13, Xcode 11.1, Swift 5.1
extension UIView {
func asImage() -> UIImage? {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0)
defer { UIGraphicsEndImageContext() }
guard let currentContext = UIGraphicsGetCurrentContext() else {
return nil
}
self.layer.render(in: currentContext)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
}
I am unsure what the question means by:
what is the best way to save it in an app (Not camera roll)?
UIGraphicsBeginImageContext(self.view.bounds.size);
self.view.layer.renderInContext(UIGraphicsGetCurrentContext())
var screenShot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
For example if I have a view of size: 50 50 at 100,100. I can use the following to take a screenshot:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), false, 0);
self.view.drawViewHierarchyInRect(CGRectMake(-50,-5-,view.bounds.size.width,view.bounds.size.height), afterScreenUpdates: true)
var image:UIImage = UIGraphicsGetImageFromCurrentImageContext();
In my opinion, the approach with the initialiser isn't that great because it creates two images.
I prefer this:
extension UIView {
var snapshot: UIImage? {
UIGraphicsBeginImageContext(self.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Swift 4.2, iOS 10
extension UIView {
// If Swift version is lower than 4.2,
// You should change the name. (ex. var renderedImage: UIImage?)
var image: UIImage? {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in layer.render(in: rendererContext.cgContext) }
}
}
Sample
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = .blue
let view2 = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
view2.backgroundColor = .red
view.addSubview(view2)
let imageView = UIImageView(image: view.image)
This works for me for Xcode 9/Swift 3.2/Swift 4 and Xcode 8/Swift 3
if #available(iOS 10.0, *) {
// for Xcode 9/Swift 3.2/Swift 4 -Paul Hudson's code
let renderer = UIGraphicsImageRenderer(size: view!.bounds.size)
let capturedImage = renderer.image {
(ctx) in
view!.drawHierarchy(in: view!.bounds, afterScreenUpdates: true)
}
return capturedImage
} else {
// for Xcode 8/Swift 3
UIGraphicsBeginImageContextWithOptions((view!.bounds.size), view!.isOpaque, 0.0)
view!.drawHierarchy(in: view!.bounds, afterScreenUpdates: false)
let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return capturedImage!
}
Here's how to use it inside a function:
fileprivate func captureUIImageFromUIView(_ view:UIView?) -> UIImage {
guard (view != nil) else{
// if the view is nil (it's happened to me) return an alternative image
let errorImage = UIImage(named: "Error Image")
return errorImage
}
// if the view is all good then convert the image inside the view to a uiimage
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(size: view!.bounds.size)
let capturedImage = renderer.image {
(ctx) in
view!.drawHierarchy(in: view!.bounds, afterScreenUpdates: true)
}
return capturedImage
} else {
UIGraphicsBeginImageContextWithOptions((view!.bounds.size), view!.isOpaque, 0.0)
view!.drawHierarchy(in: view!.bounds, afterScreenUpdates: false)
let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return capturedImage!
}
}
Here's how to do something with the image returned from the function:
#IBOutlet weak fileprivate var myCustomView: UIView!
var myPic: UIImage?
let myImageView = UIImageView()
#IBAction fileprivate func saveImageButtonTapped(_ sender: UIButton) {
myPic = captureUIImageFromUIView(myCustomView)
// display the pic inside a UIImageView
myImageView.image = myPic!
}
I got the Xcode 9/Swift 3.2/Swift 4 answer from Paul Hudson convert uiview to uiimage
I got the Xcode 8/Swift 3 from somewhere on SO a longgg time ago and I forgot where :(
var snapshot = overView.snapshotViewAfterScreenUpdates(false)
or in objective-c
UIView* snapshot = [overView snapshotViewAfterScreenUpdates:NO];
You can use it easily by using the extension like this
// Take a snapshot from a view (just one view)
let viewSnapshot = myView.snapshot
// Take a screenshot (with every views in screen)
let screenSnapshot = UIApplication.shared.snapshot
// Take a snapshot from UIImage initialization
UIImage(view: self.view)
If you wanna use those extension method/variables, implement this
UIImage extension
extension UIImage {
convenience init(view: UIView) {
if let cgImage = view.snapshot?.cgImage {
self.init(cgImage: cgImage)
} else {
self.init()
}
}
}
UIView extension
extension UIView {
var snapshot: UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0.0)
if UIGraphicsGetCurrentContext() != nil {
drawHierarchy(in: bounds, afterScreenUpdates: true)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot
}
return nil
}
}
UIApplication extension
extension UIApplication {
var snapshot: UIImage? {
return keyWindow?.rootViewController?.view.snapshot
}
}
or iOS 10+ you can use the new UIGraphicsImageRenderer + the recommended drawHierarchy, which in some situations can be much faster than layer.renderInContext
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
return renderer.image { _ in
self.drawHierarchy(in: CGRect(x: 0, y: 0, width: bounds.size.width, height: bounds.size.height), afterScreenUpdates: false)
}
}
}
Thanks #Bao Tuan Diep ! I want add a supplement.
When you use the code:
yourView.layer.render(in:UIGraphicsGetCurrentContext()!)
You must notice that:
- If you had used `autoLayout` or `Masonry` in `yourView` (that you want to convert) .
- If you did not add `yourView` to another view which means that `yourView` was not used as a subview but just an object.
Then, your must use :
[yourView setNeedsLayout];
[yourView layoutIfNeeded];
to update yourView before yourView.layer.render(in:UIGraphicsGetCurrentContext()!).
Otherwise you may get an image object that contains no elements
Swift 4.2
import Foundation
import UIKit
extension UIImage {
convenience init(view: UIView) {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
using:
let img = UIImage.init(view: self.holderView)
Implementation in Swift 3 :
Add the code below , out of class scope .
extension UIImage {
convenience init(_ view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
Usage :
let image = UIImage( Your_View_Outlet )
Resorting to an extension, as some have suggested, might be okay for the common case yet not ideal in every application, depending on how one is drawing the view, i.e. as layers, or trying to display a view to its subviews.
If one is creating an image of the subview hierarchy:
let renderer = UIGraphicsImageRenderer(bounds: view.bounds)
let image = renderer.image { ctx in
self.drawHierarchy(in: self.bounds, afterScreenUpdates:
However, if your view consists of sublayers, for example CALayer, including CAShapeLayer comprised of UIBezierCurve... Something more like this may be in order:
let renderer = UIGraphicsImageRenderer(bounds: view.bounds)
let image = renderer.image { rendererContext in
for sublayer in self.layer.sublayers!.reversed() {
sublayer.render(in: rendererContext.cgContext)
}
}
And of course one might want to display a view with subviews which are layered. And since UIView.drawHierarchy() doesn't include sublayers... well, it could get more complicated
I use this for small images (because it does not work for big images):
extension UIView {
public func drawHierarchyAsImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let image = renderer.image { _ in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
For high resolution images i use this:
extension UIView {
public func renderAsImage(scale: CGFloat) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = scale
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
return renderer.image { rendererContext in
if let presentation = layer.presentation() {
presentation.render(in: rendererContext.cgContext)
}
}
}
}
In my view hierarchy i have rotated views and thats why i use the presentation layer as mentioned here:
renderInContext does not capture rotated subviews
I implemented #Naveed J.'s method like this, and it worked like a charm.
Here was his extentsion:
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
Here is how I implemented it.
//create an image from yourView to display
//determine the frame of the view/imageimage
let screen = self.superview!.bounds
let width = screen.width / 4 //make image 1/4 width of screen
let height = width
let frame = CGRect(x: 0, y: 0, width: width, height: height)
let x = (screen.size.width - frame.size.width) * 0.5
let y = (screen.size.height - frame.size.height) * 0.5
let mainFrame = CGRect(x: x, y: y, width: frame.size.width, height: frame.size.height)
let yourView = YourView() //instantiate yourView
yourView.frame = mainFrame //give it the frame
yourView.setNeedsDisplay() //tell it to display (I am not 100% sure this is needed)
let characterViewImage = yourView.asImage()
Initializer with the new UIGraphicsImageRenderer available since iOS 10:
extension UIImage{
convenience init(view: UIView) {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let canvas = CGRect(x: 0, y: 0, width: bounds.size.width, height: bounds.size.height)
let image = renderer.image { _ in
self.drawHierarchy(in: canvas, afterScreenUpdates: false)
}
self.init(cgImage: (image?.cgImage)!)
}
}
work fine with me !
Swift4
extension UIView {
func toImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0)
self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
let snapshotImageFromMyView = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return snapshotImageFromMyView!
}
}
For view contains blurred subview (e.g. UIVisualEffectView instance), only drawViewHierarchyInRect:afterScreenUpdates works.
#ViJay Avhad's answer is correct for this case.
Using UIGraphicsImageRenderer doesn't work when the view contains Scene Kit subviews, Metal or Sprite Kit ones. In these cases, use
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
please try below code.
-(UIImage *)getMainImageFromContext
{
UIGraphicsBeginImageContextWithOptions(viewBG.bounds.size, viewBG.opaque, 0.0);
[viewBG.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}

Resources