How to get entire snapshot of scrollview in iOS 13? - swift

Note: iOS 13
For below code:
extension UIScrollView {
var snapshot: UIImage? {
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0)
defer {
UIGraphicsEndImageContext()
}
guard let context = UIGraphicsGetCurrentContext() else { return nil }
let previousFrame = frame
let previousOffset = contentOffset
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
contentOffset = CGPoint.zero
layer.render(in: context)
frame = previousFrame
contentOffset = previousOffset
return UIGraphicsGetImageFromCurrentImageContext()
}
}
It did work perfect in iOS 12, however, only snapshot part of (visible area) scrollView in iOS 13.
I want to snapshot total scrollView.Any way to solve it?

The key to solving the problem is to removeFromSuperview at first :
private func snapshot() -> UIImage? {
// remove first
scrollView.removeFromSuperview()
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, false, 0)
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()
// add again
self.view.addSubview(scrollView)
scrollView.snp.remakeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalTo(naviHeight)
make.bottom.equalTo(-safeBottomHeight-105)
}
return image
}
Interesting...

Related

Take snapshot from UIView with lower resolution

I'm taking snapshot from a PDFView in PDFKit for streaming (20 times per sec), and I use this extesnsion
extension UIView {
func asImageBackground(viewLayer: CALayer, viewBounds: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: viewBounds)
return renderer.image { rendererContext in
viewLayer.render(in: rendererContext.cgContext)
}
}
}
But the output UIImage from this extension has a high resolution which make it difficult to stream. I can reduce it by this extension
extension UIImage {
func resize(_ max_size: CGFloat) -> UIImage {
// adjust for device pixel density
let max_size_pixels = max_size / UIScreen.main.scale
// work out aspect ratio
let aspectRatio = size.width/size.height
// variables for storing calculated data
var width: CGFloat
var height: CGFloat
var newImage: UIImage
if aspectRatio > 1 {
// landscape
width = max_size_pixels
height = max_size_pixels / aspectRatio
} else {
// portrait
height = max_size_pixels
width = max_size_pixels * aspectRatio
}
// create an image renderer of the correct size
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: UIGraphicsImageRendererFormat.default())
// render the image
newImage = renderer.image {
(context) in
self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
}
// return the image
return newImage
}
}
but it add an additional workload which make the process even worse. Is there any better way?
Thanks
You can downsample it using ImageIO which is recommended by Apple:
extension UIImage {
func downsample(to resolution: CGSize) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let data = self.jpegData(compressionQuality: 0.75) as? CFData, let imageSource = CGImageSourceCreateWithData(data, imageSourceOptions) else {
return nil
}
let maxDimensionInPixels = Swift.max(resolution.width, resolution.height) * 3
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}
}

How to increase quality of rendered image for UIImageView?

I have an UIImageView with contentMode = .aspectFit. I have an image in imageView, which dimension is bigger than size of imageView. User can draw some lines and save them as sublayer. After that I need to save the edited image. But quality of saved image is worse than quality of image which I load.
What am I doing wrong? I tried to use transform, but it didn't work.
import UIKit
extension UIImageView {
var contentClippingRect: CGRect {
let imgViewSize = self.frame.size
let imgSize = self.image?.size ?? .zero
let scaleW = imgViewSize.width / imgSize.width
let scaleH = imgViewSize.height / imgSize.height
let aspect = fmin(scaleW, scaleH)
let width = imgSize.width * aspect
let height = imgSize.height * aspect
let imageRect = CGRect(x: (imgViewSize.width-width)/2 + self.frame.origin.x, y: (imgViewSize.height-height)/2 + self.frame.origin.y, width: width, height: height)
return imageRect
}
func asImage() -> UIImage {
let imageRect = self.contentClippingRect
let renderer = UIGraphicsImageRenderer(bounds: imageRect)
let renderedImage = renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
return renderedImage
}
}
You can use UIGraphicsImageRendererFormat().scale it will increase quality of the rendered image a bit.
import UIKit
extension UIImageView {
var contentClippingRect: CGRect {
let imgViewSize = self.frame.size
let imgSize = self.image?.size ?? .zero
let scaleW = imgViewSize.width / imgSize.width
let scaleH = imgViewSize.height / imgSize.height
let aspect = fmin(scaleW, scaleH)
let width = imgSize.width * aspect
let height = imgSize.height * aspect
let imageRect = CGRect(x: (imgViewSize.width-width)/2 + self.frame.origin.x, y: (imgViewSize.height-height)/2 + self.frame.origin.y, width: width, height: height)
return imageRect
}
func asImage() -> UIImage {
let imageRect = self.contentClippingRect
//add this
let format = UIGraphicsImageRendererFormat()
format.scale = 2
let renderer = UIGraphicsImageRenderer(bounds: imageRect, format: format)
let renderedImage = renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
return renderedImage
}
}

How to dispose from UIView subviews on one ViewController when navigating away from it

I'm starting to learn Swift and decided to build an app but without storyboard.
My SceneDelegate scene function instantiates a TabBarController
window = UIWindow(frame: UIScreen.main.bounds)
let tb = TabBarController()
window?.rootViewController = tb
window?.makeKeyAndVisible()
window?.windowScene = windowSceme
I have a TabBarController that extends from UITabBarController which pretty much styles the tab bar and sets it all up
For each item of the Tabbar I have a ViewController. For the purpose of this question I'm going to specify the first "Home".
In Home, which extends ViewController, I have the following
let homePageView = HomePageView()
override func viewWillLayoutSubviews() {
navigationController?.isNavigationBarHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
homePageView.setupHomePage()
view.addSubview(homePageView)
}
override func viewWillDisappear(_ animated: Bool) {
homePageView.dispose()
}
The Controller is pretty much only in charge of calling whatever is going to be displayed on the screen within HomePageView(), the main component. This last, holds two more views. One carrousel HomePageCarrousel() and one header HomePageSocialUp(). This view instantiates the two latter referred to and sets the layout programmatically and adds them as subviews. It also includes a dispose function that sets the instantiated classes to nil such as --> Instantiate - homePageCarrousel = HomePageCarrousel() and the dispose function has homePageCarrousel = nil
That works perfectly fine but when I navigate away from the current view controller via the tab bar and navigate back to it, now I have two instances of HomePageCarrousel() and HomePageSocialUp() within HomeView
I'm probably holding a strong reference somewhere but I can't figure out when. Could someone point me to where should I look to debug it or what might be that is creating the issue.
I'm also providing the code for the two views duplicated in case the issue is there
HomePageSocialUp
class HomePageSocialUp: UIView {
let logo = UIImage(named: "LogoSmiles")
let socialImages: [UIImage] = [UIImage(named: "tripadvisor")!, UIImage(named: "instagram")!, UIImage(named: "facebook")!, UIImage(named: "linkedin")!]
func setupHeaderCircle() {
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
let circlePath = UIBezierPath(arcCenter: CGPoint(x: UIScreen.main.bounds.width / 2, y: insetTop + 60), radius: CGFloat(90), startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
layer.addSublayer(shapeLayer)
}
func setupSocialHeader() {
setupHeaderCircle()
layer.masksToBounds = true;
backgroundColor = UIColor.TintColor
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOpacity = 0.8
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = 3.0
layer.masksToBounds = false
frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 280)
let imageView = UIImageView()
guard let logo = logo else {return}
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
imageView.frame = CGRect(x: CGFloat(Int((UIScreen.main.bounds.width - (logo.size.width)))) / 2, y: 120 - (logo.size.width / 2), width: logo.size.width, height: logo.size.height)
imageView.image = logo
addSubview(imageView)
}
}
HomePageCarrousel
class HomePageCarrousel: UIScrollView {
var images: [UIImage]?
var originX = 0
var numberOfIterations = 0
var timer: Timer?
func setupCarrousel() {
let x = (Int(UIScreen.main.bounds.width) - (Int(UIScreen.main.bounds.width) - 60)) / 2
frame = CGRect(x: x, y: (Int(frame.origin.y) - 350) / 2, width: Int(UIScreen.main.bounds.width) - 60, height: 350)
let newImage = textToImage(drawText: "Creating Smiles in unique places.", frame: frame, inImage: UIImage(named: "smiles1")!, atPoint: CGPoint(x: frame.origin.x + 20, y: frame.height - 20))
images = [newImage, UIImage(named: "smiles2")!]
guard timer == nil else { return }
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(startScrolling), userInfo: nil, repeats: true)
guard let imageCount = images?.count, let images = images else { return }
contentSize = CGSize(width: frame.width * CGFloat(images.count), height: frame.height)
for image in 0..<imageCount {
let imageView = UIImageView()
imageView.image = images[image]
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let xPosition = frame.width * CGFloat(image)
imageView.frame = CGRect(x: xPosition, y: 0, width: frame.width, height: frame.height)
addSubview(imageView)
}
timer?.fire()
}
func textToImage(drawText text: String, frame: CGRect, inImage image: UIImage, atPoint point: CGPoint) -> UIImage {
let textColor = UIColor.white
let textFont = UIFont(name: "Helvetica Bold" , size: 12)!
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(image.size, false, scale)
let textFontAttributes = [
NSAttributedString.Key.font: textFont,
NSAttributedString.Key.foregroundColor: textColor,
] as [NSAttributedString.Key : Any]
image.draw(in: CGRect(origin: CGPoint.zero, size: image.size))
text.draw(in: frame, withAttributes: textFontAttributes)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
#objc func startScrolling() {
print(originX)
guard let images = images else { return }
if originX == images.count {
originX = 0
numberOfIterations += 1
}
if numberOfIterations > 2 {
timer?.invalidate()
timer = nil
numberOfIterations = 0
}
let x = CGFloat(originX) * frame.size.width
setContentOffset(CGPoint(x: x, y: 0), animated: true)
originX += 1
}
}
Thanks upfront
Did you tried to remove the subviews, something like
homePageView.subviews.forEach { (view) in
//taking appropriate action to whichever view you want
view.removeFromSuperview()//for removing views
}

Trying to change the frame of UIView in swift whilst the app is running?

I am trying to change the frame of a UIView when the orientation of my device is changed but nothing appears to work. Here is my code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard
let portraitScreenView = viewScreenSizePortrait,
let overLay = overLayView else{return}
if UIDevice.current.orientation.isPortrait{
print("isPortrait")
overLay.frame = CGRect.init(x: 0, y: 0, width: portraitScreenView.width, height: portraitScreenView.height)
self.overLayView.frame = overLay.frame
}else{
print("isLandscape")
overLay.frame = CGRect.init(x: 0, y: 0, width: portraitScreenView.height, height: portraitScreenView.width)
self.overLayView.frame = overLay.frame
}
print("self.overLayView.frame: \(self.overLayView.frame)")
}
Add self.setNeedsDisplay() after your frame change.

Take a Screenshot of the entire contents of a UICollectionView

The app I'm working on uses collection view cells to display data to the user. I want the user to be able to share the data that's contained in the cells, but there are usually too many cells to try to re-size and fit onto a single iPhone-screen-sized window and get a screenshot.
So the problem I'm having is trying to get an image of all the cells in a collection view, both on-screen and off-screen. I'm aware that off-screen cells don't actually exist, but I'd be interested in a way to kind of fake an image and draw in the data (if that's possible in swift).
In short, is there a way to programmatically create an image from a collection view and the cells it contains, both on and off screen with Swift?
Update
If memory is not a concern :
mutating func screenshot(scale: CGFloat) -> UIImage {
let currentSize = frame.size
let currentOffset = contentOffset // temp store current offset
frame.size = contentSize
setContentOffset(CGPointZero, animated: false)
// it might need a delay here to allow loading data.
let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
frame.size = currentSize
setContentOffset(currentOffset, animated: false)
return resizeUIImage(image, scale: scale)
}
This works for me:
github link -> contains up to date code
getScreenshotRects creates the offsets to which to scroll and the frames to capture. (naming is not perfect)
takeScreenshotAtPoint scrolls to the point, sets a delay to allow a redraw, takes the screenshot and returns this via completion handler.
stitchImages creates a rect with the same size as the content and draws all images in them.
makeScreenshots uses the didSet on a nested array of UIImage and a counter to create all images while also waiting for completion. When this is done it fires it own completion handler.
Basic parts :
scroll collectionview -> works
take screenshot with delay for a redraw -> works
crop images that are overlapping -> apparently not needed
stitch all images -> works
basic math -> works
maybe freeze screen or hide when all this is happening (this is not in my answer)
Code :
protocol ScrollViewImager {
var bounds : CGRect { get }
var contentSize : CGSize { get }
var contentOffset : CGPoint { get }
func setContentOffset(contentOffset: CGPoint, animated: Bool)
func drawViewHierarchyInRect(rect: CGRect, afterScreenUpdates: Bool) -> Bool
}
extension ScrollViewImager {
func screenshot(completion: (screenshot: UIImage) -> Void) {
let pointsAndFrames = getScreenshotRects()
let points = pointsAndFrames.points
let frames = pointsAndFrames.frames
makeScreenshots(points, frames: frames) { (screenshots) -> Void in
let stitched = self.stitchImages(images: screenshots, finalSize: self.contentSize)
completion(screenshot: stitched!)
}
}
private func makeScreenshots(points:[[CGPoint]], frames : [[CGRect]],completion: (screenshots: [[UIImage]]) -> Void) {
var counter : Int = 0
var images : [[UIImage]] = [] {
didSet {
if counter < points.count {
makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
} else {
completion(screenshots: images)
}
}
}
makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
private func makeScreenshotRow(points:[CGPoint], frames : [CGRect],completion: (screenshots: [UIImage]) -> Void) {
var counter : Int = 0
var images : [UIImage] = [] {
didSet {
if counter < points.count {
takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
} else {
completion(screenshots: images)
}
}
}
takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
private func getScreenshotRects() -> (points:[[CGPoint]], frames:[[CGRect]]) {
let vanillaBounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
let xPartial = contentSize.width % bounds.size.width
let yPartial = contentSize.height % bounds.size.height
let xSlices = Int((contentSize.width - xPartial) / bounds.size.width)
let ySlices = Int((contentSize.height - yPartial) / bounds.size.height)
var currentOffset = CGPoint(x: 0, y: 0)
var offsets : [[CGPoint]] = []
var rects : [[CGRect]] = []
var xSlicesWithPartial : Int = xSlices
if xPartial > 0 {
xSlicesWithPartial += 1
}
var ySlicesWithPartial : Int = ySlices
if yPartial > 0 {
ySlicesWithPartial += 1
}
for y in 0..<ySlicesWithPartial {
var offsetRow : [CGPoint] = []
var rectRow : [CGRect] = []
currentOffset.x = 0
for x in 0..<xSlicesWithPartial {
if y == ySlices && x == xSlices {
let rect = CGRect(x: bounds.width - xPartial, y: bounds.height - yPartial, width: xPartial, height: yPartial)
rectRow.append(rect)
} else if y == ySlices {
let rect = CGRect(x: 0, y: bounds.height - yPartial, width: bounds.width, height: yPartial)
rectRow.append(rect)
} else if x == xSlices {
let rect = CGRect(x: bounds.width - xPartial, y: 0, width: xPartial, height: bounds.height)
rectRow.append(rect)
} else {
rectRow.append(vanillaBounds)
}
offsetRow.append(currentOffset)
if x == xSlices {
currentOffset.x = contentSize.width - bounds.size.width
} else {
currentOffset.x = currentOffset.x + bounds.size.width
}
}
if y == ySlices {
currentOffset.y = contentSize.height - bounds.size.height
} else {
currentOffset.y = currentOffset.y + bounds.size.height
}
offsets.append(offsetRow)
rects.append(rectRow)
}
return (points:offsets, frames:rects)
}
private func takeScreenshotAtPoint(point point_I: CGPoint, completion: (screenshot: UIImage) -> Void) {
let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
let currentOffset = contentOffset
setContentOffset(point_I, animated: false)
delay(0.001) {
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setContentOffset(currentOffset, animated: false)
completion(screenshot: image)
}
}
private func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
private func crop(image image_I:UIImage, toRect rect:CGRect) -> UIImage? {
guard let imageRef: CGImageRef = CGImageCreateWithImageInRect(image_I.CGImage, rect) else {
return nil
}
return UIImage(CGImage:imageRef)
}
private func stitchImages(images images_I: [[UIImage]], finalSize : CGSize) -> UIImage? {
let finalRect = CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)
guard images_I.count > 0 else {
return nil
}
UIGraphicsBeginImageContext(finalRect.size)
var offsetY : CGFloat = 0
for imageRow in images_I {
var offsetX : CGFloat = 0
for image in imageRow {
let width = image.size.width
let height = image.size.height
let rect = CGRect(x: offsetX, y: offsetY, width: width, height: height)
image.drawInRect(rect)
offsetX += width
}
offsetX = 0
if let firstimage = imageRow.first {
offsetY += firstimage.size.height
} // maybe add error handling here
}
let stitchedImages = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return stitchedImages
}
}
extension UIScrollView : ScrollViewImager {
}
Draw the bitmap data of your UICollectionView into a UIImage using UIKit graphics functions. Then you'll have a UIImage that you could save to disk or do whatever you need with it. Something like this should work:
// your collection view
#IBOutlet weak var myCollectionView: UICollectionView!
//...
let image: UIImage!
// draw your UICollectionView into a UIImage
UIGraphicsBeginImageContext(myCollectionView.frame.size)
myCollectionView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
For swift 4 to make screenshot of UICollectionView
func makeScreenShotToShare()-> UIImage{
UIGraphicsBeginImageContextWithOptions(CGSize.init(width: self.colHistory.contentSize.width, height: self.colHistory.contentSize.height + 84.0), false, 0)
colHistory.scrollToItem(at: IndexPath.init(row: 0, section: 0), at: .top, animated: false)
colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
let row = colHistory.numberOfItems(inSection: 0)
let numberofRowthatShowinscreen = self.colHistory.size.height / (self.arrHistoryData.count == 1 ? 130 : 220)
let scrollCount = row / Int(numberofRowthatShowinscreen)
for i in 0..<scrollCount {
colHistory.scrollToItem(at: IndexPath.init(row: (i+1)*Int(numberofRowthatShowinscreen), section: 0), at: .top, animated: false)
colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
}
let image:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
return image
}