pdfView pinch zooming out - ios

I am trying to move my project that was using a webview to display some pdf to a pdfView to take advantage of the latest PDFKit features.
in the webview when pinching to zoom out the document was always scaling to fill the screen. basically you could not zoom out the page it was bouncing back to fill the screen.
Now with a pdfView, I can zoom out by pinching and it does not look good at all there is no need to have the pdf page to be smaller than the screen...
Is there any way to activate the autoscale once you release your fingers from the screen. I know there is the gesture func but I am not familiar with its use.

Just to confirm that the accepted answer is the correct one, but also to highlight where the code needs to be used (as stated in the comment by answer author). i.e. the code must be used AFTER setting the pdf document:
pdfView.document = pdfDocument
pdfView.autoScales = true
pdfView.maxScaleFactor = 4.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit

to answer my own question, it was actually very easy...
pdfView.autoScales = true
pdfView.maxScaleFactor = 4.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit

This solution will automatically adjust the PDFView once you set the document property. Just use NoZoomOutPDFView instead of PDFView as your view for displaying a PDFDocument.
import Foundation
import PDFKit
final class NoZoomOutPDFView: PDFView {
init() {
super.init(frame: .zero)
NotificationCenter
.default
.addObserver(
self,
selector: #selector(update),
name: .PDFViewDocumentChanged,
object: nil
)
}
deinit {
// If your app targets iOS 9.0 and later the following line can be omitted
NotificationCenter.default.removeObserver(self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func update() {
// PDF can be zoomed in but not zoomed out
DispatchQueue.main.async {
self.autoScales = true
self.maxScaleFactor = 4.0
self.minScaleFactor = self.scaleFactorForSizeToFit
}
}
}

I had the same problem and the accepted answer did not work for me. scaleFactorForSizeToFit always returned 0.0, and therefore I was still able to zoom out until nothing was visible anymore. I don't know if this is specific to my application or something changed in iOS, but I had to trigger a layout update on the PDFView before setting the scale factor. This is how it works for me on iOS15:
if let document = PDFDocument(url: URL(string: "https://www.adobe.com/pdf/pdfs/ISO32000-1PublicPatentLicense.pdf")!) {
pdfView.displayDirection = .vertical
pdfView.autoScales = true
pdfView.document = document
pdfView.setNeedsLayout()
pdfView.layoutIfNeeded()
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
}

If overriding PDFView's method is restricted, it can be done with adding an observer;
NotificationCenter.default.addObserver(self, selector: #selector(scaleChanged), name: NSNotification.Name.PDFViewScaleChanged, object: nil)
and using it like:
#objc func scaleChanged() {
self.pdfView.maxScaleFactor = 3.0
self.pdfView.minScaleFactor = self.pdfView.scaleFactorForSizeToFit
}

Related

Swift PDFKit: Inconsistent behaviour with PDFView.currentDestination and PDFView.go(to destination: PDFDestination)

Note: The question remains unsolved for now; the marked answer provides a good workaround - it works while the application is still open. Answers are still welcomed!
Background
I'm currently developing an app that consists of a full-screen PDFView, and I want the program to remember the position in the document before the view is dismissed so the user can pick up where they've left.
Implementation
A simplified version of the app can be understood as a PDF Viewer using PDFKit.PDFView. The storyboard consists of an UIView that's connected to a PDFView class, DocumentView (which conforms to UIViewController). The view is initialised through the following process:
In viewDidLoad:
let PDF: PDFDocument = GetPDFFromServer()
DocumentView.document = PDF!
DocumentView.autoScales = true
... (other settings)
// Set PDF Destination
let Destination: PDFDestination = GetStoredDestination()
// Code with issues
DocumentView.go(to: Destination)
In viewWillDisappear:
StoreDestination(DocumentView.currentDestination)
The Issue & Tests
I realised that the code does not work as expected; the view does not return to its previous location.
Through debugging, I realised that this might be due to the inconsistent behaviour of DocumentView.go(to destination: PDFDestination) and DocumentView.currentDestination.
To ensure the bug is not introduced by errors while storing the location, the following code is used to verify the issue, with a multi-page document:
In viewDidLoad
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
DispatchQueue.main.async {
self.DocumentView.go(to:self.DocumentView.currentDestination!)
}
})
Expected & Observed behaviour
Expected: The location of the document should not change - the code is going to its current destination every 1 second which should have no effects. as "currentDestination" should be the "current destination of the document, per docs")
Observed: Upon execution, the page would spontaneously scroll down by a fixed offset.
The same outcome was observed on an iPadOS 14.5 simulator and an iPadOS 15 iPad Air (Gen 4).
What might have gone wrong?
It'd be great if somebody can help.
Cheers,
Lincoln
This question was originally published on the Apple Developer Forum over a week ago; No responses were heard for over a week, so I thought I might try my luck here on StackOverflow <3
I tried PDFView.go() for this scenario and I managed to get it work for some cases but found that it fails in some other scenarios such as with zoomed documents, changed orientations.
So going back to what you are trying to achieve,
I'm currently developing an app that consists of a full-screen
PDFView, and I want the program to remember the position in the
document before the view is dismissed so the user can pick up where
they've left.
this can be done from a different approach. With this approach, you need to always keep a reference to the PDFView you created. If the previous pdf needs to be loaded again, then you pass the PDFView instance you have to the viewController as it is. Otherwise you load the new pdf to the PDFView instance and pass it to the viewController.
DocumentViewController gets the PDFView when it gets initialized.
import UIKit
import PDFKit
protocol DocumentViewControllerDelegate: AnyObject {
func needsContinuePDF(continuePDF: Bool)
}
class DocumentViewController: UIViewController {
var pdfView: PDFView!
weak var delegate: DocumentViewControllerDelegate!
init(pdfView: PDFView, delegate: DocumentViewControllerDelegate){
self.pdfView = pdfView
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
view.addSubview(pdfView)
NSLayoutConstraint.activate([
pdfView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
pdfView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
pdfView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
pdfView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
override func viewWillDisappear(_ animated: Bool) {
delegate.needsContinuePDF(continuePDF: true)
}
}
You can initialize DocumentViewController like below. MainViewController has the responsibility for initializing PDFView.
import UIKit
import PDFKit
class MainViewController: UIViewController {
var pdfView: PDFView = PDFView()
var continuePreviousPDF = false
let button = UIButton(frame: .zero)
override func loadView() {
super.loadView()
button.setTitle("View PDF", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemBlue
button.addTarget(self, action: #selector(openDocumentView(_:)), for: .touchUpInside)
self.view.backgroundColor = .systemGray5
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 100),
button.heightAnchor.constraint(equalToConstant: 50),
button.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor),
])
}
#objc func openDocumentView(_ sender: UIButton) {
//open a nee PDF if not continue previous one
if !self.continuePreviousPDF {
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
pdfView.translatesAutoresizingMaskIntoConstraints = false
guard let path = Bundle.main.url(forResource: "sample copy", withExtension: "pdf") else { return }
if let document = PDFDocument(url: path) {
pdfView.document = document
}
}
let documentViewController = DocumentViewController(pdfView: pdfView, delegate: self)
self.present(documentViewController, animated: true, completion: nil)
}
}
extension MainViewController: DocumentViewControllerDelegate {
func needsContinuePDF(continuePDF: Bool) {
self.continuePreviousPDF = continuePDF
}
}

AVPlayerViewController ignores all touches after returning from fullscreen

I faced a very strange behaviour of AVPlayerViewController.
I use this code to add play video inside view:
class ViewShowTest:UIViewController
{
let view_for_media = UIView()
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .red
self.view.addSubview(view_for_media)
view_for_media.snp.makeConstraints(
{ make in
make.top.equalToSuperview().offset(100)
make.left.right.equalToSuperview()
make.height.equalTo(300)
})
setVideo()
}
func setVideo()
{
let url = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")!
let player = AVPlayer(url: url)
let player_vc = AVPlayerViewController(nibName: nil, bundle: nil)
player_vc.player = player
self.view_for_media.addSubview(player_vc.view)
self.addChild(player_vc)
player_vc.didMove(toParent: self)
player_vc.view.frame = view_for_media.bounds
}
}
Video is playing as expected inside view. But after going to fullScreen and returning back (no matter with swipe or close button) ViewShowTest ignores all touches. I still can drag default ios Menus from top and bottom but ViewShowTest view and player_vc.view does not reacts any touches. This bug appears only on ios 12.4 and below. How can i solve this problem?

iOS PDFKit Disable vertical scroll bounce

How does one disable scroll bounce in a PDFView using PDFKit?
The view where the PDF is shown doesn't have a scroll bounce option.
Here's my code:
if let path = Bundle.main.path(forResource: pdfObject, ofType: "pdf") {
let url = URL(fileURLWithPath: path)
if let pdfDocument = PDFDocument(url: url) {
pdfView.autoresizesSubviews = true
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight,
.flexibleTopMargin, .flexibleLeftMargin]
pdfView.autoScales = true
pdfView.displaysPageBreaks = true
pdfView.displayDirection = .vertical
pdfView.displayMode = .singlePageContinuous
pdfView.document = pdfDocument
pdfView.maxScaleFactor = 4.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
}
}
Thanks in advance (for what is likely a ridiculously simple question!)
Unfortunately, there isn't an exported API to set the PDFView desired bouncing behavior.
Having said that, you can (safely) exploit a PDFView implementation detail to hack your way around it for now:
extension PDFView {
/// Disables the PDFView default bouncing behavior.
func disableBouncing() {
for subview in subviews {
if let scrollView = subview as? UIScrollView {
scrollView.bounces = false
return
}
}
print("PDFView.disableBouncing: FAILED!")
}
}
and then use it like this in your code:
pdfView.disableBouncing()
Caveat. Please keep in mind that such solution might break in future iOS releases. Nevertheless, rest assured your app won't crash as a result (you only won't be disabling the bouncing behavior at all).

change all view controller background in swift

I am working on my project app where I want to allow my user to change the background wallpaper to be same on all screens. I have a settings screen where I am adding that. I have added ImageViews to all view controllers and I have some view controllers that have UIscrollview so I added Imageview to slides template. Now my dilemma is how can I allow the user to pick the preview wallpaper so it changes the Imageview image on every view controller. I already created such #IBOutlets as shown below.
#IBOutlet weak var slideBackground: UIImageView!
#IBOutlet weak var homeScreenBackground: UIImageView!
#IBOutlet weak var settingsBackground: UIImageView!
You should use the system wide NotificationCenter.
Simply put, you can have objects subscribe to the default NotificationCenter and specify a selector (method) to execute when a notification is posted.
You can also post custom notifications that represent the wallpaper change event.
I have used this in an app I built to achieve a system wide 'dark mode' transition.
To post:
#objc func postWallpaperChangeNotification() {
NotificationCenter.default.post(name: .wallpaperChanged, object: nil)
}
To subscribe:
NotificationCenter.default.addObserver(self, selector: #selector(someMethodToRunWhenWallpaperChanges(_:)), name: . wallpaperChanged, object: nil)
You also need to remove the observer in deinit().
This is approximate code to give you a flavour, any questions hmu.
You have a few options.. what option is best depends on your needs really.
As Woodstock mentioned in his answer, you could use NotificationCentre, I wont re-explain that.
Another option would be to store the image name in UserDefaults that you are going to use for the background image.
any time you change the background... set the value
UserDefaults.standard.set(imageName, forKey "backgroundImageName")
Then in each view controller, just load the value on viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated: animated)
if let backgroundImageName = UserDefaults.standard.string(forKey: "backgroundImageName") {
self.backgroundImageView.image = UIImage(named: backgroundImageName)
}
}
Both of these options are not very easy to unit test though. You could use Mocks to test it.
A bonus of this method is that it will 'remember' the setting inbetween the user restarting the app.
An example of how you might reduce code repetition using subclassing:
class WallpaperedViewController: UIViewController {
lazy private var imageView: UIImageView = {
let imageView = UIImageView()
view.addSubview(imageView)
view.sendSubview(toBack: imageView)
return imageView
}()
private func setWallPaperImage() {
let imageName = UserDefaults.standard.string(forKey: "backgroundImageName")
let image = UIImage(named: imageName!)
imageView.image = image
//imageView.contentMode = .scaleAspectFill
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
imageView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setWallPaperImage()
}
}
Then for each ViewController that has this background image wallpaper just extend this class:
class SomeViewController: WallpaperedViewController {}

how to prevent UITextfield from changing alignment when input language is RTL/Arabic?

The app language is English, the illustration on the left shows UITextfields content aligned to the left.. which is normal, but when the user selects an RTL/Arabic input language, the fields alignment are flipped automatically, how to force the alignment to be left disregarding the input language direction?
EDIT :
I tried this, and it's not solving the problem
let _beginningOfDocument = fieldPassword!.beginningOfDocument
let _endOfDocument = fieldPassword!.endOfDocument
fieldPassword!.setBaseWritingDirection(.leftToRight, for: fieldPassword!.textRange(from: _beginningOfDocument , to: _endOfDocument )! )
To be honest I tried all of them and doesn't work for me because it's related to the view hierarchy . For more information, you can read this note
Note
iOS applies appearance changes when a view enters a window, it doesn’t change the appearance of a view that’s already in a window. To change the appearance of a view that’s currently in a window, remove the view from the view hierarchy and then put it back.
So try this one, it worked for me:
extension UITextField {
open override func awakeFromNib() {
super.awakeFromNib()
if Utilities.getCurrentLanguage() == "ar" {
if textAlignment == .natural {
self.textAlignment = .right
}
}
}
}
By the way, "Utilities.getCurrentLanguage()" is a function to get the current language.
It came out that some library I was using caused this effect, it's MOLH library, it uses method swizzling, this is why this was difficult to debug...
I will be making a pull request to it soon to make this effect optional...
func listenToKeyboard() {
NotificationCenter.default.removeObserver(self, name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
}
#objc func inputModeDidChange(_ notification: Notification) {
if let language = self.textInputMode?.primaryLanguage, MOLHLanguage.isRTLLanguage(language: language) {
self.textAlignment = .right
} else {
self.textAlignment = .left
}
}
Thanks.
In Swift 4
textField.semanticContentAttribute = UISemanticContentAttribute.forceLeftToRight
In your app delegate, add this:
if #available(iOS 9.0, *) {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}

Resources