Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }}
e2e_android: ${{ steps.filter.outputs.e2e_android }}
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
swift_package: ${{ steps.filter.outputs.swift_package }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

Expand Down Expand Up @@ -145,6 +146,13 @@ jobs:
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# Swift Package build/test scope (the package published to optimization.swift).
swift_package:
- 'packages/ios/ContentfulOptimization/**'
- 'packages/universal/optimization-js-bridge/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'

setup:
name: 🛠️ pnpm install
Expand Down Expand Up @@ -880,6 +888,9 @@ jobs:

- run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge
run: pnpm run ios:bridge

- name: Build iOS UI test bundles (SwiftUI + UIKit)
run: pnpm run implementation:ios-sdk -- test:e2e:ios:build:release

Expand All @@ -896,6 +907,45 @@ jobs:
if-no-files-found: error
retention-days: 1

swift-package-build:
name: 🍎 Swift Package Build & Test
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
timeout-minutes: 30
needs: [setup, changes]
if: needs.changes.outputs.swift_package == 'true'
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
package-manager-cache: false

- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: pnpm
path: |
~/Library/Caches/org.swift.swiftpm

- name: Show toolchain
run: |
swift --version
xcodebuild -version

- run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge
run: pnpm run ios:bridge

- name: Swift build
run: swift build --package-path packages/ios/ContentfulOptimization

- name: Swift test
run: swift test --package-path packages/ios/ContentfulOptimization

e2e-react-native-android:
name: 📱 E2E React Native Android (shard ${{ matrix.shard }}/2)
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
Expand Down Expand Up @@ -1072,8 +1122,6 @@ jobs:
IOS_SCHEME: ${{ matrix.scheme }}
IOS_SIM_NAME: 'iPhone 16'
IOS_SIM_OS: 'latest'
# Smoke mode for the first PR — restrict to one test class per scheme.
# Remove this env var in a follow-up PR to enable the full suite.
IOS_ONLY_TESTING: OptimizationAppUITests${{ matrix.scheme }}/PreviewPanelTests
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
Expand Down
87 changes: 87 additions & 0 deletions .github/workflows/publish-spm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Publish Swift Package

permissions:
contents: read

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag (e.g. v1.2.3)'
required: true

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Derive release version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
TAG='${{ github.event.release.tag_name }}'
else
TAG='${{ inputs.tag }}'
fi
echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV"
echo "RELEASE_VERSION=${TAG#v}" >> "$GITHUB_ENV"

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
package-manager-cache: false

- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3

- run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge (stamps the version into the UMD)
run: pnpm --filter @contentful/optimization-js-bridge build
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}

- name: Assemble the package payload
run: |
SRC=packages/ios/ContentfulOptimization
DST="$RUNNER_TEMP/pkg"
mkdir -p "$DST"
# Swift sources, Package.swift, polyfills, README, LICENSE — but not the UMD (built below)
rsync -a --exclude='Resources/optimization-ios-bridge.umd.js' "$SRC"/ "$DST"/
cp packages/universal/optimization-js-bridge/dist/optimization-ios-bridge.umd.js \
"$DST/Sources/ContentfulOptimization/Resources/"

- name: Verify the payload is complete
run: |
test -f "$RUNNER_TEMP/pkg/Package.swift"
test -f "$RUNNER_TEMP/pkg/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js"
test -d "$RUNNER_TEMP/pkg/Sources/ContentfulOptimization/Resources/polyfills"

- name: Set up the deploy key for SSH push
env:
MIRROR_DEPLOY_KEY: ${{ secrets.SPM_MIRROR_DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$MIRROR_DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null

- name: Commit, tag, and push to the distribution repo
run: |
git clone --depth 1 git@github.com:contentful/optimization.swift.git mirror
# --delete so files removed in the monorepo also disappear from the mirror
rsync -a --delete --exclude='.git' "$RUNNER_TEMP/pkg"/ mirror/
cd mirror
git add -A
if git diff --cached --quiet; then
echo "No content changes since the last release commit; moving the tag only."
else
git -c user.name='contentful-ci' -c user.email='ci@contentful.com' \
commit -m "Release $RELEASE_TAG (from contentful/optimization@${GITHUB_SHA:0:7})"
git push origin HEAD:main
fi
git tag --force "$RELEASE_TAG"
git push origin "refs/tags/$RELEASE_TAG" --force
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ tmp
.build/
.swiftpm/

# Generated JS bridge bundles (built by @contentful/optimization-js-bridge, not committed)
packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js
packages/android/ContentfulOptimization/src/main/assets/optimization-android-bridge.umd.js

# iOS Performance Test Results
implementations/ios-poc/results/
*.xcresult
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"implementation:lint": "eslint implementations --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr-csr --cache --cache-location .cache/eslint/implementations && pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr lint",
"implementation:lint:fix": "eslint implementations --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr-csr --fix && pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr lint",
"implementation:typecheck": "pnpm run implementation:run -- --all -- typecheck",
"ios:bridge": "pnpm --filter @contentful/optimization-js-bridge build",
"ios:build": "pnpm run ios:bridge && swift build --package-path packages/ios/ContentfulOptimization",
"ios:test": "pnpm run ios:bridge && swift test --package-path packages/ios/ContentfulOptimization",
"lint": "eslint lib packages --cache --cache-location .cache/eslint/workspace",
"lint:fix": "eslint lib packages --fix",
"notices:generate": "pnpm --filter \"@contentful/*\" licenses list --prod --json | pnpm dlx @quantco/pnpm-licenses generate-disclaimer --json-input --output-file THIRD_PARTY_NOTICES.txt",
Expand Down

This file was deleted.

9 changes: 7 additions & 2 deletions packages/ios/ContentfulOptimization/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ Swift tests.
- Keep native runtime concerns here. TypeScript bridge behavior belongs in
`../../universal/optimization-js-bridge/`; shared optimization behavior belongs in
`packages/universal/core-sdk`.
- Treat `Resources/optimization-ios-bridge.umd.js` as generated bridge output. Update it by building
`@contentful/optimization-js-bridge`, not by hand-editing the copied file.
- Treat `Resources/optimization-ios-bridge.umd.js` as generated bridge output. It is gitignored and
not committed; build it by running `@contentful/optimization-js-bridge` (`pnpm run ios:bridge`)
before `swift build`/`swift test`, and never hand-edit the copied file.
- This package is published to the generated distribution repo `contentful/optimization.swift` by
`.github/workflows/publish-spm.yaml` on each `v*` release. The mirror is generated output; nobody
pushes to it by hand. Files in this directory (sources, `Package.swift`, polyfills, `README.md`,
`LICENSE`) are what ships, so keep them consumer-facing and free of monorepo-internal references.
- Keep Swift payload models and bridge method expectations aligned with
`../../universal/optimization-js-bridge/src/index.ts`.
- Keep resource additions reflected in `Package.swift` when they must ship with the Swift package.
Expand Down
21 changes: 21 additions & 0 deletions packages/ios/ContentfulOptimization/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (C) 2026 Contentful Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
109 changes: 109 additions & 0 deletions packages/ios/ContentfulOptimization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<p align="center">
<a href="https://www.contentful.com/developers/docs/personalization/">
<img alt="Contentful Logo" title="Contentful" src="https://raw.githubusercontent.com/contentful/optimization/main/contentful-icon.png" width="150">
</a>
</p>

<h1 align="center">Contentful Personalization & Analytics</h1>

<h3 align="center">Optimization iOS SDK</h3>

<div align="center">

[Guides](https://contentful.github.io/optimization/documents/Documentation.Guides.html) ·
[Reference](https://contentful.github.io/optimization) ·
[Source](https://github.com/contentful/optimization)

</div>

> [!WARNING]
>
> The Optimization SDK Suite is pre-release (alpha). Breaking changes can be published at any time.

> [!NOTE]
>
> Source lives at
> [https://github.com/contentful/optimization](https://github.com/contentful/optimization). This
> repository is generated by a release workflow; do not file PRs here.

`ContentfulOptimization` is the native Swift Package for the
[Contentful Optimization SDK Suite](https://github.com/contentful/optimization). It runs shared
optimization behavior through a local JavaScriptCore bridge while Swift code owns native app
concerns such as persistence, networking, lifecycle handling, SwiftUI views, and preview-panel UI.

Requires iOS 15 / macOS 12 or later.

## Installation

### Swift Package Manager

In `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/contentful/optimization.swift.git", from: "0.1.0"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "ContentfulOptimization", package: "optimization.swift"),
]
),
],
```

Or in Xcode: **File > Add Package Dependencies…** and paste
`https://github.com/contentful/optimization.swift`.

## Quick start

### SwiftUI

Wrap your app in `OptimizationRoot` to initialize the client and provide it to the view tree:

```swift
import ContentfulOptimization
import SwiftUI

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
OptimizationRoot(
config: OptimizationConfig(
clientId: "<your-client-id>",
environment: "master"
)
) {
ContentView()
.trackScreen(name: "Home")
}
}
}
}
```

Render personalized Contentful entries with `OptimizedEntry`, and track screens with the
`.trackScreen(name:)` modifier.

### UIKit / direct client

```swift
import ContentfulOptimization

let client = OptimizationClient()
try client.initialize(
config: OptimizationConfig(clientId: "<your-client-id>", environment: "master")
)

let result = try await client.screen(name: "Home")
```

See the [guides](https://contentful.github.io/optimization/documents/Documentation.Guides.html) and
[API reference](https://contentful.github.io/optimization) for the full API, SwiftUI helpers, and
preview-panel setup.

## License

[MIT](./LICENSE)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -993,9 +993,11 @@ final class OptimizationClientTests: XCTestCase {
controller.pause()
XCTAssertFalse(controller.isVisible)

// resume() resets visibility flag so it can be re-evaluated
// resume() resets the visibility flag and immediately re-evaluates from the
// last known geometry, so a still-visible element starts a fresh cycle and
// becomes visible again without waiting for an external geometry callback.
controller.resume()
XCTAssertFalse(controller.isVisible)
XCTAssertTrue(controller.isVisible)
}

@MainActor
Expand Down
15 changes: 15 additions & 0 deletions packages/ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ persistence, networking, lifecycle handling, SwiftUI views, and preview-panel UI
- [Current status](#current-status)
- [When to use this package](#when-to-use-this-package)
- [Package layout](#package-layout)
- [Releasing](#releasing)
- [Related](#related)

<!-- mtoc-end -->
Expand Down Expand Up @@ -62,6 +63,20 @@ stable mobile integration can start with the JavaScript
TypeScript bridge compiled into the JavaScriptCore UMD bundle consumed by the Swift Package
- [`CODE_MAP.md`](./CODE_MAP.md) - architecture map for the current native iOS implementation

## Releasing

The Swift Package is published to a separate distribution repository,
[`contentful/optimization.swift`](https://github.com/contentful/optimization.swift), so consumers
can add it by URL (`from: "x.y.z"`) without cloning the monorepo. Publishing is automated by
[`publish-spm.yaml`](../../.github/workflows/publish-spm.yaml): on each `v*` release it builds the
JS bridge (stamping the version into the UMD), assembles the package payload, and pushes a commit
and tag to the distribution repo.

The distribution repo is generated output, like an npm `dist/`: nobody pushes to it by hand. The UMD
bundle is no longer committed to the monorepo — it is built on demand (and gitignored). Build it
locally before `swift build`/`swift test` with `pnpm run ios:bridge`, or use the convenience scripts
`pnpm run ios:build` and `pnpm run ios:test` from the repo root.

## Related

- [iOS reference app](../../implementations/ios-sdk/README.md) - Native app and XCUITest surface for
Expand Down
2 changes: 1 addition & 1 deletion packages/universal/optimization-js-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "pnpm clean && pnpm build:dist",
"build:ci": "pnpm build:dist",
"build:dist": "rslib build",
"postbuild": "cp dist/optimization-ios-bridge.umd.js ../../ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/ && cp dist/optimization-android-bridge.umd.js ../../android/ContentfulOptimization/src/main/assets/",
"postbuild": "mkdir -p ../../ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources ../../android/ContentfulOptimization/src/main/assets && cp dist/optimization-ios-bridge.umd.js ../../ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/ && cp dist/optimization-android-bridge.umd.js ../../android/ContentfulOptimization/src/main/assets/",
"clean": "rimraf ./.rslib ./dist ./coverage .tsbuildinfo",
"test:unit": "echo 'No tests yet'",
"typecheck": "tsc --noEmit"
Expand Down
Loading