Files
Facundo MenzellaandRCGitBot 3cc82762f7 Add workflow prewarming performance benchmark (#8206)
* Add workflow prewarming performance benchmark

* Add performance baseline recording and comparison

* Move performance baseline recording and checks into XCTest

* Declare performance tolerance in tests and exclude legacy project checks

* Simplify memory benchmark and run it on each PR

* Use published workflow for performance benchmark

* Add PR job to record and push performance baselines

* Update performance baselines

* Run baseline automation tests on PRs and use Fastlane with Xcode 27

* Update performance baselines

* Require performance benchmark in PR merge gate

---------

Co-authored-by: RCGitBot <dev+RCGitBot@revenuecat.com>
2026-11-05 14:16:45 +00:00

19 KiB
Raw Permalink Blame History

purchases-ios — Development Guidelines

This file provides guidance to AI coding agents when working with code in this repository.

Before changing or reviewing Swift code, read Contributing/SwiftStyleGuide.swift. Check new or modified code against its guidelines. During reviews, report actionable violations alongside correctness and API compatibility issues, even when builds and tests pass.

Project Overview

RevenueCat's official iOS SDK for in-app purchases and subscriptions. Supports iOS, macOS, tvOS, watchOS, and visionOS.

Related repositories:

When implementing features or debugging, check these repos for reference and patterns.

Important: Public API Stability

Do NOT introduce breaking changes to the public API. The SDK is used by thousands of apps.

Safe changes:

  • Adding new optional parameters to existing methods
  • Adding new classes, methods, or properties
  • Bug fixes that don't change method signatures
  • Internal implementation changes

Requires explicit approval:

  • Removing or renaming public classes/methods/properties
  • Changing method signatures (parameter types, required params)
  • Changing return types
  • Modifying behavior in ways that break existing integrations

Do NOT add new public enum types to the SDK's consumer-facing surface. Adding a case to an existing public enum is a source-breaking change — any consumer with an exhaustive switch will fail to compile. Use structs with static constants or other patterns instead when exposing new option sets or categories. Enums marked @_spi(Internal) (on the same line as the declaration) are exempt because they are not part of the SDK's consumer-facing surface.

This policy is enforced by the no_new_public_enums SwiftLint custom rule defined in .swiftlint.yml. Pre-existing public enums are grandfathered in swiftlint-baseline.json.

Do NOT add new public inits to the SDK's consumer-facing surface. Public initializers are part of the binary API and cannot change without breaking consumers. Keep initializers internal or private, or mark them @_spi(Internal) (on the same line or the line above) when hybrid SDKs need them. Initializers required by Decodable, RawRepresentable and NSCoding are exempt. The rule cannot see an enclosing @_spi(Internal) extension, so inits inside one still need @_spi(Internal) on the init itself.

This policy is enforced by the no_new_public_inits SwiftLint custom rule defined in .swiftlint.yml. Pre-existing public initializers are grandfathered in swiftlint-baseline.json.

Baseline entries are matched by line text, so moving code around does not invalidate them. To accept an intentional new violation, regenerate the baseline with swiftlint lint --write-baseline swiftlint-baseline.json and commit it. Custom rules have fixtures in Tests/SwiftLintCustomRules/Fixtures; when changing a rule, extend its fixture and run ruby Tests/SwiftLintCustomRules/swiftlint_custom_rules_test.rb.

The Tests/APITesters/ targets run in CI to catch unintended API changes. The api/*.swiftinterface files track the public API surface. If API tests fail, you've likely broken the public API.

Objective-C Compatibility

Many core SDK classes are exposed to Objective-C and must stay compatible. Key rules:

  • NSObject subclasses (Purchases, CustomerInfo, EntitlementInfo, StoreProduct, StoreTransaction, etc.) must remain @objc-compatible. Don't add Swift-only types (e.g., generics, async without a completion-handler wrapper, Swift enums without @objc, default parameter values) to their public API without providing an Obj-C equivalent.
  • @objc(RC...) prefixed names are used for Obj-C class names (e.g., @objc(RCPurchases), @objc(RCCustomerInfo)). Don't remove or change these.
  • New public properties/methods on @objc-exposed classes must be marked @objc unless there's a deliberate reason to exclude them (document why).
  • Enums exposed to Obj-C use @objc with Int raw values. Swift-only enums with associated values or string raw values can't be used from Obj-C.
  • Both Swift and Obj-C API testers exist in Tests/APITesters/. When modifying public API on an @objc class, update both SwiftAPITester and ObjcAPITester targets.
  • Don't break existing Obj-C callers — if a method is currently callable from Obj-C, it must remain so.

Common Development Commands

Quick reference for the most common operations:

swift build                        # Build via SPM
swift test                         # Run unit tests via SPM
tuist generate                     # Generate Tuist workspace (preferred)
swiftlint                          # Run linter
swiftlint --fix                    # Auto-fix lint issues
bundle exec fastlane test_ios      # Run iOS tests via Fastlane
bundle exec fastlane run_api_tests # Verify public API surface

For the full set of build, test, Tuist, and Fastlane commands, see:

  • Contributing/CONTRIBUTING.md — environment setup, workflow, style guide
  • Contributing/DEVELOPMENT.md — Tuist workspace generation, targets, troubleshooting
  • fastlane/README.md — complete list of available Fastlane lanes

Project Architecture

Module Structure

This is a multi-target Swift project supporting iOS, macOS, tvOS, watchOS, and visionOS:

  • RevenueCat (Sources/) — Core SDK: API, business logic, networking, StoreKit abstractions
  • RevenueCat_CustomEntitlementComputation (CustomEntitlementComputation/ → symlink to Sources/) — Same source with ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION flag
  • ReceiptParser (LocalReceiptParsing/ → symlink to Sources/LocalReceiptParsing/) — Local receipt parsing library
  • RevenueCatUI (RevenueCatUI/) — SwiftUI paywalls and customer center (depends on RevenueCat)

Key top-level directories: Sources/, RevenueCatUI/, Tests/, Examples/, Projects/, api/, fastlane/, Tuist/, Contributing/, scripts/. Explore the filesystem directly for current subdirectory layout.

Example apps

  • Examples/SampleCat/ is the reference for app integration code (offerings, paywalls, checkpoints, Customer Center). The dashboard download writes over its Constants.swift, so keep other values out of that file.
  • PaywallsTester, AutomatedSDKTestsApp, and Tests/ are test tools with debug seams. Do not copy them into app code.

Key Architectural Patterns

Core Purchases Module

  • Singleton Pattern: Purchases.shared as the main entry point
  • Delegate Pattern: PurchasesDelegate for event callbacks
  • Manager Pattern: IdentityManager, SubscriberAttributesManager, EventsManager
  • Backend/Cache Layer: Backend for networking, DeviceCache for local storage
  • StoreKit 1 / StoreKit 2 dual implementation: The SDK maintains parallel code paths in Sources/Purchasing/StoreKit1/ and Sources/Purchasing/StoreKit2/, with shared abstractions in Sources/Purchasing/StoreKitAbstractions/. Bug fixes or behavior changes often need to be applied to both paths. Always check whether the other StoreKit implementation is affected.

RevenueCatUI Module

  • SwiftUI-based: Modern declarative UI
  • MVVM Pattern: ViewModels with SwiftUI views
  • Main Components: PaywallView, CustomerCenterView

Thread Safety

The SDK is heavily concurrent — StoreKit callbacks, delegate calls, caching, and network responses can all arrive on different threads. See Contributing/ThreadSafety.md for the full guide. Key primitives:

  • Lock / Lock(.recursive) — low-level synchronization for critical sections
  • Atomic<T> — thread-safe wrapper for mutable state
  • SynchronizedUserDefaults — thread-safe UserDefaults access (used by DeviceCache)

When modifying internal state, use these primitives. Don't introduce bare property access to shared mutable state without synchronization.

Logging

When logging to the debug console, follow the pattern with the strings in RevenueCatUI/Data/Strings.swift

API Annotations

  • @_spi(Internal) — APIs that are public only to be accessible by other modules or hybrid SDKs, not intended for external developer use
  • @available — platform availability annotations for StoreKit 2 and other iOS version-specific features

Testing

Tests use XCTest, Nimble, swift-snapshot-testing, and OHHTTPStubs. Test targets live under Tests/ — explore subdirectories directly for the current layout.

Before you add or change a test, read Contributing/TESTING.md.

Performance tests

  • Add memory benchmarks in Tests/PerformanceTests/. Keep setup outside the measurement interval.
  • Use fixed fixtures. Verify that each iteration completes the expected work.
  • Set the tolerance in each test. Keep the measured baseline in Baselines.json.
  • Verify that a known memory regression fails before you use a benchmark as evidence.
  • Keep the Xcode version, simulator, and workload consistent with the baseline.
  • Never record a new baseline automatically after a failure.

The performance-tests CI job runs on each PR branch push. It uses Xcode 27.0.0, an iPhone 17 simulator with iOS 27.0, and Release configuration. It does not need an API key.

The workflow fixture is the published revision 24 of [performance-benchmark-1] (wff675a07c6c8b4540) in project 6627e5bc. It contains seven screens and its UI configuration. The benchmark loads Tests/PerformanceTests/Fixtures/performance-benchmark-1-revision-24.json without duplicate workflows or screens.

Export an approved published revision with:

mafdet workflow content --project-id 6627e5bc --workflow-id wff675a07c6c8b4540 \
  --output /tmp/performance-benchmark-1.json

Keep the export unchanged. Include the revision in its filename. Review the fixture and record a new baseline after an intentional workload change.

For a local comparison:

xcodebuild test -workspace RevenueCat-Tuist.xcworkspace -scheme PerformanceTests \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0' \
  -parallel-testing-enabled NO ENABLE_TESTABILITY=YES CODE_SIGNING_ALLOWED=NO

To update the baseline on a PR, comment @RCGitBot please test record-performance. The job records the measurements and pushes only Baselines.json to the PR branch. It stops if the branch changed during the run. Review the baseline diff before you merge.

To record a candidate baseline, start a CircleCI pipeline with action=record_performance_baselines. Download the Baselines.json artifact. Review its measurements and workload hash before you update the checked-in file. The job does not commit the file.

For local record mode, prefix the command with TEST_RUNNER_RECORD_PERFORMANCE_BASELINES=1. Use the same test selection for record and comparison runs. CI runs the full PerformanceTests target. Disable record mode before you compare results.

Development Workflow

For environment setup, see Contributing/CONTRIBUTING.md. For code style, see Contributing/SwiftStyleGuide.swift.

Main Entry Points

  • Purchases class: Primary SDK entry point (Sources/Purchasing/Purchases/Purchases.swift)
  • Purchases.configure(withAPIKey:): Configuration method
  • UI Components: PaywallView, CustomerCenterView in RevenueCatUI

Key Dependencies

  • Swift: Primary language with async/await support
  • StoreKit / StoreKit 2: Apple's in-app purchase framework
  • SwiftUI: UI framework for RevenueCatUI
  • Foundation: Core Apple framework

Build Configuration

Package Manager Support

  • Swift Package Manager: Package.swift (primary)
  • CocoaPods: RevenueCat.podspec, RevenueCatUI.podspec
  • Carthage: Supported via XCFramework

Tuist Project Management

The project uses Tuist for managing the Xcode workspace. See Contributing/DEVELOPMENT.md for full Tuist commands, environment variables, and troubleshooting.

Tuist environment variables (prefix all with TUIST_ when passing to tuist generate):

Variable Purpose Default
TUIST_RC_REMOTE=true Use remote instead of local RevenueCat dependency local
TUIST_RC_XCODE_PROJECT=true Use Xcode project instead of Swift Package dependency SPM
TUIST_INCLUDE_TEST_DEPENDENCIES=false Skip test/dev dependencies (Nimble, OHHTTPStubs, etc.) to speed up tuist install true
TUIST_INCLUDE_XCFRAMEWORK_INSTALLATION_TESTS=true Include XCFrameworkInstallationTests project false
TUIST_SK_CONFIG_PATH=/path/to/file.storekit Custom StoreKit config for PaywallsTester scheme —
TUIST_RC_API_KEY=appl_xxxxx RevenueCat API key written to Local.xcconfig at generation time —
TUIST_LAUNCH_ARGUMENTS="-Flag1 -Flag2" Space-separated launch arguments injected into PaywallsTester scheme run action (enabled by default) —
TUIST_SWIFT_CONDITIONS="FLAG1 FLAG2" Space-separated Swift compilation conditions injected into all targets —
TUIST_PAYWALLS_TESTER_BUNDLE_ID=com.x.PaywallsTester Custom bundle identifier for the PaywallsTester target (e.g. testing against another RevenueCat project) com.revenuecat.PaywallsTester

Example combining multiple variables:

TUIST_RC_API_KEY=appl_xxxxx TUIST_SWIFT_CONDITIONS="MY_FEATURE_FLAG" tuist generate PaywallsTester

Target Specifications

  • Minimum Deployment: iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, visionOS 1.0
  • Swift: 5.9+
  • Xcode: 15.0+

These minimums are enforced at compile time. Concretely:

  • Don't use APIs unavailable on the minimum target without an @available check and a fallback path (e.g., AttributedString is iOS 15+, Swift concurrency back-deployment requires iOS 13+ but some features need iOS 15+).
  • StoreKit 2 is iOS 15+ — all SK2 code paths must be gated with @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) or equivalent.
  • Never assume the latest OS — CI tests run on multiple OS versions, and customers run the SDK on older devices.

Development Notes

For snapshot testing, sample applications, pre-commit hooks, and release process details, see the docs in Contributing/ (in particular CONTRIBUTING.md, DEVELOPMENT.md, and RELEASING.md).

Important Files

  • .version - Current SDK version
  • Local.xcconfig - Local development configuration (not committed)
  • CI.xcconfig - CI-specific configuration
  • api/*.swiftinterface - Public API surface tracking
  • swiftlint-baseline.json - Pre-existing SwiftLint violations grandfathered by the linter (notably for no_new_public_enums and no_new_public_inits)

Pull Requests

Size & Structure

  • Prefer smaller, self-contained stacked PRs when possible. "Self-contained" means the PR compiles, passes tests, and is reviewable on its own. As a rule of thumb, aim to keep the diff under ~300 lines of hand-written source — excluding generated, serialized, or non-reviewed files (e.g. JSON, snapshots, lockfiles, api/*.swiftinterface) and test or example app files. For larger tasks, plan the PR breakdown up front and stack interdependent changes instead of bundling everything into one large PR.

Description

  • Use the repo's PR template (.github/PULL_REQUEST_TEMPLATE.md), applying the rules below.
  • Keep PR descriptions concise and easy to read. The description answers one question: what does the reviewer need that the diff doesn't already show? Concretely:
    • Motivation: 1–2 sentences. State why the change exists; don't recap the whole backstory.
    • Don't restate the diff — no file-by-file or change-by-change enumeration.
    • No "Commits" section.
    • Testing: only mention what the diff doesn't show. Don't enumerate added test cases.

Labels

When creating a pull request, always add one of these labels to categorize the change. These labels determine automatic version bumps and changelog generation:

Label When to Use
pr:feat New user-facing features or enhancements
pr:fix Bug fixes
pr:other Internal changes, refactors, CI, docs, or anything that shouldn't trigger a release

Additional scope labels (add alongside the primary label above):

  • pr:RevenueCatUI — Changes specific to the RevenueCatUI module (paywalls, customer center)
  • feat:PaywallsV2 — Changes related to Paywalls V2 (requires pr:RevenueCatUI as well)
  • feat:Customer Center — Changes related to Customer Center (requires pr:RevenueCatUI as well)

Code Review Guidelines

When reviewing a pull request:

  1. Check for linked specs — If the PR description contains a link to https://github.com/RevenueCat/sdk-specs, fetch and read the spec before reviewing the code. The spec defines the expected behavior and requirements.
  2. Verify the implementation matches the spec — Ensure the code implements what the spec describes, including edge cases and error handling.
  3. Check cross-platform consistency — If the spec applies to multiple platforms, verify the implementation follows patterns from other SDKs (especially purchases-android).

When the Task is Ambiguous

  1. Search for similar existing implementation in this repo first
  2. Check purchases-android and purchases-hybrid-common for patterns
  3. If there's a pattern, follow it exactly
  4. If not, propose options with tradeoffs and pick the safest default

Guardrails

  • Don't invent APIs or file paths — verify they exist before referencing them
  • Don't remove code you don't understand — ask for context first
  • Don't make large refactors unless explicitly requested
  • Keep diffs minimal — only touch what's necessary, preserve existing formatting
  • Don't break the public API — if API tests fail, investigate why
  • Fix root causes — don't add workarounds or suppress errors without understanding the underlying issue
  • Verify changes build — run swift build or the relevant Fastlane lane before considering work done
  • Run SwiftLint before committing (swiftlint or swiftlint --fix)
  • Follow the style guide in Contributing/SwiftStyleGuide.swift
  • Check Android SDK when unsure about cross-platform implementation details — new features should follow existing patterns across SDKs
  • Never commit Claude-related files — do not stage or commit .claude/ directory, settings.local.json, or any AI tool configuration files
  • Never commit API keys or secrets — do not stage or commit API keys, tokens, credentials, or any sensitive data
  • Remove dead code in your PR — delete unused parameters, unreachable branches, and leftover scaffolding before requesting review
  • Don't widen access control for tests — don't add internal/public accessors solely for test access; inject dependencies via protocols or test at the appropriate layer
  • Extract shared helpers — before duplicating logic, check for existing utilities (e.g., AvailabilityChecks for OS-specific test skips); factor common code into helpers
  • Prefer structs for data holders — use structs instead of classes for value-like types unless Obj-C compatibility is required