Render SwiftUI view offscreen and save view as UIImage to share - swift

I'm trying to create a share button with SwiftUI that when pressed can share a generated image. I've found some tutorials that can screen shot a current displayed view and convert it to an UIImage. But I want to create a view programmatically off the screen and then save that to a UIImage that users can then share with a share sheet.
import SwiftUI
import SwiftyJSON
import MapKit
struct ShareRentalView : View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 32.786038, longitude: -117.237324) , span: MKCoordinateSpan(latitudeDelta: 0.025, longitudeDelta: 0.025))
#State var coordinates: [JSON] = []
#State var origin: CGPoint? = nil
#State var size: CGSize? = nil
var body: some View {
GeometryReader{ geometry in
VStack(spacing: 0) {
ZStack{
HistoryMapView(region: region, pointsArray: $coordinates)
.frame(height: 300)
}.frame(height: 300)
}.onAppear {
self.origin = geometry.frame(in: .global).origin
self.size = geometry.size
}
}
}
func returnScreenShot() -> UIImage{
return takeScreenshot(origin: self.origin.unsafelyUnwrapped, size: self.size.unsafelyUnwrapped)
}
}
extension UIView {
var renderedImage: UIImage {
// rect of capure
let rect = self.bounds
// create the context of bitmap
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
// get a image from current context bitmap
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(origin: CGPoint, size: CGSize) -> UIImage {
let window = UIWindow(frame: CGRect(origin: origin, size: size))
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
This is kind of my code idea at the moment. I have a view I've built that onAppear sets the CGpoint and CGsize of the screen capture. Then an attached method that can then take the screen shot of the view. The problem right now this view never renders because I never add this to a parent view as I don't want this view to appear to the user. In the parent view I have
struct HistoryCell: View {
...
private var shareRental : ShareRentalView? = nil
private var uiimage: UIImage? = nil
...
init(){
...
self.shareRental = ShareRentalView()
}
var body: some View {
...
Button{action: {self.uiimage = self.shareRental?.returnScreenShot()}}
...
}
}
This doesn't work because there view I want to screen shot is never rendered? Is there a way to render it in memory or off screen and then create an image from it? Or do I need to think of another way of doing this?

This ended up working to get the a screen shot of a view that was not presented on the screen to save as a UIImage
extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
let image = controller.view.asImage()
return image
}
}
And then in my parent view
var shareRental: ShareRentalView?
init(){
....
self.shareRental = ShareRentalView()
}
var body: some View {
Button(action: {
let shareImage = self.shareRental.asImage()
}
This gets me almost there. The MKMapSnapshotter has a delay while loading and the image creation happens too fast and there is no map when the UIImage is created.
In order to get around the issue with the delay in the map loading I created an array in a class that builds all the UIImages and stores them in an array.
class MyUser: ObservableObject {
...
public func buildHistoryRental(){
self.historyRentals.removeAll()
MapSnapshot().generateSnapshot(completion: self.snapShotRsp)
}
}
}
}
private func snapShotRsp(image: UIImage){
self.historyRentals.append(image))
}
And then I made a class to create snap shot images like this
func generateSnapshot(completion: #escaping (JSON, UIImage)->() ){
let mapSnapshotOptions = MKMapSnapshotOptions()
// Set the region of the map that is rendered. (by polyline)
let polyLine = MKPolyline(coordinates: &yourCoordinates, count: yourCoordinates.count)
let region = MKCoordinateRegionForMapRect(polyLine.boundingMapRect)
mapSnapshotOptions.region = region
// Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens.
mapSnapshotOptions.scale = UIScreen.main.scale
// Set the size of the image output.
mapSnapshotOptions.size = CGSize(width: IMAGE_VIEW_WIDTH, height: IMAGE_VIEW_HEIGHT)
// Show buildings and Points of Interest on the snapshot
mapSnapshotOptions.showsBuildings = true
mapSnapshotOptions.showsPointsOfInterest = true
let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions)
var image: UIImage = UIImage()
snapshotter.start(completionHandler: { (snapshot: MKMapSnapshotter.Snapshot?, Error) -> Void in
if(Error != nil){
print("\(String(describing: Error))");
}else{
image = self.drawLineOnImage(snapshot: snapshot.unsafelyUnwrapped, pointsToUse: pointsToUse)
}
completion(image)
})
}
}
func drawLineOnImage(snapshot: MKMapSnapshot) -> UIImage {
let image = snapshot.image
// for Retina screen
UIGraphicsBeginImageContextWithOptions(self.imageView.frame.size, true, 0)
// draw original image into the context
image.draw(at: CGPoint.zero)
// get the context for CoreGraphics
let context = UIGraphicsGetCurrentContext()
// set stroking width and color of the context
context!.setLineWidth(2.0)
context!.setStrokeColor(UIColor.orange.cgColor)
// Here is the trick :
// We use addLine() and move() to draw the line, this should be easy to understand.
// The diificult part is that they both take CGPoint as parameters, and it would be way too complex for us to calculate by ourselves
// Thus we use snapshot.point() to save the pain.
context!.move(to: snapshot.point(for: yourCoordinates[0]))
for i in 0...yourCoordinates.count-1 {
context!.addLine(to: snapshot.point(for: yourCoordinates[i]))
context!.move(to: snapshot.point(for: yourCoordinates[i]))
}
// apply the stroke to the context
context!.strokePath()
// get the image from the graphics context
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
// end the graphics context
UIGraphicsEndImageContext()
return resultImage!
}
It's important to return the image back async with the callback. Trying to return the image directly from the func call yielded a blank map.

Related

get the cgImage from a UIImage that was selected using UIImagePickerController

I am trying to build something for my own learning where I select an image from my photo library then divide that image up into sections. I had found info on how to split a single UIImage into sections, but in order to do that I need to have access to the cgImage property of the UIImage object. My problem is, the cgImage is always nil/null when selecting an image from the UIImagePickerController. Below is a stripped down version of my code, I'm hoping someone knows why the cgImage is always nil/null...
class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#IBOutlet weak var selectButton: UIButton!
let picker = UIImagePickerController()
var image: UIImage!
var images: [UIImage]!
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = self
picker.sourceType = .photoLibrary
}
#objc func selectPressed(_ sender: UIButton) {
self.present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info [UIImagePickerController.InfoKey : Any]) {
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
self.picker.dismiss(animated: true, completion: nil)
return
}
self.image = image
self.picker.dismiss(animated: true, completion: nil)
self.makePuzzle()
}
func makePuzze() {
let images = self.image.split(times: 5)
}
}
extension UIImage {
func split(times: Int) -> [UIImage] {
let size = self.size
var xpos = 0, ypos = 0
var images: [UIImage] = []
let width = Int(size.width) / times
let height = Int(size.height) / times
for x in 0..<times {
xpos = 0
for y in 0..<times {
let rect = CGRect(x: xpos, y: ypos, width: width, height: height)
let ciRef = self.cgImage?.cropping(to: rect) //this is always nil
let img = UIImage(cgImage: ciRef!) //crash because nil
xpos += width
images.append(img)
}
ypos += height
}
return images
}
}
I can't seem to get the cgImage to be anything but nil/null and the app crashes every time. I know I can change the ! to ?? nil or something similar to avoid the crash, or add a guard or something, but that isn't really the problem, the problem is the cgImage is nil. I have looked around and the only thing I can find is how to get the cgImage with something like image.cgImage but that doesn't work. I think it has something to do with the image being selected from the UIImagePickerController, maybe that doesn't create the cgImage properly? Honestly not sure and could use some help. Thank you.
This is not an answer, just a beefed up comment with code.
Your assumption that the problem may be due to the UIImagePickerController could be correct.
Here is my SwiftUI test code. It shows your split(..) code (with some minor mods) working.
extension UIImage {
func split(times: Int) -> [UIImage] {
let size = self.size
var xpos = 0, ypos = 0
var images: [UIImage] = []
let width = Int(size.width) / times
let height = Int(size.height) / times
if let cgimg = self.cgImage { // <-- here
for _ in 0..<times {
xpos = 0
for _ in 0..<times {
let rect = CGRect(x: xpos, y: ypos, width: width, height: height)
if let ciRef = cgimg.cropping(to: rect) { // <-- here
let img = UIImage(cgImage: ciRef)
xpos += width
images.append(img)
}
}
ypos += height
}
}
return images
}
}
struct ContentView: View {
#State var imgSet = [UIImage]()
var body: some View {
ScrollView {
ForEach(imgSet, id: \.self) { img in
Image(uiImage: img).resizable().frame(width: 100, height: 100)
}
}
.onAppear {
if let img = UIImage(systemName: "globe") { // for testing
imgSet = img.split(times: 2)
}
}
}
}

White strip on iPad when rendering a view to an image

I am trying to render a black rectangle to an image and save it to the photo library. But every time I render it on my iPad, the picture has a white strip on the top, that doesn’t happen if I do this on the iPhone.
I am using Swift Playgrounds 4, so maybe that’s the reason. It’s a bit strange, since both Views the small and the bigger one are both „iPads“.
Thank you for your help!
That’s my code so far:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Button("Snapshot") {
// Save Screenshot
let image = snapshotView.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
}
var snapshotView: some View {
VStack {
Rectangle()
.frame(width: 200, height: 200)
}
}
}
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
Image of the Rectangle

How do I position an image correctly in MTKView?

I am trying to implement an image editing view using MTKView and Core Image filters and have the basics working and can see the filter applied in realtime. However the image is not positioned correctly in the view - can someone point me in the right direction for what needs to be done to get the image to render correctly in the view. It needs to fit the view and retain its original aspect ratio.
Here is the metal draw function - and the empty drawableSizeWillChange!? - go figure. its probably also worth mentioning that the MTKView is a subview of another view in a ScrollView and can be resized by the user. It's not clear to me how Metals handles resizing the view but it seems that doesn't come for free.
I am also trying to call the draw() function from a background thread and this appears to sort of work. I can see the filter effects as they are applied to the image using a slider. As I understand it this should be possible.
It also seems that the coordinate space for rendering is in the images coordinate space - so if the image is smaller than the MTKView then to position the image in the centre the X and Y coordinates will be negative.
When the view is resized then everything gets crazy with the image suddenly becoming way too big and parts of the background not being cleared.
Also when resting the view the image gets stretched rather than redrawing smoothly.
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
public func draw(in view: MTKView) {
if let ciImage = self.ciImage {
if let currentDrawable = view.currentDrawable { // 1
let commandBuffer = commandQueue.makeCommandBuffer()
let inputImage = ciImage // 2
exposureFilter.setValue(inputImage, forKey: kCIInputImageKey)
exposureFilter.setValue(ev, forKey: kCIInputEVKey)
context.render(exposureFilter.outputImage!,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(origin: .zero, size: view.drawableSize),
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable)
commandBuffer?.commit()
}
}
}
As you can see the image is on the bottom left
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
That should help you out. The issue is that your CIImage, wherever it might come from, is not the same size as the view you are rendering it in.
So what you could opt to do is calculate the scale, and apply it as a filter:
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
scaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
scaleFilter?.setValue(scale, forKey: kCIInputScaleKey)
This resolves your scale issue; I currently do not know what the most efficient approach would be to actually reposition the image
Further reference: https://nshipster.com/image-resizing/
The problem is your call to context.render — you are calling render with bounds: origin .zero. That’s the lower left.
Placing the drawing in the correct spot is up to you. You need to work out where the right bounds origin should be, based on the image dimensions and your drawable size, and render there. If the size is wrong, you also need to apply a scale transform first.
Thanks to Tristan Hume's MetalTest2 I now have it working nicely in two synchronised scrollViews. The basics are in the subclass below - the renderer and shaders can be found at Tristan's MetalTest2 project. This class is managed by a viewController and is a subview of the scrollView's documentView. See image of the final result.
//
// MetalLayerView.swift
// MetalTest2
//
// Created by Tristan Hume on 2019-06-19.
// Copyright © 2019 Tristan Hume. All rights reserved.
//
import Cocoa
// Thanks to https://stackoverflow.com/questions/45375548/resizing-mtkview-scales-old-content-before-redraw
// for the recipe behind this, although I had to add presentsWithTransaction and the wait to make it glitch-free
class ImageMetalView: NSView, CALayerDelegate {
var renderer : Renderer
var metalLayer : CAMetalLayer!
var commandQueue: MTLCommandQueue!
var sourceTexture: MTLTexture!
let colorSpace = CGColorSpaceCreateDeviceRGB()
var context: CIContext!
var ciMgr: CIManager?
var showEdits: Bool = false
var ciImage: CIImage? {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
#objc dynamic var fileUrl: URL? {
didSet {
if let url = fileUrl {
self.ciImage = CIImage(contentsOf: url)
}
}
}
/// Bind to this property from the viewController to receive notifications of changes to CI filter parameters
#objc dynamic var adjustmentsChanged: Bool = false {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
override init(frame: NSRect) {
let _device = MTLCreateSystemDefaultDevice()!
renderer = Renderer(pixelFormat: .bgra8Unorm, device: _device)
self.commandQueue = _device.makeCommandQueue()
self.context = CIContext()
self.ciMgr = CIManager(context: self.context)
super.init(frame: frame)
self.wantsLayer = true
self.layerContentsRedrawPolicy = .duringViewResize
// This property only matters in the case of a rendering glitch, which shouldn't happen anymore
// The .topLeft version makes glitches less noticeable for normal UIs,
// while .scaleAxesIndependently matches what MTKView does and makes them very noticeable
// self.layerContentsPlacement = .topLeft
self.layerContentsPlacement = .scaleAxesIndependently
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func makeBackingLayer() -> CALayer {
metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.device = renderer.device
metalLayer.delegate = self
// If you're using the strategy of .topLeft placement and not presenting with transaction
// to just make the glitches less visible instead of eliminating them, it can help to make
// the background color the same as the background of your app, so the glitch artifacts
// (solid color bands at the edge of the window) are less visible.
// metalLayer.backgroundColor = CGColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
metalLayer.allowsNextDrawableTimeout = false
// these properties are crucial to resizing working
metalLayer.autoresizingMask = CAAutoresizingMask(arrayLiteral: [.layerHeightSizable, .layerWidthSizable])
metalLayer.needsDisplayOnBoundsChange = true
metalLayer.presentsWithTransaction = true
metalLayer.framebufferOnly = false
return metalLayer
}
override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
self.size = newSize
renderer.viewportSize.x = UInt32(newSize.width)
renderer.viewportSize.y = UInt32(newSize.height)
// the conversion below is necessary for high DPI drawing
metalLayer.drawableSize = convertToBacking(newSize)
self.viewDidChangeBackingProperties()
}
var size: CGSize = .zero
// This will hopefully be called if the window moves between monitors of
// different DPIs but I haven't tested this part
override func viewDidChangeBackingProperties() {
guard let window = self.window else { return }
// This is necessary to render correctly on retina displays with the topLeft placement policy
metalLayer.contentsScale = window.backingScaleFactor
}
func display(_ layer: CALayer) {
if let drawable = metalLayer.nextDrawable(),
let commandBuffer = commandQueue.makeCommandBuffer() {
let passDescriptor = MTLRenderPassDescriptor()
let colorAttachment = passDescriptor.colorAttachments[0]!
colorAttachment.texture = drawable.texture
colorAttachment.loadAction = .clear
colorAttachment.storeAction = .store
colorAttachment.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
if let outputImage = self.ciImage {
let xscale = self.size.width / outputImage.extent.width
let yscale = self.size.height / outputImage.extent.height
let scale = min(xscale, yscale)
if let scaledImage = self.ciMgr!.scaleTransformFilter(outputImage, scale: scale, aspectRatio: 1),
let processed = self.showEdits ? self.ciMgr!.processImage(inputImage: scaledImage) : scaledImage {
let x = self.size.width/2 - processed.extent.width/2
let y = self.size.height/2 - processed.extent.height/2
context.render(processed,
to: drawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(x:-x, y:-y, width: self.size.width, height: self.size.height),
colorSpace: colorSpace)
}
} else {
print("Image is nil")
}
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
drawable.present()
}
}
}

Download and cache the MKMapSnapshotter image using SDWebimage

I want to show mapView in UITableViewCells,
So I actually wrote this code
class DataPointTableCell: UITableViewCell {
#IBOutlet weak var mapView: MKMapView!
func addAnnotaionToMapView(_ coordinates: Coordinate) {
removePreviousCoordinate()
let viewCoorindate = CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
let annotation = MKPointAnnotation()
annotation.coordinate = viewCoorindate
mapView.addAnnotation(annotation)
// animate is turned to false on purpose.
mapView.animateToPoint(viewCoorindate, animated: false)
}
private func removePreviousCoordinate() {
let annotations = self.mapView.annotations
self.mapView.removeAnnotations(annotations)
}
}
The problem with this approach is that it animates the mapView location and then adds the marker as it deques the cell.
I went with another approach i.e. by using MKMapSnapshotter
private func setMapImage() {
let rect = self.mapImageView.bounds
let mapSnapshotOptions = MKMapSnapshotter.Options()
// Set the region of the map that is rendered.
let needleLocation = CLLocationCoordinate2DMake(15.4952364, 73.8343293)
let region = MKCoordinateRegion(center: needleLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
mapSnapshotOptions.region = region
// Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens.
mapSnapshotOptions.scale = UIScreen.main.scale
// Set the size of the image output.
mapSnapshotOptions.size = CGSize(width: 300, height: 300)
// Show buildings and Points of Interest on the snapshot
mapSnapshotOptions.showsBuildings = true
mapSnapshotOptions.showsPointsOfInterest = false
let snapshot = MKMapSnapshotter(options: mapSnapshotOptions)
snapshot.start { snapshot, error in
guard let snapshot = snapshot, error == nil else {
print("\(error!.localizedDescription)")
return
}
UIGraphicsBeginImageContextWithOptions(mapSnapshotOptions.size, true, 0)
snapshot.image.draw(at: .zero)
let pinView = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
let pinImage = pinView.image
var point = snapshot.point(for: needleLocation)
if rect.contains(point) {
let pinCenterOffset = pinView.centerOffset
point.x -= pinView.bounds.size.width / 2
point.y -= pinView.bounds.size.height / 2
point.x += pinCenterOffset.x
point.y += pinCenterOffset.y
pinImage?.draw(at: point)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
DispatchQueue.main.async {
self.mapImageView.image = image
}
}
}
This works fine but I can't cache image based on anything.
So the help that I wanted is how to download and cache the mapImage using SDWebimage.
First add a unique key for your cell
var youUniqueCellKey = "Key"
Then in setMapImage first check if an image is already Cached or not and assign it to your mapImageView
private func setMapImage() {
if let image = SDImageCache.shared().imageFromCache(forKey: youUniqueCellKey) {
self.mapImageView.image = image
} else {
// Add your rest of your code here
// At the end
var imageToStore = UIImage() // Your mapImage
SDImageCache.shared().store(imageToStore, forKey: youUniqueCellKey, completion: nil)
}
}

How to Screenshot NSView for Mac OS?

I currently have a function that creates a screenshot of the entire window in a Mac application. However, I would like to target a particular view. I know in iOS you can do the following:
extension WKWebView {
var screenshot: UIImage {
if #available(iOS 10.0, *) {
return UIGraphicsImageRenderer(size: bounds.size).image { _ in
drawHierarchy(in: CGRect(origin: .zero, size: bounds.size), afterScreenUpdates: true)
}
} else {
UIGraphicsBeginImageContext(bounds.size)
drawHierarchy(in: bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()!
return image
}
}
}
How would I convert this to Mac OS using AppKit instead of UIKit. The following is my current computed property for screenshots of the entire window.
extension WKWebView {
var screenshot: NSImage {
let displayID = CGWindowID() // cgmaindisplayid
let imageRef = CGDisplayCreateImage(displayID)
return NSImage(cgImage: imageRef!, size: (NSScreen.main?.frame.size)!)
}
}
You can use NSView method func dataWithPDF(inside rect: NSRect) -> Data
extension NSView {
var image: NSImage? {
return NSImage(data: dataWithPDF(inside: bounds))
}
}