Greetings
Hello, I’m AJ, an iOS software engineer at Money Forward, Inc.'s Tax Return Group.
Recently, we switched our project to a package-based architecture. It took us approximately three weeks to implement the idea and bring it into production.
As of 2023, the adoption of Swift Package Manager (SPM) for application target management has become prevalent. One of the main advantages of this is the reduction of project file conflicts. Before this, XcodeGen helps to avoid the conflict, but with SPM, we can now move away from XcodeGen. Furthermore, SPM offers a shorter test execution time since the time required to launch the test application has decreased.
In this article, I would like to share the steps, challenges, and perhaps some tips that could be useful if you're considering transitioning your project to a package-based architecture. It's worth noting that our project does not use CocoaPods.
Let's start with The Steps.
The Steps
1. Run XcodeGen for one last time and then remove your project.yml
2. Remove all XcodeGen relate code and files
3. Add package, import it into the project
The directory where your package will be placed:
YourProject/ ├─MoveableFolderA/ ├─MoveableFolderB/ ├─... ├─YourPackageName/ <- HERE
4. Move files into package
In this step, you have the option to either transfer files individually (one by one) or consolidate all movable files into the package simultaneously (in one go).
When moving the files one by one into the package, it become imperative to change the access control of specific classes or variables to Public
if they will be accessed from the project. However, it's important to recognize that this approach incurs a duplication of effort if you’re going to revert the access control back to Internal
once all the files have been relocated to the package.
I opted with latter approach (moving all at once). If your project builds successfully after this comprehensive move, you hit the jackpot! However, encountering issues or errors is entirely acceptable. For now, let's set aside any encountered errors and proceed with the subsequent step.
The directory after the move:
YourProject/ ├─YourPackageName/ │ ├─Package.swift │ ├─Sources/ │ │ ├─YourPackage/ │ │ │ ├─MoveableFolderA/ <- HERE │ │ │ ├─MoveableFolderB/ <- HERE │ │ │ ├─... │ ├─Tests/ │ │ ├─YourPackageTests/ │ │ │ ├─...
5. Commit YourProject.xcodeproj
Ensure to exclude it from the .gitignore
files
6. Include dependencies in the package (Package.swift)
You can assess the necessary dependencies for inclusion by navigating to your PROJECT
> TARGETS
> General
> Frameworks, Libraries, and Embedded Content
.
Once you've added them, you're good to remove the dependencies in Frameworks, Libraries, and Embedded Content
from your project.
7. Code modification
As delineated in step 4, in cases where you are accessing code within the package from the project (e.g., AppDelegate), it becomes imperative to make appropriate adjustments to the access control settings.
Tips and Challenges
1.
Do not underestimate the migration, especially when your project is substantial in size. Regrettably, I didn't take that into consideration and ended up having more than 2,000 file changes in the migration's Pull Request(PR).
So the first thing first, initiate deliberations with your team members to delineate a strategic breakdown of the task into discrete subtasks, which can then be addressed in separate PRs.
2.
Say I'm in-charged of the migration, conflicts invariably arise whenever team members commit changes to the main branch. To prevent these conflicts, the sole recourse is to finalize the migration before engaging on other tasks. However, this approach could potentially turn the migration into a blocking task.
Consequently, our team reached a consensus to address the conflicts at the end of the migration, which, as it turned out, wasn't as complicated as I initially thought it would be.
It remains incumbent upon your team to deliberate and select the most judicious course of action moving forward.
3.
Throughout the migration process, Xcode might be a little bit buggy. For instance, the Package.swift file could become uneditable.
What I did was:
- Reset the package
- File > Packages > Reset Package Caches
- Clean build
- Product > Clean Build Folder (or ⇧+⌘+K)
- Reset Xcode
- Close, quit and reopen
- Wait
- That's why it's time consuming
In the following section, I will elaborate on key actions to mitigate potential errors or crashes during the migration process.
4.
In your storyboard, update the Module to the appropriate module while ensuring that Inherit Module From Target
unchecked.
5.
As for Bundle(for: type(of: self))
, refactor it into Bundle.module
.
6.
To enable the package test to access the resources such as JSON file, it is essential to tell your package about the presence of the resource.
eg:
.testTarget( name: "YourPackageTests", resources: [ .process("YourResourceFolder") ] )
The directory:
YourPackage/ ├─Package.swift ├─Source/ ├─Test/ │ ├─YourPackageTests/ │ │ ├─YourResourceFolder/ │ │ │ ├─eg: json file
7.
If you're using R.swift.Library, replace it with R.swift.git to avoid cycle in dependencies between targets 'yourProject' and 'yourPackage'. You'll need to add the following plugin:
.target( name: "YourPackage", dependencies: [ ... ], plugins: [.plugin(name: "RswiftGenerateInternalResources", package: "R.swift")] ),
After the replacement,
- Refactor
Image(R.image.yourImageName.name)
toImage(R.image.yourImageName)
. - Refactor
R.storyboard.yourStoryboard().instantiateInitialViewController
toR.storyboard.yourStoryboard.instantiateInitialViewController
- If you're using
Self
as the return value to return the view controller, refactor it toYourViewContoller
- If you're using
- Add
xcodebuild_options: -skipPackagePluginValidation
if you're using CI/CD
Pro Tip: Since R.swift is a generated file, if you are utilizing it within your project – let's say in order to access R.color
in AppDelegate, which you can't, you will need to initiate the color within your package with a Public
access control and access the initiated color instead.
eg:
public enum RColor { public static let white = R.color.white()! }
This way, you can access the white color with RColor.white
.
As you may be aware, I'm actually using the RswiftGenerateInternalResources
plugin. To get around the additional effort highlighted in the pro tip earlier, an alternative option is to use the RswiftGeneratePublicResources
plugin instead. For more information, please refer here.
8.
For OHHTTPStubs, include the necessary dependencies within the testTarget of your package.
eg:
.testTarget( name: "YourPackageTests", dependencies: [ "YourPackage", .product(name: "OHHTTPStubs", package: "OHHTTPStubs"), .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs") ] )
9.
Refactor @testable import yourProject
to @testable import yourPackage
in all your test files.
10.
Please bear in mind that SPM does not support custom build configurations; only Debug and Release configurations are supported.
Pro Tip: If you are utilizing #if DEBUG
within your project and you possess a custom configuration (let's say Staging), consider prefixing it with Debug_
(Debug_Staging
) and incorporate the following setting into your Package.swift. Xcode will then automatically recognize it as a Debug configuration.
.target( name: "YourPackage", dependencies: [ ... ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ] )
Conclusion
After the migration, whenever there are file changes, we are no longer required to re-run XcodeGen! (I'm still trying to get used to it, actually)
This is the significant step that we've taken to SPM-lies our project, aligning with our plans to implement multi-modularization in the near future for a more organized, maintainable, and scalable codebase.
There are various steps that can be taken to transition a project into package-based architecture. The steps written above serve as a guideline, and you are encouraged to explore and experiment with other approaches that align with your project's needs and your own preferences.
I hope I didn't missed anything and wishing you a happy migration! Thanks for reading.