Не так давно прогремела новость, что 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, все достаточно просто:
-
Ставим Mise
curl https://mise.run | sh -
Ставим 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/
Добавить комментарий