-
Notifications
You must be signed in to change notification settings - Fork 3
How it works
This page aims to describe the different moving parts of Phoenix.
The ash
file is a package that you add to your Xcode project modules folder, and it contains a group of JSON
files that includes the definition of your Swift Packages.
In the Phoenix codebase, this ash
file is Encoded/Decoded into the following struct:
public struct PhoenixDocument {
public var families: [ComponentsFamily]
public var projectConfiguration: ProjectConfiguration
public var remoteComponents: [RemoteComponent]
...
}
Breaking down this struct into multiple JSON
files avoids having one big file that is annoying to review in pull requests. Instead, you can identify which component the changes affect from the filename.
Phoenix is a visual editor that helps you manage your Xcode project's Ash file and uses it to generate and maintain your Swift Packages. It aims to help you be productive by automating manually editing your Package.swift
files.
It is the ability to group related code and set clear physical boundaries between different parts of your architecture. Modularisation allows Xcode to compile a smaller subset of your codebase related to the code changes you made in your incremental builds. To take advantage of that, you need an optimized dependency graph. There is no one-size-fits-all solution for modularisation regarding how you break down your architecture. You could package your code by layer, feature, or component. There seems to be a trend, however, where the team breaks down each component into multiple modules, like:
- The Public Interface
- The Implementation
- A Mock Implementation + Spies and Test Helpers
Doing this work manually while maintaining your team's guidelines can be a cumbersome process that reduces productivity. Teams that do this create tools that allow you to generate and maintain those packages. Phoenix is an open-source project that provides solutions for those common challenges.
This section describes the different parts that make up the ash file.
The Project Configuration includes the following:
- swiftVersion: String
This defines the version of Swift in which the Swift Packages are generated
- packageConfigurations: [PackageConfiguration]
The array of Swift Packages each component has, including their internal dependencies, folder names, etc.
- defaultDependencies: [PackageTargetType: String]
The definition of dependencies between different components. For a project with Contract/Implementation(+Tests)/Mock packages for each component, you could specify that Implementations depend on Contracts, and Tests depend on Mocks. This is merely a convenience setup that automatically selects this relationship between components when you add dependencies, and it does not enforce this relationship upon all components.
To access and modify the Project Configuration, you can either click on the Configuration
Button in the toolbar or press ⌘ + ,
.
A sample Project Configuration would look like the following:
The ComponentsFamily is the struct that groups Components that belong together and includes the definition of the Family itself.
You could group components under one family if they follow the same architectural rules and guidelines. For example, you may want to group all your features under the Feature
family and set rules that exclude Features from being used in lower-layer components.
Some of the Family settings include:
- ignoreSuffix: Bool
This is represented by a toggle in the UI. Ignoring the suffix means that the family name is not appended to the Component. By default, all families will append their names to the component name, but you can choose to change that. For example, you may want all your Feature packages to have names like
HomeFeature
,SettingsFeature
, etc. But for other components that are under families likeSupport
, you can choose to ignore the suffix, and instead have components under that family with names likeNavigator
,APIClient
, etc.
- folder: String?
This variable is optional. If it is nil, it will use the family name's plural form as the folder name. (It can generate wrong plurals for some words 🫣) The Feature family's default folder name would be
Features
. The Support family would default toSupports
, you can choose to specifySupport
as the folder name instead.
- defaultDependencies: [PackageTargetType: String]
This is similar to the Project Configuration's defaultDependencies, and when you set it, it overrides the Project Configuration's values.
- excludedFamilies: [String]
This variable allows you to enforce guidelines in your project.
The Component is what includes the definition of what Swift Packages to generate, what other components to depend on, etc.
To add a new Component, make sure you have the Components
tab selected ⌘ + 1
. Then you can either press ⌘ + ⇧ + A
or click on the New Component
button located at the top of the components list.
You will see a popover that allows you to enter the Component's given and family names:
One of the limitations of modularising your project using Swift Packages is that you cannot depend on multiple versions of the same Swift Package. Remote Components are supposed to centralize the definition of the version of your dependencies that come from remote repositories. This means, in case you want to update the version of your remote dependency, it can be done in on location for the whole project.
After defining your remote component, you can depend on it in your local components.
To add a new Remote Component make sure you have the Remote
tab selected ⌘ + 2
. Then you can either press ⌘ + ⇧ + A
or click on the New Remote Dependency
button located at the top of the remote components list.
You will see a popover that allows you to enter the Remote Component's repository URL, and the version:
After adding the Remote Dependency, you can start listing the different Name and Packages that will be used while generating your local Swift Packages
Afterward, go to your local Component, and add it to the Remotes Dependencies using the +
button.
The UI allows you to specify in which Swift Package you want to depend on the remote component.