This is a repository for an e-commerce iOS app template. Its current behavior can be seen in the following demonstration:
alfie-demo.mp4
- This project contains sensitive files encrypted using
git-secret. Before starting, you must decrypt them locally to build the project. To do this, request the public and private GPG keys from the team. See this section for details. - This project fetches data from the Alfie-BFF GraphQL API, run locally on
localhost:3000. SeeDocs/GraphQL.mdfor the local loop.
MVVM with flow-based navigation, in Swift Package modules under Alfie/AlfieKit/. Each feature
module owns its Views, ViewModels, DependencyContainers and Navigation (FlowViewModel + Route).
See Docs/Architecture.md for the layer-by-layer patterns, the module
graph, and the service/dependency-injection rules.
When working in a shared repository, security measures are essential—especially if the repository is public.
If a file containing sensitive data must be stored in the repository, it should be encrypted. For this, we use git-secret, specification can be found here.
A GPG key pair was created for the team, along with a separate pair for CI/CD. To decrypt secrets, you need to:
- Install project dependencies (
git-secretandgnupgwhich is agit-secretdependency, if not already installed). - Import both the public and private keys locally.
Steps
# Install project dependencies
brew bundle install
# Check if `git-secret` is installed
git-secret --version
# If it's not installed, install it
brew install git-secret
# Check if `gnupg` is installed
gpg --version
# If it's not installed, install it
brew install gnupg
# Import the public key (needed to encrypt secrets)
gpg --import public-key-path.gpg
# Import the private key (needed to decrypt secrets)
gpg --import private-key-path.gpg
# Reveal the decrypted files
git secret reveal
After running git secret reveal, it may seem like nothing happened. However, the decrypted files are automatically ignored by Git via .gitignore, preventing accidental commits.
To verify which files were revealed, check .gitsecret/paths/mapping.cfg. This file maintains a list of all encrypted files.
To encrypt a new file:
- Add it only locally (do not commit it yet).
- Register it as a secret.
- Encrypt it.
# Ensure the file is only local and not tracked by Git
git rm --cached path-to-the-sensitive-file
# Add it to the secrets list
git secret add path-to-the-sensitive-file
# Encrypt the sensitive file
git secret hide
Before committing, confirm that:
- The unencrypted file is not committed.
- Only the
.secretversion of the file is included in the commit.
By default, git-secret adds the sensitive file to .gitignore, but double-check to ensure it's excluded from commits.
This project uses Apple String Catalog. It requires iOS 16 which brings a new way to localise strings with LocalizedStringResource.
It is possible to initialise a localised string or a localised attributed string from a LocalizedStringResource beforehand or keep it to localise later when needed. The later approach enables having dynamic localisation on SwiftUI Previews by injecting different environment locales, while initialising a localised string/attributed string beforehand disables the ability to automatic lookup for a localisable resource in a different language.
This project uses SwiftGen to generate strongly-typed accessors for the string catalogue, so a removed or renamed key is a compile error rather than a runtime miss.
- Open the String Catalog table
L10n. - Manually add the entries in the base language and any other languages. Please use
ReverseDomainconvention along withSnakeCaseconvention for keys naming (ex:plp.error_view.title) and give translation keys meaningful names. - Mark for Review any entry not officially provided/approved to easily track the translations state (Mark as Reviewed when this happens too)
- Build the project. Using SwiftGen, will automatically update the
L10n+Generated.swiftfile.
Note: New tables are discouraged, the goal is to have everything in the L10n table.
Sample
// L10n+Generated.swift
enum L10n {
enum Account {
/// Account
static let title = L10n.tr("L10n", "account.title")
}
enum Home {
/// Home
static let title = L10n.tr("L10n", "home.title")
enum LoggedIn {
/// Member Since: %@
static func subtitle(_ p1: Any) -> String {
return L10n.tr("L10n", "home.logged_in.subtitle", String(describing: p1))
}
/// Hi, %@
static func title(_ p1: Any) -> String {
return L10n.tr("L10n", "home.logged_in.title", String(describing: p1))
}
}
}
}
...SwiftUI...
Text(L10n.Account.title)
Text(L10n.Home.LoggedIn.title("Title"))
LocalizationTests contains some tests that already handle the supported languages. If for some specific reason you have created a new table, you should include in testLocalizationTables test that will go through all the keys and validate translations are in place for all supported languages. Regarding testing localisation with arguments it's recommended to create a test to validate each variation (pluralization, devices, etc.) you may need to customize.
For example for the key plp.number_of_results.message below with pluralization, a test testLocalizableProductListingResultsWithArgs could be designed to lookup for each variation:
- One: %d result
- Other: %d results
func testLocalizableProductListingResultsWithArgs() {
localizations.forEach { localization in
let resources = [0, 1, 2].map { L10n.Plp.NumberOfResults.message($0) }
XCTAssertTrue(validateLocalizedStrings(resources, for: localization))
}
}
This project utilizes SwiftGenPlugin to streamline the integration of SwiftGen into our workflow. By leveraging SwiftGenPlugin, we can manage the SwiftGen dependency directly through Swift Package Manager (SPM), avoiding the need to install the SwiftGen binary locally or rely on Cocoapods for dependency management. This approach ensures a cleaner, more organized setup for integrating SwiftGen, simplifies dependency handling, and makes the project easier to maintain and share across teams.
Both dependencies currently rely on forks of the original repositories, as the original projects appear to be abandoned. The solution for supporting string catalogues is still pending merge in a long-standing pull request (PR link).
The SwiftGenPlugin leverages Swift Package Manager (SPM) plugins by providing both a build tool plugin and a command plugin. While both plugins serve the same purpose of generating type-safe files, there are key differences:
The build tool plugin does not have write permissions, restricting the generated L10n+Generated.swift file to the DerivedData folder.
The command plugin allows generating the L10n+Generated.swift file directly in the desired location, but it must be run manually with the command:
swift package --allow-writing-to-package-directory generate-code-for-resources.
Given these constraints, we opted for the command plugin. It is integrated into a build phase under the Run Build Tool Plug-ins section, where it generates a L10n+generated.swift file in SharedUI library. To address this, a custom build phase script, Run SwiftGen, is used to run the command swift package --allow-writing-to-package-directory generate-code-for-resources --config swiftgen.yml.
For more information about SPM plugins, see the official documentation.
This project uses GraphQL to fetch data from the BFF API. The schema is owned by the BFF and synced into this repo (committed at Alfie/AlfieKit/Sources/BFFGraph/CodeGen/Schema/schema.graphqls), so codegen and builds stay self-contained.
For the BFF integration workflow, see Docs/GraphQL.md:
- Running the app against a local BFF — starting the BFF, pointing the app at it
- Syncing the BFF schema —
Alfie/scripts/sync-bff-schema.sh - Adding / updating queries — query, fragment, and converter patterns
Work in progress
Work in progress