From Cocoapods to Tuist+SPM in quick steps

It thundered not long ago newsthat Cocoapods is moving into support mode. In this regard, the question arose of what's next. At first, we were leaning towards purely Swift Package Manager, but then we realized that it would be nice to get away from conflicts in the project file and make a reserve for modularity. In this article, we will go from our old application to the new one and finish where it remains to transfer the source code and everything will work.

Preparation

To start with, I recommend making a file where you put the dependencies from the Podfile on one side, and the URL for the SPM with the version number on the other, this will make it easier to add them to Tuist.

Example

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' is part of RxSwift
pod 'RxDataSources' https://github.com/RxSwiftCommunity/RxDataSources.git 5.0.2

As you can see, there are dependencies that are contained in almost all iOS applications (Firebase, Kingfisher) and there are also old ones that need to be replaced with new ones (YandexMobileMetrica/Dynamic)

Now let's put it Tuisteverything is quite simple:

  1. We put Mise curl https://mise.run | sh

  2. We install Tuist mise install tuist and activate it in the project folder mise use tuist

Start

I recommend practicing on a new project before editing a production one.

Let's create a new project:
tuist init --name Demo

Let's start setting up the project:
tuist edit

Let's deal with dependencies right away, this is the longest step, let's open the file Manifests/Tuist/Package.swift and let's begin:
We add our dependencies to the dependencies array, specifying the url and version from our file
.package(url: "URL", .upToNextMajor(from: "VERSION")),
as a result we will get our dependencies

In the form of code
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"))
    ]
)

We are doing it tuist install to install dependencies.

Project setup

Let's move on to Manifests/Project.swift here is where the most interesting part awaits us. Let's delete the target with tests for now and work on the application.

In Project, in addition to the name, you can specify organizationName – the organization for copyright.

Now let's move on to the target and its settings, what we see here:

destinations – these are supported devices, there are many and you can choose everything you need, I have this destinations: [.iPhone],

product – what will this target turn into, I have it product: .app,

bundleId – id of our application, you can immediately specify the id of the original one, so that there are fewer problems with firebase and other dependencies

deploymentTargets – minimum iOS version, for some reason this parameter was missing in the template, let's set it deploymentTargets: .iOS("15.0"),

infoPlist – the main plist of our application, I recommend immediately transferring the original infoPlist: .file(path: "demo/Info.plist"),

sources – where our code will be, I recommend leaving it unchanged for now, and moving everything there in the future sources: ["demo/Sources/**"],

resources – the storage location of our resources, I recommend moving it above to a separate variable, because there will be not only the path to our images (Assets.xcassets), xib, storyboard (if you have them) and GoogleService-Info.plist, but also PrivacyManifest. After filling PrivacyManifest I recommend checking the original

let resources: ProjectDescription.ResourceFileElements =
    .resources(
        [
            "demo/Resources/**",
            "demo/**/*.storyboard",
            "demo/**/*.xib"
        ],
        privacyManifest: 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 – our entitlements, if they exist, I recommend transferring them from the old application as well entitlements: Entitlements(stringLiteral: "demo/demo.entitlements"),

scripts – our scripts, in this case only the Firebase script, from the documentation ("${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run") it is distinguished by the path. Also here you can add a script for SwiftLint, the main thing is to set in .swiftlint.yml the check of only our sources (demo/Sources)

scripts: scripts,
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 – here we come to our dependencies. It should be said that not everything is so simple with them, sometimes it is not enough to just copy the names from Package.swift, then the project file will be generated with an error, for example:

  • AppsFlyerFramework, we specify in Package.swift as
    .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", .upToNextMajor(from: "6.15.0")),
    but specifying the dependency as AppsFlyerFramework-Static we will get an error. In such circumstances, I recommend creating a new project and adding this dependency via File -> Add Package Dependencies…, go to Package dependencies and find the name field, in this case it is name: “AppsFlyerLib-Static”, which will need to be specified in our Project.swift

  • Firebase, we don't need it all, here we need to specify only the necessary parts, in our case it is FirebaseCrashlytics, but for some reason it is silent about this Get started libraries (or I didn't find it, but it says about the flag -ObjC We'll come back to it when we deal with the settings field)

  • With Appmetrica, just like with FirebaseCrashlytics, you only need to specify what AppMetricaCore needs.

dependencies
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 – project settings
everything is clear with configurations: debug and release
with base settings everything is more fun:

  • To run on a device, you need to specify CODE_SIGN_STYLE: manualCodeSigning, .automaticCodeSigning(devTeam: "КОМАНДА") and so on, we prefer for now .codeSignIdentityAppleDevelopment

  • for Firebase, according to the instructions, you need to specify .otherLinkerFlags(["-ObjC"])

  • we also specify for Firebase .debugInformationFormat(.dwarfWithDsym)

  • To prevent the application from crashing on startup, you must specify: .marketingVersion("1.0.0") + .currentProjectVersion("1")

  • 4. I recommend specifying .otherSwiftFlags(["-D IS_PRODUCTION"])to be able to check via #if which target is being used, if you have more than one

All that remains is to launch tuist generate if everything is done correctly, the project will open in Xcode:

That's it, all you have to do is delete ContentView.swift + DemoApp.swift and move all your code to Sources. Thanks for your attention.

Project.swift
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)
                ]
            )
        ),
    ]
)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *