Creating a Fixed-Sized Annotation on iOS

This article explains how to create an annotation that has a fixed size and that doesn’t scale when the page is zoomed in or out. A possible simpler alternative — using note annotations with custom icons — is described at the end.

ℹ️ Note: This will only work with annotations that have the isOverlay flag set, which is not preserved when a document is saved and reopened. As a result, this only works for and is respected by new annotations. Please delete and add annotations that are already in the document when it is being opened if you want to give them a fixed size.

First you need to inherit from the Annotation subclass you want to use a fixed size on, and then you need to override the following properties and methods:

override var fixedSize: CGSize {
    return CGSize(width: 100, height: 100)
}

override var isOverlay: Bool {
    get {
        return true
    }
    set {}
}
- (CGSize)fixedSize {
    return CGSizeMake(100.f, 100.f);
}

- (BOOL)isOverlay {
    return YES;
}

You also need to create a custom AnnotationView subclass that will be used to display the annotation on the page view, as it can’t be rendered on the page like annotations usually would. Rather, it will need to be a custom view on the page view.

The following code shows how to create a custom view that respects the fixed size:

class FixedSizeAnnotationView: AnnotationView {

    private lazy var annotationImageView: UIImageView = {
        let imageView = UIImageView(frame: self.bounds)
        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(annotationImageView)
    }

    override var annotation: Annotation? {
        didSet {
            updateImage()
        }
    }

    override var zoomScale: CGFloat {
        didSet {
            let scale = fmax(1, zoomScale)
            annotationImageView.transform = CGAffineTransform(scaleX: 1 / scale, y: 1 / scale)
        }
    }

    func renderAnnotationImage() -> UIImage? {
        guard let annotation = annotation else { return nil }
        return annotation.image(size: annotation.fixedSize, options: nil)
    }

    override func annotationChangedNotification(_ notification: Notification) {
        super.annotationChangedNotification(notification)
        updateImage()
    }

    func updateImage() {
        annotationImageView.image = renderAnnotationImage()
        annotationImageView.alpha = annotation?.alpha ?? 0
    }
}
@interface PSCFixedSizeAnnotationView : PSPDFAnnotationView
@property (nonatomic, nullable) UIImageView *annotationImageView;
@end

@implementation PSCFixedSizeAnnotationView

- (void)setAnnotation:(PSPDFAnnotation *)annotation {
    super.annotation = annotation;

    if (!self.annotationImageView) {
        UIImageView *annotationImageView = [[UIImageView alloc] initWithFrame:self.bounds];
        annotationImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        annotationImageView.contentMode = UIViewContentModeScaleAspectFit;
        [self addSubview:annotationImageView];
        self.annotationImageView = annotationImageView;
    }
    [self updateImage];
}

- (void)setZoomScale:(CGFloat)zoomScale {
    _zoomScale = zoomScale;
    CGFloat scale = fmax(1.f, zoomScale);
    self.annotationImageView.transform = CGAffineTransformMakeScale(1 / scale, 1 / scale);
}

- (UIImage *)renderAnnotationImage {
    CGSize fixedSize = self.annotation.fixedSize;
    return [self.annotation imageWithSize:fixedSize withOptions:nil];
}

- (void)annotationChangedNotification:(NSNotification *)notification {
    [super annotationChangedNotification:notification];
    [self updateImage];
}

- (void)updateImage {
    self.annotationImageView.image = self.renderAnnotationImage;
    self.annotationImageView.alpha = self.annotation.alpha;
}

@end

To make PSPDFKit use this custom annotation view class, you will need to conform to PDFViewControllerDelegate, implement pdfViewController(:annotationView:for:on:), and return an object of the custom annotation view instead of the default annotation view:

func pdfViewController(_ pdfController: PDFViewController, annotationView: (UIView & AnnotationPresenting)?, for annotation: Annotation, on pageView: PDFPageView) -> (UIView & AnnotationPresenting)? {
    if annotation is StampAnnotation {
        let frame = annotation.boundingBox(forPageRect: pageView.bounds)
        return FixedSizeAnnotationView(frame: frame)
    }
    return annotationView
}
- (nullable UIView<PSPDFAnnotationPresenting> *)pdfViewController:(PSPDFViewController *)pdfController annotationView:(nullable UIView<PSPDFAnnotationPresenting> *)annotationView forAnnotation:(PSPDFAnnotation *)annotation onPageView:(PSPDFPageView *)pageView {
    if ([annotation isKindOfClass:PSPDFStampAnnotation.class]) {
        CGRect frame = [annotation boundingBoxForPageRect:pageView.bounds];
        return [[PSCFixedSizeAnnotationView alloc] initWithFrame:frame];
    }
    return annotationView;
}

Finally, don’t forget to register the custom annotation class with PDFConfigurationBuilder.overrideClass(_:with:) to override the default class defined in PSPDFKit.

Please also have a look at FloatingStampsExample.swift in the PSPDFKit Catalog example project to see this in action.

A Simpler Alternative Using Note Annotations

If you don’t need (and haven’t used) note annotations for other purposes, a simpler alternative to the approach described above is to leverage the fact that note annotations are already displayed by PSPDFKit without zooming. You can register a subclass of the NoteAnnotationView that changes the note annotation rendering to use a different image:

class CustomNoteAnnotationView: NoteAnnotationView {
    override var renderNoteImage: UIImage? {
        UIImage(named: "pspdfkit-logo")
    }
}
@interface CustomNoteAnnotationView : PSPDFNoteAnnotationView
@end

@implementation CustomNoteAnnotationView

- (UIImage *)renderNoteImage {
    return [UIImage imageNamed:@"pspdfkit-logo"];
}

@end