Skip to content
Draft
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
9 changes: 9 additions & 0 deletions documentation/internal/sdk-knowledge/native/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ viewportHeight:)` from its own scroll/layout callbacks and the controller applie
so typed native renderers can distinguish baseline and variant shapes after resolution. It does
not validate fields or choose content-type-specific UI.
source: extern:packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift
- `CTEntry` is constructible by consumers from all three entry shapes, matching
`CTEntry.from(...)` on Android: `init(_: Contentful.Entry)`, `init(dictionary:fallback:)`, and
`init(json:)`. The dictionary initializer is fail-soft — a value with no JSON representation logs
and yields `fallback`, defaulting to the public `CTEntry.empty` (no `sys`, no fields, every read
absent) — while `init(json:)` throws instead, since a malformed JSON string carries a decoding
error worth surfacing. SDK APIs still accept entries as dictionaries, not `CTEntry`, so these
initializers are for reading an entry through `getField`/`hasField` rather than for passing one
back into resolution.
source: extern:packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift; extern:packages/android/ContentfulOptimization/src/main/kotlin/com/contentful/optimization/contentful/CTEntry.kt

## Identifier ownership

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
137A73B22636AC4E7B2E0EF0 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102D5A209AF6AC8B69E4BD24 /* AnalyticsTests.swift */; };
19E3767A1A9F73198E5BD235 /* XCTestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9711C5717754C40619B585EE /* XCTestExtensions.swift */; };
1A3EC2767577C51E53AB19D2 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102D5A209AF6AC8B69E4BD24 /* AnalyticsTests.swift */; };
1FC8A89C845F30ACAD88586B /* Contentful in Frameworks */ = {isa = PBXBuildFile; productRef = E897BA8CDB1DDBF1C164A4E4 /* Contentful */; };
244AA9A414B83F37FAE72F42 /* ContentEntryUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E433A2784433D5F7632A8098 /* ContentEntryUIView.swift */; };
24EF9DE5BDCB8CF94011F6E4 /* UnidentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E8E4E3EC9B5BB52E93ECC8 /* UnidentifiedVariantsTests.swift */; };
27E21567FB96A9E2EC57EE81 /* XCTestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9711C5717754C40619B585EE /* XCTestExtensions.swift */; };
Expand All @@ -28,10 +29,13 @@
471F733790E71327EAA41CA8 /* LiveUpdatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85C1A5AF835283004FFE76D /* LiveUpdatesTests.swift */; };
4FEBB65BC56EFBD45D81C803 /* PreviewPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCEDAABD36DADC093A8FD78 /* PreviewPanelTests.swift */; };
53F94866D0BA3E09271D73F7 /* ContentfulOptimization in Frameworks */ = {isa = PBXBuildFile; productRef = B622890DF93C3F71C32F81AB /* ContentfulOptimization */; };
55EBF3F91D016BC49C2C077E /* MockContentfulTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C4493D66695EEA5637A015 /* MockContentfulTransport.swift */; };
58116AD3ED6029892390DFCE /* ScreenTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2A09744E23C92FDCD9F5439 /* ScreenTrackingTests.swift */; };
58EF6936C43184E5E5BB149B /* Contentful in Frameworks */ = {isa = PBXBuildFile; productRef = 223614F3D65269BC06E8BC2A /* Contentful */; };
5C1226FB8DC19DE4D3783F11 /* UnidentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E8E4E3EC9B5BB52E93ECC8 /* UnidentifiedVariantsTests.swift */; };
5F679B3B001F53CD0A6BBC98 /* IdentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44D0B58FB5CDE1A33ADEC3E /* IdentifiedVariantsTests.swift */; };
61D00339E0A84FBA44A0AF62 /* FlagViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79C1C23CB5619203CAD28A3 /* FlagViewTrackingTests.swift */; };
63D62559B4584A3C778A232C /* MockContentfulTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C4493D66695EEA5637A015 /* MockContentfulTransport.swift */; };
68EBA430F06D7974D323C432 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F03F8B039B4ED4A7E7A32D5 /* SceneDelegate.swift */; };
68FDF01398E5C12FD8A37277 /* AnalyticsEventDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC9CA7E33164E91046FF6DF /* AnalyticsEventDisplay.swift */; };
6BEDE4E72E002CF5C3A8A9E1 /* ExtendedViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324154A5A5CE58F32970813 /* ExtendedViewTrackingTests.swift */; };
Expand All @@ -51,6 +55,7 @@
9F3986444C6D6D3BCB8ECEA3 /* RichText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820BE574DFF5E022220890CD /* RichText.swift */; };
A375768DD5B98ECEC51B04C9 /* FlagViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79C1C23CB5619203CAD28A3 /* FlagViewTrackingTests.swift */; };
B1A5A931BBDFC3438E935012 /* OfflineBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */; };
B4EF5067C86627BF93949985 /* MockContentfulClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC271C5DD5F3CFBBFF9A24A /* MockContentfulClient.swift */; };
C04EF55982CA837E62CC0669 /* ContentfulFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F383552F49942336F9725020 /* ContentfulFetcher.swift */; };
C23B6242DA30D283D76C1B3E /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DDE184BA14D36BAC6A5936 /* Config.swift */; };
C4757AF18FE122AC807B27E7 /* NestedButtonTapTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B353BE52F9AEFA1D6213FB4 /* NestedButtonTapTestScreen.swift */; };
Expand All @@ -61,6 +66,7 @@
E3C31DA281B4A6D1DF55CE29 /* TapTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE746138D269D770FA86FA /* TapTrackingTests.swift */; };
ECBF1C0DC16E4532F4EEE49A /* ExtendedViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324154A5A5CE58F32970813 /* ExtendedViewTrackingTests.swift */; };
EFEFDCD73D2FE1854045F5DB /* PreviewPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCEDAABD36DADC093A8FD78 /* PreviewPanelTests.swift */; };
F00EF0DEE25152C62840DF82 /* MockContentfulClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC271C5DD5F3CFBBFF9A24A /* MockContentfulClient.swift */; };
F510376F8D6D3EFE7BB197E1 /* NestedContentEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354A498C8D7BC1CB974D2454 /* NestedContentEntryView.swift */; };
FE0F74AA3E04EB42454F14D1 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38EFCB367C35AF0AEB0A4AE /* TestHelpers.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -90,6 +96,7 @@
102D5A209AF6AC8B69E4BD24 /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
1324154A5A5CE58F32970813 /* ExtendedViewTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedViewTrackingTests.swift; sourceTree = "<group>"; };
1B353BE52F9AEFA1D6213FB4 /* NestedButtonTapTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedButtonTapTestScreen.swift; sourceTree = "<group>"; };
26C4493D66695EEA5637A015 /* MockContentfulTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockContentfulTransport.swift; sourceTree = "<group>"; };
29032ACE1D4AEB3E5D1BB51A /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
2E855C4FA2D4C37DB188398F /* OptimizationAppSwiftUI.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = OptimizationAppSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
354A498C8D7BC1CB974D2454 /* NestedContentEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedContentEntryView.swift; sourceTree = "<group>"; };
Expand All @@ -99,6 +106,7 @@
5F03F8B039B4ED4A7E7A32D5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBehaviorTests.swift; sourceTree = "<group>"; };
6B60B2B8362A57886F415EE4 /* LiveUpdatesTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTestViewController.swift; sourceTree = "<group>"; };
6FC271C5DD5F3CFBBFF9A24A /* MockContentfulClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockContentfulClient.swift; sourceTree = "<group>"; };
72BC55BDAE08B0B386CCFA65 /* ContentfulOptimization */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ContentfulOptimization; path = ../../packages/ios/ContentfulOptimization; sourceTree = SOURCE_ROOT; };
7A41CEBEA796C5A3179D691E /* LiveUpdatesTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTestScreen.swift; sourceTree = "<group>"; };
7FDD088B0ACEC1271B3C5509 /* OptimizedEntryUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizedEntryUIView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -132,6 +140,7 @@
buildActionMask = 2147483647;
files = (
42933080441B12904731C661 /* ContentfulOptimization in Frameworks */,
1FC8A89C845F30ACAD88586B /* Contentful in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -140,6 +149,7 @@
buildActionMask = 2147483647;
files = (
53F94866D0BA3E09271D73F7 /* ContentfulOptimization in Frameworks */,
58EF6936C43184E5E5BB149B /* Contentful in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -213,6 +223,8 @@
D5DDE184BA14D36BAC6A5936 /* Config.swift */,
F383552F49942336F9725020 /* ContentfulFetcher.swift */,
89CE81FC20185EE7C2FF7BEF /* EventStore.swift */,
6FC271C5DD5F3CFBBFF9A24A /* MockContentfulClient.swift */,
26C4493D66695EEA5637A015 /* MockContentfulTransport.swift */,
A72A25C095AB9AA5B35E3F57 /* MockPreviewContentfulClient.swift */,
820BE574DFF5E022220890CD /* RichText.swift */,
);
Expand Down Expand Up @@ -306,6 +318,7 @@
name = OptimizationAppSwiftUI;
packageProductDependencies = (
B622890DF93C3F71C32F81AB /* ContentfulOptimization */,
223614F3D65269BC06E8BC2A /* Contentful */,
);
productName = OptimizationAppSwiftUI;
productReference = 2E855C4FA2D4C37DB188398F /* OptimizationAppSwiftUI.app */;
Expand Down Expand Up @@ -343,6 +356,7 @@
name = OptimizationAppUIKit;
packageProductDependencies = (
244F9A9729F24EEBFDC1862F /* ContentfulOptimization */,
E897BA8CDB1DDBF1C164A4E4 /* Contentful */,
);
productName = OptimizationAppUIKit;
productReference = BB9EE54CEA502AA570FA0154 /* OptimizationAppUIKit.app */;
Expand Down Expand Up @@ -393,6 +407,7 @@
mainGroup = 603D5CDC672E61359F8B4927;
minimizedProjectReferenceProxies = 1;
packageReferences = (
D519C5C4AF908F3FD0450A60 /* XCRemoteSwiftPackageReference "contentful.swift" */,
435335CDD6AD4D4978CBCABF /* XCLocalSwiftPackageReference "../../packages/ios/ContentfulOptimization" */,
);
preferredProjectObjectVersion = 77;
Expand Down Expand Up @@ -441,6 +456,8 @@
4447121F6CD440EAC27713E7 /* EventStore.swift in Sources */,
93DDB4FCE61B74F35663301D /* LiveUpdatesTestScreen.swift in Sources */,
8BE046C5FD5539712A40551C /* MainScreen.swift in Sources */,
B4EF5067C86627BF93949985 /* MockContentfulClient.swift in Sources */,
63D62559B4584A3C778A232C /* MockContentfulTransport.swift in Sources */,
7E0AE3A92E2EB13F275145C5 /* MockPreviewContentfulClient.swift in Sources */,
71EE02D789A2D2D922B4DC0A /* NavigationTestScreen.swift in Sources */,
C4757AF18FE122AC807B27E7 /* NestedButtonTapTestScreen.swift in Sources */,
Expand Down Expand Up @@ -481,6 +498,8 @@
73A7EECB26977730B7241077 /* EventStore.swift in Sources */,
D28A6D01EBA9877DD9174BCB /* LiveUpdatesTestViewController.swift in Sources */,
91A5DFA622284D666F20D46E /* MainViewController.swift in Sources */,
F00EF0DEE25152C62840DF82 /* MockContentfulClient.swift in Sources */,
55EBF3F91D016BC49C2C077E /* MockContentfulTransport.swift in Sources */,
3EC1F3129468B71C9DE3C486 /* MockPreviewContentfulClient.swift in Sources */,
7CE0E6C067281C01D8F0A85F /* NavigationTestViewController.swift in Sources */,
9550770C73B171616E3E23C4 /* NestedButtonTapTestViewController.swift in Sources */,
Expand Down Expand Up @@ -821,7 +840,23 @@
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
D519C5C4AF908F3FD0450A60 /* XCRemoteSwiftPackageReference "contentful.swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/contentful/contentful.swift";
requirement = {
kind = exactVersion;
version = 5.5.15;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
223614F3D65269BC06E8BC2A /* Contentful */ = {
isa = XCSwiftPackageProductDependency;
package = D519C5C4AF908F3FD0450A60 /* XCRemoteSwiftPackageReference "contentful.swift" */;
productName = Contentful;
};
244F9A9729F24EEBFDC1862F /* ContentfulOptimization */ = {
isa = XCSwiftPackageProductDependency;
productName = ContentfulOptimization;
Expand All @@ -830,6 +865,11 @@
isa = XCSwiftPackageProductDependency;
productName = ContentfulOptimization;
};
E897BA8CDB1DDBF1C164A4E4 /* Contentful */ = {
isa = XCSwiftPackageProductDependency;
package = D519C5C4AF908F3FD0450A60 /* XCRemoteSwiftPackageReference "contentful.swift" */;
productName = Contentful;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 59C62CFF73857D71C2743362 /* Project object */;
Expand Down
27 changes: 23 additions & 4 deletions implementations/ios-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,32 @@ Both apps share `shared/` for configuration, Contentful fetching, and analytics
run the same UI test source tree from `uitests/` against their respective host apps so SDK behavior
can be compared across UI frameworks.

## Contentful entry fetching

Entries are fetched with Contentful's official Swift SDK,
[`contentful.swift`](https://github.com/contentful/contentful.swift), which is the integration path
recommended to customers. `shared/ContentfulFetcher.swift` queries by entry ID with `include(10)`,
and the fetched `Contentful.Entry` values are handed to the Optimization SDK's typed entry APIs:
`OptimizedEntry(entry:)` in SwiftUI and `resolveOptimizedEntry(baseline:)` in UIKit. The SDK encodes
them through `CTEntry`, so the app does no CDA JSON parsing or link resolution of its own.

Entry-ID lookup stays app-owned; the Optimization SDK resolves personalization against entries the
app supplies rather than fetching them itself.

`shared/MockContentfulTransport.swift` exists only to reach this repo's mock server and is not part
of the recommended integration. The mock multiplexes the Contentful, Experience, and Insights APIs
onto one port and namespaces the CDA under `/contentful/`, which `Contentful.Client` cannot express,
and it serves no `/locales` route, which the SDK requires before its first entry fetch. A production
app builds `Contentful.Client(spaceId:accessToken:)` against `cdn.contentful.com` and needs none of
it.

## CDA locale handling

The app defines one locale in shared config, passes it to the native SDK as top-level `locale`, and
passes it directly to the shared raw CDA fetch helper. Entries passed to SDK resolution use the
standard single-locale CDA entry shape. Do not use all-locale CDA responses or `locale=*`, because
entry resolution expects direct single-locale fields such as `fields.nt_experiences` and
`fields.nt_variants`. See
passes it to CDA requests through `localizeResults(withLocaleCode:)`. Entries passed to SDK
resolution use the standard single-locale CDA entry shape. Do not use all-locale CDA responses or
`locale=*`, because entry resolution expects direct single-locale fields such as
`fields.nt_experiences` and `fields.nt_variants`. See
[Locale handling in the Optimization SDK Suite](../../documentation/concepts/locale-handling-in-the-optimization-sdk-suite.md)
for the broader locale model and
[Entry personalization and variant resolution](../../documentation/concepts/entry-personalization-and-variant-resolution.md#single-locale-cda-entry-contract)
Expand Down
11 changes: 11 additions & 0 deletions implementations/ios-sdk/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ options:
packages:
ContentfulOptimization:
path: ../../packages/ios/ContentfulOptimization
# The app fetches CDA entries with Contentful's official Swift SDK, the same
# way a customer app would. Pinned to the exact version
# packages/ios/ContentfulOptimization already resolves, so SPM has a single
# version to pick for both the app targets and the SDK package.
contentful.swift:
url: https://github.com/contentful/contentful.swift
exactVersion: 5.5.15

settings:
base:
Expand All @@ -30,6 +37,8 @@ targets:
INFOPLIST_FILE: swiftui/Info.plist
dependencies:
- package: ContentfulOptimization
- package: contentful.swift
product: Contentful
scheme:
testTargets: [OptimizationAppUITestsSwiftUI]
# Scheme pre-actions fire before any target (including Swift Package
Expand Down Expand Up @@ -76,6 +85,8 @@ targets:
INFOPLIST_FILE: uikit/Info.plist
dependencies:
- package: ContentfulOptimization
- package: contentful.swift
product: Contentful
scheme:
testTargets: [OptimizationAppUITestsUIKit]
preActions:
Expand Down
16 changes: 15 additions & 1 deletion implementations/ios-sdk/shared/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@ struct AppConfig {
static let experienceBaseUrl = "http://localhost:8000/experience/"
static let insightsBaseUrl = "http://localhost:8000/insights/"

static let contentfulBaseUrl = "http://localhost:8000/contentful/"
/// Host (with port) for the mock Contentful CDA, in the `host[:port]` form
/// `Contentful.Client` expects. A production app omits this entirely and
/// gets the default `cdn.contentful.com`.
static let contentfulHost = "localhost:8000"

/// Path prefix the mock server namespaces the CDA under, so one process can
/// also serve the Experience and Insights APIs. Consumed only by
/// `MockContentfulTransport` — `Contentful.Client` builds paths from the
/// host root and has nowhere to put a prefix.
static let contentfulMockPathPrefix = "/contentful"

static let contentfulSpaceId = "mock-space-id"

/// The mock CDA ignores authorization, but `Contentful.Client` requires a
/// token to build its `Authorization` header.
static let contentfulAccessToken = "mock-access-token"

static let entryIds = [
"1MwiFl4z7gkwqGYdvCmr8c",
"4ib0hsHWoSOnCVdDkizE8d",
Expand Down
Loading
Loading