Tuisting The Oldest iOS Project

Lee young-jun
17 min readJul 6, 2024

--

In last articles, I setup Tuist for my recent SwiftUI project, but I wanted to advance my old projects also.

So I decide to setup Tuist for my oldest project I built since 2016. I can’t be sure I could achieve it yet …

Just let’s start!!

Installation

Following first article, I started initializing Tuist. However, before, I had to first install Tuist again, otherwise we can’t use Tuist.(Please read the article, if you want to know how to install mise)

mise “tuist” couldn’t exec process: No such file or directory

Extracting Configuation

mise x -- tuist migration settings-to-xcconfig -p WhoCallMe.xcodeproj -x app.debug.xcconfig -t WhoCallMe

Creating Project

Next, I ran a command to initial Tuist Project.

mise x -- tuist init --platform ios

However this command was not working on the directory has something.

Can’t initialize a project in the non-empty directory at path ...

Therefore I moved all files excluding hidden to the temporary directory and ran again the command.

Creating Workspace

I realized this project Pods, so I should create a Workspace.swift instead of the project. I created a workspace following latest article.

Renaming Project

To simplify the dependency graph later, I renamed the project App without the app name(xxxxxApp).

Replacing Tests

The oldest project doesn’t have any test, but initial test generated automatically. However I just copied tests files into Tests directory.

The old tests directory had Info.plist, I renamed it as Tests-Info.plist also and moved it into Derived/infoPlists/Tests directory.

Moving Resources

Assets and Image should be placed in Resources directory. So I moved them into the directory. LaunchScreen.storyboard is also a resource.

Info Plist

App target also have plist like Tests project. So I moved it into Derived directory.

Sources

Finally the source files were remained. So I moved all files of the project excepting related to CocoaPods under Sources.

AppIcon

Now can I open the workspace? Unfortunately generating the project was failed.

Trying to add a file at path … to a build phase that hasn’t been added to the project

I didn’t know what made the problem. So I opened the workspace and found one reason. Assets.xcasset is bundled into the App project, but there was AppIcon.appiconset out of the asset. It was old legacy file therefore I removed it.

Localization

My project has Main.storyboard, but I couldn’t see it in my project.

Because the storyboard was localized. Tuist can’t recognized .lproj? According to the guide, Tuist supports .lproj, but I placed the localized files under Sources directory. So I put the files into Resources directory.

Now I can see the localized files in App project.

Storyboard

Main storyboard is directly under Resources directory.

I wanted put it under Storyboards directory like Image, even though I have only one storyboard. However it is place in Base.lproj now.

So how to move it into Storyboards? I cloned the .lproj directories and moved them into the Storyboard directory without the files not related to Main storyboard. There are only Main.strings and Main.storyboard.

I can see the Main storyboard under the directory with localizations after regenerating the project.

infoPlist

As I did above, info plist file was located under Derived directory. But where should be localization files should be?

I experimented to put them into Derived first, but the way didn’t work.

I was surprised that all .lproj files were removed after ran generate. I guess it is not allowed.

Therefore I revived them under Resources/infoPlist.

I’m not sure whether the localization works well before launching the app.

Configuration

You might remember I extracted xcconfig file. I moved it into Configs directory.

Then I cloned the xcconfig as app.release.xccconfig

app.debug doesn’t have to include Release config, otherwise exclude Debug config.

settings property enables to apply the configurations.

settings: .settings(configurations: [
.debug(
name: "Debug",
xcconfig: "Configs/app.debug.xcconfig"),
.release(
name: "Release",
xcconfig: "Configs/app.release.xcconfig")
])

Pods

There is no special way to integrate Pods with Tuist. They don’t have a plan to support Pods, since many libraries started deprecating.

So I planed to create a ThirdParty module to integrate Pods.

I first created a module only to contain Pods.

let project = Project(
name: "ThirdParty",
targets: [
.target(
name: "ThirdParty",
destinations: .iOS,
product: .framework,
...
),

Then I made the app depending on the module.

To install Pods, I made Podfile with pod command

pod init Projects/ThirdParty/ThirdParty.xcodeproj/

However, ThirdParty.xcworkspace was generated when installing Pods. So I removed the workspace and specify workspace name in Podfile.

workspace 'WhoCallMe'
project 'Projects/ThirdParty/ThirdParty.xcodeproj/'

Now ThirdParty module contains Pods.

Lastly, I put Pods in Podfiles.

pod 'Firebase/Crashlytics'
...

Entitlements

After setting up Pods, I attempted to build first time and I encountered this error.

My app is accessing to the note of contacts, so I should be granted by Apple with the entitlement. We can set the path for the entitlement file in Build Settings.

I corrected the path and the error has been disappeared, but this approach is going be canceled after regenerating.

To solve this problem, I defined entitlements in the project manifest.

resources: ["Resources/**"],
entitlements: .file(path: .relativeToCurrentFile("Sources/App.entitlements")),
dependencies:

Then I also renamed the file.

Scheme

However another error was still raised.

I solved similar issues by switching to static before, but now the solution is not working.

let project = Project(
name: "ThirdParty",
targets: [
.target(
name: "ThirdParty",
destinations: .iOS,
product: .staticFramework,

I found ThirdParty can import Pods, but App couldn’t it.

Pods to SPM

I can’t find a solution yet, so I will replace libraries with SPM as much as possible.

pod 'Firebase/Crashlytics'
pod 'Firebase/Analytics'
pod 'RxSwift', '~> 5'
pod 'RxCocoa', '~> 5'

pod 'GADManager'
pod 'LSCircleProgressView'
pod 'StringLogger'

Firebase

We can install Firebase SDK through SPM following the guide. Pods’ Crashlytics was Firebase/Crashlytics but Swift Package Name is different.

packages: [.remote(url: "https://github.com/firebase/firebase-ios-sdk", 
requirement: .upToNextMajor(from: "10.4.0"))],
targets: [
.target(
...,
dependencies: [...,
.package(product: "FirebaseCrashlytics",
type: .runtime),
.package(product: "FirebaseAnalytics",
type: .runtime)]
),

I embeded Crashlytics using the nameFirebaseCrashlytics

Firebase can’t it’s configuration file if the plist file is under Sources.

So I put it under Resources directory.

Build Phrase

Rx

RxSwift also provides Swift Package and the install guide.

packages: [...,
.remote(url: "https://github.com/ReactiveX/RxSwift",
requirement: .upToNextMajor(from: "5.0.0"))],
targets: [
.target(
name: "ThirdParty",
...,
dependencies: [...,
.package(product: "RxSwift", type: .runtime),
.package(product: "RxCocoa", type: .runtime)]

StringLogger

This library is my own Pods to make logging easy. So I made it to support SPM also.

packages: [...,
.remote(url: "https://github.com/2sem/StringLogger",
requirement: .upToNextMajor(from: "0.7.0"))],
targets: [
.target(
...,
dependencies: [...,
.package(product: "StringLogger", type: .runtime)
]

LSCircleProgressView

My app presents progress of converting contacts in circular shape and there is no built-in view for such a feature. So I made a custom component. It also supported only CocoaPods.

packages: [...,
.remote(url: "https://github.com/2sem/LSCircleProgressView",
requirement: .upToNextMajor(from: "0.1.0"))],
targets: [
.target(
...,
dependencies: [...,
.package(product: "LSCircleProgressView",
type: .runtime)
]I

GADManager

My all apps are monetized usin Admob and I didn’t want to write boilerplate codes. So I created a library to manage presenting ads periodically.

packages: [...,
.remote(url: "https://github.com/2sem/GADManager",
requirement: .upToNextMajor(from: "1.3.0")),
],
targets: [
.target(
...,
dependencies: [...,
.package(product: "GADManager", type: .runtime)
]

It also embeds Google Mobile Ads SDK, therefore I took the dependency into SPM.

dependencies: [
.package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", from: "11.5.0")
],
targets: [
.target(
name: "GADManager",
dependencies: [.product(name: "GoogleMobileAds",
package: "swift-package-manager-google-mobile-ads")
]),

However this library compilation has been failed. Because it is based on Swift 4.

let indicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge);

Even though my library depend on the google sdk, my app project couldn’t find GAD classes, because they are Obj-C??

So App project embeded google mobile ads sdk. I don’t know how to solve without this yet.

Kakao Open SDK

Kakao SDK is also installed by Pods before.

packages: [...,
.remote(url: "https://github.com/kakao/kakao-ios-sdk",
requirement: .upToNextMajor(from: "2.22.2"))
],
targets: [
.target(
...,
dependencies: [...,
.package(product: "KakaoSDK", type: .runtime)
]
),

Kakao Swift Package doesn’t support legacy methods.

So I sould migrate Kakao Link to Kakao Share

import KakaoSDKCommon

Build Configuration

I would like not to use CocoaPods anymore. So I removed flag for CocoaPods.

OTHER_SWIFT_FLAGS[config=Debug]=$(inherited) -D DEBUG // -D COCOAPODS

Running

All build errors has been solved. It’s time to run. However my app was crashed because Main Storyboard is not setup in info.plist.

My original info.plist has this key.

<key>UIMainStoryboardFile</key>
<string>Main</string>

So I will add this into Manifest like UILaunchStoryboardName

infoPlist: .extendingDefault(
with: [
"UILaunchStoryboardName": "LaunchScreen.storyboard",
"UIMainStoryboardFile": "Main",
]
),

The app was not able to find Core Data Model file like google service plist. I also moved it under Resources.

I realized Tuist generates App-Info.plist every times. So I appended additional properties into Manifest.

infoPlist: .extendingDefault(
with: [
"UILaunchStoryboardName": "LaunchScreen.storyboard",
"UIMainStoryboardFile": "Main",
"GADApplicationIdentifier": "ca-app-pub-...",
"GoogleADUnitID": ["FullAd" : "ca-app-pub-...",
"RewardAd" : "ca-app-pub-...",
"TopBanner" : "..."],
"Itunes App Id": "...",
"NSContactsUsageDescription": "This app needs access contacts to convert",
"NSUserTrackingUsageDescription": "Your data will be used to deliver personalized ads to you",
]
),

To use google ads, I should many SKAdNetworkIdentifier, but I don’t like repeat. So I created the array using map

let skAdNetworks: [Plist.Value] = ["cstr6suwn9",
"4fzdc2evr5",
...

infoPlist: .extendingDefault(
with: [
...,
"SKAdNetworkItems": .array(skAdNetworks)
]
),

I could see the launched app, however app was still crashed when to load the Ads.

In Github Issues, people recommended to add -ObjC into Build Settings, but the solution made a conflict.

I thought ThirdParty is a reason, so I moved the dependency to the app project. However the problem was still not solved.

GADManager

So I had to embed my library to App project directly and depended on Firebase SDK in both App and ThirdParty.

let project = Project(
name: "App",
packages: [...,
.remote(url: "https://github.com/firebase/firebase-ios-sdk",
requirement: .upToNextMajor(from: "10.4.0")),
.remote(url: "https://github.com/2sem/GADManager",
requirement: .upToNextMajor(from: "1.3.2")),
],
...
targets: [
.target(
name: "App",
destinations: .iOS,
product: .app,
...
dependencies: [...,
.package(product: "GADManager", type: .runtime),
]

Regenerating GADManager emitted this error.

Default source path not found for target GADManager in package at /Users/LYJ/Projects/NCredif/WhoCallMe/src/WhoCallMe/Tuist/.build/checkouts/GADManager. Source path must be one of [“Sources/GADManager”, “Source/GADManager”, “src/GADManager”, “srcs/GADManager”]

The Swift file was in Sources directory of GADManager. So I moved it under Sources/GADManager in GADManager Swift Packg.

I removed tag 1.3.2 and push again.

And I got Tuist error.

Revision … version 1.3.2 does not match previously recorded value

I don’t know why, but I found with several tests. The reason was there was no specification for source path. So I gave path explicitly.

targets: [
.target(
...,
path: "Sources"),

tuist clean could not solve the error, so I had to create a new tag 1.3.3.

Even after converting my library SPM, the error was still there.

let project = Project(
name: "App",
targets: [
.target(
name: "App",
dependencies: [...,
.external(name: "GADManager", condition: .none),
]

Through many experiments I found a solution. Above manifest made embedded libraries in the project.

To use external, I appended my package into Package also.

Now I can build and run my app using Tuist!

However this way also often crashes the app… So I had to rollback Dependency Graph to like this.

Github Deployment

I am deploying my apps using Github Workflow and Fastlane.

So I modified it to run Tuist.

Select Xcode

I didn’t specify Swift version to my library, but I had to upgrade Xcode due to the Swift(5.10) version incompatible error while installing dependencies.

- name: Select Xcode
run: |
sudo xcode-select -s /Applications/Xcode_15.4.app

Install Tuist

To run Tuist on Github runner, I should install mise and Tuist. I wouldn’t mention details for this.

- name: Install Mise
run: |
brew install mise

- name: Install Tuist
run: |
mise install tuist

- name: Install Dependencies
run: |
mise x -- tuist install

Caches

My project used Pods, therefore my CD cached Pods before. It’s useless now. I just removed this lines.

- name: Load cached pods
uses: actions/cache@v4
with:
path: ./Pods
key: ${{ needs.install-pods.outputs.pods-cache-key }}

- name: Install CocoaPods
run: |
pod install

I migrated my library to Swift Package. Can I cache it? I would first archive without caching.

Tuist

I attempted to find the way to archive using Tuist, however I couldn’t see it. So I just built it to create cache?

- name: Build Tuist
run: |
mise x -- tuist build

Fastlane

I created the name of my project’s Primary Project to ‘App’. So I modified my Fastfile.

# @xcodeproj = "./#{@project_name}.xcodeproj"
@xcodeproj = "./Projects/App/App.xcodeproj"

App project has App target, not WhoCallMe now.

@app_version = get_version_number(
xcodeproj: @xcodeproj,
target: 'App' //@project_name
)

build_app command also need to be updated for scheme.

build_app(
workspace: "./#{@project_name}.xcworkspace",
scheme: 'App',

The building had been success, but I can’t upload because app version is 1.0. So I changed Short Bundle Version String to ${MARKETING_VERSION}

.target(
name: "App",
...,
infoPlist: .extendingDefault(
with: [
...,
"CFBundleShortVersionString": "${MARKETING_VERSION}"
]
),

I modified Version in General Tab,

however the modification had not been applied to Build Configuration.

So I directly modified the configuration file. Maybe I could modify version in Manifest instead to use Marketing Version. But I didn’t that because General can make me confuse later.

I guessed this error would be last, but one more error bothered me.

I couldn’t understand because my project already have it. 🤔

I could solve this last problem with the guide. So I eliminated .storyboard from name.

Now I am happy with Tuising 😄

Localized Strings

I found one more problem while testing the app before submission.

I realized that I missed Localizable Strings in localization.

strings file is a resource, so Sources can’t recognize it.

Now I can see Localizable.strings in App project.

And my app can present localized strings!!

Supported Devices

After resolving the all issues, I attempted to submit my app. But the store didn’t allow to it.

The reason was Destination(Supported Devices?)

Actually this app doesn’t support iPad and so there is no screenshot for iPads. So how to set the section using Tuist? It’s very easy.

targets: [
.target(
name: "App",
destinations: [.iPhone], //.iOS

Now my app support only iPhone again.

Display Name

Finally I submitted the app, but it was rejected soon.

This means my app is displayed as ‘App’ on English.

So I checked info.Plist and Build Settings and found there is not Display Name specified.

But Korean version of app name is given by localization file.

CFBundleDisplayName = "누군지다알아";

The default the display name seems following app project name. So I set DisplayName to Manifest.

targets: [
.target(
name: "App",
...,
infoPlist: .extendingDefault(
with: [
...,
"CFBundleDisplayName": "WhoCallMe"
]
),

I can see it in Xcode by regenerating the project.

Now my app name is correct in English!

Missing Images

In fact, I thought the reason would bedark mode when the app was rejected.

I have never noticed while developing, however it seems like above when I run the app installed by TestFlight. I didn’t find the reason. So I uploaded to TestFlight generated by Xcode, not using Github Action and I could see light contents.

I thought it was affected due to the black background, however real problem was what some images are missing.

I searched where background.png file is.

find ./ -name “background.png”

.//Projects/App/Resources/Image/icon/background.png

It should be under icon folder, however Git didn’t have the folder.

I bring Icon from main branch. However the images is still missing. So I checked .gitignore and found this.

# Icon must end with two
Icon

I don’t know why Tuist ignores Icon yet. Anyway I just commented out the line. And the new version 2.3.0 generated by Tuist is now published!

If you found this post helpful, please give it a round of applause 👏. Explore more iOS-related content in my other posts.

For additional insights and updates, check out my LinkedIn profile. Thank you for your support!

Troubleshooting

Trying to add a file at path …/Projects/App/Resources/AppIcon.appiconset/Contents.json to a build phase that hasn’t been added to the project.

Move AppIcon.appiconset into Assets.xcassets

CocoaPods could not find compatible versions for pod “RxCocoa”:

platform :ios, '13.0'

Build input file cannot be found: ‘…/WhoCallMe.entitlements

xcodebuild: error: Could not resolve package dependencies:

unknown package ‘unknown package name’ in dependencies of target ‘GADManager’; valid packages are: ‘swift-package-manager-google-mobile-adsk’ (from ‘https://github.com/googleads/swift-package-manager-google-mobile-adsk')

.target(
name: "GADManager",
dependencies: [.product(name: "GoogleMobileAds",
package: "swift-package-manager-google-mobile-ads")
]),

Dependencies could not be resolved because no versions of ‘swift-package-manager-google-mobile-ads’ match the requirement 1.2.26..<2.0.0 and ‘gadmanager’ depends on ‘swift-package-manager-google-mobile-ads’ 1.2.26..<2.0.0.

No such module ‘KakaoLink’

Cannot find type ‘Analytics’ in scope

import FirebaseAnalytics

could not find a valid GoogleService-Info.plist

.window!: Fatal error: Unexpectedly found nil while unwrapping an Optional value

infoPlist: .extendingDefault(
with: [
"UILaunchStoryboardName": "LaunchScreen.storyboard",
"UIMainStoryboardFile": "Main",
]
),

[NSData gul_dataByGzippingData:error:]: unrecognized selector

 .external(name: "GADManager", condition: .none),

Default source path not found for target … in package … Source path must be one of

moves sources to under Sources/{Package Name}/

or specify the source path of Swift Package

targets: [
.target(
...,
path: "Sources"),

Revision … version a.b.c does not match previously recorded value

create a new tag (remove and push is not working)

error: Dependencies could not be resolved because ‘…’ contains incompatible tools version (5.10.0)

xcode-select -s /Applications/Xcode_15.3.app

Multiple schemes found but you haven’t specified one. Couldn’t find any schemes in this project

build_app(
...,
scheme: {App Project Scheme},

package ‘…’ @ 0.7.0 is using Swift tools version 5.10.0 but the installed version is 5.9.0

Add xcode_select or remove it to fit to workflow’s Xcode

Info.plist file must contain a higher version than that of the previously approved version

Asset validation failed (90476) Invalid bundle. Because your app supports Multitasking on iPad

According to the official guide,

I should remove .storyboard from the name.

.target(
name: "App",
...,
infoPlist: .extendingDefault(
with: [
"UILaunchStoryboardName": "LaunchScreen",

References

--

--

No responses yet