Skip to content

Instantly share code, notes, and snippets.

@jevinskie
Created April 16, 2025 20:58
Show Gist options
  • Save jevinskie/56100a079c1c264d50f23a8af1ddf327 to your computer and use it in GitHub Desktop.
Save jevinskie/56100a079c1c264d50f23a8af1ddf327 to your computer and use it in GitHub Desktop.
Spotlight Importer.xctemplate
//==============================================================================
// Core Data Application Spotlight Importer
//==============================================================================
Spotlight importers should be provided by all applications that support custom
document formats. A Spotlight importer parses your document format for relevant
information and assigning that information to the appropriate metadata keys.
The bundle target in this project creates a Spotlight importer bundle installed
inside of the wrapper of the application target. This bundle includes all of
the code necessary to import two types of information into Spotlight:
1) The metadata information from Core Data stores.
The only default metadata for a Core Data store is the store ID and store type,
neither of which is imported. To have metadata from your stores imported, you
must first add the information you are interested to the metadata for your
store (see the NSPersistentStoreCoordinator setMetadataForPersistentStore: API)
and then pull the information for import in the GetMetadataForFile function in
the 'GetMetadataForFile.c' file.
2) Instance Level indexing information
For each instance of an entity that contains properties indexed by Spotlight
(defined in your Core Data Model), the importer will extract the values. This
extraction is done in MySpotlightImporter.m
Additionally, the importer must contain a list of the Uniform Type Identifiers
(UTI) for your application in order to import the data. (The UTI information is
used by Spotlight to know which importer to invoke for a given file.) If the
UTI is not already registered by your application, you will need to register it
in the importer bundle. (For more information on registering UTIs for
applications, consult the documentation at http://developer.apple.com)
-----------------------------------------------------------------------------
To configure this project
Search for all occurrences of the string YOUR_ and replace with the appropriate
values
When importing store file metadata
YOUR_STORE_FILE_UTI - UTI of your store file
YOUR_INFO - metadata information you want Spotlight to have for
your store file
when importing record level information
YOUR_EXTERNAL_RECORD_UTI - UTI of the Core Data Spotlight external record file
YOUR_EXTERNAL_RECORD_EXTENSION - extension of the Core Data external record file
YOUR_STORE_TYPE - type of your persistent store
Replace occurrences of the above strings in the following files
GetMetadataForFile.c
MySpotlightImporter.m
Info.plist
-----------------------------------------------------------------------------
//
// ___FILENAME___
// ___PACKAGENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
//___COPYRIGHT___
//
#include <CoreFoundation/CoreFoundation.h>
#import <CoreData/CoreData.h>
#import "MySpotlightImporter.h"
Boolean GetMetadataForFile(void *thisInterface, CFMutableDictionaryRef attributes, CFStringRef contentTypeUTI, CFStringRef pathToFile);
//==============================================================================
//
// Get metadata attributes from document files
//
// The purpose of this function is to extract useful information from the
// file formats for your document, and set the values into the attribute
// dictionary for Spotlight to include.
//
//==============================================================================
Boolean GetMetadataForFile(void *thisInterface, CFMutableDictionaryRef attributes, CFStringRef contentTypeUTI, CFStringRef pathToFile)
{
// Pull any available metadata from the file at the specified path
// Return the attribute keys and attribute values in the dict
// Return TRUE if successful, FALSE if there was no data provided
// The path could point to either a Core Data store file in which
// case we import the store's metadata, or it could point to a Core
// Data external record file for a specific record instances
Boolean ok = FALSE;
@autoreleasepool {
NSError *error = nil;
if ([(__bridge NSString *)contentTypeUTI isEqualToString:@"YOUR_STORE_FILE_UTI"]) {
// import from store file metadata
// Create the URL, then attempt to get the meta-data from the store
NSURL *url = [NSURL fileURLWithPath:(__bridge NSString *)pathToFile];
// This uses NSXMLStoreType. If your store is a different type, specify the correct type here.
NSDictionary *metadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:YOUR_STORE_TYPE URL:url options:nil error:&error];
// If there is no error, add the info
if (error == NULL) {
// Get the information you are interested in from the dictionary
// "YOUR_INFO" should be replaced by key(s) you are interested in
NSObject *contentToIndex = metadata[@"YOUR_INFO"];
if (contentToIndex != nil) {
// Add the metadata to the text content for indexing
((__bridge NSMutableDictionary *)attributes)[(NSString *)kMDItemTextContent] = contentToIndex;
ok = TRUE;
}
}
} else if ([(__bridge NSString *)contentTypeUTI isEqualToString:@"YOUR_EXTERNAL_RECORD_UTI"]) {
// import from an external record file
MySpotlightImporter *importer = [[MySpotlightImporter alloc] init];
ok = [importer importFileAtPath:(__bridge NSString *)pathToFile attributes:(__bridge NSMutableDictionary *)attributes error:&error];
}
}
// Return the status
return ok;
}
//
// ___FILENAME___
// ___PACKAGENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
//___COPYRIGHT___
//
//==============================================================================
//
// DO NO MODIFY THE CONTENT OF THIS FILE
//
// This file contains the generic CFPlug-in code necessary for your importer
// To complete your importer implement the function in GetMetadataForFile.c
//
//==============================================================================
#import <CoreFoundation/CoreFoundation.h>
#import <CoreFoundation/CFPlugInCOM.h>
#import <CoreServices/CoreServices.h>
// -----------------------------------------------------------------------------
// constants
// -----------------------------------------------------------------------------
#define PLUGIN_ID "___UUID___"
//
// Below is the generic glue code for all plug-ins.
//
// You should not have to modify this code aside from changing
// names if you decide to change the names defined in the Info.plist
//
// -----------------------------------------------------------------------------
// typedefs
// -----------------------------------------------------------------------------
// The import function to be implemented in GetMetadataForFile.c
Boolean GetMetadataForFile(void *thisInterface,
CFMutableDictionaryRef attributes,
CFStringRef contentTypeUTI,
CFStringRef pathToFile);
// The layout for an instance of MetaDataImporterPlugIn
typedef struct __MetadataImporterPluginType
{
MDImporterInterfaceStruct *conduitInterface;
CFUUIDRef factoryID;
UInt32 refCount;
} MetadataImporterPluginType;
// -----------------------------------------------------------------------------
// prototypes
// -----------------------------------------------------------------------------
// Forward declaration for the IUnknown implementation.
//
MetadataImporterPluginType *AllocMetadataImporterPluginType(CFUUIDRef inFactoryID);
void DeallocMetadataImporterPluginType(MetadataImporterPluginType *thisInstance);
HRESULT MetadataImporterQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv);
void *MetadataImporterPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID);
ULONG MetadataImporterPluginAddRef(void *thisInstance);
ULONG MetadataImporterPluginRelease(void *thisInstance);
// -----------------------------------------------------------------------------
// testInterfaceFtbl definition
// -----------------------------------------------------------------------------
// The TestInterface function table.
//
static MDImporterInterfaceStruct testInterfaceFtbl = {
NULL,
MetadataImporterQueryInterface,
MetadataImporterPluginAddRef,
MetadataImporterPluginRelease,
GetMetadataForFile
};
// -----------------------------------------------------------------------------
// AllocMetadataImporterPluginType
// -----------------------------------------------------------------------------
// Utility function that allocates a new instance.
// You can do some initial setup for the importer here if you wish
// like allocating globals etc...
//
MetadataImporterPluginType *AllocMetadataImporterPluginType(CFUUIDRef inFactoryID)
{
MetadataImporterPluginType *theNewInstance;
theNewInstance = (MetadataImporterPluginType *)malloc(sizeof(MetadataImporterPluginType));
memset(theNewInstance,0,sizeof(MetadataImporterPluginType));
/* Point to the function table */
theNewInstance->conduitInterface = &testInterfaceFtbl;
/* Retain and keep an open instance refcount for each factory. */
theNewInstance->factoryID = CFRetain(inFactoryID);
CFPlugInAddInstanceForFactory(inFactoryID);
/* This function returns the IUnknown interface so set the refCount to one. */
theNewInstance->refCount = 1;
return theNewInstance;
}
// -----------------------------------------------------------------------------
// Dealloc___PACKAGENAMEASIDENTIFIER___MDImporterPluginType
// -----------------------------------------------------------------------------
// Utility function that deallocates the instance when
// the refCount goes to zero.
// In the current implementation importer interfaces are never deallocated
// but implement this as this might change in the future
//
void DeallocMetadataImporterPluginType(MetadataImporterPluginType *thisInstance)
{
CFUUIDRef theFactoryID;
theFactoryID = thisInstance->factoryID;
free(thisInstance);
if (theFactoryID){
CFPlugInRemoveInstanceForFactory(theFactoryID);
CFRelease(theFactoryID);
}
}
// -----------------------------------------------------------------------------
// MetadataImporterQueryInterface
// -----------------------------------------------------------------------------
// Implementation of the IUnknown QueryInterface function.
//
HRESULT MetadataImporterQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv)
{
CFUUIDRef interfaceID;
interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault,iid);
if (CFEqual(interfaceID,kMDImporterInterfaceID)){
/* If the Right interface was requested, bump the ref count,
* set the ppv parameter equal to the instance, and
* return good status.
*/
((MetadataImporterPluginType*)thisInstance)->conduitInterface->AddRef(thisInstance);
*ppv = thisInstance;
CFRelease(interfaceID);
return S_OK;
}else{
if (CFEqual(interfaceID,IUnknownUUID)){
/* If the IUnknown interface was requested, same as above. */
((MetadataImporterPluginType*)thisInstance )->conduitInterface->AddRef(thisInstance);
*ppv = thisInstance;
CFRelease(interfaceID);
return S_OK;
}else{
/* Requested interface unknown, bail with error. */
*ppv = NULL;
CFRelease(interfaceID);
return E_NOINTERFACE;
}
}
}
// -----------------------------------------------------------------------------
// MetadataImporterPluginAddRef
// -----------------------------------------------------------------------------
// Implementation of reference counting for this type. Whenever an interface
// is requested, bump the refCount for the instance. NOTE: returning the
// refcount is a convention but is not required so don't rely on it.
//
ULONG MetadataImporterPluginAddRef(void *thisInstance)
{
((MetadataImporterPluginType *)thisInstance )->refCount += 1;
return ((MetadataImporterPluginType*) thisInstance)->refCount;
}
// -----------------------------------------------------------------------------
// SampleCMPluginRelease
// -----------------------------------------------------------------------------
// When an interface is released, decrement the refCount.
// If the refCount goes to zero, deallocate the instance.
//
ULONG MetadataImporterPluginRelease(void *thisInstance)
{
((MetadataImporterPluginType*)thisInstance)->refCount -= 1;
if (((MetadataImporterPluginType*)thisInstance)->refCount == 0){
DeallocMetadataImporterPluginType((MetadataImporterPluginType*)thisInstance );
return 0;
}else{
return ((MetadataImporterPluginType*) thisInstance )->refCount;
}
}
// -----------------------------------------------------------------------------
// ___PACKAGENAMEASIDENTIFIER___MDImporterPluginFactory
// -----------------------------------------------------------------------------
// Implementation of the factory function for this type.
//
void *MetadataImporterPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID)
{
MetadataImporterPluginType *result;
CFUUIDRef uuid;
/* If correct type is being requested, allocate an
* instance of TestType and return the IUnknown interface.
*/
if (CFEqual(typeID,kMDImporterTypeID)){
uuid = CFUUIDCreateFromString(kCFAllocatorDefault,CFSTR(PLUGIN_ID));
result = AllocMetadataImporterPluginType(uuid);
CFRelease(uuid);
return result;
}
/* If the requested type is incorrect, return NULL. */
return NULL;
}
//
// ___FILENAME___
// ___PACKAGENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
//___COPYRIGHT___
//
#import <Cocoa/Cocoa.h>
#define YOUR_STORE_TYPE NSXMLStoreType
@interface MySpotlightImporter : NSObject
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
- (BOOL)importFileAtPath:(NSString *)filePath attributes:(NSMutableDictionary *)attributes error:(NSError **)error;
@end
//
// ___FILENAME___
// ___PACKAGENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
//___COPYRIGHT___
//
#import "MySpotlightImporter.h"
@interface MySpotlightImporter ()
@property (nonatomic, strong) NSURL *modelURL;
@property (nonatomic, strong) NSURL *storeURL;
@end
@implementation MySpotlightImporter
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize managedObjectContext = _managedObjectContext;
- (BOOL)importFileAtPath:(NSString *)filePath attributes:(NSMutableDictionary *)spotlightData error:(NSError **)error
{
NSDictionary *pathInfo = [NSPersistentStoreCoordinator elementsDerivedFromExternalRecordURL:[NSURL fileURLWithPath:filePath]];
self.modelURL = [NSURL fileURLWithPath:[pathInfo valueForKey:NSModelPathKey]];
self.storeURL = [NSURL fileURLWithPath:[pathInfo valueForKey:NSStorePathKey]];
NSURL *objectURI = [pathInfo valueForKey:NSObjectURIKey];
NSManagedObjectID *oid = [[self persistentStoreCoordinator] managedObjectIDForURIRepresentation:objectURI];
if (!oid) {
NSLog(@"%@:%@ to find object id from path %@", [self class], NSStringFromSelector(_cmd), filePath);
return NO;
}
NSManagedObject *instance = [[self managedObjectContext] objectWithID:oid];
// how you process each instance will depend on the entity that the instance belongs to
if ([[[instance entity] name] isEqualToString:@"YOUR_ENTITY_NAME"]) {
// set the display name for Spotlight search result
NSString *yourDisplayString = [NSString stringWithFormat:@"YOUR_DISPLAY_STRING %@", [instance valueForKey:@"SOME_KEY"]];
spotlightData[(NSString *)kMDItemDisplayName] = yourDisplayString;
/*
Determine how you want to store the instance information in 'spotlightData' dictionary.
For each property, pick the key kMDItem... from MDItem.h that best fits its content.
If appropriate, aggregate the values of multiple properties before setting them in the dictionary.
For relationships, you may want to flatten values.
id YOUR_FIELD_VALUE = [instance valueForKey:ATTRIBUTE_NAME];
spotlightData[(NSString *) kMDItem...] = YOUR_FIELD_VALUE;
... more property values;
To determine if a property should be indexed, call isIndexedBySpotlight
*/
}
return YES;
}
static NSURL *cachedModelURL = nil;
static NSManagedObjectModel *cachedModel = nil;
static NSDate *cachedModelModificationDate =nil;
// Returns the managed object model. The last read model is cached in a global variable and reused if the URL and modification date are identical
- (NSManagedObjectModel *)managedObjectModel
{
if (_managedObjectModel != nil)
return _managedObjectModel;
NSDictionary *modelFileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.modelURL path] error:nil];
NSDate *modelModificationDate = modelFileAttributes[NSFileModificationDate];
if ([cachedModelURL isEqual:self.modelURL] && [modelModificationDate isEqualToDate:cachedModelModificationDate]) {
_managedObjectModel = cachedModel;
}
if (!_managedObjectModel) {
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL];
if (!_managedObjectModel) {
NSLog(@"%@:%@ unable to load model at URL %@", [self class], NSStringFromSelector(_cmd), self.modelURL);
return nil;
}
// Clear out all custom classes used by the model to avoid having to link them
// with the importer. Remove this code if you need to access your custom logic.
NSString *managedObjectClassName = [NSManagedObject className];
for (NSEntityDescription *entity in _managedObjectModel) {
[entity setManagedObjectClassName:managedObjectClassName];
}
// cache last loaded model
cachedModelURL = self.modelURL;
cachedModel = _managedObjectModel;
cachedModelModificationDate = modelModificationDate;
}
return _managedObjectModel;
}
// Returns the persistent store coordinator for the importer.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator)
return _persistentStoreCoordinator;
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:YOUR_STORE_TYPE configuration:nil URL:self.storeURL options:nil error:&error]) {
NSLog(@"%@:%@ unable to add persistent store coordinator - %@", [self class], NSStringFromSelector(_cmd), error);
}
return _persistentStoreCoordinator;
}
// Returns the managed object context for the importer; already bound to the persistent store coordinator.
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
NSLog(@"%@:%@ unable to get persistent store coordinator", [self class], NSStringFromSelector(_cmd));
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
@end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Kind</key>
<string>Xcode.Xcode3.ProjectTemplateUnitKind</string>
<key>Identifier</key>
<string>com.apple.dt.unit.spotlightImporter</string>
<key>Ancestors</key>
<array>
<string>com.apple.dt.unit.systemPlugInBase</string>
</array>
<key>Concrete</key>
<true/>
<key>Description</key>
<string>This template builds a Core Data Spotlight Importer.</string>
<key>AssociatedTargetSpecification</key>
<dict>
<key>PopUpTitle</key>
<string>Embed in Application:</string>
<key>PopUpDescription</key>
<string>The application target that will host this new importer. The application will be set up to embed the new importer.</string>
<key>AllowableProductTypes</key>
<array>
<string>com.apple.product-type.application</string>
</array>
<key>AllowablePlatforms</key>
<array>
<string>com.apple.platform.macosx</string>
</array>
<key>AssociatedTargetIsDependent</key>
<true/>
<key>AssociatedTargetNeedsProductBuildPhaseInjection</key>
<true/>
</dict>
<key>Targets</key>
<array>
<dict>
<key>ProductType</key>
<string>com.apple.product-type.spotlight-importer</string>
<key>TargetIdentifier</key>
<string>com.apple.dt.cocoaSpotlightImporterTarget</string>
<key>SharedSettings</key>
<dict>
<key>INSTALL_PATH</key>
<string>@executable_path/../Contents/Library/Spotlight</string>
<key>SKIP_INSTALL</key>
<string>YES</string>
<key>WRAPPER_EXTENSION</key>
<string>mdimporter</string>
<key>ALWAYS_SEARCH_USER_PATHS</key>
<string>YES</string>
<key>LIBRARY_STYLE</key>
<string>BUNDLE</string>
</dict>
<key>BuildPhases</key>
<array>
<dict>
<key>Class</key>
<string>Sources</string>
</dict>
<dict>
<key>Class</key>
<string>Frameworks</string>
</dict>
<dict>
<key>Class</key>
<string>Resources</string>
</dict>
</array>
</dict>
</array>
<key>Nodes</key>
<array>
<string>main.c</string>
<string>GetMetadataForFile.m</string>
<string>MySpotlightImporter.h</string>
<string>MySpotlightImporter.m</string>
<string>Info.plist:spotlight</string>
<string>Info.plist:NSHumanReadableCopyright</string>
<string>Importer Read Me.txt</string>
</array>
<key>Definitions</key>
<dict>
<key>main.c</key>
<dict>
<key>Path</key>
<string>main.c</string>
<key>Group</key>
<string>Supporting Files</string>
</dict>
<key>GetMetadataForFile.m</key>
<dict>
<key>Path</key>
<string>GetMetadataForFile.m</string>
</dict>
<key>MySpotlightImporter.h</key>
<dict>
<key>Path</key>
<string>MySpotlightImporter.h</string>
</dict>
<key>MySpotlightImporter.m</key>
<dict>
<key>Path</key>
<string>MySpotlightImporter.m</string>
</dict>
<key>Info.plist:spotlight</key>
<string><![CDATA[<!--
If your application does not already define a UTI, you may want to declare it
here, so that your documents are recognized by systems which do not have your
application installed.
To export this declaration, fill in the fields with your application&apos;s
information, and uncomment the block of XML.
-->
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>YOUR_EXTERNAL_RECORD_UTI</string>
<key>UTTypeReferenceURL</key>
<string>http://www.company.com/yourproduct</string>
<key>UTTypeDescription</key>
<string>Your Document Kind String</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>com.apple.ostype</key>
<string>XXXX</string>
<key>public.filename-extension</key>
<array>
<string>YOUR_EXTERNAL_RECORD_EXTENSION</string>
</array>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>MDImporter</string>
<key>LSItemContentTypes</key>
<array>
<string>YOUR_EXTERNAL_RECORD_UTI</string>
</array>
</dict>
</array>
<key>CFPlugInDynamicRegisterFunction</key>
<string></string>
<key>CFPlugInDynamicRegistration</key>
<string>NO</string>
<key>CFPlugInFactories</key>
<dict>
<key>___UUID___</key>
<string>MetadataImporterPluginFactory</string>
</dict>
<key>CFPlugInTypes</key>
<dict>
<key>8B08C4BF-415B-11D8-B3F9-0003936726FC</key>
<array>
<string>___UUID___</string>
</array>
</dict>
<key>CFPlugInUnloadFunction</key>
<string></string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
]]></string>
<key>Importer Read Me.txt</key>
<dict>
<key>Path</key>
<string>Importer Read Me.txt</string>
<key>TargetIdentifiers</key>
<array/>
</dict>
</dict>
</dict>
</plist>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment