Skip to content

How it works

Tawa Nicolas edited this page Jan 21, 2023 · 7 revisions

Introduction

This page aims to describe the different moving parts of Phoenix.

The Ash File

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.

The Document-Based App

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.

Modularisation

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.

The File Format

This section describes the different parts that make up the ash file.

The ProjectConfiguration

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:

image

The ComponentsFamily

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 like Support, you can choose to ignore the suffix, and instead have components under that family with names like Navigator, 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 to Supports, you can choose to specify Support 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.

image

The Component

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:

image

The RemoteComponent

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:

image

After adding the Remote Dependency, you can start listing the different Name and Packages that will be used while generating your local Swift Packages

image

Afterward, go to your local Component, and add it to the Remotes Dependencies using the + button.

image

The UI allows you to specify in which Swift Package you want to depend on the remote component.