Tuisting The Oldest iOS Project
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",