Customize PDF View Controller States on iOS

PDFViewController is PSPDFKit’s principle UI component for displaying documents, and it can be in five different states:

State Screenshot
Default Screenshot of default state showing the first page of the PSPDFKit 11 Quickstart Guide document.
Loading Screenshot of loading state with a progress bar.
Empty Screenshot of empty state with message “No Document Set.”
Locked Screenshot of locked state with a password text field. Locked: Please enter the password.
Error Screenshot of error state with error message “Unable to Display Document: The document couldn’t be accessed.”

You can see each of these states in ControllerStateExample in the PSPDFKit Catalog app on GitHub.

Take a look at the ControllerState API reference for more information.

Custom Overlay UI

You can change the state’s strings and images, or you can set PDFViewController.overlayViewController to take care of state handling yourself.

To create an overlay view controller, you have to implement the ControllerStateHandling protocol in a UIViewController subclass.

Implementing all of the states, with the exception of locked, is pretty straightforward, because they don’t feature any interaction. To unlock documents, you need to add a text field and handle the keyboard accordingly. Use Document.unlock(withPassword:) to unlock the document. After that, you need to reload the PDFViewController with reloadData().

The following code snippets should help you correctly create your own overlay view controller:

class OverlayViewController: UIViewController, ControllerStateHandling {

    // MARK: Properties

    weak var pdfController: PDFViewController!

    private let label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.numberOfLines = 0
        label.textColor = .gray
        label.textAlignment = .center
        return label
    }()

    private let textField: UITextField = {
        let textField = UITextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.isSecureTextEntry = true
        textField.autocorrectionType = .no
        textField.autocapitalizationType = .none
        textField.borderStyle = .roundedRect
        return textField
    }()

    private let button: UIButton = {
        let button = UIButton(type: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Unlock", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.sizeToFit()
        return button
    }()

    // MARK: UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        button.addTarget(self, action: #selector(unlock), for: .touchUpInside)

        let stackView = UIStackView(arrangedSubviews: [label, textField, button])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        stackView.spacing = 20
        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.widthAnchor.constraint(equalToConstant: 300),
            stackView.heightAnchor.constraint(equalToConstant: 150),
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    // MARK: Button Actions

    @objc
    private func unlock(sender: UIButton) {
        guard let document = document, let password = textField.text else { return }
        document.unlock(withPassword: password)
        pdfController.reloadData()
    }

    // MARK: ControllerStateHandling

    var document: Document?

    public func setControllerState(_ state: ControllerState, error: Error?, animated: Bool) {
        var text = ""
        var backgroundColor: UIColor? = .white

        switch state {
        case .default:
            backgroundColor = nil
        case .empty:
            text = "No document set"
        case .loading:
            text = "Loading..."
        case .locked:
            text = "Password:"
        case .error:
            text = "Unable to display document:\n\(error!.localizedDescription)"
        }

        label.text = text
        view.backgroundColor = backgroundColor
        view.isUserInteractionEnabled = state != .default

        if state == .locked {
            textField.isHidden = false
            button.isHidden = false
            textField.becomeFirstResponder()
        } else {
            textField.isHidden = true
            button.isHidden = true
            textField.resignFirstResponder()
        }
    }
}
@interface OverlayViewController : UIViewController <PSPDFControllerStateHandling>

@property (nonatomic, weak) PSPDFViewController *pdfController;

@end

@interface OverlayViewController ()

@property (nonatomic) UILabel *label;
@property (nonatomic) UITextField *textField;
@property (nonatomic) UIButton *button;

@end

@implementation OverlayViewController

#pragma mark - UIViewController

-(void)viewDidLoad {
    [super viewDidLoad];

    self.label = [UILabel new];
    self.label.translatesAutoresizingMaskIntoConstraints = NO;
    self.label.numberOfLines = 0;
    self.label.textColor = [UIColor grayColor];
    self.label.textAlignment = NSTextAlignmentCenter;

    self.textField = [UITextField new];
    self.textField.translatesAutoresizingMaskIntoConstraints = NO;
    self.textField.secureTextEntry = YES;
    self.textField.autocorrectionType = UITextAutocorrectionTypeNo;
    self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
    self.textField.borderStyle = UITextBorderStyleRoundedRect;

    self.button = [UIButton buttonWithType:UIButtonTypeSystem];
    self.button.translatesAutoresizingMaskIntoConstraints = NO;
    [self.button setTitle:@"Unlock" forState:UIControlStateNormal];
    [self.button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [self.button sizeToFit];
    [self.button addTarget:self action:@selector(unlock:) forControlEvents:UIControlEventTouchUpInside];

    UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[self.label, self.textField, self.button]];
    stackView.translatesAutoresizingMaskIntoConstraints = NO;
    stackView.axis = UILayoutConstraintAxisVertical;
    stackView.distribution = UIStackViewDistributionFillEqually;
    stackView.spacing = 20;
    [self.view addSubview:stackView];

    [NSLayoutConstraint activateConstraints:@[
        [stackView.widthAnchor constraintEqualToConstant:300],
        [stackView.heightAnchor constraintEqualToConstant:150],
        [stackView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [stackView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
    ]];
}

#pragma mark - Button Actions

- (void)unlock:(id)sender {
    NSString *password = self.textField.text;
    [self.document unlockWithPassword:password];
    [self.pdfController reloadData];
}

#pragma mark - PSPDFControllerStateHandling

@synthesize document;

-(void)setControllerState:(PSPDFControllerState)state error:(NSError *)error animated:(BOOL)animated {
    NSString *text = @"";
    UIColor *backgroundColor = [UIColor whiteColor];

    switch (state) {
        case PSPDFControllerStateDefault:
            backgroundColor = nil;
            break;

        case PSPDFControllerStateEmpty:
            text = @"No document set";
            break;

        case PSPDFControllerStateLoading:
            text = @"Loading...";
            break;

        case PSPDFControllerStateLocked:
            text = @"Password:";
            break;

        case PSPDFControllerStateError:
            text = [NSString stringWithFormat:@"Unable to display document:\n%@", error.localizedDescription];
            break;

        default:
            break;
    }

    self.label.text = text;
    self.view.backgroundColor = backgroundColor;
    self.view.userInteractionEnabled = state != PSPDFControllerStateDefault;

    if (state == PSPDFControllerStateLocked) {
        self.textField.hidden = NO;
        self.button.hidden = NO;
        [self.textField becomeFirstResponder];
    } else {
        self.textField.hidden = YES;
        self.button.hidden = YES;
        [self.textField resignFirstResponder];
    }
}

@end

Connect the custom overlay view controller to the PDF view controller like this:

let pdfController = PDFViewController(document: document)
let overlayViewController = OverlayViewController()
overlayViewController.pdfController = pdfController
pdfController.overlayViewController = overlayViewController
PSPDFViewController *pdfController = [[PSPDFViewController alloc] initWithDocument:document];
OverlayViewController *overlayViewController = [OverlayViewController new];
overlayViewController.pdfController = pdfController;
pdfController.overlayViewController = overlayViewController;