Adding Custom Views to a Page

Some behaviors or appearance customizations can only be achieved by adding custom views to a page. If, for example, you want to customize interactions with a certain annotation type, or you want to display additional UI elements next to certain annotations, or you want to place auxiliary UI elements at certain positions of pages that satisfy particular criteria, adding custom views may be your best option to achieve this.

This guide covers the case where you want to customize the interactions with annotations of a certain type. If this approach doesn’t work for you, (see Caveats). Or, if you want to learn how to place auxiliary content on a page, please look at the companion guide, Adding Auxiliary or Decorative Views.

Use Case: Selectively Adding UI Elements

If you want all annotations of a certain type to display additional UI elements, you can use subclasses of AnnotationView for this purpose. By default, PSPDFKit renders most annotations as part of the page, but it creates an overlay view when an annotation is selected. LinkAnnotation, NoteAnnotation, SoundAnnotation, and FileAnnotation are always displayed in overlay mode.

The first step to using a custom view class in overlay mode is to identify what to subclass. If the annotation has drawn content like, for example, a stamp, a good starting point is HostingAnnotationView and its subclasses. If the annotation is more of a placeholder for content, LinkAnnotationBaseView and its subclasses might be more appropriate.

For this example, let’s say we want a view that adds an information button below some kinds of stamps. We don’t ship a more specific view class for stamps, so we use HostingAnnotationView as our starting point:

class InfoDisplayingStampView: HostingAnnotationView {
    private let infoButton: UIButton
    override required init(frame: CGRect) {
        infoButton = UIButton(type: .infoLight)
        infoButton.translatesAutoresizingMaskIntoConstraints = false

        super.init(frame: frame)

        infoButton.isHidden = false
        infoButton.add(self, action: #selector(showImageInfo(_:)), for: .touchUpInside)

        addSubview(infoButton)
        NSLayoutConstraint.activate([
            leadingAnchor.constraint(equalTo: infoButton.leadingAnchor),
            bottomAnchor.constraint(equalTo: infoButton.bottomAnchor)
        ])
    }

    override var annotation: Annotation? {
        willSet {
            infoButton.isHidden = newValue != nil
        }
    }

    @objc
    func showInfo(_ sender: Any?) {
        // Not useful, but logging does show some info…
        print("Showing \(String(describing: annotation))")
    }
}
@interface PSCInfoDisplayingStampView : PSPDFHostingAnnotationView @end

@implementation PSCInfoDisplayingStampView {
    UIButton *_infoButton;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        _infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
        _infoButton.hidden = YES;
        [_infoButton addTarget:self action:@selector(showInfo:) forControlEvent:UIControlEventTouchUpInside];

        [self addSubview:_infoButton];
        [NSLayoutContstraint activateConstraints:@[
            [self.leadingAnchor constraintEqualToAnchor:_infoButton.leadingAnchor],
            [self.bottomAnchor constraintEqualToAnchor:_infoButton.bottomAnchor],
        ]];
    }

    return self;
}

- (void)setAnnotation:(PSPDFAnnotation *)annotation {
    [super setAnnotation:annotation];
    _infoButton.hidden = [(PSPDFStampAnnotation *)annotation image] != nil;
}

- (void)showInfo:(id)sender {
    // Not useful, but logging does show some info…
    NSLog(@"Showing %@", self.annotation);

}

@end

We don’t want this class to behave drastically different, but we want to have a secondary action that can only be triggered when tapping a smaller portion of the stamp. The information button we added here gives us just that, and the showInfo(_:) action acts as a stand-in for any actual functionality we want to trigger.

Now that we have a custom view class, we need to make sure we can actually use it. Since we said we only want to show the information button on some kinds of stamps instead of all stamps, we can’t rely on type-based overrides for this customization. Instead, we need to decide whether we want our custom view class by inspecting the actual annotation instances.

When a PageView needs to instantiate a new annotation view, it asks AnnotationManager for the class it should use. This is where we can dock in: Whenever a page view needs a new view for a stamp, we’ll return our custom HostingAnnotationView subclass, and we’ll extend StampAnnotation with a computed property to tell us if the stamp has additional information:

class ViewCustomizingAnnotationManager: AnnotationManager {
    override func annotationView(for annotation: Annotation) -> AnyClass? {
        guard
            let stamp = annotation as? StampAnnotation,
            stamp.hasAdditionalMetadata
        else {
            return super.annotationView(for: annotation)
        }

        return InfoDisplayingStampView.self
    }
}

extension StampAnnotation {
    var hasAdditionalMetadata: Bool {
        // Replace with a useful predicate in your own code…
        objectNumber % 2 == 0
    }
}
@interface PSPDFStampAnnotation (PSCHasAdditionalMetadata)

@property (nonatomic, readonly) BOOL psc_hasAdditionalMetadata;

@end

@implementation PSPDFStampAnnotation (PSCHasAdditionalMetadata)

- (BOOL)psc_hasAdditionalMetadata {
    // Replace with a useful predicate in your own code…
    return self.objectNumber % 2 == 0;
}

@end

@interface PSCViewCustomizingAnnotationManager : PSPDFAnnotationManager @end

@implementation PSCViewCustomizingAnnotationManager

- (Class)annotationViewClassForAnnotation:(PSPDFAnnotation *)annotation {
    if ([annotation isKindOfClass:PSPDFStampAnnotation.class] &&
        [(PSPDFStampAnnotation *)annotation psc_hasAdditionalMetadata]) {
        return PSCInfoDisplayingStampView.class;
    }

    return [super annotationViewClassForAnnotation:annotation];
}

@end

There’s at least one more step we need to take, though: By default, Document will create instances of AnnotationManager — not our ViewCustomizingAnnotationManager.

To change this, we have to register a class override with the Document first. For more in-depth information on how to achieve this, please refer to our subclassing guide. Once that’s set up, all stamp annotations returning true from hasAdditionalMetadata will display an information button when they’re selected.

If you also want to use your custom view class when the annotation isn’t selected, you’ll need to tell PSPDFKit that every stamp annotation where the computed property returns true should be displayed in overlay mode. The simplest way to achieve that is by also replacing the built-in StampAnnotation class and overriding its isOverlay getter:

class EagerlyOverlayingStampAnnotation: StampAnnotation {
    override var isOverlay: Bool {
        get { super.isOverlay ?? hasAdditionalMetadata }
        set { super.isOverlay = newValue }
    }
}
@interface PSCEagerlyOverlayingStampAnnotation : PSPDFStampAnnotation @end

@implementation PSCEagerlyOverlayingStampAnnotation

- (BOOL)isOverlay {
    return [super isOverlay] ?: self.psc_hasAdditionalMetadata;
}

@end

Caveats

Customizing the display of TextMarkupAnnotation subclasses in this way isn’t supported. In addition to the rects of the annotation, strikethroughs, underlines, and squiggly lines all need information about the baseline offset within these rects for drawing — information which isn’t readily available. To make matters worse, the marked-up text isn’t limited to being set horizontally or vertically on the page. Instead, it can flow at arbitrary angles, even varying within the same annotation, which can’t be expressed through the rects property.

For these annotation types, consider working with auxiliary views instead.