Reputation: 21
I'm trying to block text selection and editing in a QLPreviewController displaying PDF and text files in a Swift app.
I’ve had success using method swizzling on buildMenu for UIApplication and UIResponder, which works effectively on standard text fields. However, with QLPreviewController, my attempts to intercept or modify the UIMenu calls haven’t succeeded.
The QLPreviewController class only has methods related to opening and displaying QLPreviewItem, but doesn’t expose any clear API for handling or disabling the UIMenu. Additionally, QLPreviewItem itself has no methods that I could use to influence selection behavior.
Any ideas on how to prevent text selection in QLPreviewController for these file types?
Code for example:
import UIKit
import QuickLook
class ViewController: UIViewController {
var previewItems: [URL] = []
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
createMultiPagePDF()
createMultiPageTextFileWithBlankLines()
UIResponder.swizzle()
UIApplication.swizzleAction()
}
@objc private func openPreview() {
guard !previewItems.isEmpty else {
print("Not files for previews")
return
}
let previewController = CustomQLPreviewController()
previewController.dataSource = self
previewController.delegate = previewController
present(previewController, animated: true, completion: nil)
}
}
private extension ViewController {
func setupViews() {
let previewButton = UIButton(type: .system)
previewButton.setTitle("Open preview", for: .normal)
previewButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
previewButton.translatesAutoresizingMaskIntoConstraints = false
previewButton.addTarget(self, action: #selector(openPreview), for: .touchUpInside)
view.addSubview(previewButton)
NSLayoutConstraint.activate([
previewButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
previewButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let previewTextField = UITextField()
previewTextField.font = UIFont.systemFont(ofSize: 12)
previewTextField.textColor = .black
previewTextField.borderStyle = .roundedRect
previewTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(previewTextField)
NSLayoutConstraint.activate([
previewTextField.topAnchor.constraint(equalTo: previewButton.bottomAnchor, constant: 50),
previewTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
func createMultiPagePDF() {
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, CGRect(x: 0, y: 0, width: 200, height: 200), nil)
let pdfTexts = [
"It's first page PDF for testing",
"It's second page PDF for testing",
"It's third page PDF for testing"
]
for text in pdfTexts {
UIGraphicsBeginPDFPage()
let textRect = CGRect(x: 20, y: 20, width: 160, height: 160)
text.draw(in: textRect, withAttributes: [.font: UIFont.systemFont(ofSize: 18)])
}
UIGraphicsEndPDFContext()
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let pdfURL = documentsDirectory.appendingPathComponent("multiPageTestFile.pdf")
pdfData.write(to: pdfURL, atomically: true)
previewItems.append(pdfURL)
}
}
func createMultiPageTextFileWithBlankLines() {
let pageContents = [
"It's first page PDF for testing",
"It's second page PDF for testing",
"It's third page PDF for testing",
"It's fourth page PDF for testing"
]
var fullText = ""
for (index, pageText) in pageContents.enumerated() {
fullText += "=== Page number \(index + 1) ===\n"
fullText += pageText
fullText += "\n" + String(repeating: "\n", count: 50)
}
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let textFileURL = documentsDirectory.appendingPathComponent("multiPageTestFile_BlankLines.txt")
do {
try fullText.write(to: textFileURL, atomically: true, encoding: .utf8)
previewItems.append(textFileURL)
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
}
extension ViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return previewItems.count
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return previewItems[index] as QLPreviewItem
}
}
extension UIResponder {
static func swizzle() {
guard #available(iOS 13.0, *) else {
return
}
guard
let originalMethod = class_getInstanceMethod(self, #selector(buildMenu(with:))),
let swizzledMethod = class_getInstanceMethod(self, #selector(swizzledBuildMenu(with:)))
else {
fatalError("UIResponder swizzling failed")
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}
@objc dynamic
func swizzledBuildMenu(with builder: UIMenuBuilder) {
if #available(iOS 16, *) {
builder.remove(menu: .application)
builder.remove(menu: .share)
builder.remove(menu: .find)
builder.remove(menu: .file)
builder.remove(menu: .edit)
builder.remove(menu: .view)
builder.remove(menu: .window)
builder.remove(menu: .help)
builder.remove(menu: .about)
builder.remove(menu: .lookup)
builder.remove(menu: .preferences)
builder.remove(menu: .services)
builder.remove(menu: .standardEdit)
}
}
}
extension UIApplication {
static func swizzleAction() {
guard #available(iOS 13.0, *) else {
return
}
guard
let originalMethod = class_getInstanceMethod(self, #selector(buildMenu(with:))),
let swizzledMethod = class_getInstanceMethod(self, #selector(swizzledMenu(with:)))
else {
fatalError("UIResponder swizzling failed")
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}
@objc dynamic
func swizzledMenu(with builder: UIMenuBuilder) {
if #available(iOS 16, *) {
builder.remove(menu: .application)
builder.remove(menu: .share)
builder.remove(menu: .find)
builder.remove(menu: .file)
builder.remove(menu: .edit)
builder.remove(menu: .view)
builder.remove(menu: .window)
builder.remove(menu: .help)
builder.remove(menu: .about)
builder.remove(menu: .lookup)
builder.remove(menu: .preferences)
builder.remove(menu: .services)
builder.remove(menu: .standardEdit)
}
}
}
import Foundation
import QuickLook
class CustomQLPreviewController: QLPreviewController {
private var blockingLayer: CALayer?
private var blockingView: UIView?
private var remoteView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
}
}
private extension CustomQLPreviewController {
func addBlockingView() {
guard blockingView == nil else { return }
if let previewCollectionView = findPreviewCollectionView(in: view) {
self.remoteView = previewCollectionView
let overlay = OverlayView(frame: view.bounds)
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.2)
overlay.translatesAutoresizingMaskIntoConstraints = false
let scrollView = UIScrollView()
scrollView.frame = view.bounds
scrollView.contentSize = CGSize(width: view.bounds.width, height: view.bounds.height * 2)
scrollView.backgroundColor = .systemBackground.withAlphaComponent(0.5)
scrollView.isScrollEnabled = true
if let superView = previewCollectionView.superview {
superView.addSubview(scrollView)
superView.addSubview(overlay)
NSLayoutConstraint.activate([
overlay.topAnchor.constraint(equalTo: superView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: superView.bottomAnchor),
overlay.leadingAnchor.constraint(equalTo: superView.leadingAnchor),
overlay.trailingAnchor.constraint(equalTo: superView.trailingAnchor)
])
}
}
}
func addBlockingLayer() {
guard blockingLayer == nil else { return }
if let previewCollectionView = findPreviewCollectionView(in: view) {
let layer = CALayer()
layer.frame = previewCollectionView.bounds
layer.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.1).cgColor
previewCollectionView.layer.superlayer?.addSublayer(layer)
blockingLayer = layer
}
}
func findPreviewCollectionView(in view: UIView) -> UIView? {
for subview in view.subviews {
if NSStringFromClass(type(of: subview)) == "_UIRemoteView" {
return subview
} else if let foundView = findPreviewCollectionView(in: subview) {
return foundView
}
}
return nil
}
}
extension CustomQLPreviewController: QLPreviewControllerDelegate {
func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? {
addBlockingView()
return nil
}
func previewControllerWillDismiss(_ controller: QLPreviewController) {
blockingView?.removeFromSuperview()
blockingView = nil
}
}
Upvotes: 0
Views: 34