Skip to content

refactor(implementations): fetch iOS reference entries with contentful.swift [NT-3946] - #429

Merged
David Nalchevanidze (nalchevanidze) merged 11 commits into
mainfrom
nt-3946-ios-contentful-swift
Aug 19, 2026
Merged

refactor(implementations): fetch iOS reference entries with contentful.swift [NT-3946]#429
David Nalchevanidze (nalchevanidze) merged 11 commits into
mainfrom
nt-3946-ios-contentful-swift

Conversation

@nalchevanidze

@nalchevanidze David Nalchevanidze (nalchevanidze) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Why

The iOS reference app fetched Contentful entries with hand-rolled HTTP — a URL string, URLSession, JSONSerialization into untyped dictionaries, and its own link resolver. Reference implementations are meant to consume the SDK surface the way customers do, and the recommended path is Contentful's official CDA SDK. The Optimization SDK has shipped the adapter for it since NT-3808 (#393); no reference app used it.

Closes NT-3946.

Decisions

Fetch with contentful.swift, hand the typed entry to the SDK. Both shells query with Query/include(10)/localizeResults(withLocaleCode:) and pass Contentful.Entry to OptimizedEntry(entry:) (SwiftUI) and resolveOptimizedEntry(baseline:) (UIKit). App-owned request building, JSON parsing, and link resolution are deleted — the SDK's CTEntry encoding and LinkResolver own that now, including cycle handling that replaces the old depth-10 hop budget.

One client for every CDA read. shared/ContentfulClient.swift owns the single Contentful.Client, exposes ContentfulClient.fetchEntries(ids:locale:), and conforms to PreviewContentfulClient so the preview panel reads through the same client — which is the integration that protocol documents, as opposed to the built-in ContentfulHTTPPreviewClient. Sharing one client also avoids repeating the /locales bootstrap contentful.swift performs per client. No Mock prefix: nothing in it is mock-specific beyond two arguments.

Fix the mock server rather than shim around it in the app. See below.

Two SDK fixes ride along, both prerequisites found while migrating:

  • SwiftUI tap tracking was fully broken on main. The NT-3829 observer attached its UITapGestureRecognizer to superview, but SwiftUI draws content into a shared display list and puts the observer in its own container, so that container isn't reliably an ancestor of the tapped region — the SwiftUI shell emitted no component_click events at all. Now attached to the window and scoped to the observer's frame, with all touch-stealing options off so NT-3829's nested-button behaviour is preserved by construction.
  • CTEntry dictionary/JSON initializers made public, matching Android's from(Map)/from(String). SDK APIs still take entries as dictionaries, so these are for reading an entry, not for passing one back into resolution.

Why the mock changes were necessary

An earlier revision of this PR carried a 138-line in-app URLProtocol shim instead. Two properties of lib/mocks made the SDK unusable against it, and neither is expressible as a Contentful.Client argument:

  1. No /locales route. contentful.swift resolves locale fallback chains client-side, so it fetches /locales before its first entry query and completes that query with the locales error if it fails (Client.swift:203-217); every entry decode then hard-requires the resulting LocalizationContext. Real CDA has this endpoint and the mock never did — while the locale set sat unused in ctfl-space-data.json. The handler now serves it.
  2. The CDA was only reachable under /contentful/. The prefix exists so one port can also serve /experience/ and /insights/. Contentful.Client takes only a host[:port] and builds /spaces/... from the root, with no basePath — the knob contentful.js has and uses (web-sdk_react/src/services/contentfulClient.ts). The CDA is now also mounted at the host root; /contentful/ stays for Android, web, and React Native.

Fixing the mock was the better trade because the shim was demo-only plumbing sitting in a file customers are meant to copy from, and because the /locales gap is a genuine fidelity hole rather than an iOS quirk. The result: ContentfulClient differs from a production integration by exactly host and clientConfiguration.secure = false.

While in there, skip was silently ignored in the content-type query (skip: 0 echoed back, slice(0, limit)), so every page returned the first one and paging consumers looped over the same entries. Now honored.

Validation

Check State Result
Full XCUITest suite, both shells migration 69/69 each
pnpm ios:test migration 187/187 (was 182)
Full XCUITest suite, both shells head running locally; CI is authoritative
pnpm lint, format:check, mocks typecheck + unit head clean

SwiftUI went 67/69 → 69/69: the two failures were the pre-existing tap-tracking bug.

Two assumptions carry the least local coverage: msw's * spanning slashes for the root mount (if wrong, iOS entry fetches 404 and the iOS jobs fail loudly), and skip now genuinely paginating (any scenario that had encoded the old bug will change).

For reviewers

  • Commits are separable. If squash-merged, only the title's scope reaches the changelog, so the two fix(swift) changes get no Swift changelog entries. Happy to split.
  • CI's iOS matrix runs only PreviewPanelTests, which is why a fully broken tap-tracking path survived on main. Widening it is a cheap separate improvement.
  • Android still has MockPreviewContentfulClient.kt, so the platforms now differ on that type's name. Happy to follow up for parity.
  • The preview panel still needs a dictionary hand-off. PreviewContentfulClient is dictionary-shaped because loadDefinitions serializes it for the JS bridge, so the app converts typed entries back down with CTEntry.toDictionary(). That mapping belongs in the SDK, which already depends on contentful.swift — filed as NT-3994.
  • Left alone deliberately: the mock still ignores include (fixtures ship pre-resolved) and locale (fixtures are single-locale). No consumer needs either.

🤖 Generated with Claude Code

…ks [NT-3946]

The mock CDA only ever had to satisfy contentful.js. The Android and iOS SDKs read
the CDA with their own platform clients, and the iOS reference app is moving onto
contentful.swift, which reaches the mock in ways the handlers did not support.
Three gaps, and why each one blocks or degrades a native client:

Locales. contentful.swift resolves locale fallback chains client-side.
Client.fetchLocalesIfNecessary requests /locales before any other endpoint, and
entries cannot be decoded until it succeeds, so a stock Contentful.Client failed
on its first entry query against the mock. The new route is built from the locale
set already in the space fixture and honors the limit of 1000 the SDK sends.
contentful.js sends locale to the API instead and never requests the endpoint,
which is why the gap stayed invisible while only web consumed the mock.

Root mount. Contentful.Client builds every request as scheme://host[:port]/spaces/…
and ClientConfiguration exposes no base-path hook, so it cannot address the
/contentful/ namespace the mock server used. The same handlers are now also
mounted at the host root. contentful.js has basePath and the Android app builds
its URLs from AppConfig.contentfulBaseUrl, so both keep using the prefixed mount
unchanged; the iOS app can now differ from a production integration only by host
and secure = false, instead of installing a URLProtocol shim.

Skip. The content-type query parsed skip and then ignored it while echoing a skip
of zero back, so any page after the first returned the first page again and a
paging consumer accumulated duplicates. The clients that page this way are the
native ones: fetchAllEntries in the iOS SDK and the URL built by the Android
PreviewContentfulClient both walk skip, and contentful.swift pages the same way.
With 45 fixture entries and a 100-item batch nothing pages today, so this is a
latent fidelity gap rather than a live failure.

Both routes are additive and no existing consumer changes mounts. The first
consumer of the new behavior is the iOS reference app's move to contentful.swift,
which lands on top of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tap observer added for NT-3829 attached its UITapGestureRecognizer to
its `superview`, assuming `.background()` makes the observer a sibling of a
content *view*. SwiftUI draws Text and friends into a shared display list
rather than one UIView per view, and places the observer in a container of
its own, so that container is not reliably an ancestor of the region the
user taps and the recognizer never saw the touch. No `component_click`
event was emitted from the SwiftUI shell at all.

Attach to the window instead, which is always an ancestor, and scope each
tap to the observer's own frame — `.background()` sizes it to the tracked
content. `cancelsTouchesInView`, `delaysTouchesBegan`, and
`delaysTouchesEnded` are all off, so observing from the window still never
competes for touches a nested interactive child needs, preserving the
NT-3829 fix. Window-level recognizers need removing, so detach on window
change and deinit.

The nested-Button regression test added by NT-3829 only asserts the
Button's own action fires, so it passed either way; the two XCUITests that
assert the click event are not in CI, which runs only PreviewPanelTests
for iOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… [NT-3946]

`CTEntry` was constructible by consumers only from a `Contentful.Entry`;
`init(any:)` and `init(json:)` were internal, so a caller holding an entry
as a dictionary — an expanded child entry, a locally built one — could not
wrap it to read fields through `getField`/`hasField`.

Android already exposes all three shapes (`CTEntry.from(CDAEntry)`,
`from(Map)`, `from(String)`), so this closes an iOS/Android gap rather than
adding new API surface. The dictionary initializer takes `[String: Any]`
instead of the internal `Any` overload, matching Android's `Map` signature,
and stays fail-soft; `init(json:)` keeps throwing, which is the idiomatic
Swift equivalent of Android's fallback parameter.

`empty` becomes public because Swift rejects an internal declaration in a
public default argument, where Kotlin permits it — hence Android keeping
`EMPTY` internal.

Note that SDK APIs still accept entries as dictionaries, not `CTEntry`, so
these initializers are for reading an entry rather than passing one back
into resolution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l.swift [NT-3946]

The iOS reference app fetched Contentful entries with hand-rolled HTTP: a
URL string, URLSession, JSONSerialization into untyped dictionaries, and its
own `includes` link resolver with a depth-10 hop budget. Reference
implementations are meant to consume the public SDK surface the way
customers do, and the recommended path is Contentful's official CDA SDK —
which the Optimization SDK already ships an adapter for (NT-3808, #393)
without any reference app using it.

Both shells now fetch with contentful.swift and hand the resulting
`Contentful.Entry` to the SDK's typed entry points: `OptimizedEntry(entry:)`
in SwiftUI and `resolveOptimizedEntry(baseline:)` in UIKit. App-owned CDA
request construction, JSON parsing, and link resolution are deleted;
link resolution and cycle handling now come from contentful.swift's
LinkResolver plus the SDK's `CTEntry` encoding. `isNestedContent` collapses
to `entry.sys.contentTypeId`. Requests stay single-locale via
`localizeResults(withLocaleCode:)` with `include(10)`, per the CDA entry
contract. The preview panel's client is migrated too, mapping results back
down with `CTEntry.toDictionary()` for the dictionary-shaped
`PreviewContentfulClient` protocol.

Expanded child entries and locally built test entries only exist as
dictionaries, so the nested renderers and the dictionary initializers stay —
`CTEntry` cannot be constructed from a dictionary at every SDK input
boundary, since those accept dictionaries rather than `CTEntry`.

contentful.swift 5.5.15 cannot express the mock server's `/contentful/` path
prefix (it builds `/spaces/...` from the host root) and fetches `/locales`
before its first entry request, which the mock does not serve. Rather than
change test infrastructure shared with Android and the web SDKs,
`MockContentfulTransport` adapts both locally. It is demo-only plumbing: a
production app builds `Contentful.Client(spaceId:accessToken:)` against
cdn.contentful.com and needs none of it, which its doc comment and the
README both state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…[NT-3946]

Fold MockContentfulTransport into MockContentfulClient as a private nested
Transport. The URLProtocol shim is only meaningful as part of the client it is
installed on, and keeping the mock-only plumbing in one file leaves
ContentfulFetcher readable as the integration a customer would copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lient [NT-3946]

Merge ContentfulFetcher and MockPreviewContentfulClient into a single
ContentfulClient that owns the contentful.swift client, exposes the app-owned
entry-ID fetch as a static API, and conforms to PreviewContentfulClient so the
preview panel reads through the same client. Drop the Mock prefix: the type is
production-shaped code, and only its nested Transport targets the mock server.

Sharing one client also avoids repeating the /locales bootstrap that
contentful.swift performs per client before it can decode entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-3946]

The mock server now serves the CDA from the host root and exposes /locales, so
contentful.swift can talk to it directly. ContentfulClient no longer needs its
bespoke URLProtocol transport and differs from a production integration only by
host and secure = false.

Requires the mock locales route and root mount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lients [NT-3946]

PreviewContentfulClient is dictionary-shaped because loadDefinitions serializes
its result for the JS core, which runs the shared entry mappers. Apps that
already read Contentful through contentful.swift therefore had to encode typed
entries back down themselves, even though the SDK already depends on
contentful.swift and exposes Contentful.Entry publicly.

ContentfulSDKPreviewClient wraps an existing Contentful.Client and owns that
mapping, so the panel shares the app's client configuration and session instead
of opening a second connection. Choose it when the app already has a Contentful
client; ContentfulHTTPPreviewClient stays for apps that do not.

The iOS reference app now passes ContentfulClient.previewClient and implements no
entry mapping of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-3946]

PreviewPanelConfig, PreviewPanelOverlay, PreviewPanelViewController, and
addFloatingButton each gain an overload taking the app's contentful.swift
client and wrap it internally, so an app that already reads Contentful
shares one client and implements no entry mapping. ContentfulSDKPreviewClient
becomes internal because no caller needs to name it; the public
PreviewContentfulClient protocol is unchanged, so custom conformers and
ContentfulHTTPPreviewClient keep working.

The iOS reference app drops its previewClient wrapper and shared singleton:
one static ContentfulClient.client feeds both preview panel surfaces, and the
duplicate fetchEntries(matching:) bridge folds into fetchEntry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ulClient [NT-3946]

The adapter is internal, so a dedicated file for one 40-line type was the
outlier in Preview/: PreviewContentfulClient.swift already holds the protocol,
both response structs, the HTTP implementation, the error enum, and the
pagination helpers. It now sits under its own MARK beside the HTTP sibling.

Keeping the type is load-bearing, not habit: Swift rejects an internal witness
for a public protocol requirement on an imported type ("must be declared
public"), so conforming Contentful.Client directly would publish
getEntries(contentType:include:skip:limit:) on every consumer's client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from nt-3946-mocks-cda-locales-root to main August 19, 2026 10:39
@bito-code-review

Copy link
Copy Markdown

Functional Validation by Bito

SourceRequirement / Code AreaStatusNotes
NT-3932, NT-3946Fetch iOS reference entries with contentful.swift CDA SDK✅ MetThe diff replaces hand-rolled HTTP with shared/ContentfulClient.swift using `contentful.swift` Client. Fetching uses `Query.where(sys: .id, .equals(id)).include(10).localizeResults(withLocaleCode:)` (lines 462-464). Both SwiftUI (`MainScreen` at line 909) and UIKit (`MainViewController` at line 1307) call `ContentfulClient.fetchEntries()` instead of the removed `ContentfulFetcher`. The `contentful.swift` v5.5.15 dependency is added to project.yml and project.pbxproj.
NT-3932, NT-3946Pass typed Contentful.Entry objects through the Optimization SDK's typed entry APIs✅ MetThe diff exercises the SDK's typed entry APIs. SwiftUI's ContentEntryView.swift passes `Contentful.Entry` to `OptimizedEntry(entry:)` (lines 727-728). UIKit's OptimizedEntryUIView.swift calls `client.resolveOptimizedEntry(baseline: entry)` through the typed overload (lines 1189-1193). The CTEntry bridge (line 1342-1344) encodes `Contentful.Entry` through `CDA.Entry(contentfulEntry, ancestors: [])`. Custom CDA request building and JSON parsing are removed — ContentfulFetcher.swift and MockPreviewContentfulClient.swift are deleted.
NT-3994, NT-3946Provide ContentfulSDKPreviewClient wrapper implementing PreviewContentfulClient protocol✅ MetContentfulSDKPreviewClient implements `PreviewContentfulClient` (lines 1410-1448), encoding `Contentful.Entry` through `CTEntry($0).toDictionary()`. The SDK exposes `Contentful.Client` overloads on [[STRUCT:PreviewPanelConfig]] (lines 1482-1487), [[STRUCT:PreviewPanelOverlay]] (lines 1521-1526), PreviewPanelViewController (lines 1565-1570), and `addFloatingButton` (lines 1590-1599). Both app targets pass `ContentfulClient.client` directly — no manual `CTEntry`-to-dictionary mapping needed.
NT-3829Fix SwiftUI tap tracking by attaching observer to window✅ MetThe SwiftUI tap tracking fix in TapTrackingModifier.swift attaches the observer to the window (lines 1645-1651) instead of the superview. The window-level recognizer scopes each tap to the observer's frame via `bounds.contains(recognizer.location(in: self))` (line 1685), with `cancelsTouchesInView` and `delaysTouchesBegan/Ended` all off (lines 1659-1661) so nested-button behavior is preserved. The updated comment (lines 1615-1625) explains why superview attachment was unreliable.
NT-3808Ship Entry to OptimizedEntry conversion path in SDK with type-safe public surface✅ MetThe SDK ships three entry construction paths matching Android's `CTEntry.from(...)` APIs: `init(_: Contentful.Entry)` (line 1342), `init(dictionary:fallback:)` (line 1354), and `init(json:)` (line 1362). `CTEntry.empty` is now `public static let` (line 1340). Documentation confirms metadata requirement is immutable — every initializer goes through the same `CDA.Entry` path (lines 72-79 in ios.md). Type safety improves as apps move from `as? [String: Any]` casts to `getField`/`hasField` and subscript access on `CTEntry`.
NT-3932Preserve existing loading, fallback, personalization, nested-entry, live-update, preview-panel, and tracking behavior✅ MetAll existing functionality is preserved. Live updates support remains with `Contentful.Entry` types in LiveUpdatesTestScreen.swift (lines 843-886) and LiveUpdatesTestViewController.swift (lines 1260-1284). Nested entry handling uses the `Source` enum pattern for both fetched and expanded entries (lines 767-786 in SwiftUI, 976-987 in UIKit). Preview panel behavior is maintained through `ContentfulSDKPreviewClient`. Tracking remains functional — `TrackingMetadata` still receives dictionary-form entries.
NT-3932Update implementation READMEs and ensure Xcode project stays synchronized with project.yml✅ Metimplementations/ios-sdk/README.md is updated with a new 'Contentful entry fetching' section explaining the `contentful.swift` integration and `ContentfulClient` usage (lines 295-321). project.yml declares `contentful.swift` with exact version 5.5.15 (lines 346-352) and both targets depend on it (lines 360-361, 369-370). project.pbxproj is synchronized with the package reference and product dependencies for both SwiftUI and UIKit targets (lines 220, 254-281).
NT-3946Add /locales route to mock server🟡 Partial[Non-Diff Requirement] The mock server implementation is external to the provided diff. The README documents that `lib/mocks` serves `/locales` (line 320), and ContentfulClient is configured for it, but the actual server endpoint code is not shown in this diff. Validation requires inspecting the mock server implementation separately.
NT-3808Replace single-shot JSONSerialization with per-field handling in resolveOptimizedEntry🟡 Partial[Non-Diff Requirement] Per-field error handling in `resolveOptimizedEntry` is internal SDK implementation not visible in the reference app diff. The change description mentions moving away from single-shot JSONSerialization, but the actual implementation of per-field logging would be in the SDK package internals, not the reference app changes shown here.
NT-3932, NT-3829Verify all existing iOS XCUITest and Android Maestro test suites pass for both UI frameworks🟡 Partial[Non-Diff Requirement] Test suite validation requires runtime execution, not just source code review. The diff modifies test targets in project.pbxproj (removing `ContentfulFetcher`/`MockPreviewContentfulClient` references, adding `ContentfulClient`) and adds `CTEntryTests` coverage for the new public initializers (lines 1699-1743). Actual test execution to verify 69/69 SwiftUI tests and Android Maestro tests pass is outside the scope of diff review.

@bito-code-review

Copy link
Copy Markdown

Impact Analysis by Bito

Cross-Repository Impact Analysis
What Changed Impact of Change Suggested Review Actions
Replaced app-owned manual CDA fetching (ContentfulFetcher with raw JSONSerialization + manual link resolution) with contentful.swift SDK; replaced custom mock preview client with shared contentful.swift client wrapped in internal ContentfulSDKPreviewClient adapter - No cross-repo consumers found for ContentfulFetcher: searchCode for 'ContentfulFetcher' returned zero hits across all indexed repositories — the removed type is internal to this repo's demo app only.
- No cross-repo consumers found for MockPreviewContentfulClient: searchCode for 'MockPreviewContentfulClient' returned zero hits across all indexed repositories — the removed class is internal to this repo's demo app only.
- No cross-repo consumers found for ContentfulSDKPreviewClient: searchCode for 'ContentfulSDKPreviewClient' returned zero hits — the new internal adapter is not yet referenced externally.
- contentful.swift dependency is new and not referenced in other repos: searchCode for 'contentful.swift' returned zero hits across all indexed repositories — the new external SDK dependency is only in this repo.
- Verify this is a standalone SDK repo without known cross-repo consumers by checking if the repo is referenced by any other project in the organization's CI/CD pipeline.
- Confirm the PR title/description links to the correct repo — MCP could not locate a repo named 'optimization' in the indexed organization, suggesting either a private repo or a repo name that differs from the search term.
Code Paths Analyzed

Impact:
Migrates the iOS demo app and SDK from raw URLSession + JSONSerialization + manual CDA link resolution to the official contentful.swift SDK (v5.5.15). The preview panel now shares the app's contentful.swift client instead of maintaining a separate HTTP connection. New public CTEntry initializers (dictionary/json) give consumers a typed entry-reading API matching the Android SDK's surface. Tap tracking in SwiftUI is fixed to attach gesture recognizers to the window (not the superview) to correctly handle taps on SwiftUI's shared display list.

Flow:
Entry fetching: App constructs one Contentful.Client (shared) → ContentfulClient.fetchEntries calls contentful.swift CDA → Contentful.Entry goes to OptimizedEntry/resolveOptimizedEntry → SDK encodes through CTEntry → JS bridge resolves personalization. Preview panel: App passes its Contentful.Client to PreviewPanelConfig/PreviewPanelViewController/addFloatingButton → SDK wraps it in internal ContentfulSDKPreviewClient → panel calls getEntries → returns CTEntry.toDictionary shape to JS core → bridge bakes preview model.

Direct Changes (Diff Files):
• documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md [1009-1014] — Updated step 4 to recommend passing the app's Contentful.Client directly (SDK wraps it) vs using ContentfulHTTPPreviewClient
• documentation/guides/integrating-the-optimization-ios-sdk-in-a-uikit-app.md [1194-1203] — Updated step 1 to document the two-path Contentful client integration (pass existing Client or use ContentfulHTTPPreviewClient)
• documentation/internal/sdk-knowledge/native/ios.md [28-80, 324-326] — Added ContentfulSDKPreviewClient to public symbol table; documented new CTEntry consumer constructors; updated preview panel section to describe Client overloads
• implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj [12-847] — Added contentful.swift SPM remote package (5.5.15), Contentful product dependencies to both targets, new ContentfulClient.swift, removed ContentfulFetcher.swift and MockPreviewContentfulClient.swift
• implementations/ios-sdk/README.md [36-337] — Rewrote entry fetching section to document contentful.swift integration; updated locale handling to reference localizeResults(withLocaleCode:)
• implementations/ios-sdk/project.yml [8-373] — Added contentful.swift SPM package (5.5.15) to packages section; added Contentful product dependency to both SwiftUI and UIKit targets
• implementations/ios-sdk/shared/Config.swift [15-394] — Replaced contentfulBaseUrl (URL string) with contentfulHost (host:port string) and contentfulAccessToken; added documentation comments
• implementations/ios-sdk/shared/ContentfulClient.swift [1-476] — New file: one contentful.swift Client shared across all CDA consumers (entry fetching + preview panel); conforms to PreviewContentfulClient so preview panel uses it directly
• implementations/ios-sdk/shared/ContentfulFetcher.swift [1-567] — REMOVED: raw URLSession + JSONSerialization entry fetching with manual link resolution — replaced by contentful.swift in ContentfulClient.swift
• implementations/ios-sdk/shared/MockPreviewContentfulClient.swift [1-645] — REMOVED: custom HTTP PreviewContentfulClient for mock CDA — replaced by ContentfulClient conforming to PreviewContentfulClient
• implementations/ios-sdk/shared/RichText.swift [16-684] — Added resolveText(_:field:client:) overload for CTEntry; updated comment to reflect CDA/CTEntry stub framing
• implementations/ios-sdk/swiftui/App.swift [1-702] — Added import Contentful; changed previewPanel contentfulClient from MockPreviewContentfulClient() to ContentfulClient.client
• implementations/ios-sdk/swiftui/Components/ContentEntryView.swift [1-750] — Changed entry type from [String: Any] to Contentful.Entry; uses entry.sys.id; calls new RichText.resolveText(entry:field:client:)
• implementations/ios-sdk/swiftui/Components/NestedContentEntryView.swift [1-829] — Added Source enum (.fetched/.expanded) to handle both Contentful.Entry root and dictionary children; added init(entry:)/init(expandedEntry:) initializers
• implementations/ios-sdk/swiftui/Screens/LiveUpdatesTestScreen.swift [1-887] — Added import Contentful; changed entry state to Contentful.Entry?; replaced ContentfulFetcher.fetchEntries with ContentfulClient.fetchEntries; contentSections now typed Contentful.Entry; LiveUpdatesEntryDisplay uses CTEntry subscript
• implementations/ios-sdk/swiftui/Screens/MainScreen.swift [1-929] — Added import Contentful; changed entries state to [Contentful.Entry]; replaced ContentfulFetcher.fetchEntries with ContentfulClient.fetchEntries; isNestedContent uses entry.sys.contentTypeId
• implementations/ios-sdk/uikit/Components/ContentEntryUIView.swift [1-959] — Added import Contentful; changed entry parameter from [String: Any] to Contentful.Entry; uses entry.sys.id directly; removed entryId(for:) helper
• implementations/ios-sdk/uikit/Components/NestedContentEntryUIView.swift [1-1037] — Added Source enum with convenience init(entry:)/init(expandedEntry:) overloads; handles both Contentful.Entry root and dictionary children
• implementations/ios-sdk/uikit/Components/OptimizedEntryUIView.swift [1-1227] — Added import Contentful; added Baseline enum (.fetched/.expanded) with two convenience initializers; encodes Contentful.Entry through CTEntry.toDictionary(); resolve() dispatches on baseline case
• implementations/ios-sdk/uikit/SceneDelegate.swift [1-1244] — Added import Contentful; changed addFloatingButton contentfulClient from MockPreviewContentfulClient() to ContentfulClient.client
• implementations/ios-sdk/uikit/Screens/LiveUpdatesTestViewController.swift [1-1284] — Added import Contentful; changed entry to Contentful.Entry?; replaced ContentfulFetcher.fetchEntries with ContentfulClient.fetchEntries; makeSection typed Contentful.Entry
• implementations/ios-sdk/uikit/Screens/MainViewController.swift [1-1327] — Added import Contentful; changed entries to [Contentful.Entry]; replaced ContentfulFetcher.fetchEntries with ContentfulClient.fetchEntries; isNestedContent uses entry.sys.contentTypeId
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift [1336-1367] — Made CTEntry.empty public; added init(dictionary:fallback:) fail-soft constructor; updated init(json:) documentation to clarify throwing behavior
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewContentfulClient.swift [1-1453] — Updated PreviewContentfulClient protocol docs; added ContentfulSDKPreviewClient — internal adapter wrapping contentful.swift Client for the preview panel
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelConfig.swift [1-1488] — Added import Contentful; added init(enabled:contentfulClient: Contentful.Client) overload wrapping the client in ContentfulSDKPreviewClient
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelOverlay.swift [1-1532] — Added import Contentful; added init(contentfulClient: Contentful.Client) overload for SwiftUI overlay
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelViewController.swift [1-1601] — Added import Contentful; added convenience init(client:contentfulClient: Contentful.Client) and addFloatingButton overload accepting Contentful.Client
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Tracking/TapTrackingModifier.swift [1-1687] — Fixed tap observer to attach to the window (not superview); added deinit detach; added bounds containment check so taps outside the entry's frame are ignored; updated tap handling to accept the recognizer parameter and check bounds.contains(recognizer.location(in: self))
• packages/ios/ContentfulOptimization/Tests/ContentfulOptimizationTests/CTEntryTests.swift [1698-1744] — Added 5 new tests for CTEntry consumer constructors: testInitDictionaryReadsSysAndFields, testInitDictionaryFallsBackForUnsupportedType, testInitDictionaryUsesProvidedFallback, testInitJSONReadsSysAndFields, testInitJSONThrowsForMalformedJSON

Repository Impact:
iOS demo app entry fetching: All entry fetching (MainScreen, LiveUpdatesTestScreen, UIKit controllers) migrated from raw URLSession + JSONSerialization to contentful.swift. State types changed from [[String: Any]] to [Contentful.Entry]. Link resolution now done by the SDK instead of manual code.
Preview panel integration path: Preview panel now accepts the app's contentful.swift Client directly via new overloads on PreviewPanelConfig, PreviewPanelOverlay, PreviewPanelViewController, and addFloatingButton. The SDK internally wraps it in ContentfulSDKPreviewClient so no entry mapping is needed in the app.
CTEntry consumer surface: New public initializers (dictionary, json) give consumers a typed way to read entries without as? casts. CTEntry.empty is now public. These mirror the Android SDK's CTEntry.from(any:)/from(json:) surface.
SwiftUI tap tracking: Gesture recognizer now attaches to the window instead of the superview, fixing the bug where SwiftUI's shared display list meant taps on tracked content were not observed.
SDK documentation: Integration guides and internal SDK knowledge docs updated to recommend passing contentful.swift Client directly vs using ContentfulHTTPPreviewClient.

Cross-Repository Dependencies:
contentful.swift v5.5.15: New SPM remote dependency added to both app targets. The SDK surface (PreviewPanelConfig, PreviewPanelOverlay, PreviewPanelViewController) gains new overloads accepting Contentful.Client from this library.

Database/Caching Impact:
• None

API Contract Violations:
• No breaking API changes to the public SDK surface. All new APIs are additive (new overloads). The removed types (ContentfulFetcher, MockPreviewContentfulClient) are demo-app internals only.
• CTEntry.empty visibility changed from internal to public — this is a non-breaking additive change (previously implicitly accessible via fallback, now explicitly accessible).

Infrastructure Dependencies:
• SPM package manager: new XCRemoteSwiftPackageReference for contentful.swift 5.5.15 in Xcode project; both SwiftUI and UIKit test targets gain the dependency.
• App Config: replaced contentfulBaseUrl (full URL string) with contentfulHost (host:port only) and contentfulAccessToken — these match what Contentful.Client requires vs the old URLSession approach.
• Mock server: /locales endpoint must be present on the mock CDA — contentful.swift fetches it to build localization context before the first entry query.

Additional Insights:
Locale handling change: App now passes locale via contentful.swift's localizeResults(withLocaleCode:) rather than manually encoding it into a URL query parameter — cleaner and consistent with SDK patterns.
Link resolution: contentful.swift resolves links in place; the manual link resolution code in ContentfulFetcher is now unnecessary since contentful.swift handles include depth internally.
CTEntry Android parity: New init(dictionary:fallback:) and init(json:) mirror Android's CTEntry.from(any:fallback:)/from(json:), enabling consumers holding raw CDA entries (expanded children, locally built entries) to read them through getField/hasField without as? casts.

Testing Recommendations

Frontend Impact:
• Test that PreviewPanelConfig(contentfulClient: myContentfulClient) correctly wraps the client and the preview panel loads nt_audience/nt_experience definitions from the app's space/environment.
• Test that PreviewPanelOverlay(contentfulClient: myContentfulClient) renders the FAB and opens the preview sheet correctly in SwiftUI apps.
• Test that PreviewPanelViewController(client:myClient, contentfulClient: myContentfulClient) and addFloatingButton(to:client:contentfulClient: myContentfulClient) work in UIKit apps.
• Test SwiftUI tap tracking: verify that taps on OptimizedEntry views (both baseline and variant) are correctly captured — this is a regression test for the window-level gesture recognizer fix.
• Test nested content entries: verify that a root entry from the CDA (Contentful.Entry) and its expanded children (dictionaries) both resolve personalization correctly through OptimizedEntry/OptimizedEntryUIView.

Service Integration:
• Test the full entry resolution path: ContentfulClient.fetchEntries → OptimizedEntry/resolveOptimizedEntry → variant rendering, verifying that contentful.swift's link resolution produces correct nt_variants resolution.
• Test locale handling: verify that entries fetched with localizeResults(withLocaleCode:) produce the same resolution behavior as before, confirming single-locale CDA entry shape is preserved.

Data Serialization:
• Test CTEntry(dictionary: ...) with valid entries: verify sys.id, contentTypeId, and field reads via subscript work correctly for standard field types (String, Int, Bool, arrays, nested objects).
• Test CTEntry(dictionary: ...) fail-soft: verify that Date values and other types with no JSON representation yield fallback (default: CTEntry.empty) rather than throwing.
• Test CTEntry(json: ...) with valid JSON: verify it parses sys.id, contentTypeId, and fields correctly.
• Test CTEntry(json: ...) with malformed JSON: verify it throws OptimizationError.configError with 'JSON string is not valid UTF-8' or similar decoding error.
• Test that CTEntry.empty (now public) is accessible and behaves as a safe fallback — all field reads return nil/absent.

Privacy Compliance:
• Verify that no PII is logged when contentful.swift fetches /locales on client initialization — the mock server uses mock tokens but production config patterns should be reviewed.

Backward Compatibility:
• Verify that existing PreviewContentfulClient implementations (apps that already implement the protocol) continue to work — the protocol itself is unchanged, only new overloads added.
• Verify that existing ContentfulHTTPPreviewClient usage (without a contentful.swift Client) still works for apps that have no Contentful SDK client to share.
• Verify that the dictionary-based SDK entry APIs (entry: [String: Any]) continue to work — OptimizedEntry and resolveOptimizedEntry still accept dictionaries for test entries and expanded children.

OAuth Functionality:
• None

Cross-Service Communication:
• Verify that the mock CDA server serves /locales (required by contentful.swift bootstrap) in addition to the /spaces/... path — this is a new requirement introduced by the migration.
• Test with HTTPS/production Contentful: verify that Contentful.Client(spaceId:accessToken:) with default host and secure configuration works in production builds (the demo uses localhost:8000 over plain HTTP).

Reliability Testing:
• None

Additional Insights:
• Run the full iOS UI test suite (both SwiftUI and UIKit targets) to verify no regressions from the entry type migration ([String: Any] → Contentful.Entry) and the ContentfulFetcher → ContentfulClient swap.
• Review PreviewPanelTests for any test fixtures that used MockPreviewContentfulClient — those fixtures may need updating to reflect the new ContentfulClient pattern.
• Verify OfflineBehaviorTests still work: ContentfulClient.fetchEntries uses a continuation over contentful.swift's completion handler, and a failed fetch renders as loading state — confirm this matches the offline-behavior test expectations.

Analysis based on known dependency patterns and edges. Actual impact may vary.

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #af13dc

Actionable Suggestions - 1
  • implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj - 1
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • implementations/ios-sdk/shared/MockPreviewContentfulClient.swift - 1
    • Non-existent file referenced in diff · Line 1-73
Review Details
  • Files reviewed - 29 · Commit Range: cd302fa..99db9e1
    • documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md
    • documentation/guides/integrating-the-optimization-ios-sdk-in-a-uikit-app.md
    • documentation/internal/sdk-knowledge/native/ios.md
    • implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj
    • implementations/ios-sdk/README.md
    • implementations/ios-sdk/project.yml
    • implementations/ios-sdk/shared/Config.swift
    • implementations/ios-sdk/shared/ContentfulClient.swift
    • implementations/ios-sdk/shared/ContentfulFetcher.swift
    • implementations/ios-sdk/shared/MockPreviewContentfulClient.swift
    • implementations/ios-sdk/shared/RichText.swift
    • implementations/ios-sdk/swiftui/App.swift
    • implementations/ios-sdk/swiftui/Components/ContentEntryView.swift
    • implementations/ios-sdk/swiftui/Components/NestedContentEntryView.swift
    • implementations/ios-sdk/swiftui/Screens/LiveUpdatesTestScreen.swift
    • implementations/ios-sdk/swiftui/Screens/MainScreen.swift
    • implementations/ios-sdk/uikit/Components/ContentEntryUIView.swift
    • implementations/ios-sdk/uikit/Components/NestedContentEntryUIView.swift
    • implementations/ios-sdk/uikit/Components/OptimizedEntryUIView.swift
    • implementations/ios-sdk/uikit/SceneDelegate.swift
    • implementations/ios-sdk/uikit/Screens/LiveUpdatesTestViewController.swift
    • implementations/ios-sdk/uikit/Screens/MainViewController.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewContentfulClient.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelConfig.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelOverlay.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Preview/PreviewPanelViewController.swift
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Tracking/TapTrackingModifier.swift
    • packages/ios/ContentfulOptimization/Tests/ContentfulOptimizationTests/CTEntryTests.swift
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Default Agent You can customize the agent settings here or contact your Bito workspace admin at jared.jolton@contentful.com.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj
@nalchevanidze
David Nalchevanidze (nalchevanidze) merged commit 30eae80 into main Aug 19, 2026
40 checks passed
@nalchevanidze
David Nalchevanidze (nalchevanidze) deleted the nt-3946-ios-contentful-swift branch August 19, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants