От Cocoapods к Tuist+SPM быстрыми шагами

от автора

Не так давно прогремела новость, что Cocoapods переходит в режим поддержки. В связи с этим встал вопрос, что дальше. В начале мы склонялись к чисто Swift Package Manager, но потом пришло понимание, что неплохо было бы уйти от конфликтов в project файле и сделать задел на модульность. В этой статье мы пройдем от нашего старого приложения к новому и закончим там, где останется перенести исходный код и все заработает.

Подготовка

Для начала я рекомендую сделать файл, куда с одной стороны поместить зависимости из Podfile, а с другой URL для SPM с номером версии, так будет проще вносить их в Tuist.

Скрытый текст

pod ‘GoogleMaps’ https://github.com/googlemaps/ios-maps-sdk 9.0.1
pod ‘Google-Maps-iOS-Utils’ https://github.com/googlemaps/google-maps-ios-utils 6.0.0
pod ‘Firebase/Crashlytics’ https://github.com/firebase/firebase-ios-sdk.git 11.0.0
pod ‘Kingfisher’ https://github.com/onevcat/Kingfisher 7.12.0
pod ‘Moya’ https://github.com/Moya/Moya.git 15.0.3
pod ‘ObjectMapper’ https://github.com/tristanhimmelman/ObjectMapper.git 4.4.3
pod ‘SideMenu’ https://github.com/jonkykong/SideMenu.git 6.5.0
pod ‘FloatingPanel’ https://github.com/scenee/FloatingPanel.git 2.8.5
pod ‘YandexMobileMetrica/Dynamic’ https://github.com/appmetrica/appmetrica-sdk-ios 5.0.0
pod ‘AppsFlyerFramework’ https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static 6.15.0
pod ‘SkeletonView’ https://github.com/Juanpe/SkeletonView.git 1.31.0
pod ‘Swinject’ https://github.com/Swinject/Swinject.git 2.9.1
pod ‘SwinjectStoryboard’ https://github.com/Swinject/SwinjectStoryboard.git 2.2.3
pod ‘SnapKit’ https://github.com/SnapKit/SnapKit.git 5.7.1
pod ‘RxSwift’ https://github.com/ReactiveX/RxSwift.git 6.7.1
pod ‘RxCocoa’ входит в RxSwift
pod ‘RxDataSources’ https://github.com/RxSwiftCommunity/RxDataSources.git 5.0.2

Как видите тут зависимости, которые содержатся почти во всех iOS приложениях(Firebase, Kingfisher) и еще есть старые, которые пора заменить на новые(YandexMobileMetrica/Dynamic)

Теперь поставим Tuist, все достаточно просто:

  1. Ставим Mise curl https://mise.run | sh

  2. Ставим Tuist mise install tuist и активируем его в папке с проектом mise use tuist

Начало

Я рекомендую потренироваться на новом проекте, прежде чем править боевой.

Создадим новый проект:
tuist init --name Demo  

Приступим к настройке проекта:
tuist edit

Займемся сразу зависимостями, это самый долгий шаг, откроем файл Manifests/Tuist/Package.swift и начнем:
В массив dependencies добавляем наши зависимости, указывая url и версию из нашего файла
.package(url: "URL", .upToNextMajor(from: "VERSION")),
в итоге получим наши зависимости

Скрытый текст
let package = Package(     name: "demo",     dependencies: [         .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "7.12.0")),         .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "11.0.0")),         .package(url: "https://github.com/googlemaps/ios-maps-sdk", .upToNextMajor(from: "9.0.1")),         .package(url: "https://github.com/googlemaps/google-maps-ios-utils", .upToNextMajor(from: "6.0.0")),         .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.3")),         .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.7.1")),         .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", .upToNextMajor(from: "5.0.2")),         .package(url: "https://github.com/tristanhimmelman/ObjectMapper.git", .upToNextMajor(from: "4.4.3")),         .package(url: "https://github.com/jonkykong/SideMenu.git", .upToNextMajor(from: "6.5.0")),         .package(url: "https://github.com/scenee/FloatingPanel.git", .upToNextMajor(from: "2.8.5")),         .package(url: "https://github.com/SwiftKickMobile/SwiftMessages.git", .upToNextMajor(from: "10.0.0")),         .package(url: "https://github.com/appmetrica/appmetrica-sdk-ios", .upToNextMajor(from: "5.0.0")),         .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", .upToNextMajor(from: "6.15.0")),         .package(url: "https://github.com/Juanpe/SkeletonView.git", .upToNextMajor(from: "1.31.0")),         .package(url: "https://github.com/Swinject/Swinject.git", .upToNextMajor(from: "2.9.1")),         .package(url: "https://github.com/Swinject/SwinjectStoryboard.git", .upToNextMajor(from: "2.2.3")),         .package(url: "https://github.com/SnapKit/SnapKit.git", .upToNextMajor(from: "5.7.1"))     ] ) 

Выполняем tuist install для установки зависимостей.

Настройка проекта

Переходим в Manifests/Project.swift тут нас ждет самое интересное. Удалим пока target с тестами, займемся приложением.

В Project кроме имени можно указать organizationName — организацию для copyright.

Теперь перейдем к target и ее настройке, что мы тут видим:

destinations — это поддерживаемые устройства, это множество и можно выбрать все, что необходимо, у меня это destinations: [.iPhone],

product — во что превратится эта target, у меня это product: .app,

bundleId — id нашего приложения, можно сразу указать id исходного, чтобы было меньше проблем с firebase и другими зависимостями

deploymentTargets — минимальная версия iOS, почему-то в темплейте этот параметр отсутствовал, зададим deploymentTargets: .iOS("15.0"),

infoPlist — главный plist нашего приложения, рекомендую сразу перенести исходный infoPlist: .file(path: "demo/Info.plist"),

sources — то, где будет наш код, рекомендую оставить пока без изменений, в будущем перенести все туда sources: ["demo/Sources/**"],

resources — место хранения наших ресурсов, рекомендую вынести выше в отдельную переменную т.к. там будут не только путь до наших картинок(Assets.xcassets), xib, storyboard(если они у вас есть) и GoogleService-Info.plist , но и PrivacyManifest. После заполнения PrivacyManifest я рекомендую сверится с оригиналом

let resources: ProjectDescription.ResourceFileElements =     .resources(         [             "demo/Resources/**",             "demo/**/*.storyboard",             "demo/**/*.xib"         ],         privacyManifest: privacyManifest     )
Скрытый текст
let privacyManifest: ProjectDescription.PrivacyManifest = .privacyManifest(     tracking: true,     trackingDomains: [         "firebase-settings.crashlytics.com",         "report.appmetrica.yandex.net",         "usccgg-launches.appsflyersdk.com",         "firebaselogging-pa.googleapis.com"     ],     collectedDataTypes: [         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeName",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeProductPersonalization",             ],         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeEmailAddress",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising",                 "NSPrivacyCollectedDataTypePurposeProductPersonalization",                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePhoneNumber",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePaymentInfo",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeCrashData",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAnalytics"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePreciseLocation",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeDeviceID",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": true,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising",                 "NSPrivacyCollectedDataTypePurposeAnalytics",                 "NSPrivacyCollectedDataTypePurposeAppFunctionality",             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeProductInteraction",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": true,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality",                 "NSPrivacyCollectedDataTypePurposeAnalytics",             ]         ]     ],     accessedApiTypes: [         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategorySystemBootTime",             "NSPrivacyAccessedAPITypeReasons": [                 "35F9.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",             "NSPrivacyAccessedAPITypeReasons": [                 "CA92.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",             "NSPrivacyAccessedAPITypeReasons": [                 "E174.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",             "NSPrivacyAccessedAPITypeReasons": [                 "3B52.1",             ],         ],     ] )

entitlements — наши entitlements, если они есть, то рекомендую так же перенести из старого приложения entitlements: Entitlements(stringLiteral: "demo/demo.entitlements"),

scripts — наши скрипты, в данном случае только Firebase скрипт, от документации ("${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run") его отличает путь. Так же тут можно добавить скрипт для SwiftLint, главное задать в .swiftlint.yml проверку только наших исходников(demo/Sources)

Скрытый текст
let firebaseScript = """                     if [ "${CONFIGURATION}" != "Debug" ]; then                         "$SRCROOT/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run"                     fi                     """ let scripts: [ProjectDescription.TargetScript] = [     .post(script: firebaseScript, name: "firebase", inputPaths: [         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}",         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",         "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",         "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)"     ], basedOnDependencyAnalysis: false) ]

dependencies — вот мы и подошли к нашим зависимостям. Нужно сказать, что с ними не все так просто, иногда недостаточно просто скопировать названия из Package.swift, тогда файл проект сгенерируется с ошибкой, например:

  • AppsFlyerFramework, мы указываем в Package.swift как
    .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", .upToNextMajor(from: "6.15.0")),
    но указывая зависимость как AppsFlyerFramework-Static мы получим ошибку. При таких обстоятельствах я рекомендую создать новый проект и добавить эту зависимость через File -> Add Package Dependencies…, перейти в Package зависимости и найти поле name, в данном случае это name: «AppsFlyerLib-Static», что и нужно будет указать в нашем Project.swift

  • Firebase, он нам не нужен весь, тут нам необходимо указать только нужные части, в нашем случае это FirebaseCrashlytics, но об этом почему-то молчит Get started библиотеки(или я не нашел, но там сказано про флаг -ObjC к нему мы вернемся когда будем разбираться с полем settings)

  • С Appmetrica так же как с FirebaseCrashlytics, необходимо указать только то, что нужно AppMetricaCore

Скрытый текст
dependencies: [                 .external(name: "Kingfisher"),                 .external(name: "FirebaseCrashlytics"),                 .external(name: "GoogleMaps"),                 .external(name: "GoogleMapsUtils"),                 .external(name: "Moya"),                 .external(name: "RxSwift"),                 .external(name: "RxDataSources"),                 .external(name: "ObjectMapper"),                 .external(name: "SideMenu"),                 .external(name: "FloatingPanel"),                 .external(name: "SwiftMessages"),                 .external(name: "AppMetricaCore"),                 .external(name: "AppsFlyerLib-Static"),                 .external(name: "SkeletonView"),                 .external(name: "Swinject"),                 .external(name: "SwinjectStoryboard"),                 .external(name: "SnapKit"),             ]

settings — настройки проекта
с configurations все ясно: debug и release
с base настройками все веселее:

  • Для запуска на устройстве, нужно указать CODE_SIGN_STYLE: manualCodeSigning, .automaticCodeSigning(devTeam: "КОМАНДА") и так далее, мы предпочтем пока .codeSignIdentityAppleDevelopment

  • для Firebase, согласно инструкции, необходимо указать .otherLinkerFlags(["-ObjC"])

  • так же для Firebase указываем .debugInformationFormat(.dwarfWithDsym)

  • Чтобы приложение не упало при запуске, необходимо указать: .marketingVersion("1.0.0") + .currentProjectVersion("1")

  • 4. Рекомендую указать .otherSwiftFlags(["-D IS_PRODUCTION"]), чтобы иметь возможность через #if проверять какой target используется, если он у вас не один

Осталось запустить tuist generate ,если все сделано верно, то откроется проект в Xcode:

Вот и все, вам останется только удалить ContentView.swift + DemoApp.swift и перенести весь свой код в Sources. Спасибо за внимание.

Скрытый текст
import ProjectDescription let privacyManifest: ProjectDescription.PrivacyManifest = .privacyManifest(     tracking: true,     trackingDomains: [         "firebase-settings.crashlytics.com",         "report.appmetrica.yandex.net",         "usccgg-launches.appsflyersdk.com",         "firebaselogging-pa.googleapis.com"     ],     collectedDataTypes: [         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeName",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeProductPersonalization",             ],         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeEmailAddress",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising",                 "NSPrivacyCollectedDataTypePurposeProductPersonalization",                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePhoneNumber",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePaymentInfo",             "NSPrivacyCollectedDataTypeLinked": true,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeCrashData",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAnalytics"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypePreciseLocation",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": false,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality"             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeDeviceID",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": true,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising",                 "NSPrivacyCollectedDataTypePurposeAnalytics",                 "NSPrivacyCollectedDataTypePurposeAppFunctionality",             ]         ],         [             "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeProductInteraction",             "NSPrivacyCollectedDataTypeLinked": false,             "NSPrivacyCollectedDataTypeTracking": true,             "NSPrivacyCollectedDataTypePurposes": [                 "NSPrivacyCollectedDataTypePurposeAppFunctionality",                 "NSPrivacyCollectedDataTypePurposeAnalytics",             ]         ]     ],     accessedApiTypes: [         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategorySystemBootTime",             "NSPrivacyAccessedAPITypeReasons": [                 "35F9.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",             "NSPrivacyAccessedAPITypeReasons": [                 "CA92.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",             "NSPrivacyAccessedAPITypeReasons": [                 "E174.1",             ],         ],         [             "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",             "NSPrivacyAccessedAPITypeReasons": [                 "3B52.1",             ],         ],     ] ) let resources: ProjectDescription.ResourceFileElements =     .resources(         [             "demo/Resources/**",             "demo/**/*.storyboard",             "demo/**/*.xib"         ],         privacyManifest: privacyManifest     ) let firebaseScript = """                     if [ "${CONFIGURATION}" != "Debug" ]; then                         "$SRCROOT/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run"                     fi                     """ let scripts: [ProjectDescription.TargetScript] = [     .post(script: firebaseScript, name: "firebase", inputPaths: [         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}",         "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",         "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",         "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)"     ], basedOnDependencyAnalysis: false) ] let project = Project(     name: "demo",     organizationName: "DEMO",     targets: [         .target(             name: "demo",             destinations: [.iPhone],             product: .app,             bundleId: "io.tuist.demo",             deploymentTargets: .iOS("15.0"),             infoPlist: .file(path: "demo/Info.plist"),             sources: ["demo/Sources/**"],             resources: resources,             entitlements: Entitlements(stringLiteral: "demo/demo.entitlements"),             scripts: scripts,             dependencies: [                 .external(name: "Kingfisher"),                 .external(name: "FirebaseCrashlytics"),                 .external(name: "GoogleMaps"),                 .external(name: "GoogleMapsUtils"),                 .external(name: "Moya"),                 .external(name: "RxSwift"),                 .external(name: "RxDataSources"),                 .external(name: "ObjectMapper"),                 .external(name: "SideMenu"),                 .external(name: "FloatingPanel"),                 .external(name: "SwiftMessages"),                 .external(name: "AppMetricaCore"),                 .external(name: "AppsFlyerLib-Static"),                 .external(name: "SkeletonView"),                 .external(name: "Swinject"),                 .external(name: "SwinjectStoryboard"),                 .external(name: "SnapKit"),             ],             settings: .settings(                 base: SettingsDictionary()                     .codeSignIdentityAppleDevelopment()                     .otherLinkerFlags(["-ObjC"])                     .debugInformationFormat(.dwarfWithDsym)                     .marketingVersion("1.0.0")                     .otherSwiftFlags(["-D IS_PRODUCTION"])                     .currentProjectVersion("1"),                                  configurations: [                     .debug(name: .debug),                     .release(name: .release)                 ]             )         ),     ] ) 


ссылка на оригинал статьи https://habr.com/ru/articles/837456/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *