Использование Cocoapods для приложения Qt на примере Google MLKit

adb64baac3c08b2fed11b9f42874d29a

Возникла необходимость встроить MLKit в приложение айос. Началось с того, что по каким-то причинам используемые в приложении (андроид и айос) zbar и zxing (работали параллельно для улучшения результатов на обоих ОС) стали плохо работать. В чём проблема я так и не понял, потому-что решил попробовать MLKit — тем более, что они обещали поддержку как для андроида, так и для айос. А ещё потому, что клиенты давно просят добавить распознование текста — совсем я их разбаловал сканами штрихкодов,  VIN (приложение для СТО) и т.п. А тут ещё добавляем новый складской функционал, где для инвентаризации и приёмки нужно много вводить текста, и это на мобильном девайсе. В общем решено было выбросить zbar/zxing связку и воспользоваться возможностями Google MLKit.

Первым делом, конечно, сделал сборку для андроид. Сканирование баркодов действительно стало просто летать, даже со сложными случаями (у мерседеса VIN почему-то всегда не очень хорошо сканируется). Распознавание текста пока не пробовал — там нужно под функционал ещё кучу обвязки писать, а времени как всегда мало. И вот пришло время попробовать MLKit на ios.

И вот тут то и начались проблемы. Для начала я попытался понять где, что скачать, как установить. Вот тут написано https://developers.google.com/ml-kit/vision/barcode-scanning/ios, что для этого нужно будет использовать Cocoapods. До сих пор я сним дела не имел, поэтому полез смотреть что к чему. И тут сразу пришлось чесать репу — принцип работы Cocoapods примерно такой:

  1. Рядом со своим проектом xcodeproj создаём Podfile и в нём прописываем какие pods нужно интегрировать в проект

  2. Запускаем pod install и нужные модули скачиваются и устанавливаются в папку Pods, под них создаётся проект xcodeproj, затем создаётся xworkspace в который добавляется как ваш исходный проект, так и созданный Pods проект, затем с какой-то до конца непонятной магией в оригинальный проект добавляется обобщающий фреймворк

  3. Закрываем исходный xcodeproj, открываем xworkspace и собираем наш проект из этого workspace

Как видно, процесс предполагает, что вы работаете исключительно в xcode и у вас обычный проект swift или, на худой конец,  objective-c. Ни о каких qmake,  QtCreator даже речи не идёт (да, я, к сожалению, пока не перешёл на Qt6 и cmake — опять же проблема со временем). Как их впихнуть в этот процесс совершенно непонятно — qmake собирает свой исходно сгенерированный xcodeproj и ни про какой xworkspace знать не знает. Поначалу после чтения документации решил отказаться от MLKit на айос, тем более что у Apple есть свой Vision, который всё то же самое и делает. Но, как оказалось, качество скана у Vision оказалось примерно таким же, как и связки zbar/zxing — особенно тут проявились проблемы с трудными мерсовскими VIN. Клиенты оказались недовольны и мне пришлось засесть за изучение Cocoapods (включая исходники),  qmake (там тоже оказалось много неизведанного, хотя я на Qt пишу уже лет так 20) и даже bash. Программисту всегда найдётся, чему поучиться.

В общем потратил я на всё 2 недели, но таки запихнул MLKit в приложение iOs. Очень помог вот этот пост на stackoverflow:  https://stackoverflow.com/questions/76510626/how-to-embed-sub-project-frameworks-through-cli

Здесь сразу покажу как, и по каждой строке объясню что к чему. Хотя решение не выглядит особо сложным, каждая строка далась с большим трудом, поэтому, уверен, данная статья может оказаться полезной многим.

Итак, первым делом в pro сделаем новый раздел «ios {}», где у нас и будет вся наша кухня по встраиванию mlkit. И в неё сразу добавим:

OTHER_FILES += ios/Podfile

Это позволит нам добавить Podfile в проект и редактировать его в QtCreator.

А вот, собственно, сам Podfile:

platform: ios, '13.0'
use_frameworks!

install! 'cocoapods',
    : integrate_targets => false

project 'MyProject'.xcodeproj'

pod 'GoogleMLKit/BarcodeScanning', '4.0.0'
pod 'GoogleMLKit/TextRecognition', '4.0.0'

target 'MyProject' do
end

# Disable signing for pods
post_install do |installer|
  installer.generated_projects.each do |project|
    project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
            config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
        end
    end
  end
end

В принципе, большую часть настроек из глобальной секции можно было внести в секцию target, но я решил, что раз у нас всё равно один проект, то пусть будет в глобальной секции. Ну, с первой строкой всё понятно, здесь мы установим версию айос, минимальную для нашего приложения. Вторая строка же говорит, что мы собираемся использовать фреймворки вместо статических библиотек. Можно убрать эту строку и тогда вместо фреймворков будут использоваться статические библиотеки. Но в принципе проще это процесс сборки не сделал, поэтому я оставил фреймворки. Поэтому дальнейшее описание будет именно для этого варианта. Кстати,»!» в конце это не NOT. Просто Cocoapodsнаписан на ruby и синтаксис Podfile это синтаксис ruby. Вроде как это означает, что команда имеет сторонний эффект (тут меня знатоки ruby могут поправить, я решил, что изучать ещё и ruby в рамках данной задачи — перебор).

Следующая срока 

install! 'cocoapods', : integrate_targets => false

означает, что при выполнении команды install нам не нужно интегрировать Pods.xcodeproj в воркспейс и наш проект. Потому-что мы не сможем тогда использовать qmake для сборки. Далее мы определяем наш проект, хотя я не уверен, нужно ли нам это, ведь мы интеграции не делаем. Далее идут две команды pod, собственно они и указывают, что мы собираемся установить. Я эти строки взял из примеров гугла на гитхабе вместе с номерами версий. Затем опять же определение target внутри проект, что тоже, не уверен, что необходимо.

Следующий блок — это небольшая магия, которая меняет некоторые настройки в установленных подпроектах (а кроме самих Pods там ещё тянутся зависимости). 'CODE_SIGNING_ALLOWED' — опция запрещающая подписывание. Так рекомендует делать гугл, я не очень понял почему (если кто подскажет — буду рад, лучше понимать все детали). В итоговом приложении я таки подписываю всё — без этого в апстор не удастся загрузить приложение.

'IPHONEOS_DEPLOYMENT_TARGET' — это то, что я уже добавил сам, поскольку уткнулся на самом последнем этапе в невозможность загрузки приложения в апстор из-за того, что во встроенных фреймворках была установлены разные версии от 9 до 11. Почему эпл запрещает это — тоже непонятно. Я бы понял если бы приложение имело версию ниже, но ведь это фреймворки используемые приложением. Тоже хотелось бы услышать мнения по этому поводу. Кстати, у статической сборки наверное этой проблемы не будет, но я не пробовал.

А теперь перейдём к блоку проекта qmake:

ios {
    debug {
        XCFG = Debug
    } else {
        XCFG = Release
    }
    PODS_BUILD_DIR = $$OUT_PWD/pods-build/$$XCFG-iphoneos
    PODS_ROOT = $$OUT_PWD/Pods
    mlkitpods.commands = export LANG=en_US.UTF-8
    mlkitpods.commands += && cp $$PWD/ios/Podfile $$OUT_PWD/Podfile
    mlkitpods.commands += && cd $$OUT_PWD && /usr/local/bin/pod install --clean-install
    mlkitpods.commands += && xcodebuild install
        -project $$PODS_ROOT/Pods.xcodeproj
        -destination generic/platform=iOS
        -destination-timeout 1
        -alltargets
        SYMROOT=$$OUT_PWD/pods-build
    mlkitpods.commands += -configuration $$XCFG

    mlkitpods.commands += && echo ---Modify project---
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/GTMSessionFetcher/GTMSessionFetcher.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/GoogleDataTransport/GoogleDataTransport.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/GoogleToolboxForMac/GoogleToolboxForMac.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/GoogleUtilities/GoogleUtilities.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/GoogleUtilitiesComponents/GoogleUtilitiesComponents.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/PromisesObjC/FBLPromises.framework
        --sign-on-copy
    mlkitpods.commands += && python3 -m pbxproj file
        --target B2QScan
        --parent Frameworks
        $$OUT_PWD/B2QScan.xcodeproj/project.pbxproj
        $$PODS_BUILD_DIR/nanopb/nanopb.framework
        --sign-on-copy

    PRE_TARGETDEPS +=      mlkitpods
    QMAKE_EXTRA_TARGETS += mlkitpods
    OTHER_FILES += ios/Podfile
    INCLUDEPATH += $$PODS_ROOT/Headers/Public/GoogleMLKit
    LIBS +=
        -F$$PODS_BUILD_DIR/GTMSessionFetcher
        -F$$PODS_BUILD_DIR/GoogleDataTransport
        -F$$PODS_BUILD_DIR/GoogleToolboxForMac
        -F$$PODS_BUILD_DIR/GoogleUtilities
        -F$$PODS_BUILD_DIR/GoogleUtilitiesComponents
        -F$$PODS_BUILD_DIR/PromisesObjC
        -F$$PODS_BUILD_DIR/nanopb
        -F$$PODS_ROOT/MLImage/Frameworks
        -F$$PODS_ROOT/MLKitBarcodeScanning/Frameworks
        -F$$PODS_ROOT/MLKitCommon/Frameworks
        -F$$PODS_ROOT/MLKitTextRecognition/Frameworks
        -F$$PODS_ROOT/MLKitTextRecognitionCommon/Frameworks
        -F$$PODS_ROOT/MLKitVision/Frameworks
    LIBS +=
        -framework Accelerate
        -framework CoreGraphics
        -framework CoreImage
        -framework CoreVideo
        -framework FBLPromises
        -framework GTMSessionFetcher
        -framework GoogleDataTransport
        -framework GoogleToolboxForMac
        -framework GoogleUtilities
        -framework GoogleUtilitiesComponents
        -framework MLImage
        -framework MLKitBarcodeScanning
        -framework MLKitCommon
        -framework MLKitTextRecognition
        -framework MLKitTextRecognitionCommon
        -framework MLKitVision
        -framework nanopb
    # embedFrameworks.files =
    #     $$PODS_BUILD_DIR/GTMSessionFetcher/GTMSessionFetcher.framework
    #     $$PODS_BUILD_DIR/GoogleDataTransport/GoogleDataTransport.framework
    #     $$PODS_BUILD_DIR/GoogleToolboxForMac/GoogleToolboxForMac.framework
    #     $$PODS_BUILD_DIR/GoogleUtilities/GoogleUtilities.framework
    #     $$PODS_BUILD_DIR/GoogleUtilitiesComponents/GoogleUtilitiesComponents.framework
    #     $$PODS_BUILD_DIR/PromisesObjC/FBLPromises.framework
    #     $$PODS_BUILD_DIR/nanopb/nanopb.framework
    # embedFrameworks.path = Frameworks
    # QMAKE_BUNDLE_DATA += embedFrameworks

    embedOCRBundle.files = $$PODS_BUILD_DIR/MLKitTextRecognition/LatinOCRResources.bundle
    QMAKE_BUNDLE_DATA += embedOCRBundle
    QMAKE_LFLAGS += -ObjC
}

Объявления переменных в начале очевидны, кроме, пожалуй PODS_BUILD_DIR — с его помощью мы собираемся явно указать где собирать Pods. Затем мы начинаем большой скрипт mlkitpods.commands, по сути это консольные команды которые можно выполнять в терминале последовательно. Разберем что там происходит:

1.     export LANG=en_US.UTF-8 — странно, что QtCreator не поднимает профайл консоли, там эта строка уже есть, а без неё ругается команда pods и что-то криво устанавливается

2.     cp $$PWD/ios/Podfile $$OUT_PWD/Podfile — копируем наш podfile в каталог билда

3.     cd $$OUT_PWD && /usr/local/bin/pod install --clean-install — переходим в каталог билда и устанавливаем pods

4.     дальше идёт xcodebuild install. Здесь важный момент это параметр SYMROOT=$$OUT_PWD/pods-build, который указывает куда установлен результат сборки. При этом полный путь будет меняться в зависимости от варианта сборки release или debug, и он будет в PODS_BUILD_DIR

5.     Следующим этапом нам нужно добавить фреймворки в наш целевой проект. В принципе это можно сделать через QMAKE_BUNDLE_DATA, там есть закомментированный блок с embedFrameworks. Этот вариант прекрасно работает, но добавляет фреймворки в корень проекта. Если вы никогда не открываете ваш xcodeproj, то это не важно, но я из xcode отправляю в AppStore и меня раздражает, что при наличии в проекте папки Frameworks фреймворки из Podsдобавляются в корень проекта. Поэтому вместо стандартного Qt-ового способа я использую питоновский модуль pbxproj, который позволяет менять xcodeproj. Здесь следует обратить внимание, что в проект добавляются только те фреймворки, что собираются из исходников, а все фреймворки с префиксом MLKit, которые установлены Cocoapods уже скомпилированными, в проект добавлять не нужно. Почему так? Ещё одна загадка, которую я бы хотел понять, но пока ответа у меня нет. Каким образом всё работает, если главные фреймворки в проект не добавлены — непонятно. Но работает. Если же я пытаюсь их добавить в проект, то приложение просто не загружается на устройство.

Собственно, на этом команды большого скрипта определены и можно их добавлять в проект:

PRE_TARGETDEPS +=      mlkitpods

QMAKE_EXTRA_TARGETS += mlkitpods

Затем строка OTHER_FILES, которую мы выше уже упоминали, и инклад для того, чтобы использовать MLKit в своём Objective-C коде:

INCLUDEPATH += $$PODS_ROOT/Headers/Public/GoogleMLKit

Затем блок с добавлением путей к фреймворкам, и здесь, кстати, мы указываем все, включая MLkit*. После этого сами фреймворки. Всё это в стандартной qmake переменной LIBS.

После закомментированного блока, альтернативного варианта добавления фреймворков в проект, нам нужно добавить LatinOCRResources.bundle в проект, для распознавания латинского текста, при помощи стандартного qmake способа через QMAKE_BUNDLE_DATA.

Ну и напоследок оказалось, что нужно добавить вот этот флаг:

QMAKE_LFLAGS += -ObjC

Без него проект не хотел собираться.

Ну и напоследок вынесу вопросы, на которые я не нашёл ответов, но хотел бы их получить:

1.     Зачем мы запрещаем 'CODE_SIGNING_ALLOWED'?

2.     Почему эпл запрещает использование фреймворков в которых минимальная версия ниже минимальной версии iOS проекта?

3.     Можно ли как-то заставить QtCreator поднимать пользовательский профиль консоли при запуске? Почему он этого не делает?

4.     Самая большая загадка: почему фреймворки MLKit не нужно интегрировать в проект? Я уж грешным делом подумал, что они их скачивает (хотя такое вообще на айос невозможно, насколько я знаю), проверил с отключенной связью — всё работает.

© Habrahabr.ru