refactor(implementations): fetch iOS reference entries with contentful.swift [NT-3946] - #429
Draft
David Nalchevanidze (nalchevanidze) wants to merge 3 commits into
Draft
refactor(implementations): fetch iOS reference entries with contentful.swift [NT-3946]#429David Nalchevanidze (nalchevanidze) wants to merge 3 commits into
David Nalchevanidze (nalchevanidze) wants to merge 3 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The iOS reference app fetched Contentful entries with hand-rolled HTTP — a URL string,
URLSession,JSONSerializationinto untyped dictionaries, and its ownincludeslink 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. The Optimization SDK has shipped the adapter for it since NT-3808 (#393); no reference app used it.Closes NT-3946.
What
Three separable commits, ordered so every commit is green:
fix(swift): restore SwiftUI OptimizedEntry tap tracking [NT-3829]fix(swift): expose CTEntry dictionary and JSON initializers as public [NT-3946]refactor(implementations): fetch iOS reference entries with contentful.swift [NT-3946]1. SwiftUI tap tracking was broken
The tap observer added for NT-3829 attached its
UITapGestureRecognizerto itssuperview, assuming.background()makes the observer a sibling of a content view. SwiftUI drawsTextand friends into a shared display list rather than oneUIViewper view, and places the observer in a container of its own — so that container isn't reliably an ancestor of the tapped region, and the recognizer never saw the touch. The SwiftUI shell emitted nocomponent_clickevents at all.Now attached to the window (always an ancestor) and scoped to the observer's own frame, which
.background()sizes to the tracked content.cancelsTouchesInView,delaysTouchesBegan, anddelaysTouchesEndedare all off, so it never competes for a nested child's touches — NT-3829 is preserved by construction. Detaches on window change anddeinit.This was pre-existing on
main, confirmed by stashing this branch's changes and reproducing identically atHEAD. It went unnoticed because NT-3829's regression test only asserts the nested Button's own action fires, so it passed either way — and the two tests that assert the click event aren't in CI, which runs onlyPreviewPanelTestsfor iOS.2.
CTEntryconstruction parityCTEntrywas constructible only from aContentful.Entry;init(any:)/init(json:)were internal. Android already exposes all three shapes (from(CDAEntry),from(Map),from(String)), so this closes a gap rather than inventing API. The dictionary initializer takes[String: Any]to match Android'sMapsignature.emptybecame public because Swift rejects an internal declaration in a public default argument, where Kotlin permits it.Note SDK APIs still accept entries as dictionaries, not
CTEntry, so these initializers are for reading an entry — not for passing one back into resolution.3. The migration
Both shells fetch with
contentful.swiftand hand theContentful.Entryto the typed SDK entry points —OptimizedEntry(entry:)in SwiftUI,resolveOptimizedEntry(baseline:)in UIKit. App-owned CDA request construction, JSON parsing, and link resolution are deleted; link resolution and cycle handling come fromLinkResolverplus the SDK'sCTEntryencoding.isNestedContentcollapses toentry.sys.contentTypeId. Requests stay single-locale vialocalizeResults(withLocaleCode:)withinclude(10). The preview panel's client is migrated too, mapping back down withCTEntry.toDictionary()for the dictionary-shapedPreviewContentfulClient.Expanded child entries and locally built test entries only exist as dictionaries, so the nested renderers keep the dictionary initializers.
The mock transport shim
contentful.swift5.5.15 can't express the mock server's/contentful/path prefix (it builds/spaces/...from the host root, andhostaccepts onlyhost[:port]), and it fetches/localesbefore its first entry request, which the mock doesn't serve.Rather than change test infrastructure shared with Android and the web SDKs,
MockContentfulTransportadapts both in-app. This is demo-only plumbing — a production app buildsContentful.Client(spaceId:accessToken:)againstcdn.contentful.comand needs none of it. Its doc comment and the README both say so. The alternative — additive root-level CDA routes plus a/localesroute inlib/mocks, reusable by NT-3947 — is still on the table if reviewers prefer it.Validation
pnpm ios:testformat:check/git diff --checkSwiftUI went 67/69 → 69/69: the two tap-tracking failures were pre-existing.
Behaviour parity was checked specifically where the CDA migration could regress quietly — merge-tag resolution through rich text, all three nested-variant levels, and variant→baseline back-edges under
CTEntry's ancestor-based cycle handling, which replaces the old depth-10 budget.For reviewers
fix(swift)changes would not get Swift changelog entries. Happy to split.TapTrackingModifieris a SwiftUIViewModifierreachable only via SwiftUIOptimizedEntry, and the UIKit shell renders throughOptimizedEntryUIView— so it can't be affected. That's reasoning, not a test run; say the word and I'll re-run.PreviewPanelTests, which is why a fully broken tap-tracking path survived. Widening it would be a cheap separate improvement.documentation/guides/integrating-the-optimization-ios-sdk-in-{a-swiftui,a-uikit}-app.mdalready documented this integration, so they needed no edits.🤖 Generated with Claude Code