This recipe, based on John Calhoun’s PDFKitViewer2 example program, lets you edit, save and restore PDFAnnotationWidget values with PDFKit 10.5. Pretty much everything is shoehorned into a category of MyPDFDocument to minimize change to the original code base
(Note that PDFAnnotationWidget isn’t itself a class; it’s a reference to the widget classes like PDFAnnotationTextWidget.)
The recipe simply calls “restorePDFWidgets” when a document is opened and “cachePDFWidgets” when the document is closed.
As you can see, I’m using a free text annotation named “widgetArchive” to hold the PDFAnnotationWidgets. It has a zero dimension, doesn’t display or print and caches the PDFAnnotationWidgets in its contents field.
1. In MyPDFDocument->windowControllerDidLoadNib call the category method [self restorePDFWidgets].
A good place to do this is just after you’ve set the delegate.
2. Complete dataRepresentationOfType to archive the PDFAnnotationWidgets:
- (NSData *) dataRepresentationOfType: (NSString *) aType
{
// For each non-empty widget append the type, fieldName and value to theWidgetCache
[self cachePDFWidgets]; // <-- Call the category method to archive widgets.
NSData * pdfData = [[_pdfView document] dataRepresentation];
return pdfData;
}
3. Add a category for the PDFAnnotationWidget persistence methods:
// MyPDFDocument+PDFForms.m #import "MyPDFDocument+PDFAdditions.h" #import "NSString+PDFAdditions.h" @implementation MyPDFDocument (PDFForms)
4. Implement a method to restore PDFAnnotationWidgets when a document is opened:
- (void) restorePDFWidgets
{
PDFAnnotation * widgetArchive = [self findWidgetArchive];
// Don't do anything here if widgetArchive doesn't exist.
if (widgetArchive != NULL)
{
// internalize widgets from archive.
NSString * widgetCache = [widgetArchive contents];
NSCharacterSet *tokenSeperatorSet = [NSCharacterSet characterSetWithCharactersInString:@"\t"];
NSArray *fieldArray = [widgetCache tokensSeparatedByCharactersFromSet: tokenSeperatorSet];
int i;
int cacheCount = [fieldArray count]/3;
for (i = 0; i/3 < cacheCount; i = i + 3)
{
// fieldArray[i ] -> fieldname
// fieldArray[1+1] -> "Text" or "Button"
// fieldArray[i+2] -> stringValue or state
// Find annot with fieldName
NSString * theFieldName = (NSString *) [fieldArray objectAtIndex: i];
PDFAnnotation * annot = [self annotWithFieldname: theFieldName];
if (annot != NULL)
{
NSString * widgetType = (NSString *) [fieldArray objectAtIndex: i + 1];
if ([widgetType isEqualToString: @"Text"])
{
NSString * theStringValue = (NSString *) [fieldArray objectAtIndex: i + 2];
[(PDFAnnotationTextWidget *) annot setStringValue: theStringValue];
}
else
{
NSString * buttonState = (NSString *) [fieldArray objectAtIndex: i + 2];
if ([buttonState isEqualToString: @"NSOnState"])
{
[(PDFAnnotationButtonWidget *) annot setState: NSOnState];
}
else
{
[(PDFAnnotationButtonWidget *) annot setState: NSOffState];
}
}
}
}
}
}
5. Implement a method to find the PDFAnnotation that contains the widget archive.
- (PDFAnnotation *) findWidgetArchive
{
PDFAnnotation * theWidgetArchive = NULL;
// Iterate the list of pages.
int pageIndex;
int pageCount = [[_pdfView document] pageCount];
for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
PDFPage * currentPage = [[_pdfView document] pageAtIndex: pageIndex];
// Iterate the array of annotations.
int annotIndex;
NSArray * annotations = [currentPage annotations];
int annotCount = [annotations count];
for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
{
PDFAnnotation * annotation = [annotations objectAtIndex: annotIndex];
if ([[annotation userName] isEqualToString: @"widgetArchive"])
{
theWidgetArchive = annotation;
break;
}
}
}
return theWidgetArchive;
}
6. Implement a method to get the PDFAnnotationWidget by name that we want to restore.
- (PDFAnnotation *) annotWithFieldname: (NSString *) fieldName
{
PDFAnnotation * annot = NULL;
// Iterate the list of pages.
int pageIndex;
int pageCount = [[_pdfView document] pageCount];
for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
PDFPage * currentPage = [[_pdfView document] pageAtIndex: pageIndex];
// Iterate the array of annotations.
int annotIndex;
NSArray * annotations = [currentPage annotations];
int annotCount = [annotations count];
for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
{
PDFAnnotation * annotation = [annotations objectAtIndex: annotIndex];
if ([annotation isKindOfClass: [PDFAnnotationTextWidget class]])
{
NSString * theFieldName = [(PDFAnnotationTextWidget *) annotation fieldName];
if ([theFieldName isEqualToString: fieldName])
{
annot = annotation;
break;
}
}
}
}
return annot;
}
7. Implement a method to save PDFAnnotationWidgets when a document is closed:
- (void) cachePDFWidgets
{
PDFAnnotation * widgetArchive = [self findWidgetArchive];
if (widgetArchive != NULL)
{
[[_pdfView currentPage] removeAnnotation: widgetArchive];
}
NSString * theCache = [self widgetsToCache];
// Don't do anything here if there aren't any annots to cache.
if ([theCache length] > 0)
{
// Create new widgetArchive
widgetArchive = [[PDFAnnotationFreeText alloc] initWithBounds: NSMakeRect(0, 0, 0, 0)];
[widgetArchive setUserName: @"widgetArchive"];
[widgetArchive setShouldDisplay: NO];
[widgetArchive setShouldPrint: NO];
[widgetArchive setContents: theCache];
[[_pdfView currentPage] addAnnotation: widgetArchive];
}
}
8. Implement a method to get the widgets to cache:
- (NSString *) widgetsToCache
{
NSString * theWidgetCache = @"";
// Iterate the list of pages.
int pageIndex;
int pageCount = [[_pdfView document] pageCount];
for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
PDFPage * currentPage = [[_pdfView document] pageAtIndex: pageIndex];
// Iterate the array of annotations.
int annotIndex;
NSArray * annotations = [currentPage annotations];
int annotCount = [annotations count];
for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
{
PDFAnnotation * annotation = [annotations objectAtIndex: annotIndex];
NSString * theFieldName = @"";
NSString * theWidgetString = @"";
if ([annotation isKindOfClass: [PDFAnnotationTextWidget class]])
{
theFieldName = [(PDFAnnotationTextWidget *) annotation fieldName];
theWidgetString = [(PDFAnnotationTextWidget *) annotation stringValue];
if ([theWidgetString length] > 0)
{
theWidgetCache = [theWidgetCache stringByAppendingString: theFieldName];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"Text"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
theWidgetCache = [theWidgetCache stringByAppendingString: theWidgetString];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
}
}
else
if ([annotation isKindOfClass: [PDFAnnotationButtonWidget class]])
{
theFieldName = [(PDFAnnotationButtonWidget *) annotation fieldName];
int theWidgetstate = [(PDFAnnotationButtonWidget *) annotation state];
if (theWidgetstate == NSOnState)
{
theWidgetCache = [theWidgetCache stringByAppendingString: theFieldName];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"Button"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"ON"];
theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
}
}
}
}
return theWidgetCache;
}
@end // End of MyPDFDocument (PDFForms)
9. Finally, implement an NSString category containing this utility method that tokenizes the widget string.
// NSString+PDQAdditions.m
#import "NSString+PDQAdditions.h"
@implementation NSString (PDQAdditions)
- (NSArray *)tokensSeparatedByCharactersFromSet:(NSCharacterSet *)separatorSet
{
NSScanner *scanner = [NSScanner scannerWithString:self];
NSCharacterSet *tokenSet = [separatorSet invertedSet];
NSMutableArray *tokens = [NSMutableArray array];
[scanner setCharactersToBeSkipped:separatorSet];
while (![scanner isAtEnd])
{
NSString *destination = [NSString string];
if ([scanner scanCharactersFromSet:tokenSet intoString:&destination])
{
[tokens addObject:[NSString stringWithString:destination]];
}
}
return [NSArray arrayWithArray:tokens];
}
@end
And one more thing…
In order to enable the Save menu command, it is necessary to change the “Role” from Viewer to Editor in the Target Info Properties panel!