Возникла необходимость встроить MLKit в приложение айос. Началось с того, что по каким-то причинам используемые в приложении (андроид и айос) zbar и zxing (работали параллельно для улучшения результатов на обоих ОС) стали плохо работать. В чём проблема я так и не понял, потому-что решил попробовать MLKit – тем более, что они обещали поддержку как для андроида, так и для айос. А ещё потому, что клиенты давно просят добавить распознавание текста – совсем я их разбаловал сканами штрихкодов, VIN (приложение для СТО) и т.п. А тут ещё добавляем новый складской функционал, где для инвентаризации и приёмки нужно много вводить текста, и это на мобильном девайсе. В общем решено было выбросить zbar/zxing связку и воспользоваться возможностями Google MLKit.
Первым делом, конечно, сделал сборку для андроид. Сканирование баркодов действительно стало просто летать, даже со сложными случаями (у мерседеса VIN почему-то всегда не очень хорошо сканируется). Распознавание текста пока не пробовал – там нужно под функционал ещё кучу обвязки писать, а времени как всегда мало. И вот пришло время попробовать MLKit на ios.
И вот тут то и начались проблемы. Для начала я попытался понять где, что скачать, как установить. Вот тут написано, что для этого нужно будет использовать Cocoapods. До сих пор я с ним дела не имел, поэтому полез смотреть что к чему. И тут сразу пришлось чесать репу – принцип работы Cocoapods примерно такой:
Рядом со своим проектом xcodeproj создаём Podfile и в нём прописываем какие pods нужно интегрировать в проект
Запускаем pod install и нужные модули скачиваются и устанавливаются в папку Pods, под них создаётся проект xcodeproj, затем создаётся xworkspace в который добавляется как ваш исходный проект, так и созданный Pods проект, затем с какой-то до конца непонятной магией в оригинальный проект добавляется обобщающий фреймворк
Закрываем исходный 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 'B2QScan.xcodeproj'
pod 'GoogleMLKit/BarcodeScanning', '4.0.0'
pod 'GoogleMLKit/TextRecognition', '4.0.0'
target 'B2QScan' 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, по сути это консольные команды которые можно выполнять в терминале последовательно. Разберем что там происходит:
export LANG=en_US.UTF-8
– странно, что QtCreator не поднимает профайл консоли, там эта строка уже есть, а без неё ругается команда pods и что-то криво устанавливаетсяcp $$PWD/ios/Podfile $$OUT_PWD/Podfile
– копируем наш podfile в каталог билдаcd $$OUT_PWD && /usr/local/bin/pod install --clean-install
– переходим в каталог билда и устанавливаем podsдальше идёт
xcodebuild install
. Здесь важный момент это параметрSYMROOT=$$OUT_PWD/pods-build
, который указывает куда установлен результат сборки. При этом полный путь будет меняться в зависимости от варианта сборки release или debug, и он будет в PODS_BUILD_DIRСледующим этапом нам нужно добавить фреймворки в наш целевой проект. В принципе это можно сделать через 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
Без него проект не хотел собираться.
Ну и напоследок вынесу вопросы, на которые я не нашёл ответов, но хотел бы их получить:
Зачем мы запрещаем 'CODE_SIGNING_ALLOWED'?
Почему эпл запрещает использование фреймворков в которых минимальная версия ниже минимальной версии iOS проекта?
Можно ли как-то заставить QtCreator поднимать пользовательский профиль консоли при запуске? Почему он этого не делает?
Самая большая загадка: почему фреймворки MLKit не нужно интегрировать в проект? Я уж грешным делом подумал, что они их скачивает (хотя такое вообще на айос невозможно, насколько я знаю), проверил с отключенной связью – всё работает.
ЗЫ: Сюда добавлю ответы, которые получил в комментариях и от коллег из чата https://t.me/qt_chat
1.
Чтобы на каждый build гугловых зависимостей не прокидывать кем подписывать, ведь когда будете собирать в стор или на запуск на девайсе все равно произойдет их переподпись. Но теоретически вы можете убрать эту строку, но тогда вам нужно прокинуть кем подписывать, через флаги это можно сделать так
xcodebuild .... DEVELOPMENT_TEAM=5HUHB9JAAA
или в том же подфайле
config.build_settings['DEVELOPMENT_TEAM'] = '5HUHB9JAAA'
Цитата: "таковы требования аппстора по минимальной версии, тут сложно однозначно ответить почему. Отрезают супер старые девайсы, стимулируют использовать более новые апи. Наверняка еще на уровне системы есть какие-то проверки."
То есть, как я понял, это не требования по отношению версия приложения и используемых фреймворков, а требование аппстор к самой минимальной версии.
Пока непонятно, резервирую место
Тут сыграло роль моё заблуждение относительно фреймворков - я был уверен, что они могут быть только динамическими. Оказалось, что бывают и статические и таковыми являются фреймворки MLKit*