# Proton's iOS and macOS app is a layered Swift workspace

> The ProtonVPN iOS and macOS app is published under GPLv3 and built as an Xcode workspace split into Foundations, Shared and Features packages. Getting it to compile takes Git LFS, submodules, a paid Apple developer account and a private repository of obfuscated constants.

**ProtonVPN/ios-mac-app** — Official ProtonVPN iOS and macOS app

- Repository: https://github.com/ProtonVPN/ios-mac-app
- Stars: 663 · Forks: 142
- Language: Swift
- License: GPL-3.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/protonvpn-ios-mac-app

## Three package layers with one hard rule

The project is packaged by layer specifically to avoid module dependency problems, and the layering is stated as a set of constraints rather than a suggestion. Foundations holds small, single-purpose libraries used to build bigger things, and these should only ever link each other, if they do so at all. Shared holds slightly larger libraries meant for code shared between applications or extensions, and it may only link Foundations libraries, with the stated exception of each of them linking CoreCommon. Features holds self-contained libraries, one feature each, which link only from Shared or Foundations and are instantiated inside application, test or extension code. The direction of dependency never reverses, which is the whole point. Two legacy packages, LegacyCommon and NEHelper, are explicitly marked as places where new code should be avoided, since they are meant to be chipped away and their components moved into one of the three proper categories.

## Building it needs LFS, submodules and Homebrew

The setup steps are short and each one blocks the next. Git LFS has to be enabled on the machine, then the repository is cloned, and the submodules have to be cloned too, which the README spells out as two commands, git submodule init followed by git submodule update. Homebrew dependencies are installed with brew bundle, which is why a Brewfile sits in the repository root. Then comes the constraint that shapes everything else: you need Xcode 14.x for now. Code signing has to be configured for all targets with a paid Apple developer account, which the README attributes to the VPN entitlements, and the bundle identifiers must be changed to something unique so your build does not collide with Proton's. Finally a clean build folder in Xcode. All dependencies come through Swift Package Manager, and the instruction is to let Xcode resolve packages before running any target.

## The build depends on secrets you probably cannot see

A set of secrets is required to build the project, including URLs and keys, and they live in files named ObfuscatedConstants.swift. Those files are kept in a separate repository that needs additional setup to stay synchronized. The script Integration/Scripts/credentials.sh handles it, and the setup invocation takes the local path for the credentials repository and the remote URL:

```bash
./Integration/Scripts/credentials.sh setup -p <path to credentials repo> -r <credentials remote>
```

You verify the setup by running the checkout command, which lists what is available:

```bash
./Integration/Scripts/credentials.sh checkout
```

Once that works, the obfuscated constants are synchronized automatically during the Generate Obfuscated Constants build phase of each app. This is the point where an outside contributor stops, and it is worth being blunt about it. Without access to the secrets repository there is no build, so the practical audience for the public repository is people reading the code, running the linters, and working inside Proton where the credentials are already provisioned.

## Strict linting on CI, forgiving linting locally

Code style is enforced with swiftlint, and the two modes are described precisely. During development swiftlint runs in non-strict mode, which the README frames as making it easier to develop without worrying about formatting, while on CI the linting is strict and fails on any warning. That gap between local and CI is a common source of failed pipelines, so the project ships a mitigation. Before committing, a script is run to add a pre-commit hook that checks all new and modified files in strict mode and stops you from committing code that would not pass CI. The invocation is Integration/Scripts/pre_commit_lint.sh setup. The repository also carries a .swiftlint.yml, a .swiftformat file, a .swift-version file, an .editorconfig and a Gemfile, so formatting is defined in several places and a contributor has to satisfy all of them.

## Strings are generated from the Strings package by SwiftGen

Localization runs through SwiftGen, which generates the Localizable.strings file used to access app strings, with the strings themselves stored in the standard Localizable.strings files. The workflow is described as adding a new string to the Strings package and running the swiftgen command in a directory where swiftgen.yml is defined. A Mintfile is present in the root, which is how the tool itself gets installed on a machine. The generated file approach means the app never hand edits the compiled strings bundle, so a translator or a new locale works by adding entries and regenerating. The repository also carries a .locale-state.metadata file, which suggests the set of translated locales is tracked as a distinct piece of state rather than inferred from the file list.

## One workspace, two apps and a network extension

The repository layout matches a project that serves two products from one codebase. ProtonVPN.xcworkspace sits at the root as the Xcode workspace, with apps/ holding the application targets, libraries/ holding the layered packages, Integration/ holding the scripts and build phases, external/ holding vendored code, sampleapps/ for small demonstration targets, and git-hooks/ for the version control side. The mention of NEHelper as a legacy package that should be avoided points to a network extension target, which is the piece that a VPN client cannot avoid on Apple platforms. The CI configuration is GitLab-based, with a .gitlab-ci.yml and a .gitlab/ directory, so the pipeline does not live on the same host as the repository.

## Reading it as a reference rather than a dependency

The alternative to depending on this project is writing a network extension client from scratch, or using a smaller open source VPN client. The difference in approach is what the public repository actually offers. As a source of reference it is unusually instructive, because a production VPN client has to solve entitlement signing, per-platform targets, extension lifecycle and string localization under a single build system, and the layering rules show how one team kept a codebase of that size from turning into a dependency mess. As a dependency it is far weaker. The build needs secrets you do not have, an Apple developer account you must pay for, and Xcode 14.x, and the licence is GPLv3, which is fine for reading and for reuse under its terms but a constraint if you expected a permissive library. Recent releases are tagged separately per platform, mac/6.5.0 and ios/7.3.0, both from 2026-04-23, and the last push was on 2026-10-13.

## Conclusion

Proton's iOS and macOS app is worth reading for anyone building a VPN client in Swift, since the layering rule and the entitlement handling are hard won, and the GPLv3 licence makes the code freely reusable. It is not a codebase you can pick up and build on an afternoon, since the secrets live in a separate repository you may not have access to, code signing needs a paid Apple developer account, and the README still points at Xcode 14.x. Verify first that you can obtain the obfuscated constants repository and the signed entitlements, that your Xcode version matches what the project expects, and that the licence terms suit what you intend to do with the code.

## FAQ

### What are the steps to build the ProtonVPN iOS and macOS app?

Enable Git LFS, clone the repository and its submodules with git submodule init and git submodule update, install Homebrew dependencies with brew bundle, use Xcode 14.x, configure code signing for all targets with a paid Apple developer account, change the bundle identifiers, and clean the build folder. All dependencies come through Swift Package Manager.

### Why can I not build the ProtonVPN app without extra access?

The build requires a set of secrets including URLs and keys, held in files named ObfuscatedConstants.swift that live in a separate repository. You run Integration/Scripts/credentials.sh setup with the local path and remote URL for that repository, and after checkout the constants synchronize during the Generate Obfuscated Constants build phase.

### How is the ProtonVPN codebase organised into packages?

It is split into three layers with one-way dependencies: Foundations holds small single-purpose libraries, Shared holds larger libraries for code shared between apps and extensions and may link Foundations, and Features holds self-contained feature libraries that link only from Shared or Foundations. New code should avoid the legacy LegacyCommon and NEHelper packages.

### How does the project enforce code style before a commit?

swiftlint runs in non-strict mode during development and in strict mode on CI, where any warning fails the build. To avoid that, running ./Integration/Scripts/pre_commit_lint.sh setup adds a pre-commit hook that checks new and modified files in strict mode before the commit goes through.

## Sources

- [Issues](https://github.com/ProtonVPN/ios-mac-app/issues)
- [License: GPL-3.0](https://github.com/ProtonVPN/ios-mac-app/blob/develop/LICENSE)
- [ProtonVPN/ios-mac-app on GitHub](https://github.com/ProtonVPN/ios-mac-app)
- [README](https://github.com/ProtonVPN/ios-mac-app/blob/develop/README.md)
- [Releases](https://github.com/ProtonVPN/ios-mac-app/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/protonvpn-ios-mac-app
