Vitaliy
Vitaliy

Reputation: 21

How to Disable Text Selection and Editing in QLPreviewController for PDF and Text Files in Swift?

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

Answers (0)

Related Questions