Multiple pages view of the same PDF - swift

I am using PDFView from PDFKit (swift programming language, macOS) in order to display some PDF documents.
I would like to display multiple pages of the same PDF.
A solution would be to create multiple PDFView from the same PDF but in this way I have to allocate multiple time the same memory and this can be a problem for PDF rich in images.
Is there any fast way to display different pages of the same PDF in, say, 4/8 different views?
EDIT:
The precious suggestions of #danielv brought to this code
import Cocoa
import Quartz
class PDFPageView: NSView {
var pdfPage: PDFPage?
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if(pdfPage != nil) {
let context = NSGraphicsContext.current()
let pdfBox = PDFDisplayBox.mediaBox
let pdfBounds = pdfPage?.bounds(for: pdfBox)
let scaleX = visibleRect.width/(pdfBounds?.width)!
let scaleY = visibleRect.height/(pdfBounds?.height)!
let scale = min(scaleX,scaleY)
context?.cgContext.scaleBy(x: scale, y: scale)
pdfPage?.draw(with: pdfBox, to: (context?.cgContext)!)
}
}
}

PDFViewmost likely won't serve well for the purpose of displaying multiple pages of the same PDF document.
But you can to do it yourself. PDFPage can draw itself to any given graphics context. You can create a subclass of NSView to draw a page or use other more fancy views like NSCollectionView and draw pages to its rows/columns/grid.
Here is roughly how you'd do it with NSView:
Create a subclass of NSView
class PDFPageView: NSView {
var pdfPage: PDFPage?
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let context = NSGraphicsContext.current()!
pdfPage?.draw(with: .mediaBox, to: context.cgContext)
}
}
Important: you will need to transform your view's context before drawing to fit the page and make up for any page rotations. PDFPage has some helper functions for this.
Also note that draw(with box: PDFDisplayBox, to context: CGContext) is since MacOS 10.12, to support previous versions use draw(with box: PDFDisplayBox)
In your controller, load the PDF document and assign each page to the view that needs to display it.
class ViewController: NSViewController {
#IBOutlet weak var pdfPageView: PDFPageView?
var pdfDocumenet: PDFDocument?
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(fileURLWithPath: "... pdf filename ...")
pdfDocumenet = PDFDocument(url: url)
pdfPageView?.pdfPage = pdfDocumenet?.page(at: 0)
}
}
Edit (to include author's comments):
When you try draw the PDF page on the NSView's context, the page drawing doesn't respect the view's bounds. Try it yourself, you will see what I mean. You need to do the scaling of the context yourself, usually by transforming the current graphics context to the desired coordinates. Search for Cocoa Drawing Guide for detailed information about contexts and transformations. PDFPage provides some helper methods to let you get the original page bounds and the transformation that the page may have used to rotate the page, if it was needed.
To get the page's rect you use bounds(for box: PDFDisplayBox), not the context size. You need to transform the context before you draw on it. But it's all in the docs, you really need to read the Cocoa/Quartz Drawing Guides and the docs for PDF page. So be sure to take a look there.

Related

How, exactly, do I render Metal on a background thread?

This problem is caused by user interface interactions such as showing the titlebar while in fullsreen. That question's answer provides a solution, but not how to implement that solution.
The solution is to render on a background thread. The issue is, the code provided in Apple's is made to cover a lot of content so most of it will extraneous code, so even if I could understand it, it isn't feasible to use Apple's code. And I can't understand it so it just plain isn't an option. How would I make a simple Swift Metal game use a background thread being as concise as possible?
Take this, for example:
class ViewController: NSViewController {
var MetalView: MTKView {
return view as! MTKView
}
var Device: MTLDevice = MTLCreateSystemDefaultDevice()!
override func viewDidLoad() {
super.viewDidLoad()
MetalView.delegate = self
MetalView.device = Device
MetalView.colorPixelFormat = .bgra8Unorm_srgb
Device = MetalView.device
//setup code
}
}
extension ViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
//drawing code
}
}
That is the start of a basic Metal game. What would that code look like, if it were rendering on a background thread?
To fix that bug when showing the titlebar in Metal, I need to render it on a background thread. Well, how do I render it on a background thread?
I've noticed this answer suggests to manually redraw it 60 times a second. Presumably using a loop that is on a background thread? But that seems... not a clean way to fix it. Is there a cleaner way?
The main trick in getting this to work seems to be setting up the CVDisplayLink. This is awkward in Swift, but doable. After some work I was able to modify the "Game" template in Xcode to use a custom view backed by CAMetalLayer instead of MTKView, and a CVDisplayLink to render in the background, as suggested in the sample code you linked — see below.
Edit Oct 22:
The approach mentioned in this thread seems to work just fine: still using an MTKView, but drawing it manually from the display link callback. Specifically I was able to follow these steps:
Create a new macOS Game project in Xcode.
Modify GameViewController to add a CVDisplayLink, similar to below (see this question for more on using CVDisplayLink from Swift). Start the display link in viewWillAppear and stop it in viewWillDisappear.
Set mtkView.isPaused = true in viewDidLoad to disable automatic rendering, and instead explicitly call mtkView.draw() from the display link callback.
The full content of my modified GameViewController.swift is available here.
I didn't review the Renderer class for thread safety, so I can't be sure no more changes are required, but this should get you up and running.
Older implementation with CAMetalLayer instead of MTKView:
This is just a proof of concept and I can't guarantee it's the best way to do everything. You might find these articles helpful too:
I didn't try this idea, but given how much convenience MTKView generally provides over CAMetalLayer, it might be worth giving it a shot:
https://developer.apple.com/forums/thread/89241?answerId=268384022#268384022
Is drawing to an MTKView or CAMetalLayer required to take place on the main thread? and https://developer.apple.com/documentation/quartzcore/cametallayer/1478157-presentswithtransaction
class MyMetalView: NSView {
var displayLink: CVDisplayLink?
var metalLayer: CAMetalLayer!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setupMetalLayer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupMetalLayer()
}
override func makeBackingLayer() -> CALayer {
return CAMetalLayer()
}
func setupMetalLayer() {
wantsLayer = true
metalLayer = layer as! CAMetalLayer?
metalLayer.device = MTLCreateSystemDefaultDevice()!
// ...other configuration of the metalLayer...
}
// handle display link callback at 60fps
static let _outputCallback: CVDisplayLinkOutputCallback = { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
// convert opaque context pointer back into a reference to our view
let view = Unmanaged<MyMetalView>.fromOpaque(context!).takeUnretainedValue()
/*** render something into view.metalLayer here! ***/
return kCVReturnSuccess
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
guard CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) == kCVReturnSuccess,
let displayLink = displayLink
else {
fatalError("unable to create display link")
}
// pass a reference to this view as an opaque pointer
guard CVDisplayLinkSetOutputCallback(displayLink, MyMetalView._outputCallback, Unmanaged<MyMetalView>.passUnretained(self).toOpaque()) == kCVReturnSuccess else {
fatalError("unable to configure output callback")
}
guard CVDisplayLinkStart(displayLink) == kCVReturnSuccess else {
fatalError("unable to start display link")
}
}
deinit {
if let displayLink = displayLink {
CVDisplayLinkStop(displayLink)
}
}
}

Drawing directly in a NSView without using the draw(_ updateRect: NSRect) function

I would like to draw CGImage pictures directly to a View and with the normal method using the draw func I only get 7 pictures in a second on a new Mac Book Pro. So I decided to use the updateLayer func instead. I have defined wantsUpdateLayer = true and my new updateLayer func is called as expected. But then starts my problem. When using the draw func, I get the current CGContext with "NSGraphicsContext.current?.cgContext" but in my updateLayer func the "NSGraphicsContext.current?.cgContext" is nil. So I do not know where to put my CGImage, that it will be displayed on my screen. Also "self.view?.window?.graphicsContext?.cgContext" and "self.window?.graphicsContext?.cgContext" are nil, too. There are no buttons or other elements in this view and in the window of the view, only one picture, filling the complete window. And this picture must change 30 times in a second. Generating the pictures is done by a separate thread and needs about 1 millisecond for a picture. I think that from "outside" the NSView class it is not possible to write the picture but my updateLayer func is inside the class.
Here is what the func looks like actually:
override func updateLayer ()
{
let updateRect: NSRect = NSRect(x: 0.0, y: 0.0, width: 1120.0, height: 768.0)
let context1 = self.view?.window?.graphicsContext?.cgContext
let context2 = self.window?.graphicsContext?.cgContext
let context3 = NSGraphicsContext.current?.cgContext
}
And all three contexts are nil in the time the function is called automatically after I set the needsDisplay flag.
Any ideas where to draw my CGImages?
The updateLayer func is called automatically by the user interface. I do not call it manually. It is called by the view. My problem is where inside this method to put my picture to be shown on the screen. Perhaps I have to add a layer or use a default layer of the view but I do not know how to do this.
Meanwhile I have found the solution with some tipps from a good friend:
override var wantsUpdateLayer: Bool
{
return (true)
}
override func updateLayer ()
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
let nsimage: NSImage? = NSImage(cgImage: cgimage!, size: NSZeroSize)
if nsimage != nil
{
let desiredScaleFactor: CGFloat? = self.window?.backingScaleFactor
if desiredScaleFactor != nil
{
let actualScaleFactor: CGFloat? = nsimage!.recommendedLayerContentsScale(desiredScaleFactor!)
if actualScaleFactor != nil
{
self.layer!.contents = nsimage!.layerContents(forContentsScale: actualScaleFactor!)
self.layer!.contentsScale = actualScaleFactor!
}
}
}
}
}
This is the way to directly write into the layer. Depending on the format (CGImage or NSImage) you first must convert it. As soon as the func wantsUpdateLayer returns a true, the func updateLayer() is used instead of the func draw(). Thats all.
For all who want to see my "Normal" draw function:
override func draw (_ updateRect: NSRect)
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
if #available(macOS 10.10, *)
{
NSGraphicsContext.current?.cgContext.draw(cgimage!, in: updateRect)
}
}
else
{
super.draw(updateRect)
}
}
The additional speed is 2 times or more, depending on what hardware you use. On a modern Mac Pro there is only a little bit more speed but on a modern Mac Book Pro you will get 10 times or more speed. This works with Mojave 10.14.6 and Catalina 10.15.6. I did not test it with older macOS versions. The "Normal" draw function works with 10.10.6 to 10.15.6.

SwiftUI - Saving Image to Share Sheet causes image to save blurry/low res

I have a bit of code in my app that generates a QR Code and scales it up (code reference I used from this link from Hackng with Swift. Now, I'm using the share sheet to allow the user to save the qr code to their camera roll and, it is working, but saving the image low res, and it saves to the camera roll blurry (and i assume if it is shared via other methods it will also be blurry)
Here is the code of my share sheet function:
struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]?
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
}
}
and here's the code in my view struct:
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [self.qrCodeImage])
}
Is there a trick to remove the interpolation on the image when it saves to the share sheet like the .interpolation(.none) on the image view itself?
Your problem is that the QR code image is actually tiny! Like really tiny:
Printing description of image:
<UIImage:0x60000202cc60 anonymous {23, 23}>
When you share this image, the way it will be displayed is dependant on the program or app that will display it, and is out of control of your app as far as I know.
However,
there is a way that you could potentially make it "pretty" in other apps, and this would be to increase the resolution to a larger amount so that when it's rendered it'll appear to have "sharp" pixels.
How would this be accomplished? I think I have an example buried somewhere in old code, I'll dig into it and see if I can find you an example ;)
Edit
I found the code:
extension UIImage {
func resized(toWidth width: CGFloat) -> UIImage? {
let canvasSize = CGSize(width: round(width), height: CGFloat(ceil(width/size.width * size.height)))
UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext();
context?.interpolationQuality = .none
// Set the quality level to use when rescaling
draw(in: CGRect(origin: .zero, size: canvasSize))
let r = UIGraphicsGetImageFromCurrentImageContext()
return r
}
}
The trick is to provide a way to scale the image, but the real magic is on line 7:
context?.interpolationQuality = .none
If you exclude this line, you'll get blurry images, which is what the OS does by default because you don't generally want to see the pixel edges in images.
You could use this extension like so:
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [self.qrCodeImage.resized(toWidth: 512) ?? UIImage()])
}
However, this may be resizing the image way more often than necessary. Optimally you'd resize it in the same function that you generate it.

How would I make a simple spinning dash in Swift? that spins on its center like a loader in terminal

I'm trying to create a simple spinning loading dash. I know how to do the loop but I can't seem to make it on a single line. Any ideas?
let loop = 1
while loop > 0 {
// spinning dash
}
I will not provide you with all the code to your question but rather a guideline of what to do. In general, its a two step algorithm.
Draw a line
Perform a 360° rotation of it for a desired time, t
The code posted below implements the first portion. I have added comments and I believe it should be self explanatory. After reviewing it, I'd recommend you read about UIBezierPath.
As for the second part, there are two ways of going about this.
1. Rotate the line itself (recommended)
Should you choose this method, here's a tutorial from Ray Wenderlich which covers it extensively along with the Math behind it. Follow through both portions of the tutorial if possible.
2. Rotate the view encompassing the line
Changing the outer view's background color to clear then rotating itself will give the illusion that the line inside is the one rotated. Here's a video guide for view rotations.
import UIKit
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// This is the black subview in which the line will be drawn into
let lineView: GeneralDraw = GeneralDraw(frame: CGRect(origin: CGPoint(x: 20, y: 30), size: CGSize(width: 300, height: 300)))
// uncomment this to remove the black colour
// lineView.backgroundColor = .clear
// add this lineView to the mainView
self.view.addSubview(lineView)
}
}
// This handles the drawing inside a given view
class GeneralDraw: UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect)
{
let linePath = UIBezierPath()
// start point of the line
linePath.move(to: CGPoint(x:50, y:10))
// end point of the line
linePath.addLine(to: CGPoint(x:200, y:10))
linePath.close()
// cosmetic settings for the line
UIColor.red.set()
linePath.stroke()
linePath.fill()
}
}
I would use a CAReplicatorLayer for this. You start with a layer that draws a horizontal bar and combine it with transforms that show the bar in the other positions. Then you animate the fading out of the bar, with an offset coordinated to the fading.
In this gif, I've deliberately slowed down the animation. (There is a mild glitch at the point where the gif repeats, but ignore that; the real project doesn't have that glitch.)
1. Solution: rotate Images
create a set of images which shows the dash rotating.
set the images to an array. then animate that `UIImageView.startAnimating()
see section "Animating a Sequence of Images" of UIImageView.
2. Solution: standard iOS activity indicator
But better go with the standard UIActivityIndicatorView
see also:
iOS Human Interface Guidelines: Progress Indicators
Reference for UIActivityIndicatorView

How do I draw something on a PDF in Swift?

Here is what I am doing currently.
First, I get the document. Next I create a PDF View and assign the document to the view. Then I create a view and do my drawing in the view, then add the view to the PDFView as a subview. Then I convert the PDFView to data, then to PDF document. Since I'm doing the drawing on the first page of the PDF, I remove the first page from the original PDF, insert the new first page, and return the document.
guard let document = PDFDocument(url: url) else { return nil }
guard let page = document.page(at: 0) else { return nil }
let pageBounds = page.bounds(for: .mediaBox)
let pdfView = PDFView(frame: pageBounds)
pdfView.document = document
pdfView.enclosingScrollView?.autohidesScrollers = true
let view = /// code for subview that I am drawing on
pdfView.addSubview(view)
let data = pdfView.dataWithPDF(inside: pageBounds)
guard let pdf = PDFDocument(data: data) else { return nil }
document.removePage(at: 0)
document.insert(pdf.page(at: 0)!, at: 0)
return document
Is there a better way to do this? To add a wrinkle, my final product has a weird scroll bar image (see Screenshot). I tried adding auto hide scrollers & enclosingScrollView?.hasVerticalScroller = false but neither seem to hide the scroll bar.
Thanks in advance!
So I've solved my own problem. For anyone else stuck like me, here is what I did. For example to draw a box on a page:
create a CGContext (ctx) for PDFDocuments. You can do this either with data or with a URL you want to write to.
create a CGPDFDocument with the document you want to edit
get the CGPage of the CGPDF you want to edit (cgPage)
and:
ctx?.beginPDFPage(nil)
ctx?.drawPDFPage(cgPage)
ctx?.beginPath()
let path = CGPath(roundedRect: box as CGRect, cornerWidth: 5, cornerHeight: 5, transform: nil)
ctx?.setStrokeColor(CGColor.black)
ctx?.setFillColor(color.cgColor)
ctx?.setLineWidth(10.0)
ctx?.addPath(path)
ctx?.strokePath()
ctx?.addPath(path)
ctx?.fillPath()
ctx?.endPDFPage()
ctx?.closePDF()
(If you created the context with a URL, close will write to disk. Otherwise you'll have to do something with the PDF data.)
A simple solution is demonstrated in the 2017 WWDC video on PDFKit. I assume you have an existing document to which you want to add some drawing. Here's what to do:
Give the document a delegate.
In the delegate, implement classForPage to declare a class for your pages: that class should be your own PDFPage subclass.
In your PDFPage subclass, implement draw(with:to:). You are given a context; draw into it! Be sure to call super if you want the default drawing to happen.
If your PDFPage subclass needs to draw different things on different pages, it can find out where it is in the document by asking for self.document?.index(for:self).