Могу ли я написать импортер Spotlight в Swift?

Мне нужно написать Spotlight Importer для приложения, написанного на Swift, и я ссылаюсь на официальное руководство Apple по написанию Spotlight Importer.

Это кажется достаточно простым, однако создание проекта Spotlight Importer создает настройку по умолчанию для реализации Objective-C. Теперь работа с Objective-C не является большой проблемой (я использовал это много раз в прошлом), но все, что я написал для своего приложения, написано на Swift, так что я бы действительно хотел написать импортер в Swift, чтобы избежать переключения между языками, а также чтобы я мог поделиться некоторыми кодами, которые я уже сделал для чтения / записи файлов.

Во-первых, возможно ли написать Spotlight Importer, используя Swift вместо Objective-C? И если это так, с чего мне начать (например, если я возьму отправную точку Objective-C, что я сделаю, чтобы вместо этого переключиться на Swift)?

3 ответа

Решение

Так как Apple представила Swift как язык, который идеально совместим с любым существующим проектом Objective-C, я бы посоветовал вам начать с того, что облегчает вам задачу.

Если вы знаете Swift лучше всего, то ничто не мешает вам использовать это - для любого проекта, который вы можете захотеть. Если вы хотите следовать руководству, которое было написано для Objective-C и еще не обновлено для Swift, я думаю, у вас есть два варианта (я лично рекомендую перейти ко второму варианту):

  1. Напишите ту же логику, написанную на Objective-C в учебном пособии, теперь на Swift с нуля (почти все, что возможно в Objective-C, также легко возможно с помощью Swift). Для этого вам нужно понять основы Objective-C и соответствующий синтаксис и функции в Swift.

  2. Начните с Objective-C, чтобы следовать учебному пособию, и в начале все будет проще (не нужно по-настоящему разбираться в деталях учебника). Затем используйте отличную возможность смешивать и сопоставлять код Swift с кодом Objective-C, чтобы настроить код для своих нужд или дополнить его собственными существующими классами.

Более конкретно по второму варианту:

Если вы хотите писать новые классы, просто используйте Swift - вы можете прекрасно использовать все, что написано в Objective-C изнутри Swift и наоборот. Если вы чувствуете, что вам нужно изменить классы, уже написанные в Objective-C, у вас есть следующие варианты: расширить класс, написанный на Objective-C, новым классом Swift, переписать этот конкретный файл в Swift или просто отредактировать файл Objective-C напрямую,

Чтобы узнать больше о том, как смешивать и сочетать код Swift с Objective-C, я рекомендую прочитать официальную документацию Apple. Это часть бесплатного iBook "Использование Swift с какао и Objective-C", написанного инженерами Apple для разработчиков.


К сожалению, Apple на самом деле, кажется, предоставляет свой шаблон для Spotlight Importer из XCode для Objective-C только на данный момент. Не знаю, почему это так - я не вижу ничего, что мешало бы им поддержать Свифта. Вероятно, мы должны сообщить об этом в Apples Bug Reporter, чтобы подчеркнуть тот факт, что люди на самом деле просят об этом.

Надеюсь, я здесь ничего не упустил, иначе мой ответ будет бессмысленным. ^^


ОБНОВЛЕНИЕ (запрос) Вот несколько шагов о том, с чего начать реализацию первого подхода:

  • Сначала создайте проект Spotlight Importer с последней версией XCode. Создайте новый класс "Cocoa Touch", названный точно так же, как ваши предварительно созданные основные классы Objective C (например, "MySpotlightImporter").
  • Выберите Swift и "Создать заголовок моста" при запросе во время создания класса. Повторно реализуйте код, написанный в классе ObjC-MySpotlightImporter в классе Swift (вы можете создать приложение Cocoa с поддержкой Core Data в Swift и Objective-C для получить некоторое представление об их различиях) - я не уверен, что вы можете переписать GetMetaDataFile.m в Swift, я не смог понять это в своем тесте, поэтому вам, возможно, нужно сохранить его (пока) - В случае, если по пути вы получите какие-либо ошибки, которые указывают на некоторую недостающую конфигурацию, просто найдите соответствующие файлы / классы в проектах "Настройки сборки" и примените ваши изменения там.

Я надеюсь, что это поможет вам начать и достаточно конкретно. Я попытался внести необходимые изменения самостоятельно, чтобы предоставить пример проекта в Swift, но, к сожалению, я не смог заставить его работать в течение ограниченного времени. Возможно, вы захотите предоставить свой код публично (например, на GitHub со ссылкой, размещенной здесь), если вы решите портировать его самостоятельно, чтобы другие тоже могли извлечь из этого выгоду.

Удачи!

Да, импортер Spotlight можно полностью* написать на Swift!

*за исключением нескольких строк кода в main.m

Я только что опубликовал один здесь:

Вот подробный пост в блоге о процессе реализации:

Сложной частью этого является реализация плагина, совместимого с архитектурой CFPlugIn. (Логика, специфичная для MDImporter, относительно минимальна.) CFPlugIn API основан на Microsoft COM , а документам Apple уже почти 20 лет.

Ожидается, что плагин будет блоком памяти, соответствующим определенной структуре памяти — в частности, первое значение в блоке должно быть указателем на таблицу виртуальных функций (vtable) для запрошенного интерфейса (в случае MDImporter это либоMDImporterInterfaceStructилиMDImporterURLInterfaceStruct) или базовый интерфейс IUnknown. Этот макет задокументирован здесь .

Я хотел организовать код Swift в класс, но вы не можете контролировать расположение памяти экземпляра класса Swift. Поэтому я создал блок памяти «оболочки» , который содержит виртуальную таблицу и небезопасный указатель на экземпляр класса. В классе естьstatic func allocate()который использует UnsafeMutablePointer для выделения блока-оболочки, создания и хранения в нем экземпляра класса, а также инициализации vtable.

Виртуальная таблица реализует функции стандартного базового интерфейса COM (IUnknown) (QueryInterface,AddRef, иRelease), захватив экземпляр класса из оболочки и вызвав методqueryInterface(),addRef(), иrelease()методы экземпляра. Он также реализует специфичную для Spotlight функцию (илиImporterImportData). К сожалению, в моем тестировании казалось, что Spotlight не передал правильный указатель на структуру-оболочку в качестве первого аргумента дляImporterImportURLData, поэтому было невозможно вызвать метод для экземпляра класса, поэтому функция, которая фактически импортирует атрибуты для файла, должна была быть глобальной функцией . По этой причине я не смог сделать реализацию подключаемого модуля более общим классом, который можно было бы использовать с любым интерфейсом — он должен быть привязан к определенной функции глобального импортера.

Я бы посоветовал вам просмотреть полный исходный код https://github.com/foxglove/MCAPSpotlightImporter .на GitHub , но в интересах не быть ответом только по ссылке, вот основные функции:

      final class ImporterPlugin {
  typealias VTable = MDImporterURLInterfaceStruct
  typealias Wrapper = (vtablePtr: UnsafeMutablePointer<VTable>, instance: UnsafeMutableRawPointer)
  let wrapperPtr: UnsafeMutablePointer<Wrapper>
  var refCount = 1
  let factoryUUID: CFUUID

  private init(wrapperPtr: UnsafeMutablePointer<Wrapper>, factoryUUID: CFUUID) {
    self.wrapperPtr = wrapperPtr
    self.factoryUUID = factoryUUID
    CFPlugInAddInstanceForFactory(factoryUUID)
  }

  deinit {
    let uuid = UUID(factoryUUID)
    CFPlugInRemoveInstanceForFactory(factoryUUID)
  }

  static func fromWrapper(_ plugin: UnsafeMutableRawPointer?) -> Self? {
    if let wrapper = plugin?.assumingMemoryBound(to: Wrapper.self) {
      return Unmanaged<Self>.fromOpaque(wrapper.pointee.instance).takeUnretainedValue()
    }
    return nil
  }

  func queryInterface(uuid: UUID) -> UnsafeMutablePointer<Wrapper>? {
    if uuid == kMDImporterURLInterfaceID || uuid == IUnknownUUID {
      addRef()
      return wrapperPtr
    }
    return nil
  }

  func addRef() {
    precondition(refCount > 0)
    refCount += 1
  }

  func release() {
    precondition(refCount > 0)
    refCount -= 1
    if refCount == 0 {
      Unmanaged<ImporterPlugin>.fromOpaque(wrapperPtr.pointee.instance).release()
      wrapperPtr.pointee.vtablePtr.deinitialize(count: 1)
      wrapperPtr.pointee.vtablePtr.deallocate()
      wrapperPtr.deinitialize(count: 1)
      wrapperPtr.deallocate()
    }
  }

  static func allocate(factoryUUID: CFUUID) -> Self {
    let wrapperPtr = UnsafeMutablePointer<Wrapper>.allocate(capacity: 1)
    let vtablePtr = UnsafeMutablePointer<VTable>.allocate(capacity: 1)

    let instance = Self(wrapperPtr: wrapperPtr, factoryUUID: factoryUUID)
    let unmanaged = Unmanaged.passRetained(instance)

    vtablePtr.initialize(to: VTable(
      _reserved: nil,
      QueryInterface: { wrapper, iid, outInterface in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          if let interface = instance.queryInterface(uuid: UUID(iid)) {
            outInterface?.pointee = UnsafeMutableRawPointer(interface)
            return S_OK
          }
        }
        outInterface?.pointee = nil
        return HRESULT(bitPattern: 0x8000_0004) // E_NOINTERFACE <https://github.com/apple/swift/issues/61851>
      },
      AddRef: { wrapper in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          instance.addRef()
        }
        return 0 // optional
      },
      Release: { wrapper in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          instance.release()
        }
        return 0 // optional
      },
      ImporterImportURLData: { _, mutableAttributes, contentTypeUTI, url in
        // Note: in practice, the first argument `wrapper` has the wrong value passed to it, so we can't use it here
        guard let contentTypeUTI = contentTypeUTI as String?,
              let url = url as URL?,
              let mutableAttributes = mutableAttributes as NSMutableDictionary?
        else {
          return false
        }

        var attributes: [AnyHashable: Any] = mutableAttributes as NSDictionary as Dictionary
        // Call custom global function to import attributes
        let result = importAttributes(&attributes, forFileAt: url, contentTypeUTI: contentTypeUTI)
        mutableAttributes.removeAllObjects()
        mutableAttributes.addEntries(from: attributes)
        return DarwinBoolean(result)
      }
    ))
    wrapperPtr.initialize(to: (vtablePtr: vtablePtr, instance: unmanaged.toOpaque()))
    return instance
  }
}

Наконец, я создал@objcкласс, который выставляет этоallocateфункцию в Obj-C, где я могу вызвать ее изmain.mи вернуть указатель на блок-оболочку из фабричной функции. Это было необходимо, потому что я не хотел использовать нестабильный@_cdeclатрибут, чтобы предоставить функцию Swift непосредственно загрузчику подключаемого модуля.

      @objc public final class PluginFactory: NSObject {
  @objc public static func createPlugin(ofType type: CFUUID, factoryUUID: CFUUID) -> UnsafeMutableRawPointer? {
    if UUID(type) == kMDImporterTypeID {
      return UnsafeMutableRawPointer(ImporterPlugin.allocate(factoryUUID: factoryUUID).wrapperPtr)
    }
    return nil
  }
}
      // main.m
void *MyImporterPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID) {
  return [PluginFactory createPluginOfType:typeID factoryUUID:CFUUIDCreateFromString(NULL, CFSTR("your plugin factory uuid"))];
}

Подробнее см. в https://foxglove.dev/blog/implementing-a-macos-search-plugin-for-robotics-data.моем блоге .

Мне потребовалось немного времени, чтобы заставить это работать.

Вместо добавления кода Swift в mdimporter, я импортирую встроенную среду, уже настроенную для моего приложения.

Я удалил весь пример кода, кроме main.c и GetMetadataForFile.m. В последнем я импортирую свой фреймворк, где вся функциональность теперь находится в виде кода Swift.

Встроенный mdimporter добавлен в приложение. В Инспекторе Файлов установите Location относительно Относительно Продуктов сборки.

Затем приложение добавляет mdimporter с этапом сборки файлов копирования.

  • Направление: Обертка
  • Подпуть: Содержание / Библиотека / Прожектор

Следующее необходимо добавить в настройку сборки Run Search Paths, так как мы ссылаемся на встроенные фреймворки приложения.

@loader_path/../../../../../Frameworks

Если вы получаете сообщение об ошибке компилятора, что модуль фреймворка не может быть найден при сборке приложения, в зависимости от того, как настроено ваше рабочее пространство, вам может потребоваться изменить схему вашего приложения.

  • Отключить параллелизацию
  • Добавьте цели сборки в этой последовательности:
    1. Рамочный проект (ы)
    2. проект mdimporter
    3. Проект приложения

Дополнительным преимуществом наличия всей логики в фреймворке является то, что он может быть прототипирован и проверен на игровой площадке. В миллион раз проще, чем отладка плагина mdimporter.

Другие вопросы по тегам