diff --git a/implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj b/implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj
index a68dd04e..e11faad1 100644
--- a/implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj
+++ b/implementations/ios-sdk/OptimizationApp.xcodeproj/project.pbxproj
@@ -3,330 +3,548 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 56;
+ objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
- C10000000000000000000001 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000001 /* App.swift */; };
- C10000000000000000000002 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000002 /* Config.swift */; };
- C10000000000000000000003 /* MainScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000003 /* MainScreen.swift */; };
- C10000000000000000000004 /* NavigationTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000004 /* NavigationTestScreen.swift */; };
- C10000000000000000000005 /* LiveUpdatesTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000005 /* LiveUpdatesTestScreen.swift */; };
- C10000000000000000000006 /* ContentEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000006 /* ContentEntryView.swift */; };
- C10000000000000000000007 /* AnalyticsEventDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000007 /* AnalyticsEventDisplay.swift */; };
- C10000000000000000000008 /* ContentfulFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20000000000000000000008 /* ContentfulFetcher.swift */; };
- C1000000000000000000000A /* NestedContentEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000000000000000000000B /* NestedContentEntryView.swift */; };
- C10000000000000000000009 /* ContentfulOptimization in Frameworks */ = {isa = PBXBuildFile; productRef = CA0000000000000000000001 /* ContentfulOptimization */; };
- D10000000000000000000001 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000001 /* TestHelpers.swift */; };
- D10000000000000000000002 /* XCTestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000002 /* XCTestExtensions.swift */; };
- D10000000000000000000003 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000003 /* AnalyticsTests.swift */; };
- D10000000000000000000004 /* TapTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000004 /* TapTrackingTests.swift */; };
- D10000000000000000000005 /* ExtendedViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000005 /* ExtendedViewTrackingTests.swift */; };
- D10000000000000000000006 /* ScreenTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000006 /* ScreenTrackingTests.swift */; };
- D10000000000000000000007 /* LiveUpdatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000007 /* LiveUpdatesTests.swift */; };
- D10000000000000000000008 /* FlagViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000008 /* FlagViewTrackingTests.swift */; };
- D10000000000000000000009 /* OfflineBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000000000000000000009 /* OfflineBehaviorTests.swift */; };
- D1000000000000000000000A /* IdentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2000000000000000000000A /* IdentifiedVariantsTests.swift */; };
- D1000000000000000000000B /* UnidentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2000000000000000000000B /* UnidentifiedVariantsTests.swift */; };
- D1000000000000000000000C /* PreviewPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2000000000000000000000D /* PreviewPanelTests.swift */; };
+ 072516CD147CCCA5D7F0BE53 /* OptimizedEntryUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDD088B0ACEC1271B3C5509 /* OptimizedEntryUIView.swift */; };
+ 106F273577EF1A1FED81036E /* ContentEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA4937C8FF6D42B95D31BE7 /* ContentEntryView.swift */; };
+ 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 */; };
+ 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 */; };
+ 2D489A968E200B477EF8F8B9 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DDE184BA14D36BAC6A5936 /* Config.swift */; };
+ 394C0BFFE6C5F4E2E1429CEF /* IdentifiedVariantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44D0B58FB5CDE1A33ADEC3E /* IdentifiedVariantsTests.swift */; };
+ 3AE0D2F928ABA07AE77FB039 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38EFCB367C35AF0AEB0A4AE /* TestHelpers.swift */; };
+ 3EE8AB65755C4AAECE6F088E /* ContentfulFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F383552F49942336F9725020 /* ContentfulFetcher.swift */; };
+ 4067B88302C9720268628FF4 /* OfflineBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */; };
+ 42933080441B12904731C661 /* ContentfulOptimization in Frameworks */ = {isa = PBXBuildFile; productRef = 244F9A9729F24EEBFDC1862F /* ContentfulOptimization */; };
+ 437A5DD2CAFE5E72795431F2 /* AnalyticsEventDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0736290C02851112612F81F6 /* AnalyticsEventDisplayView.swift */; };
+ 4447121F6CD440EAC27713E7 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CE81FC20185EE7C2FF7BEF /* EventStore.swift */; };
+ 44AB322CF14E273E1B07B5E2 /* TapTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE746138D269D770FA86FA /* TapTrackingTests.swift */; };
+ 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 */; };
+ 58116AD3ED6029892390DFCE /* ScreenTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2A09744E23C92FDCD9F5439 /* ScreenTrackingTests.swift */; };
+ 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 */; };
+ 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 */; };
+ 6EF2FA3FE304F740FC5C8922 /* PreviewPanelOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D155E6889AE670D2B6097CC /* PreviewPanelOverridesTests.swift */; };
+ 71EE02D789A2D2D922B4DC0A /* NavigationTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDF68085F694A44D33D237A /* NavigationTestScreen.swift */; };
+ 73A7EECB26977730B7241077 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CE81FC20185EE7C2FF7BEF /* EventStore.swift */; };
+ 7CE0E6C067281C01D8F0A85F /* NavigationTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4808FBBA0BFA7C40F52FC7 /* NavigationTestViewController.swift */; };
+ 8A29BA19D8A78BC9DB688B37 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B7AD7AF5C9B56BD605DB59 /* AppDelegate.swift */; };
+ 8BE046C5FD5539712A40551C /* MainScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569F34F81667A8807581B59F /* MainScreen.swift */; };
+ 9014D5B10826A433A7412BAC /* PreviewPanelOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D155E6889AE670D2B6097CC /* PreviewPanelOverridesTests.swift */; };
+ 91A5DFA622284D666F20D46E /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29032ACE1D4AEB3E5D1BB51A /* MainViewController.swift */; };
+ 93DDB4FCE61B74F35663301D /* LiveUpdatesTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A41CEBEA796C5A3179D691E /* LiveUpdatesTestScreen.swift */; };
+ 9E261E1E35994E3D7A652291 /* ScreenTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2A09744E23C92FDCD9F5439 /* ScreenTrackingTests.swift */; };
+ A375768DD5B98ECEC51B04C9 /* FlagViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79C1C23CB5619203CAD28A3 /* FlagViewTrackingTests.swift */; };
+ B1A5A931BBDFC3438E935012 /* OfflineBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */; };
+ C04EF55982CA837E62CC0669 /* ContentfulFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F383552F49942336F9725020 /* ContentfulFetcher.swift */; };
+ C23B6242DA30D283D76C1B3E /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DDE184BA14D36BAC6A5936 /* Config.swift */; };
+ CE4104C9F52CE1A874B6D802 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = F082E0D07BC7C35FCC9901C4 /* App.swift */; };
+ D28A6D01EBA9877DD9174BCB /* LiveUpdatesTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B60B2B8362A57886F415EE4 /* LiveUpdatesTestViewController.swift */; };
+ D93DA9530B2B7E95815DB931 /* NestedContentEntryUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C832ADCDDFECC716F4B04C /* NestedContentEntryUIView.swift */; };
+ E2245ABC4EB91EF3E86E2794 /* LiveUpdatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85C1A5AF835283004FFE76D /* LiveUpdatesTests.swift */; };
+ 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 */; };
+ 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 */
/* Begin PBXContainerItemProxy section */
- D60000000000000000000001 /* PBXContainerItemProxy */ = {
+ 601E6E8C910B4E4944F371F6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
- containerPortal = C50000000000000000000001 /* Project object */;
+ containerPortal = 59C62CFF73857D71C2743362 /* Project object */;
proxyType = 1;
- remoteGlobalIDString = C40000000000000000000001;
- remoteInfo = OptimizationApp;
+ remoteGlobalIDString = DB66258D797442B93A7B2200;
+ remoteInfo = OptimizationAppUIKit;
+ };
+ 92B3C749C74E19DFFA0A2D85 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 59C62CFF73857D71C2743362 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6B18419FBDE96C10776B1E0D;
+ remoteInfo = OptimizationAppSwiftUI;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- C20000000000000000000001 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
- C20000000000000000000002 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; };
- C20000000000000000000003 /* MainScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreen.swift; sourceTree = ""; };
- C20000000000000000000004 /* NavigationTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTestScreen.swift; sourceTree = ""; };
- C20000000000000000000005 /* LiveUpdatesTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTestScreen.swift; sourceTree = ""; };
- C20000000000000000000006 /* ContentEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentEntryView.swift; sourceTree = ""; };
- C20000000000000000000007 /* AnalyticsEventDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventDisplay.swift; sourceTree = ""; };
- C20000000000000000000008 /* ContentfulFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentfulFetcher.swift; sourceTree = ""; };
- C2000000000000000000000B /* NestedContentEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedContentEntryView.swift; sourceTree = ""; };
- C20000000000000000000009 /* OptimizationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OptimizationApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
- C2000000000000000000000A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- D20000000000000000000001 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; };
- D20000000000000000000002 /* XCTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestExtensions.swift; sourceTree = ""; };
- D20000000000000000000003 /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; };
- D20000000000000000000004 /* TapTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapTrackingTests.swift; sourceTree = ""; };
- D20000000000000000000005 /* ExtendedViewTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedViewTrackingTests.swift; sourceTree = ""; };
- D20000000000000000000006 /* ScreenTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackingTests.swift; sourceTree = ""; };
- D20000000000000000000007 /* LiveUpdatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTests.swift; sourceTree = ""; };
- D20000000000000000000008 /* FlagViewTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagViewTrackingTests.swift; sourceTree = ""; };
- D20000000000000000000009 /* OfflineBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBehaviorTests.swift; sourceTree = ""; };
- D2000000000000000000000A /* IdentifiedVariantsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiedVariantsTests.swift; sourceTree = ""; };
- D2000000000000000000000B /* UnidentifiedVariantsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnidentifiedVariantsTests.swift; sourceTree = ""; };
- D2000000000000000000000C /* OptimizationAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OptimizationAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- D2000000000000000000000D /* PreviewPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewPanelTests.swift; sourceTree = ""; };
+ 00B7AD7AF5C9B56BD605DB59 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 06E8E4E3EC9B5BB52E93ECC8 /* UnidentifiedVariantsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnidentifiedVariantsTests.swift; sourceTree = ""; };
+ 0736290C02851112612F81F6 /* AnalyticsEventDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventDisplayView.swift; sourceTree = ""; };
+ 0D155E6889AE670D2B6097CC /* PreviewPanelOverridesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewPanelOverridesTests.swift; sourceTree = ""; };
+ 102D5A209AF6AC8B69E4BD24 /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; };
+ 1324154A5A5CE58F32970813 /* ExtendedViewTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedViewTrackingTests.swift; sourceTree = ""; };
+ 29032ACE1D4AEB3E5D1BB51A /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; };
+ 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 = ""; };
+ 4DA4937C8FF6D42B95D31BE7 /* ContentEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentEntryView.swift; sourceTree = ""; };
+ 51C832ADCDDFECC716F4B04C /* NestedContentEntryUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedContentEntryUIView.swift; sourceTree = ""; };
+ 569F34F81667A8807581B59F /* MainScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreen.swift; sourceTree = ""; };
+ 5F03F8B039B4ED4A7E7A32D5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBehaviorTests.swift; sourceTree = ""; };
+ 6B60B2B8362A57886F415EE4 /* LiveUpdatesTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTestViewController.swift; sourceTree = ""; };
+ 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 = ""; };
+ 7FDD088B0ACEC1271B3C5509 /* OptimizedEntryUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizedEntryUIView.swift; sourceTree = ""; };
+ 89CE81FC20185EE7C2FF7BEF /* EventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStore.swift; sourceTree = ""; };
+ 8C4808FBBA0BFA7C40F52FC7 /* NavigationTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTestViewController.swift; sourceTree = ""; };
+ 8FB2B5375330439D2F81AACF /* OptimizationAppUITestsSwiftUI.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = OptimizationAppUITestsSwiftUI.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9711C5717754C40619B585EE /* XCTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestExtensions.swift; sourceTree = ""; };
+ A38EFCB367C35AF0AEB0A4AE /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; };
+ AAC9CA7E33164E91046FF6DF /* AnalyticsEventDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventDisplay.swift; sourceTree = ""; };
+ B2A09744E23C92FDCD9F5439 /* ScreenTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackingTests.swift; sourceTree = ""; };
+ B85C1A5AF835283004FFE76D /* LiveUpdatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUpdatesTests.swift; sourceTree = ""; };
+ B8DE746138D269D770FA86FA /* TapTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapTrackingTests.swift; sourceTree = ""; };
+ BB9EE54CEA502AA570FA0154 /* OptimizationAppUIKit.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = OptimizationAppUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ C44D0B58FB5CDE1A33ADEC3E /* IdentifiedVariantsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiedVariantsTests.swift; sourceTree = ""; };
+ D5DDE184BA14D36BAC6A5936 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; };
+ E433A2784433D5F7632A8098 /* ContentEntryUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentEntryUIView.swift; sourceTree = ""; };
+ E79C1C23CB5619203CAD28A3 /* FlagViewTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagViewTrackingTests.swift; sourceTree = ""; };
+ EBCEDAABD36DADC093A8FD78 /* PreviewPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewPanelTests.swift; sourceTree = ""; };
+ EEDF68085F694A44D33D237A /* NavigationTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTestScreen.swift; sourceTree = ""; };
+ F082E0D07BC7C35FCC9901C4 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
+ F383552F49942336F9725020 /* ContentfulFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentfulFetcher.swift; sourceTree = ""; };
+ F599564CEC969B5B81DE8ABB /* OptimizationAppUITestsUIKit.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = OptimizationAppUITestsUIKit.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
- C60000000000000000000002 /* Frameworks */ = {
+ B50F9AC6418353EF14CD18F1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- C10000000000000000000009 /* ContentfulOptimization in Frameworks */,
+ 42933080441B12904731C661 /* ContentfulOptimization in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
- D50000000000000000000002 /* Frameworks */ = {
+ FA9CA9A2BC78C03F0FC7EDA7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 53F94866D0BA3E09271D73F7 /* ContentfulOptimization in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- C30000000000000000000001 /* Root */ = {
+ 02DE428FB2CE0F65A417F976 /* Components */ = {
isa = PBXGroup;
children = (
- C30000000000000000000002 /* OptimizationApp */,
- D30000000000000000000001 /* OptimizationAppUITests */,
- C30000000000000000000006 /* Products */,
+ 0736290C02851112612F81F6 /* AnalyticsEventDisplayView.swift */,
+ E433A2784433D5F7632A8098 /* ContentEntryUIView.swift */,
+ 51C832ADCDDFECC716F4B04C /* NestedContentEntryUIView.swift */,
+ 7FDD088B0ACEC1271B3C5509 /* OptimizedEntryUIView.swift */,
);
+ path = Components;
sourceTree = "";
};
- C30000000000000000000002 /* OptimizationApp */ = {
+ 25DC300A28A876E8A6498842 /* Components */ = {
isa = PBXGroup;
children = (
- C20000000000000000000001 /* App.swift */,
- C20000000000000000000002 /* Config.swift */,
- C2000000000000000000000A /* Info.plist */,
- C30000000000000000000003 /* Screens */,
- C30000000000000000000004 /* Components */,
- C30000000000000000000005 /* Utils */,
- );
- path = OptimizationApp;
+ AAC9CA7E33164E91046FF6DF /* AnalyticsEventDisplay.swift */,
+ 4DA4937C8FF6D42B95D31BE7 /* ContentEntryView.swift */,
+ 354A498C8D7BC1CB974D2454 /* NestedContentEntryView.swift */,
+ );
+ path = Components;
sourceTree = "";
};
- C30000000000000000000003 /* Screens */ = {
+ 2865F5DDEE6CA317C94A6729 /* Packages */ = {
isa = PBXGroup;
children = (
- C20000000000000000000003 /* MainScreen.swift */,
- C20000000000000000000004 /* NavigationTestScreen.swift */,
- C20000000000000000000005 /* LiveUpdatesTestScreen.swift */,
+ 72BC55BDAE08B0B386CCFA65 /* ContentfulOptimization */,
);
- path = Screens;
+ name = Packages;
sourceTree = "";
};
- C30000000000000000000004 /* Components */ = {
+ 33C7589CC593AE0942DA8A0E /* swiftui */ = {
isa = PBXGroup;
children = (
- C20000000000000000000006 /* ContentEntryView.swift */,
- C2000000000000000000000B /* NestedContentEntryView.swift */,
- C20000000000000000000007 /* AnalyticsEventDisplay.swift */,
+ 25DC300A28A876E8A6498842 /* Components */,
+ A92BCF5C1A06DE1DE359A8B8 /* Screens */,
+ F082E0D07BC7C35FCC9901C4 /* App.swift */,
);
- path = Components;
+ path = swiftui;
sourceTree = "";
};
- C30000000000000000000005 /* Utils */ = {
+ 37FE6F709096D4C894E71E26 /* Products */ = {
isa = PBXGroup;
children = (
- C20000000000000000000008 /* ContentfulFetcher.swift */,
+ 2E855C4FA2D4C37DB188398F /* OptimizationAppSwiftUI.app */,
+ BB9EE54CEA502AA570FA0154 /* OptimizationAppUIKit.app */,
+ 8FB2B5375330439D2F81AACF /* OptimizationAppUITestsSwiftUI.xctest */,
+ F599564CEC969B5B81DE8ABB /* OptimizationAppUITestsUIKit.xctest */,
);
- path = Utils;
+ name = Products;
sourceTree = "";
};
- C30000000000000000000006 /* Products */ = {
+ 5D7C00238B2B791FBF457784 /* uikit */ = {
isa = PBXGroup;
children = (
- C20000000000000000000009 /* OptimizationApp.app */,
- D2000000000000000000000C /* OptimizationAppUITests.xctest */,
+ 02DE428FB2CE0F65A417F976 /* Components */,
+ 759DD78824352770CB466F65 /* Screens */,
+ 00B7AD7AF5C9B56BD605DB59 /* AppDelegate.swift */,
+ 5F03F8B039B4ED4A7E7A32D5 /* SceneDelegate.swift */,
);
- name = Products;
+ path = uikit;
sourceTree = "";
};
- D30000000000000000000001 /* OptimizationAppUITests */ = {
+ 5F1970556A8A490A503F72A2 /* shared */ = {
isa = PBXGroup;
children = (
- D30000000000000000000002 /* Support */,
- D30000000000000000000003 /* Tests */,
+ D5DDE184BA14D36BAC6A5936 /* Config.swift */,
+ F383552F49942336F9725020 /* ContentfulFetcher.swift */,
+ 89CE81FC20185EE7C2FF7BEF /* EventStore.swift */,
);
- path = OptimizationAppUITests;
+ path = shared;
sourceTree = "";
};
- D30000000000000000000002 /* Support */ = {
+ 603D5CDC672E61359F8B4927 = {
isa = PBXGroup;
children = (
- D20000000000000000000001 /* TestHelpers.swift */,
- D20000000000000000000002 /* XCTestExtensions.swift */,
+ 2865F5DDEE6CA317C94A6729 /* Packages */,
+ 5F1970556A8A490A503F72A2 /* shared */,
+ 33C7589CC593AE0942DA8A0E /* swiftui */,
+ 5D7C00238B2B791FBF457784 /* uikit */,
+ 98CAFFCEC491619FA634D286 /* uitests */,
+ 37FE6F709096D4C894E71E26 /* Products */,
);
- path = Support;
sourceTree = "";
};
- D30000000000000000000003 /* Tests */ = {
+ 759DD78824352770CB466F65 /* Screens */ = {
+ isa = PBXGroup;
+ children = (
+ 6B60B2B8362A57886F415EE4 /* LiveUpdatesTestViewController.swift */,
+ 29032ACE1D4AEB3E5D1BB51A /* MainViewController.swift */,
+ 8C4808FBBA0BFA7C40F52FC7 /* NavigationTestViewController.swift */,
+ );
+ path = Screens;
+ sourceTree = "";
+ };
+ 98CAFFCEC491619FA634D286 /* uitests */ = {
+ isa = PBXGroup;
+ children = (
+ FCA861592DDA2D2CDF74B57B /* Support */,
+ C07429D3704BFF93E642EF1D /* Tests */,
+ );
+ path = uitests;
+ sourceTree = "";
+ };
+ A92BCF5C1A06DE1DE359A8B8 /* Screens */ = {
+ isa = PBXGroup;
+ children = (
+ 7A41CEBEA796C5A3179D691E /* LiveUpdatesTestScreen.swift */,
+ 569F34F81667A8807581B59F /* MainScreen.swift */,
+ EEDF68085F694A44D33D237A /* NavigationTestScreen.swift */,
+ );
+ path = Screens;
+ sourceTree = "";
+ };
+ C07429D3704BFF93E642EF1D /* Tests */ = {
isa = PBXGroup;
children = (
- D20000000000000000000003 /* AnalyticsTests.swift */,
- D20000000000000000000004 /* TapTrackingTests.swift */,
- D20000000000000000000005 /* ExtendedViewTrackingTests.swift */,
- D20000000000000000000006 /* ScreenTrackingTests.swift */,
- D20000000000000000000007 /* LiveUpdatesTests.swift */,
- D20000000000000000000008 /* FlagViewTrackingTests.swift */,
- D20000000000000000000009 /* OfflineBehaviorTests.swift */,
- D2000000000000000000000A /* IdentifiedVariantsTests.swift */,
- D2000000000000000000000B /* UnidentifiedVariantsTests.swift */,
- D2000000000000000000000D /* PreviewPanelTests.swift */,
+ 102D5A209AF6AC8B69E4BD24 /* AnalyticsTests.swift */,
+ 1324154A5A5CE58F32970813 /* ExtendedViewTrackingTests.swift */,
+ E79C1C23CB5619203CAD28A3 /* FlagViewTrackingTests.swift */,
+ C44D0B58FB5CDE1A33ADEC3E /* IdentifiedVariantsTests.swift */,
+ B85C1A5AF835283004FFE76D /* LiveUpdatesTests.swift */,
+ 682E3BF881B8093C2D5EF2CE /* OfflineBehaviorTests.swift */,
+ 0D155E6889AE670D2B6097CC /* PreviewPanelOverridesTests.swift */,
+ EBCEDAABD36DADC093A8FD78 /* PreviewPanelTests.swift */,
+ B2A09744E23C92FDCD9F5439 /* ScreenTrackingTests.swift */,
+ B8DE746138D269D770FA86FA /* TapTrackingTests.swift */,
+ 06E8E4E3EC9B5BB52E93ECC8 /* UnidentifiedVariantsTests.swift */,
);
path = Tests;
sourceTree = "";
};
+ FCA861592DDA2D2CDF74B57B /* Support */ = {
+ isa = PBXGroup;
+ children = (
+ A38EFCB367C35AF0AEB0A4AE /* TestHelpers.swift */,
+ 9711C5717754C40619B585EE /* XCTestExtensions.swift */,
+ );
+ path = Support;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- C40000000000000000000001 /* OptimizationApp */ = {
+ 6B18419FBDE96C10776B1E0D /* OptimizationAppSwiftUI */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1CFC762A40BCE17E78716E60 /* Build configuration list for PBXNativeTarget "OptimizationAppSwiftUI" */;
+ buildPhases = (
+ 984B58F0ECFFF4FF405AE6ED /* Sources */,
+ FA9CA9A2BC78C03F0FC7EDA7 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = OptimizationAppSwiftUI;
+ packageProductDependencies = (
+ B622890DF93C3F71C32F81AB /* ContentfulOptimization */,
+ );
+ productName = OptimizationAppSwiftUI;
+ productReference = 2E855C4FA2D4C37DB188398F /* OptimizationAppSwiftUI.app */;
+ productType = "com.apple.product-type.application";
+ };
+ B4657F1B8073EF57509BFE17 /* OptimizationAppUITestsUIKit */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8933A7433CB5EA7F7C4D6F2B /* Build configuration list for PBXNativeTarget "OptimizationAppUITestsUIKit" */;
+ buildPhases = (
+ 9A2C5471223A0343488E58F9 /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 92C01A67BB4E74A29BE2B74D /* PBXTargetDependency */,
+ );
+ name = OptimizationAppUITestsUIKit;
+ packageProductDependencies = (
+ );
+ productName = OptimizationAppUITestsUIKit;
+ productReference = F599564CEC969B5B81DE8ABB /* OptimizationAppUITestsUIKit.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+ DB66258D797442B93A7B2200 /* OptimizationAppUIKit */ = {
isa = PBXNativeTarget;
- buildConfigurationList = C80000000000000000000002 /* Build configuration list for PBXNativeTarget "OptimizationApp" */;
+ buildConfigurationList = E38E5517F5FDDE0387B045D9 /* Build configuration list for PBXNativeTarget "OptimizationAppUIKit" */;
buildPhases = (
- C60000000000000000000001 /* Sources */,
- C60000000000000000000002 /* Frameworks */,
- C60000000000000000000003 /* Resources */,
+ D327DF8AC2ED29BA27D03A42 /* Sources */,
+ B50F9AC6418353EF14CD18F1 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
- name = OptimizationApp;
+ name = OptimizationAppUIKit;
packageProductDependencies = (
- CA0000000000000000000001 /* ContentfulOptimization */,
+ 244F9A9729F24EEBFDC1862F /* ContentfulOptimization */,
);
- productName = OptimizationApp;
- productReference = C20000000000000000000009 /* OptimizationApp.app */;
+ productName = OptimizationAppUIKit;
+ productReference = BB9EE54CEA502AA570FA0154 /* OptimizationAppUIKit.app */;
productType = "com.apple.product-type.application";
};
- D40000000000000000000001 /* OptimizationAppUITests */ = {
+ F579B74E29D9881CA6DA1E7B /* OptimizationAppUITestsSwiftUI */ = {
isa = PBXNativeTarget;
- buildConfigurationList = D90000000000000000000001 /* Build configuration list for PBXNativeTarget "OptimizationAppUITests" */;
+ buildConfigurationList = DEF00DFF3F1C19254F4E7B80 /* Build configuration list for PBXNativeTarget "OptimizationAppUITestsSwiftUI" */;
buildPhases = (
- D50000000000000000000001 /* Sources */,
- D50000000000000000000002 /* Frameworks */,
+ 7BE743DCE0A29F1C1400376A /* Sources */,
);
buildRules = (
);
dependencies = (
- D70000000000000000000001 /* PBXTargetDependency */,
+ 4DDD5D1DC143F1BBF64A2218 /* PBXTargetDependency */,
+ );
+ name = OptimizationAppUITestsSwiftUI;
+ packageProductDependencies = (
);
- name = OptimizationAppUITests;
- productName = OptimizationAppUITests;
- productReference = D2000000000000000000000C /* OptimizationAppUITests.xctest */;
+ productName = OptimizationAppUITestsSwiftUI;
+ productReference = 8FB2B5375330439D2F81AACF /* OptimizationAppUITestsSwiftUI.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
- C50000000000000000000001 /* Project object */ = {
+ 59C62CFF73857D71C2743362 /* Project object */ = {
isa = PBXProject;
attributes = {
- BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1540;
- LastUpgradeCheck = 1540;
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1430;
TargetAttributes = {
- C40000000000000000000001 = {
- CreatedOnToolsVersion = 15.4;
+ B4657F1B8073EF57509BFE17 = {
+ TestTargetID = DB66258D797442B93A7B2200;
};
- D40000000000000000000001 = {
- CreatedOnToolsVersion = 15.4;
- TestTargetID = C40000000000000000000001;
+ F579B74E29D9881CA6DA1E7B = {
+ TestTargetID = 6B18419FBDE96C10776B1E0D;
};
};
};
- buildConfigurationList = C80000000000000000000001 /* Build configuration list for PBXProject "OptimizationApp" */;
- compatibilityVersion = "Xcode 14.0";
+ buildConfigurationList = 72913D77CDEC36168BF84CD2 /* Build configuration list for PBXProject "OptimizationApp" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
- en,
Base,
+ en,
);
- mainGroup = C30000000000000000000001 /* Root */;
+ mainGroup = 603D5CDC672E61359F8B4927;
+ minimizedProjectReferenceProxies = 1;
packageReferences = (
- C90000000000000000000001 /* XCLocalSwiftPackageReference "ContentfulOptimization" */,
+ 435335CDD6AD4D4978CBCABF /* XCLocalSwiftPackageReference "../../packages/ios/ContentfulOptimization" */,
);
- productRefGroup = C30000000000000000000006 /* Products */;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 37FE6F709096D4C894E71E26 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
- C40000000000000000000001 /* OptimizationApp */,
- D40000000000000000000001 /* OptimizationAppUITests */,
+ 6B18419FBDE96C10776B1E0D /* OptimizationAppSwiftUI */,
+ DB66258D797442B93A7B2200 /* OptimizationAppUIKit */,
+ F579B74E29D9881CA6DA1E7B /* OptimizationAppUITestsSwiftUI */,
+ B4657F1B8073EF57509BFE17 /* OptimizationAppUITestsUIKit */,
);
};
/* End PBXProject section */
-/* Begin PBXResourcesBuildPhase section */
- C60000000000000000000003 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
+/* Begin PBXSourcesBuildPhase section */
+ 7BE743DCE0A29F1C1400376A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 1A3EC2767577C51E53AB19D2 /* AnalyticsTests.swift in Sources */,
+ ECBF1C0DC16E4532F4EEE49A /* ExtendedViewTrackingTests.swift in Sources */,
+ A375768DD5B98ECEC51B04C9 /* FlagViewTrackingTests.swift in Sources */,
+ 394C0BFFE6C5F4E2E1429CEF /* IdentifiedVariantsTests.swift in Sources */,
+ E2245ABC4EB91EF3E86E2794 /* LiveUpdatesTests.swift in Sources */,
+ 4067B88302C9720268628FF4 /* OfflineBehaviorTests.swift in Sources */,
+ 6EF2FA3FE304F740FC5C8922 /* PreviewPanelOverridesTests.swift in Sources */,
+ 4FEBB65BC56EFBD45D81C803 /* PreviewPanelTests.swift in Sources */,
+ 58116AD3ED6029892390DFCE /* ScreenTrackingTests.swift in Sources */,
+ 44AB322CF14E273E1B07B5E2 /* TapTrackingTests.swift in Sources */,
+ 3AE0D2F928ABA07AE77FB039 /* TestHelpers.swift in Sources */,
+ 24EF9DE5BDCB8CF94011F6E4 /* UnidentifiedVariantsTests.swift in Sources */,
+ 19E3767A1A9F73198E5BD235 /* XCTestExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- C60000000000000000000001 /* Sources */ = {
+ 984B58F0ECFFF4FF405AE6ED /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- C10000000000000000000001 /* App.swift in Sources */,
- C10000000000000000000002 /* Config.swift in Sources */,
- C10000000000000000000003 /* MainScreen.swift in Sources */,
- C10000000000000000000004 /* NavigationTestScreen.swift in Sources */,
- C10000000000000000000005 /* LiveUpdatesTestScreen.swift in Sources */,
- C10000000000000000000006 /* ContentEntryView.swift in Sources */,
- C10000000000000000000007 /* AnalyticsEventDisplay.swift in Sources */,
- C10000000000000000000008 /* ContentfulFetcher.swift in Sources */,
- C1000000000000000000000A /* NestedContentEntryView.swift in Sources */,
+ 68FDF01398E5C12FD8A37277 /* AnalyticsEventDisplay.swift in Sources */,
+ CE4104C9F52CE1A874B6D802 /* App.swift in Sources */,
+ 2D489A968E200B477EF8F8B9 /* Config.swift in Sources */,
+ 106F273577EF1A1FED81036E /* ContentEntryView.swift in Sources */,
+ 3EE8AB65755C4AAECE6F088E /* ContentfulFetcher.swift in Sources */,
+ 4447121F6CD440EAC27713E7 /* EventStore.swift in Sources */,
+ 93DDB4FCE61B74F35663301D /* LiveUpdatesTestScreen.swift in Sources */,
+ 8BE046C5FD5539712A40551C /* MainScreen.swift in Sources */,
+ 71EE02D789A2D2D922B4DC0A /* NavigationTestScreen.swift in Sources */,
+ F510376F8D6D3EFE7BB197E1 /* NestedContentEntryView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
- D50000000000000000000001 /* Sources */ = {
+ 9A2C5471223A0343488E58F9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- D10000000000000000000001 /* TestHelpers.swift in Sources */,
- D10000000000000000000002 /* XCTestExtensions.swift in Sources */,
- D10000000000000000000003 /* AnalyticsTests.swift in Sources */,
- D10000000000000000000004 /* TapTrackingTests.swift in Sources */,
- D10000000000000000000005 /* ExtendedViewTrackingTests.swift in Sources */,
- D10000000000000000000006 /* ScreenTrackingTests.swift in Sources */,
- D10000000000000000000007 /* LiveUpdatesTests.swift in Sources */,
- D10000000000000000000008 /* FlagViewTrackingTests.swift in Sources */,
- D10000000000000000000009 /* OfflineBehaviorTests.swift in Sources */,
- D1000000000000000000000A /* IdentifiedVariantsTests.swift in Sources */,
- D1000000000000000000000B /* UnidentifiedVariantsTests.swift in Sources */,
- D1000000000000000000000C /* PreviewPanelTests.swift in Sources */,
+ 137A73B22636AC4E7B2E0EF0 /* AnalyticsTests.swift in Sources */,
+ 6BEDE4E72E002CF5C3A8A9E1 /* ExtendedViewTrackingTests.swift in Sources */,
+ 61D00339E0A84FBA44A0AF62 /* FlagViewTrackingTests.swift in Sources */,
+ 5F679B3B001F53CD0A6BBC98 /* IdentifiedVariantsTests.swift in Sources */,
+ 471F733790E71327EAA41CA8 /* LiveUpdatesTests.swift in Sources */,
+ B1A5A931BBDFC3438E935012 /* OfflineBehaviorTests.swift in Sources */,
+ 9014D5B10826A433A7412BAC /* PreviewPanelOverridesTests.swift in Sources */,
+ EFEFDCD73D2FE1854045F5DB /* PreviewPanelTests.swift in Sources */,
+ 9E261E1E35994E3D7A652291 /* ScreenTrackingTests.swift in Sources */,
+ E3C31DA281B4A6D1DF55CE29 /* TapTrackingTests.swift in Sources */,
+ FE0F74AA3E04EB42454F14D1 /* TestHelpers.swift in Sources */,
+ 5C1226FB8DC19DE4D3783F11 /* UnidentifiedVariantsTests.swift in Sources */,
+ 27E21567FB96A9E2EC57EE81 /* XCTestExtensions.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ D327DF8AC2ED29BA27D03A42 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 437A5DD2CAFE5E72795431F2 /* AnalyticsEventDisplayView.swift in Sources */,
+ 8A29BA19D8A78BC9DB688B37 /* AppDelegate.swift in Sources */,
+ C23B6242DA30D283D76C1B3E /* Config.swift in Sources */,
+ 244AA9A414B83F37FAE72F42 /* ContentEntryUIView.swift in Sources */,
+ C04EF55982CA837E62CC0669 /* ContentfulFetcher.swift in Sources */,
+ 73A7EECB26977730B7241077 /* EventStore.swift in Sources */,
+ D28A6D01EBA9877DD9174BCB /* LiveUpdatesTestViewController.swift in Sources */,
+ 91A5DFA622284D666F20D46E /* MainViewController.swift in Sources */,
+ 7CE0E6C067281C01D8F0A85F /* NavigationTestViewController.swift in Sources */,
+ D93DA9530B2B7E95815DB931 /* NestedContentEntryUIView.swift in Sources */,
+ 072516CD147CCCA5D7F0BE53 /* OptimizedEntryUIView.swift in Sources */,
+ 68EBA430F06D7974D323C432 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- D70000000000000000000001 /* PBXTargetDependency */ = {
+ 4DDD5D1DC143F1BBF64A2218 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6B18419FBDE96C10776B1E0D /* OptimizationAppSwiftUI */;
+ targetProxy = 92B3C749C74E19DFFA0A2D85 /* PBXContainerItemProxy */;
+ };
+ 92C01A67BB4E74A29BE2B74D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = C40000000000000000000001 /* OptimizationApp */;
- targetProxy = D60000000000000000000001 /* PBXContainerItemProxy */;
+ target = DB66258D797442B93A7B2200 /* OptimizationAppUIKit */;
+ targetProxy = 601E6E8C910B4E4944F371F6 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
- C70000000000000000000001 /* Debug */ = {
+ 01009AC7DFCF4E14AF326E67 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ INFOPLIST_FILE = uikit/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uikit;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 1ABFED42A32FF789BC82BE70 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ GENERATE_INFOPLIST_FILE = YES;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uikit.uitests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = OptimizationAppUIKit;
+ };
+ name = Debug;
+ };
+ 49FCB878B0AA288E93F36F4F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ GENERATE_INFOPLIST_FILE = YES;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uikit.uitests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = OptimizationAppUIKit;
+ };
+ name = Release;
+ };
+ 54066338503A45E85EC3CEEF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -353,43 +571,72 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
- GCC_DYNAMIC_NO_PIC = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ A37AC39518AD28F27868F099 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ INFOPLIST_FILE = swiftui/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.swiftui;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
- C70000000000000000000002 /* Release */ = {
+ D56BEAAF8578D689784C1D5F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ GENERATE_INFOPLIST_FILE = YES;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.swiftui.uitests;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = OptimizationAppSwiftUI;
+ };
+ name = Release;
+ };
+ E16D0A2E23C004D64A485EBD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -416,163 +663,154 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "DEBUG=1",
+ );
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- C70000000000000000000003 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = OptimizationApp/Info.plist;
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app;
+ ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_EMIT_LOC_STRINGS = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
- C70000000000000000000004 /* Release */ = {
+ E7579473E9FBF8F464AE0DF6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- ENABLE_TESTABILITY = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = OptimizationApp/Info.plist;
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ INFOPLIST_FILE = swiftui/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.swiftui;
+ SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
- D80000000000000000000001 /* Debug */ = {
+ EB06D6BBAB4CBE4AEBED542A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ BUNDLE_LOADER = "$(TEST_HOST)";
GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uitests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.swiftui.uitests;
+ SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = OptimizationApp;
+ TEST_TARGET_NAME = OptimizationAppSwiftUI;
};
name = Debug;
};
- D80000000000000000000002 /* Release */ = {
+ F4FB9B6F0AD58490CFC29ABD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uitests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ INFOPLIST_FILE = uikit/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.optimization.app.uikit;
+ SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = OptimizationApp;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- C80000000000000000000001 /* Build configuration list for PBXProject "OptimizationApp" */ = {
+ 1CFC762A40BCE17E78716E60 /* Build configuration list for PBXNativeTarget "OptimizationAppSwiftUI" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A37AC39518AD28F27868F099 /* Debug */,
+ E7579473E9FBF8F464AE0DF6 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ 72913D77CDEC36168BF84CD2 /* Build configuration list for PBXProject "OptimizationApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- C70000000000000000000001 /* Debug */,
- C70000000000000000000002 /* Release */,
+ E16D0A2E23C004D64A485EBD /* Debug */,
+ 54066338503A45E85EC3CEEF /* Release */,
);
defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
+ defaultConfigurationName = Debug;
};
- C80000000000000000000002 /* Build configuration list for PBXNativeTarget "OptimizationApp" */ = {
+ 8933A7433CB5EA7F7C4D6F2B /* Build configuration list for PBXNativeTarget "OptimizationAppUITestsUIKit" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- C70000000000000000000003 /* Debug */,
- C70000000000000000000004 /* Release */,
+ 1ABFED42A32FF789BC82BE70 /* Debug */,
+ 49FCB878B0AA288E93F36F4F /* Release */,
);
defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
+ defaultConfigurationName = Debug;
};
- D90000000000000000000001 /* Build configuration list for PBXNativeTarget "OptimizationAppUITests" */ = {
+ DEF00DFF3F1C19254F4E7B80 /* Build configuration list for PBXNativeTarget "OptimizationAppUITestsSwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- D80000000000000000000001 /* Debug */,
- D80000000000000000000002 /* Release */,
+ EB06D6BBAB4CBE4AEBED542A /* Debug */,
+ D56BEAAF8578D689784C1D5F /* Release */,
);
defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
+ defaultConfigurationName = Debug;
+ };
+ E38E5517F5FDDE0387B045D9 /* Build configuration list for PBXNativeTarget "OptimizationAppUIKit" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 01009AC7DFCF4E14AF326E67 /* Debug */,
+ F4FB9B6F0AD58490CFC29ABD /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- C90000000000000000000001 /* XCLocalSwiftPackageReference "ContentfulOptimization" */ = {
+ 435335CDD6AD4D4978CBCABF /* XCLocalSwiftPackageReference "../../packages/ios/ContentfulOptimization" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../packages/ios/ContentfulOptimization;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- CA0000000000000000000001 /* ContentfulOptimization */ = {
+ 244F9A9729F24EEBFDC1862F /* ContentfulOptimization */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = ContentfulOptimization;
+ };
+ B622890DF93C3F71C32F81AB /* ContentfulOptimization */ = {
isa = XCSwiftPackageProductDependency;
productName = ContentfulOptimization;
};
/* End XCSwiftPackageProductDependency section */
-
};
- rootObject = C50000000000000000000001 /* Project object */;
+ rootObject = 59C62CFF73857D71C2743362 /* Project object */;
}
diff --git a/implementations/ios-sdk/OptimizationApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/implementations/ios-sdk/OptimizationApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/implementations/ios-sdk/OptimizationApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppSwiftUI.xcscheme b/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppSwiftUI.xcscheme
new file mode 100644
index 00000000..09a2f805
--- /dev/null
+++ b/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppSwiftUI.xcscheme
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppUIKit.xcscheme b/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppUIKit.xcscheme
new file mode 100644
index 00000000..793c0607
--- /dev/null
+++ b/implementations/ios-sdk/OptimizationApp.xcodeproj/xcshareddata/xcschemes/OptimizationAppUIKit.xcscheme
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/implementations/ios-sdk/OptimizationApp/Info.plist b/implementations/ios-sdk/OptimizationApp/Info.plist
deleted file mode 100644
index 6a6654d9..00000000
--- a/implementations/ios-sdk/OptimizationApp/Info.plist
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
-
-
diff --git a/implementations/ios-sdk/README.md b/implementations/ios-sdk/README.md
index 2a4e8fff..f04c7817 100644
--- a/implementations/ios-sdk/README.md
+++ b/implementations/ios-sdk/README.md
@@ -4,79 +4,62 @@
-Contentful Personalization & Analytics
+Reference app for the iOS Optimization SDK. Two app shells that exercise the SDK against the mock
+server in `lib/mocks/`:
-iOS Reference App
-
-
-
-[Readme](./README.md) ·
-[Guides](https://contentful.github.io/optimization/documents/Documentation.Guides.html) ·
-[Reference](https://contentful.github.io/optimization) · [Contributing](../../CONTRIBUTING.md)
-
-
+- **`OptimizationAppSwiftUI`** — SwiftUI shell, sources under `swiftui/`.
+- **`OptimizationAppUIKit`** — UIKit shell, sources under `uikit/`.
-> [!WARNING]
->
-> The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
+Both apps share `shared/` (config, Contentful fetcher, analytics event store) and run the **same**
+UI test bundle from `uitests/` against their respective host app. Two host bundles, one test source
+tree — the only way to prove SDK behavior is identical across UI frameworks.
-Reference app for current native iOS bridge and preview-panel validation work. This app exercises
-the local iOS implementation surface against the mock server in `lib/mocks/` and hosts the XCUITest
-suite.
+## Project generation
-> [!NOTE]
->
-> This is not a published iOS SDK package. The public native iOS SDK is still planned; see the
-> package placeholder at [`packages/ios`](../../packages/ios/README.md).
+The Xcode project is generated by [xcodegen](https://github.com/yonaskolb/XcodeGen) from
+`project.yml`. Whenever you add, rename, or move a source file, regenerate:
-## What This Demonstrates
-
-Use this app when you need to validate current native iOS bridge and preview-panel behavior against
-the shared mock API. The XCUITest suite mirrors selected cross-platform preview-panel scenarios so
-iOS behavior can be compared with React Native E2E coverage.
-
-## Prerequisites
-
-- Xcode with an iOS Simulator available.
-- pnpm workspace dependencies installed from the monorepo root.
-- The mock server running at `http://localhost:8000`.
+```sh
+brew install xcodegen # one-time
+xcodegen generate
+```
-## Setup
+iOS Reference App
-From the monorepo root, start the mock API server before running UI tests:
+Tests live under `uitests/Tests/` and assume the mock server is running at `http://localhost:8000`:
```sh
pnpm serve:mocks
```
-## Running Tests
-
-All UI tests live under `OptimizationAppUITests/Tests/`.
-
-Run the full suite locally:
+Run the full suite against both shells:
```sh
xcodebuild test \
-project OptimizationApp.xcodeproj \
- -scheme OptimizationApp \
+ -scheme OptimizationAppSwiftUI \
+ -destination 'platform=iOS Simulator,name=iPhone 16'
+
+xcodebuild test \
+ -project OptimizationApp.xcodeproj \
+ -scheme OptimizationAppUIKit \
-destination 'platform=iOS Simulator,name=iPhone 16'
```
-Run only the preview-panel override suite (recommended during development of that feature):
+Run a single test class against the SwiftUI shell:
```sh
xcodebuild test \
-project OptimizationApp.xcodeproj \
- -scheme OptimizationApp \
+ -scheme OptimizationAppSwiftUI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
- -only-testing:OptimizationAppUITests/PreviewPanelOverridesTests
+ -only-testing:OptimizationAppUITestsSwiftUI/PreviewPanelOverridesTests
```
### Adding New Test Files
-XCUITest source files must be added to the `OptimizationAppUITests` target in Xcode before they're
-compiled. Adding a `.swift` file to `OptimizationAppUITests/Tests/` on disk is **not** enough; open
-the project in Xcode and confirm the new file's Target Membership includes `OptimizationAppUITests`.
+Drop the `.swift` into `uitests/Tests/` and run `xcodegen generate`. Both UI test bundles pick it up
+automatically — no Target Membership clicking in Xcode.
## Preview Panel Tests
diff --git a/implementations/ios-sdk/project.yml b/implementations/ios-sdk/project.yml
new file mode 100644
index 00000000..fb6fc8ae
--- /dev/null
+++ b/implementations/ios-sdk/project.yml
@@ -0,0 +1,70 @@
+name: OptimizationApp
+options:
+ bundleIdPrefix: com.contentful.optimization.app
+ deploymentTarget:
+ iOS: '16.0'
+ groupSortPosition: top
+
+packages:
+ ContentfulOptimization:
+ path: ../../packages/ios/ContentfulOptimization
+
+settings:
+ base:
+ SWIFT_VERSION: '5.0'
+ TARGETED_DEVICE_FAMILY: '1,2'
+ GENERATE_INFOPLIST_FILE: NO
+ ENABLE_USER_SCRIPT_SANDBOXING: YES
+
+targets:
+ OptimizationAppSwiftUI:
+ type: application
+ platform: iOS
+ sources:
+ - path: shared
+ excludes: ['Info.plist']
+ - path: swiftui
+ excludes: ['Info.plist']
+ settings:
+ PRODUCT_BUNDLE_IDENTIFIER: com.contentful.optimization.app.swiftui
+ INFOPLIST_FILE: swiftui/Info.plist
+ dependencies:
+ - package: ContentfulOptimization
+ scheme:
+ testTargets: [OptimizationAppUITestsSwiftUI]
+
+ OptimizationAppUIKit:
+ type: application
+ platform: iOS
+ sources:
+ - path: shared
+ excludes: ['Info.plist']
+ - path: uikit
+ excludes: ['Info.plist']
+ settings:
+ PRODUCT_BUNDLE_IDENTIFIER: com.contentful.optimization.app.uikit
+ INFOPLIST_FILE: uikit/Info.plist
+ dependencies:
+ - package: ContentfulOptimization
+ scheme:
+ testTargets: [OptimizationAppUITestsUIKit]
+
+ OptimizationAppUITestsSwiftUI:
+ type: bundle.ui-testing
+ platform: iOS
+ sources: [uitests]
+ settings:
+ PRODUCT_BUNDLE_IDENTIFIER: com.contentful.optimization.app.swiftui.uitests
+ GENERATE_INFOPLIST_FILE: YES
+ dependencies:
+ - target: OptimizationAppSwiftUI
+
+ OptimizationAppUITestsUIKit:
+ type: bundle.ui-testing
+ platform: iOS
+ sources: [uitests]
+ settings:
+ PRODUCT_BUNDLE_IDENTIFIER: com.contentful.optimization.app.uikit.uitests
+ GENERATE_INFOPLIST_FILE: YES
+ dependencies:
+ - target: OptimizationAppUIKit
diff --git a/implementations/ios-sdk/OptimizationApp/Config.swift b/implementations/ios-sdk/shared/Config.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Config.swift
rename to implementations/ios-sdk/shared/Config.swift
diff --git a/implementations/ios-sdk/OptimizationApp/Utils/ContentfulFetcher.swift b/implementations/ios-sdk/shared/ContentfulFetcher.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Utils/ContentfulFetcher.swift
rename to implementations/ios-sdk/shared/ContentfulFetcher.swift
diff --git a/implementations/ios-sdk/shared/EventStore.swift b/implementations/ios-sdk/shared/EventStore.swift
new file mode 100644
index 00000000..7c7e5395
--- /dev/null
+++ b/implementations/ios-sdk/shared/EventStore.swift
@@ -0,0 +1,57 @@
+import Combine
+import Foundation
+
+@MainActor
+final class EventStore: ObservableObject {
+ static let shared = EventStore()
+
+ struct AnalyticsEvent {
+ let type: String
+ let componentId: String?
+ let viewDurationMs: Int?
+ let viewId: String?
+ let timestamp: Date
+ }
+
+ struct ComponentStats {
+ var count: Int
+ var latestViewDurationMs: Int?
+ var latestViewId: String?
+ }
+
+ @Published private(set) var events: [AnalyticsEvent] = []
+ @Published private(set) var componentStats: [String: ComponentStats] = [:]
+
+ private var cancellable: AnyCancellable?
+
+ private init() {}
+
+ func subscribe(to publisher: AnyPublisher<[String: Any], Never>) {
+ cancellable?.cancel()
+ cancellable = publisher.sink { [weak self] dict in
+ self?.processEvent(dict)
+ }
+ }
+
+ private func processEvent(_ dict: [String: Any]) {
+ guard let type = dict["type"] as? String else { return }
+
+ let event = AnalyticsEvent(
+ type: type,
+ componentId: dict["componentId"] as? String,
+ viewDurationMs: dict["viewDurationMs"] as? Int,
+ viewId: dict["viewId"] as? String,
+ timestamp: Date()
+ )
+
+ events.insert(event, at: 0)
+
+ if type == "component", let cid = event.componentId {
+ var stats = componentStats[cid] ?? ComponentStats(count: 0)
+ stats.count += 1
+ if let ms = event.viewDurationMs { stats.latestViewDurationMs = ms }
+ if let vid = event.viewId { stats.latestViewId = vid }
+ componentStats[cid] = stats
+ }
+ }
+}
diff --git a/implementations/ios-sdk/OptimizationApp/App.swift b/implementations/ios-sdk/swiftui/App.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/App.swift
rename to implementations/ios-sdk/swiftui/App.swift
diff --git a/implementations/ios-sdk/OptimizationApp/Components/AnalyticsEventDisplay.swift b/implementations/ios-sdk/swiftui/Components/AnalyticsEventDisplay.swift
similarity index 63%
rename from implementations/ios-sdk/OptimizationApp/Components/AnalyticsEventDisplay.swift
rename to implementations/ios-sdk/swiftui/Components/AnalyticsEventDisplay.swift
index e166ba1f..fffec69e 100644
--- a/implementations/ios-sdk/OptimizationApp/Components/AnalyticsEventDisplay.swift
+++ b/implementations/ios-sdk/swiftui/Components/AnalyticsEventDisplay.swift
@@ -2,65 +2,6 @@ import Combine
import ContentfulOptimization
import SwiftUI
-// MARK: - Persisted Event Store (module-level, survives view lifecycle)
-
-@MainActor
-final class EventStore: ObservableObject {
- static let shared = EventStore()
-
- struct AnalyticsEvent {
- let type: String
- let componentId: String?
- let viewDurationMs: Int?
- let viewId: String?
- let timestamp: Date
- }
-
- struct ComponentStats {
- var count: Int
- var latestViewDurationMs: Int?
- var latestViewId: String?
- }
-
- @Published private(set) var events: [AnalyticsEvent] = []
- @Published private(set) var componentStats: [String: ComponentStats] = [:]
-
- private var cancellable: AnyCancellable?
-
- private init() {}
-
- func subscribe(to publisher: AnyPublisher<[String: Any], Never>) {
- cancellable?.cancel()
- cancellable = publisher.sink { [weak self] dict in
- self?.processEvent(dict)
- }
- }
-
- private func processEvent(_ dict: [String: Any]) {
- guard let type = dict["type"] as? String else { return }
-
- let event = AnalyticsEvent(
- type: type,
- componentId: dict["componentId"] as? String,
- viewDurationMs: dict["viewDurationMs"] as? Int,
- viewId: dict["viewId"] as? String,
- timestamp: Date()
- )
-
- events.insert(event, at: 0)
-
- if type == "component", let cid = event.componentId {
- var stats = componentStats[cid] ?? ComponentStats(count: 0)
- stats.count += 1
- if let ms = event.viewDurationMs { stats.latestViewDurationMs = ms }
- if let vid = event.viewId { stats.latestViewId = vid }
- componentStats[cid] = stats
- }
- }
-}
-
-// MARK: - Analytics Event Display View
-
struct AnalyticsEventDisplay: View {
@EnvironmentObject private var client: OptimizationClient
@ObservedObject private var store = EventStore.shared
diff --git a/implementations/ios-sdk/OptimizationApp/Components/ContentEntryView.swift b/implementations/ios-sdk/swiftui/Components/ContentEntryView.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Components/ContentEntryView.swift
rename to implementations/ios-sdk/swiftui/Components/ContentEntryView.swift
diff --git a/implementations/ios-sdk/OptimizationApp/Components/NestedContentEntryView.swift b/implementations/ios-sdk/swiftui/Components/NestedContentEntryView.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Components/NestedContentEntryView.swift
rename to implementations/ios-sdk/swiftui/Components/NestedContentEntryView.swift
diff --git a/implementations/ios-sdk/swiftui/Info.plist b/implementations/ios-sdk/swiftui/Info.plist
new file mode 100644
index 00000000..c939b915
--- /dev/null
+++ b/implementations/ios-sdk/swiftui/Info.plist
@@ -0,0 +1,42 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchScreen
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/implementations/ios-sdk/OptimizationApp/Screens/LiveUpdatesTestScreen.swift b/implementations/ios-sdk/swiftui/Screens/LiveUpdatesTestScreen.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Screens/LiveUpdatesTestScreen.swift
rename to implementations/ios-sdk/swiftui/Screens/LiveUpdatesTestScreen.swift
diff --git a/implementations/ios-sdk/OptimizationApp/Screens/MainScreen.swift b/implementations/ios-sdk/swiftui/Screens/MainScreen.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Screens/MainScreen.swift
rename to implementations/ios-sdk/swiftui/Screens/MainScreen.swift
diff --git a/implementations/ios-sdk/OptimizationApp/Screens/NavigationTestScreen.swift b/implementations/ios-sdk/swiftui/Screens/NavigationTestScreen.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationApp/Screens/NavigationTestScreen.swift
rename to implementations/ios-sdk/swiftui/Screens/NavigationTestScreen.swift
diff --git a/implementations/ios-sdk/uikit/AppDelegate.swift b/implementations/ios-sdk/uikit/AppDelegate.swift
new file mode 100644
index 00000000..d55a6412
--- /dev/null
+++ b/implementations/ios-sdk/uikit/AppDelegate.swift
@@ -0,0 +1,27 @@
+import UIKit
+
+@main
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ // UI tests launch with `--reset` to guarantee an unidentified-visitor
+ // starting state. The SDK persists `anonymousId`/profile in its own
+ // UserDefaults suite, and those outlive `terminate()` + `launch()` on
+ // the simulator — so an explicit wipe is the only reliable reset.
+ if ProcessInfo.processInfo.arguments.contains("--reset") {
+ UserDefaults.standard.removePersistentDomain(forName: "com.contentful.optimization")
+ }
+ return true
+ }
+
+ func application(
+ _ application: UIApplication,
+ configurationForConnecting connectingSceneSession: UISceneSession,
+ options: UIScene.ConnectionOptions
+ ) -> UISceneConfiguration {
+ UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ }
+}
diff --git a/implementations/ios-sdk/uikit/Components/AnalyticsEventDisplayView.swift b/implementations/ios-sdk/uikit/Components/AnalyticsEventDisplayView.swift
new file mode 100644
index 00000000..19e4a2fd
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Components/AnalyticsEventDisplayView.swift
@@ -0,0 +1,127 @@
+import Combine
+import UIKit
+
+final class AnalyticsEventDisplayView: UIView {
+
+ private let stack = UIStackView()
+ private let titleLabel = UILabel()
+ private let countLabel = UILabel()
+ private var cancellables = Set()
+
+ init() {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+ configure()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ private func configure() {
+ accessibilityIdentifier = "analytics-events-container"
+ isAccessibilityElement = false
+
+ titleLabel.text = "Analytics Events"
+ titleLabel.font = .preferredFont(forTextStyle: .headline)
+
+ countLabel.accessibilityIdentifier = "events-count"
+ countLabel.numberOfLines = 0
+
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 8
+ stack.isLayoutMarginsRelativeArrangement = true
+ stack.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: topAnchor),
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+ }
+
+ func bind(to store: EventStore) {
+ cancellables.removeAll()
+ Publishers.CombineLatest(store.$events, store.$componentStats)
+ .sink { [weak self] events, stats in
+ self?.render(events: events, stats: stats)
+ }
+ .store(in: &cancellables)
+ }
+
+ private func render(events: [EventStore.AnalyticsEvent], stats: [String: EventStore.ComponentStats]) {
+ for view in stack.arrangedSubviews {
+ stack.removeArrangedSubview(view)
+ view.removeFromSuperview()
+ }
+
+ stack.addArrangedSubview(titleLabel)
+
+ countLabel.text = "Events: \(events.count)"
+ countLabel.accessibilityLabel = "Events: \(events.count)"
+ stack.addArrangedSubview(countLabel)
+
+ if events.isEmpty {
+ let none = makeLabel(
+ text: "No events tracked yet",
+ identifier: "no-events-message"
+ )
+ stack.addArrangedSubview(none)
+ return
+ }
+
+ let nonComponent = events.filter { $0.type != "component" }
+ for (index, event) in nonComponent.enumerated() {
+ let testId = event.componentId.map { "event-\(event.type)-\($0)" }
+ ?? "event-\(event.type)-\(index)"
+ let desc = describe(event)
+ stack.addArrangedSubview(makeLabel(text: desc, identifier: testId))
+ }
+
+ for cid in stats.keys.sorted() {
+ guard let s = stats[cid] else { continue }
+ let container = UIView()
+ container.translatesAutoresizingMaskIntoConstraints = false
+ container.accessibilityIdentifier = "component-stats-\(cid)"
+
+ let inner = UIStackView()
+ inner.axis = .vertical
+ inner.alignment = .leading
+ inner.spacing = 2
+ inner.translatesAutoresizingMaskIntoConstraints = false
+ container.addSubview(inner)
+ NSLayoutConstraint.activate([
+ inner.topAnchor.constraint(equalTo: container.topAnchor),
+ inner.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+ inner.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+ inner.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+ ])
+
+ inner.addArrangedSubview(makeLabel(text: "Count: \(s.count)", identifier: "event-count-\(cid)"))
+ let durationText = s.latestViewDurationMs.map { "\($0)" } ?? "N/A"
+ inner.addArrangedSubview(makeLabel(text: "Duration: \(durationText)", identifier: "event-duration-\(cid)"))
+ let viewIdText = s.latestViewId ?? "N/A"
+ inner.addArrangedSubview(makeLabel(text: "ViewId: \(viewIdText)", identifier: "event-view-id-\(cid)"))
+
+ stack.addArrangedSubview(container)
+ }
+ }
+
+ private func makeLabel(text: String, identifier: String) -> UILabel {
+ let label = UILabel()
+ label.text = text
+ label.accessibilityLabel = text
+ label.accessibilityIdentifier = identifier
+ label.numberOfLines = 0
+ return label
+ }
+
+ private func describe(_ event: EventStore.AnalyticsEvent) -> String {
+ var desc = event.type
+ if let cid = event.componentId { desc += " - Component: \(cid)" }
+ if let ms = event.viewDurationMs { desc += " - \(ms)ms" }
+ return desc
+ }
+}
diff --git a/implementations/ios-sdk/uikit/Components/ContentEntryUIView.swift b/implementations/ios-sdk/uikit/Components/ContentEntryUIView.swift
new file mode 100644
index 00000000..b0284831
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Components/ContentEntryUIView.swift
@@ -0,0 +1,77 @@
+import ContentfulOptimization
+import UIKit
+
+final class ContentEntryUIView: UIView {
+
+ init(client: OptimizationClient, entry: [String: Any], scrollView: UIScrollView?) {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+
+ let entryId = entryId(for: entry)
+ let optimized = OptimizedEntryUIView(
+ client: client,
+ entry: entry,
+ scrollView: scrollView,
+ trackTaps: true,
+ accessibilityIdentifier: "content-entry-\(entryId)"
+ ) { resolved in
+ EntryContentView(entry: resolved, entryId: entryId)
+ }
+ addSubview(optimized)
+ NSLayoutConstraint.activate([
+ optimized.topAnchor.constraint(equalTo: topAnchor),
+ optimized.leadingAnchor.constraint(equalTo: leadingAnchor),
+ optimized.trailingAnchor.constraint(equalTo: trailingAnchor),
+ optimized.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+}
+
+private final class EntryContentView: UIView {
+
+ init(entry: [String: Any], entryId: String) {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+
+ let fields = entry["fields"] as? [String: Any]
+ let text = (fields?["text"] as? String) ?? "No content"
+
+ let textLabel = UILabel()
+ textLabel.text = text
+ textLabel.numberOfLines = 0
+
+ let idLabel = UILabel()
+ idLabel.text = "[Entry: \(entryId)]"
+ idLabel.font = .preferredFont(forTextStyle: .footnote)
+
+ let stack = UIStackView(arrangedSubviews: [textLabel, idLabel])
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 4
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ stack.isLayoutMarginsRelativeArrangement = true
+ stack.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
+ addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: topAnchor),
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ isAccessibilityElement = true
+ accessibilityLabel = "\(text) [Entry: \(entryId)]"
+ accessibilityIdentifier = "entry-text-\(entryId)"
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+}
+
+private func entryId(for entry: [String: Any]) -> String {
+ let sys = entry["sys"] as? [String: Any]
+ return (sys?["id"] as? String) ?? ""
+}
diff --git a/implementations/ios-sdk/uikit/Components/NestedContentEntryUIView.swift b/implementations/ios-sdk/uikit/Components/NestedContentEntryUIView.swift
new file mode 100644
index 00000000..125de720
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Components/NestedContentEntryUIView.swift
@@ -0,0 +1,91 @@
+import ContentfulOptimization
+import UIKit
+
+final class NestedContentEntryUIView: UIView {
+
+ init(client: OptimizationClient, entry: [String: Any], scrollView: UIScrollView?) {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+
+ let entryId = (entry["sys"] as? [String: Any])?["id"] as? String ?? ""
+
+ let stack = UIStackView()
+ stack.axis = .vertical
+ stack.alignment = .fill
+ stack.spacing = 0
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: topAnchor),
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ let optimized = OptimizedEntryUIView(
+ client: client,
+ entry: entry,
+ scrollView: scrollView,
+ accessibilityIdentifier: "content-entry-\(entryId)"
+ ) { resolved in
+ NestedEntryText(entry: resolved)
+ }
+ stack.addArrangedSubview(optimized)
+
+ for child in nestedEntries(in: entry) {
+ stack.addArrangedSubview(NestedContentEntryUIView(client: client, entry: child, scrollView: scrollView))
+ }
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ private func nestedEntries(in entry: [String: Any]) -> [[String: Any]] {
+ let fields = entry["fields"] as? [String: Any]
+ guard let nested = fields?["nested"] as? [Any] else { return [] }
+ return nested.compactMap { $0 as? [String: Any] }.filter { item in
+ (item["sys"] as? [String: Any])?["id"] != nil
+ }
+ }
+}
+
+private final class NestedEntryText: UIView {
+
+ init(entry: [String: Any]) {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+
+ let entryId = (entry["sys"] as? [String: Any])?["id"] as? String ?? ""
+ let fields = entry["fields"] as? [String: Any]
+ let text = (fields?["text"] as? String) ?? "No content"
+
+ let textLabel = UILabel()
+ textLabel.text = text
+ textLabel.numberOfLines = 0
+
+ let idLabel = UILabel()
+ idLabel.text = "[Entry: \(entryId)]"
+ idLabel.font = .preferredFont(forTextStyle: .footnote)
+
+ let stack = UIStackView(arrangedSubviews: [textLabel, idLabel])
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 4
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ stack.isLayoutMarginsRelativeArrangement = true
+ stack.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
+ addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: topAnchor),
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ isAccessibilityElement = true
+ accessibilityLabel = "\(text) [Entry: \(entryId)]"
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+}
diff --git a/implementations/ios-sdk/uikit/Components/OptimizedEntryUIView.swift b/implementations/ios-sdk/uikit/Components/OptimizedEntryUIView.swift
new file mode 100644
index 00000000..2cf08376
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Components/OptimizedEntryUIView.swift
@@ -0,0 +1,247 @@
+import Combine
+import ContentfulOptimization
+import UIKit
+
+final class OptimizedEntryUIView: UIView {
+
+ private let client: OptimizationClient
+ private let entry: [String: Any]
+ private let liveUpdates: Bool?
+ private let globalLiveUpdates: Bool
+ private let trackTaps: Bool
+ private let trackViews: Bool
+ private let contentBuilder: (_ resolved: [String: Any]) -> UIView
+
+ private weak var scrollView: UIScrollView?
+ private var trackingController: ViewTrackingController?
+ private var lockedPersonalizations: [[String: Any]]?
+ private var hasLocked = false
+ private var resolvedEntry: [String: Any]
+ private var resolvedPersonalization: [String: Any]?
+ private var contentView: UIView?
+ private var cancellables = Set()
+ private var contentOffsetObservation: NSKeyValueObservation?
+ private var boundsObservation: NSKeyValueObservation?
+
+ init(
+ client: OptimizationClient,
+ entry: [String: Any],
+ scrollView: UIScrollView?,
+ liveUpdates: Bool? = nil,
+ globalLiveUpdates: Bool = false,
+ trackTaps: Bool = false,
+ trackViews: Bool = true,
+ accessibilityIdentifier: String? = nil,
+ contentBuilder: @escaping (_ resolved: [String: Any]) -> UIView
+ ) {
+ self.client = client
+ self.entry = entry
+ self.scrollView = scrollView
+ self.liveUpdates = liveUpdates
+ self.globalLiveUpdates = globalLiveUpdates
+ self.trackTaps = trackTaps
+ self.trackViews = trackViews
+ self.contentBuilder = contentBuilder
+ self.resolvedEntry = entry
+ super.init(frame: .zero)
+ self.accessibilityIdentifier = accessibilityIdentifier
+ self.isAccessibilityElement = false
+ self.translatesAutoresizingMaskIntoConstraints = false
+
+ rebuildContent()
+
+ if isPersonalized {
+ subscribeToPersonalizations()
+ subscribeToPreviewPanel()
+ }
+
+ if trackViews {
+ installTracking()
+ }
+ if trackTaps {
+ installTapGesture()
+ }
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ // Surface the inner content's accessibility label so XCUI tests querying
+ // `otherElements[].label` see the resolved entry text.
+ override var accessibilityLabel: String? {
+ get { contentView?.accessibilityLabel ?? super.accessibilityLabel }
+ set { super.accessibilityLabel = newValue }
+ }
+
+ deinit {
+ contentOffsetObservation?.invalidate()
+ boundsObservation?.invalidate()
+ Task { @MainActor [trackingController] in
+ trackingController?.onDisappear()
+ }
+ }
+
+ // MARK: - Personalization
+
+ private var isPersonalized: Bool {
+ guard let fields = entry["fields"] as? [String: Any] else { return false }
+ return fields["nt_experiences"] != nil
+ }
+
+ private var shouldLiveUpdate: Bool {
+ if let explicit = liveUpdates { return explicit }
+ return globalLiveUpdates || client.isPreviewPanelOpen
+ }
+
+ private var effectivePersonalizations: [[String: Any]]? {
+ shouldLiveUpdate ? client.selectedPersonalizations : lockedPersonalizations
+ }
+
+ private func resolve() {
+ if isPersonalized {
+ let result = client.personalizeEntry(
+ baseline: entry,
+ personalizations: effectivePersonalizations
+ )
+ resolvedEntry = result.entry
+ resolvedPersonalization = result.personalization
+ } else {
+ resolvedEntry = entry
+ resolvedPersonalization = nil
+ }
+ }
+
+ private func subscribeToPersonalizations() {
+ client.$selectedPersonalizations
+ .sink { [weak self] _ in
+ guard let self else { return }
+ if self.shouldLiveUpdate {
+ self.rebuildContent()
+ } else if !self.hasLocked, let snapshot = self.client.selectedPersonalizations {
+ self.lockedPersonalizations = snapshot
+ self.hasLocked = true
+ self.rebuildContent()
+ }
+ }
+ .store(in: &cancellables)
+ }
+
+ private func subscribeToPreviewPanel() {
+ client.$isPreviewPanelOpen
+ .dropFirst()
+ .sink { [weak self] open in
+ guard let self else { return }
+ if !open, self.hasLocked {
+ self.lockedPersonalizations = self.client.selectedPersonalizations
+ }
+ self.rebuildContent()
+ }
+ .store(in: &cancellables)
+ }
+
+ private func rebuildContent() {
+ resolve()
+ let new = contentBuilder(resolvedEntry)
+ new.translatesAutoresizingMaskIntoConstraints = false
+ if let old = contentView {
+ old.removeFromSuperview()
+ }
+ addSubview(new)
+ NSLayoutConstraint.activate([
+ new.topAnchor.constraint(equalTo: topAnchor),
+ new.leadingAnchor.constraint(equalTo: leadingAnchor),
+ new.trailingAnchor.constraint(equalTo: trailingAnchor),
+ new.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+ contentView = new
+
+ rebuildTrackingMetadata()
+ }
+
+ // MARK: - View tracking
+
+ private func rebuildTrackingMetadata() {
+ guard trackViews else { return }
+ trackingController?.onDisappear()
+ trackingController = ViewTrackingController(
+ client: client,
+ entry: entry,
+ personalization: resolvedPersonalization
+ )
+ emitVisibility()
+ }
+
+ private func installTracking() {
+ rebuildTrackingMetadata()
+
+ contentOffsetObservation = scrollView?.observe(\.contentOffset, options: [.new]) { [weak self] _, _ in
+ Task { @MainActor in self?.emitVisibility() }
+ }
+ boundsObservation = scrollView?.observe(\.bounds, options: [.new]) { [weak self] _, _ in
+ Task { @MainActor in self?.emitVisibility() }
+ }
+ }
+
+ override func didMoveToWindow() {
+ super.didMoveToWindow()
+ if window != nil {
+ emitVisibility()
+ }
+ }
+
+ override func willMove(toWindow newWindow: UIWindow?) {
+ super.willMove(toWindow: newWindow)
+ if newWindow == nil {
+ trackingController?.onDisappear()
+ }
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ emitVisibility()
+ }
+
+ private func emitVisibility() {
+ guard trackViews, let controller = trackingController else { return }
+ guard window != nil, bounds.height > 0 else { return }
+
+ if let scrollView {
+ let frameInScroll = convert(bounds, to: scrollView)
+ controller.updateVisibility(
+ elementY: frameInScroll.minY,
+ elementHeight: bounds.height,
+ scrollY: scrollView.contentOffset.y,
+ viewportHeight: scrollView.bounds.height
+ )
+ } else {
+ // Fallback when there is no enclosing scroll view: treat as fully visible
+ // when in window with the screen as the viewport.
+ controller.updateVisibility(
+ elementY: 0,
+ elementHeight: bounds.height,
+ scrollY: 0,
+ viewportHeight: ViewTrackingController.fallbackViewportHeight
+ )
+ }
+ }
+
+ // MARK: - Tap tracking
+
+ private func installTapGesture() {
+ let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
+ addGestureRecognizer(recognizer)
+ isUserInteractionEnabled = true
+ }
+
+ @objc private func handleTap() {
+ let metadata = TrackingMetadata(entry: entry, personalization: resolvedPersonalization)
+ let payload = TrackClickPayload(
+ componentId: metadata.componentId,
+ experienceId: metadata.experienceId,
+ variantIndex: metadata.variantIndex
+ )
+ Task { @MainActor in
+ _ = try? await client.trackClick(payload)
+ }
+ }
+}
diff --git a/implementations/ios-sdk/uikit/Info.plist b/implementations/ios-sdk/uikit/Info.plist
new file mode 100644
index 00000000..7f23c201
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Info.plist
@@ -0,0 +1,54 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+
+
+
+
+ UILaunchScreen
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/implementations/ios-sdk/uikit/SceneDelegate.swift b/implementations/ios-sdk/uikit/SceneDelegate.swift
new file mode 100644
index 00000000..9198b6db
--- /dev/null
+++ b/implementations/ios-sdk/uikit/SceneDelegate.swift
@@ -0,0 +1,35 @@
+import ContentfulOptimization
+import UIKit
+
+final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+
+ var window: UIWindow?
+ let client = OptimizationClient()
+
+ func scene(
+ _ scene: UIScene,
+ willConnectTo session: UISceneSession,
+ options connectionOptions: UIScene.ConnectionOptions
+ ) {
+ guard let windowScene = scene as? UIWindowScene else { return }
+
+ try? client.initialize(config: OptimizationConfig(
+ clientId: AppConfig.clientId,
+ environment: AppConfig.environment,
+ experienceBaseUrl: AppConfig.experienceBaseUrl,
+ insightsBaseUrl: AppConfig.insightsBaseUrl,
+ debug: true
+ ))
+
+ let main = MainViewController(client: client)
+ let nav = UINavigationController(rootViewController: main)
+ nav.setNavigationBarHidden(true, animated: false)
+
+ let window = UIWindow(windowScene: windowScene)
+ window.rootViewController = nav
+ self.window = window
+ window.makeKeyAndVisible()
+
+ PreviewPanelViewController.addFloatingButton(to: main, client: client, contentfulClient: nil)
+ }
+}
diff --git a/implementations/ios-sdk/uikit/Screens/LiveUpdatesTestViewController.swift b/implementations/ios-sdk/uikit/Screens/LiveUpdatesTestViewController.swift
new file mode 100644
index 00000000..5a84681c
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Screens/LiveUpdatesTestViewController.swift
@@ -0,0 +1,355 @@
+import Combine
+import ContentfulOptimization
+import UIKit
+
+final class LiveUpdatesTestViewController: UIViewController {
+
+ private let client: OptimizationClient
+ private let personalizedEntryId = "2Z2WLOx07InSewC3LUB3eX"
+ private var entry: [String: Any]?
+ private var isIdentified = false
+ private var globalLiveUpdates = false
+ private var isPreviewPanelSimulated = false
+
+ private let scrollView = UIScrollView()
+ private let rootStack = UIStackView()
+ private let loadingLabel = UILabel()
+
+ private let identifiedStatus = UILabel()
+ private let liveUpdatesStatus = UILabel()
+ private let previewPanelStatus = UILabel()
+
+ private let identifyButton = UIButton(type: .system)
+ private let resetButton = UIButton(type: .system)
+ private let toggleLiveUpdatesButton = UIButton(type: .system)
+ private let simulatePreviewPanelButton = UIButton(type: .system)
+
+ private let sectionsHost = UIStackView()
+ private var defaultSection: OptimizedEntryUIView?
+ private var liveSection: OptimizedEntryUIView?
+ private var lockedSection: OptimizedEntryUIView?
+
+ init(client: OptimizationClient) {
+ self.client = client
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .systemBackground
+ layout()
+ Task { @MainActor in
+ let entries = await ContentfulFetcher.fetchEntries(ids: [personalizedEntryId])
+ self.entry = entries.first
+ self.refreshUI()
+ }
+ }
+
+ // MARK: - Layout
+
+ private func layout() {
+ loadingLabel.text = "Loading..."
+ loadingLabel.textAlignment = .center
+
+ scrollView.accessibilityIdentifier = "live-updates-scroll-view"
+ scrollView.alwaysBounceVertical = true
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(scrollView)
+
+ rootStack.axis = .vertical
+ rootStack.alignment = .fill
+ rootStack.spacing = 12
+ rootStack.isLayoutMarginsRelativeArrangement = true
+ rootStack.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
+ rootStack.translatesAutoresizingMaskIntoConstraints = false
+ scrollView.addSubview(rootStack)
+
+ NSLayoutConstraint.activate([
+ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
+
+ rootStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
+ rootStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
+ rootStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
+ rootStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
+ rootStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
+ ])
+
+ rootStack.addArrangedSubview(makeControlsSection())
+ rootStack.addArrangedSubview(makeStatusSection())
+ rootStack.addArrangedSubview(makePreviewPanelSection())
+ rootStack.addArrangedSubview(loadingLabel)
+
+ sectionsHost.axis = .vertical
+ sectionsHost.alignment = .fill
+ sectionsHost.spacing = 20
+ rootStack.addArrangedSubview(sectionsHost)
+ }
+
+ private func makeControlsSection() -> UIView {
+ let title = UILabel()
+ title.text = "Live Updates Test Controls"
+ title.font = .preferredFont(forTextStyle: .headline)
+
+ let closeButton = UIButton(type: .system)
+ closeButton.setTitle("Close", for: .normal)
+ closeButton.accessibilityIdentifier = "close-live-updates-test-button"
+ closeButton.addAction(UIAction { [weak self] _ in self?.dismiss(animated: false) }, for: .touchUpInside)
+
+ identifyButton.setTitle("Identify", for: .normal)
+ identifyButton.accessibilityIdentifier = "live-updates-identify-button"
+ identifyButton.addAction(UIAction { [weak self] _ in self?.handleIdentify() }, for: .touchUpInside)
+
+ resetButton.setTitle("Reset", for: .normal)
+ resetButton.accessibilityIdentifier = "live-updates-reset-button"
+ resetButton.addAction(UIAction { [weak self] _ in self?.handleReset() }, for: .touchUpInside)
+ resetButton.isHidden = true
+
+ toggleLiveUpdatesButton.accessibilityIdentifier = "toggle-global-live-updates-button"
+ toggleLiveUpdatesButton.addAction(UIAction { [weak self] _ in self?.toggleGlobal() }, for: .touchUpInside)
+ updateGlobalToggleTitle()
+
+ let buttons = UIStackView(arrangedSubviews: [closeButton, identifyButton, resetButton, toggleLiveUpdatesButton])
+ buttons.axis = .horizontal
+ buttons.spacing = 8
+ buttons.distribution = .fillProportionally
+
+ let stack = UIStackView(arrangedSubviews: [title, buttons])
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 8
+ return stack
+ }
+
+ private func makeStatusSection() -> UIView {
+ let stack = UIStackView()
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 4
+ stack.addArrangedSubview(makeRow(prefix: "Identified:", valueLabel: identifiedStatus, identifier: "identified-status"))
+ stack.addArrangedSubview(makeRow(prefix: "Global Live Updates:", valueLabel: liveUpdatesStatus, identifier: "global-live-updates-status"))
+ return stack
+ }
+
+ private func makePreviewPanelSection() -> UIView {
+ simulatePreviewPanelButton.accessibilityIdentifier = "simulate-preview-panel-button"
+ simulatePreviewPanelButton.addAction(UIAction { [weak self] _ in self?.togglePreviewPanel() }, for: .touchUpInside)
+ updatePreviewPanelButtonTitle()
+
+ let stack = UIStackView()
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 4
+ stack.addArrangedSubview(simulatePreviewPanelButton)
+ stack.addArrangedSubview(makeRow(prefix: "Preview Panel:", valueLabel: previewPanelStatus, identifier: "preview-panel-status"))
+ return stack
+ }
+
+ private func makeRow(prefix: String, valueLabel: UILabel, identifier: String) -> UIView {
+ let prefixLabel = UILabel()
+ prefixLabel.text = prefix
+
+ valueLabel.accessibilityIdentifier = identifier
+ valueLabel.numberOfLines = 0
+
+ let row = UIStackView(arrangedSubviews: [prefixLabel, valueLabel])
+ row.axis = .horizontal
+ row.alignment = .firstBaseline
+ row.spacing = 8
+ return row
+ }
+
+ // MARK: - Status text
+
+ private func refreshUI() {
+ loadingLabel.isHidden = entry != nil
+ sectionsHost.isHidden = entry == nil
+
+ identifiedStatus.text = isIdentified ? "Yes" : "No"
+ identifiedStatus.accessibilityLabel = identifiedStatus.text
+ liveUpdatesStatus.text = globalLiveUpdates ? "ON" : "OFF"
+ liveUpdatesStatus.accessibilityLabel = liveUpdatesStatus.text
+ previewPanelStatus.text = isPreviewPanelSimulated ? "Open" : "Closed"
+ previewPanelStatus.accessibilityLabel = previewPanelStatus.text
+
+ rebuildSections()
+ }
+
+ private func updateGlobalToggleTitle() {
+ toggleLiveUpdatesButton.setTitle("Global: \(globalLiveUpdates ? "ON" : "OFF")", for: .normal)
+ }
+
+ private func updatePreviewPanelButtonTitle() {
+ let title = isPreviewPanelSimulated ? "Close Preview Panel" : "Simulate Preview Panel"
+ simulatePreviewPanelButton.setTitle(title, for: .normal)
+ }
+
+ // MARK: - Sections
+
+ private func rebuildSections() {
+ for view in sectionsHost.arrangedSubviews {
+ sectionsHost.removeArrangedSubview(view)
+ view.removeFromSuperview()
+ }
+ defaultSection = nil
+ liveSection = nil
+ lockedSection = nil
+
+ guard let entry else { return }
+
+ sectionsHost.addArrangedSubview(makeSection(
+ entry: entry,
+ title: "Default Behavior (inherits global setting)",
+ subtitle: "No liveUpdates prop - inherits from OptimizationRoot (false)",
+ liveUpdates: nil,
+ prefix: "default",
+ sectionIdentifier: "default-personalization",
+ store: { [weak self] in self?.defaultSection = $0 }
+ ))
+ sectionsHost.addArrangedSubview(makeSection(
+ entry: entry,
+ title: "Live Updates Enabled (liveUpdates=true)",
+ subtitle: "Always updates when personalization state changes",
+ liveUpdates: true,
+ prefix: "live",
+ sectionIdentifier: "live-personalization",
+ store: { [weak self] in self?.liveSection = $0 }
+ ))
+ sectionsHost.addArrangedSubview(makeSection(
+ entry: entry,
+ title: "Locked (liveUpdates=false)",
+ subtitle: "Never updates - locks to first variant received",
+ liveUpdates: false,
+ prefix: "locked",
+ sectionIdentifier: "locked-personalization",
+ store: { [weak self] in self?.lockedSection = $0 }
+ ))
+ }
+
+ private func makeSection(
+ entry: [String: Any],
+ title: String,
+ subtitle: String,
+ liveUpdates: Bool?,
+ prefix: String,
+ sectionIdentifier: String,
+ store: (OptimizedEntryUIView) -> Void
+ ) -> UIView {
+ let titleLabel = UILabel()
+ titleLabel.text = title
+ titleLabel.font = .preferredFont(forTextStyle: .body)
+
+ let subtitleLabel = UILabel()
+ subtitleLabel.text = subtitle
+ subtitleLabel.font = .preferredFont(forTextStyle: .caption1)
+
+ let optimized = OptimizedEntryUIView(
+ client: client,
+ entry: entry,
+ scrollView: scrollView,
+ liveUpdates: liveUpdates,
+ globalLiveUpdates: globalLiveUpdates,
+ trackTaps: false,
+ trackViews: true,
+ accessibilityIdentifier: sectionIdentifier
+ ) { resolved in
+ LiveUpdatesEntryDisplay(entry: resolved, prefix: prefix)
+ }
+ store(optimized)
+
+ let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel, optimized])
+ stack.axis = .vertical
+ stack.alignment = .fill
+ stack.spacing = 6
+ return stack
+ }
+
+ // MARK: - Actions
+
+ private func handleIdentify() {
+ Task { @MainActor in
+ _ = try? await client.identify(userId: "charles", traits: ["identified": true])
+ self.isIdentified = true
+ self.identifyButton.isHidden = true
+ self.resetButton.isHidden = false
+ self.refreshStatusOnly()
+ }
+ }
+
+ private func handleReset() {
+ client.reset()
+ Task { @MainActor in
+ _ = try? await client.page(properties: ["url": "live-updates-test"])
+ }
+ isIdentified = false
+ identifyButton.isHidden = false
+ resetButton.isHidden = true
+ refreshStatusOnly()
+ }
+
+ private func toggleGlobal() {
+ globalLiveUpdates.toggle()
+ updateGlobalToggleTitle()
+ refreshUI()
+ }
+
+ private func togglePreviewPanel() {
+ isPreviewPanelSimulated.toggle()
+ updatePreviewPanelButtonTitle()
+ refreshUI()
+ }
+
+ private func refreshStatusOnly() {
+ identifiedStatus.text = isIdentified ? "Yes" : "No"
+ identifiedStatus.accessibilityLabel = identifiedStatus.text
+ }
+}
+
+// MARK: - LiveUpdatesEntryDisplay
+
+private final class LiveUpdatesEntryDisplay: UIView {
+
+ init(entry: [String: Any], prefix: String) {
+ super.init(frame: .zero)
+ translatesAutoresizingMaskIntoConstraints = false
+
+ let entryId = (entry["sys"] as? [String: Any])?["id"] as? String ?? ""
+ let fields = entry["fields"] as? [String: Any]
+ let text = (fields?["text"] as? String) ?? "No content"
+
+ let textLabel = UILabel()
+ textLabel.text = text
+ textLabel.accessibilityLabel = text
+ textLabel.accessibilityIdentifier = "\(prefix)-text"
+ textLabel.numberOfLines = 0
+
+ let idLabel = UILabel()
+ idLabel.text = "Entry: \(entryId)"
+ idLabel.accessibilityLabel = "Entry: \(entryId)"
+ idLabel.accessibilityIdentifier = "\(prefix)-entry-id"
+ idLabel.font = .preferredFont(forTextStyle: .footnote)
+
+ let stack = UIStackView(arrangedSubviews: [textLabel, idLabel])
+ stack.axis = .vertical
+ stack.alignment = .leading
+ stack.spacing = 4
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: topAnchor),
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ accessibilityIdentifier = "\(prefix)-container"
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+}
diff --git a/implementations/ios-sdk/uikit/Screens/MainViewController.swift b/implementations/ios-sdk/uikit/Screens/MainViewController.swift
new file mode 100644
index 00000000..c9d7feb2
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Screens/MainViewController.swift
@@ -0,0 +1,205 @@
+import Combine
+import ContentfulOptimization
+import UIKit
+
+final class MainViewController: UIViewController {
+
+ private let client: OptimizationClient
+ private var entries: [[String: Any]] = []
+ private var isIdentified = false
+ private var firstAppearHandled = false
+ private var cancellables = Set()
+
+ private let identifyButton = UIButton(type: .system)
+ private let resetButton = UIButton(type: .system)
+ private let navigationTestButton = UIButton(type: .system)
+ private let liveUpdatesTestButton = UIButton(type: .system)
+ private let scrollView = UIScrollView()
+ private let contentStack = UIStackView()
+ private let analyticsView = AnalyticsEventDisplayView()
+ private let loadingLabel = UILabel()
+
+ init(client: OptimizationClient) {
+ self.client = client
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .systemBackground
+
+ configureControls()
+ configureScrollView()
+ layout()
+
+ EventStore.shared.subscribe(to: client.eventPublisher)
+ analyticsView.bind(to: EventStore.shared)
+
+ client.$state
+ .map { $0.profile }
+ .removeDuplicates { lhs, rhs in
+ let opts: JSONSerialization.WritingOptions = [.sortedKeys]
+ let l = lhs.flatMap { try? JSONSerialization.data(withJSONObject: $0, options: opts) }
+ let r = rhs.flatMap { try? JSONSerialization.data(withJSONObject: $0, options: opts) }
+ return l == r
+ }
+ .sink { [weak self] profile in
+ guard let self, profile != nil else { return }
+ Task { @MainActor in
+ let fetched = await ContentfulFetcher.fetchEntries(ids: AppConfig.entryIds)
+ self.entries = fetched
+ self.reloadContent()
+ }
+ }
+ .store(in: &cancellables)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ guard !firstAppearHandled else { return }
+ firstAppearHandled = true
+
+ client.consent(true)
+ Task { @MainActor in
+ _ = try? await client.page(properties: ["url": "app"])
+ if ProcessInfo.processInfo.arguments.contains("--simulate-offline") {
+ client.setOnline(false)
+ }
+ }
+ }
+
+ // MARK: - Layout
+
+ private func configureControls() {
+ identifyButton.setTitle("Identify", for: .normal)
+ identifyButton.accessibilityIdentifier = "identify-button"
+ identifyButton.addAction(UIAction { [weak self] _ in self?.handleIdentify() }, for: .touchUpInside)
+
+ resetButton.setTitle("Reset", for: .normal)
+ resetButton.accessibilityIdentifier = "reset-button"
+ resetButton.addAction(UIAction { [weak self] _ in self?.handleReset() }, for: .touchUpInside)
+ resetButton.isHidden = true
+
+ navigationTestButton.setTitle("Navigation Test", for: .normal)
+ navigationTestButton.accessibilityIdentifier = "navigation-test-button"
+ navigationTestButton.addAction(UIAction { [weak self] _ in self?.openNavigationTest() }, for: .touchUpInside)
+
+ liveUpdatesTestButton.setTitle("Live Updates Test", for: .normal)
+ liveUpdatesTestButton.accessibilityIdentifier = "live-updates-test-button"
+ liveUpdatesTestButton.addAction(UIAction { [weak self] _ in self?.openLiveUpdatesTest() }, for: .touchUpInside)
+
+ loadingLabel.text = "Loading..."
+ loadingLabel.textAlignment = .center
+ }
+
+ private func configureScrollView() {
+ scrollView.accessibilityIdentifier = "main-scroll-view"
+ scrollView.alwaysBounceVertical = true
+ contentStack.axis = .vertical
+ contentStack.alignment = .fill
+ contentStack.spacing = 0
+ }
+
+ private func layout() {
+ let buttonRow = UIStackView(arrangedSubviews: [identifyButton, resetButton, navigationTestButton, liveUpdatesTestButton])
+ buttonRow.axis = .horizontal
+ buttonRow.distribution = .fillEqually
+ buttonRow.spacing = 8
+
+ let root = UIStackView(arrangedSubviews: [buttonRow, scrollView])
+ root.axis = .vertical
+ root.spacing = 8
+ root.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(root)
+
+ contentStack.translatesAutoresizingMaskIntoConstraints = false
+ scrollView.addSubview(contentStack)
+
+ loadingLabel.translatesAutoresizingMaskIntoConstraints = false
+ contentStack.addArrangedSubview(loadingLabel)
+
+ analyticsView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ root.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
+ root.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ root.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ root.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
+
+ contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
+ contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
+ contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
+ contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
+ contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
+ ])
+ }
+
+ private func reloadContent() {
+ for view in contentStack.arrangedSubviews {
+ contentStack.removeArrangedSubview(view)
+ view.removeFromSuperview()
+ }
+
+ if entries.isEmpty {
+ contentStack.addArrangedSubview(loadingLabel)
+ return
+ }
+
+ for entry in entries {
+ if isNestedContent(entry) {
+ contentStack.addArrangedSubview(NestedContentEntryUIView(client: client, entry: entry, scrollView: scrollView))
+ } else {
+ contentStack.addArrangedSubview(ContentEntryUIView(client: client, entry: entry, scrollView: scrollView))
+ }
+ }
+ contentStack.addArrangedSubview(analyticsView)
+ }
+
+ // MARK: - Actions
+
+ private func handleIdentify() {
+ Task { @MainActor in
+ _ = try? await client.identify(userId: "charles", traits: ["identified": true])
+ }
+ isIdentified = true
+ identifyButton.isHidden = true
+ resetButton.isHidden = false
+ }
+
+ private func handleReset() {
+ client.reset()
+ Task { @MainActor in
+ _ = try? await client.page(properties: ["url": "app"])
+ }
+ isIdentified = false
+ identifyButton.isHidden = false
+ resetButton.isHidden = true
+ }
+
+ private func openNavigationTest() {
+ let nav = NavigationTestViewController(client: client)
+ nav.modalPresentationStyle = .fullScreen
+ present(nav, animated: false)
+ }
+
+ private func openLiveUpdatesTest() {
+ let live = LiveUpdatesTestViewController(client: client)
+ live.modalPresentationStyle = .fullScreen
+ present(live, animated: false)
+ }
+
+ // MARK: - Helpers
+
+ private func isNestedContent(_ entry: [String: Any]) -> Bool {
+ guard let sys = entry["sys"] as? [String: Any],
+ let contentType = sys["contentType"] as? [String: Any],
+ let innerSys = contentType["sys"] as? [String: Any],
+ let id = innerSys["id"] as? String
+ else { return false }
+ return id == "nestedContent"
+ }
+
+}
diff --git a/implementations/ios-sdk/uikit/Screens/NavigationTestViewController.swift b/implementations/ios-sdk/uikit/Screens/NavigationTestViewController.swift
new file mode 100644
index 00000000..00daf0cc
--- /dev/null
+++ b/implementations/ios-sdk/uikit/Screens/NavigationTestViewController.swift
@@ -0,0 +1,235 @@
+import Combine
+import ContentfulOptimization
+import UIKit
+
+@MainActor
+final class ScreenLog: ObservableObject {
+ @Published private(set) var names: [String] = []
+
+ func append(_ name: String) {
+ names.append(name)
+ }
+}
+
+final class NavigationTestViewController: UINavigationController {
+
+ private let client: OptimizationClient
+ private let screenLog = ScreenLog()
+ private var cancellables = Set()
+
+ init(client: OptimizationClient) {
+ self.client = client
+ let home = NavigationHomeViewController(client: client, log: screenLog)
+ super.init(rootViewController: home)
+ home.onClose = { [weak self] in self?.dismiss(animated: false) }
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ client.eventPublisher
+ .sink { [weak self] event in
+ guard let type = event["type"] as? String,
+ type == "screen" || type == "screenViewEvent",
+ let name = event["name"] as? String
+ else { return }
+ Task { @MainActor in self?.screenLog.append(name) }
+ }
+ .store(in: &cancellables)
+ }
+}
+
+// MARK: - Home
+
+private final class NavigationHomeViewController: UIViewController {
+
+ var onClose: (() -> Void)?
+
+ private let client: OptimizationClient
+ private let log: ScreenLog
+ private let logLabel = UILabel()
+ private var cancellables = Set()
+
+ init(client: OptimizationClient, log: ScreenLog) {
+ self.client = client
+ self.log = log
+ super.init(nibName: nil, bundle: nil)
+ title = "Navigation Test"
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .systemBackground
+
+ let closeButton = UIButton(type: .system)
+ closeButton.setTitle("Close", for: .normal)
+ closeButton.accessibilityIdentifier = "close-navigation-test-button"
+ closeButton.addAction(UIAction { [weak self] _ in self?.onClose?() }, for: .touchUpInside)
+
+ let goButton = UIButton(type: .system)
+ goButton.setTitle("Go to View One", for: .normal)
+ goButton.accessibilityIdentifier = "go-to-view-one-button"
+ goButton.addAction(UIAction { [weak self] _ in self?.goToViewOne() }, for: .touchUpInside)
+
+ logLabel.accessibilityIdentifier = "screen-event-log"
+ logLabel.numberOfLines = 0
+
+ let stack = UIStackView(arrangedSubviews: [closeButton, goButton, logLabel])
+ stack.axis = .vertical
+ stack.alignment = .center
+ stack.spacing = 12
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(stack)
+ NSLayoutConstraint.activate([
+ stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
+ stack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
+ stack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
+ ])
+
+ log.$names
+ .map { $0.joined(separator: ",") }
+ .sink { [weak self] joined in
+ self?.logLabel.text = joined
+ self?.logLabel.accessibilityLabel = joined
+ }
+ .store(in: &cancellables)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ Task { @MainActor in
+ _ = try? await client.screen(name: "NavigationHome")
+ }
+ }
+
+ private func goToViewOne() {
+ let viewOne = NavigationViewContentVC(
+ client: client,
+ log: log,
+ screenName: "NavigationViewOne",
+ suffix: "one",
+ nextTitle: "Go to View Two",
+ nextIdentifier: "go-to-view-two-button",
+ onNavigateNext: { [weak self] in self?.goToViewTwo() }
+ )
+ navigationController?.pushViewController(viewOne, animated: false)
+ }
+
+ private func goToViewTwo() {
+ let viewTwo = NavigationViewContentVC(
+ client: client,
+ log: log,
+ screenName: "NavigationViewTwo",
+ suffix: "two",
+ nextTitle: "Go to View Two",
+ nextIdentifier: "go-to-view-two-button",
+ onNavigateNext: nil
+ )
+ navigationController?.pushViewController(viewTwo, animated: false)
+ }
+}
+
+// MARK: - Generic content VC for view one + view two
+
+private final class NavigationViewContentVC: UIViewController {
+
+ private let client: OptimizationClient
+ private let log: ScreenLog
+ private let screenName: String
+ private let suffix: String
+ private let nextTitle: String
+ private let nextIdentifier: String
+ private let onNavigateNext: (() -> Void)?
+
+ private let lastLabel = UILabel()
+ private let logLabel = UILabel()
+ private var cancellables = Set()
+
+ init(
+ client: OptimizationClient,
+ log: ScreenLog,
+ screenName: String,
+ suffix: String,
+ nextTitle: String,
+ nextIdentifier: String,
+ onNavigateNext: (() -> Void)?
+ ) {
+ self.client = client
+ self.log = log
+ self.screenName = screenName
+ self.suffix = suffix
+ self.nextTitle = nextTitle
+ self.nextIdentifier = nextIdentifier
+ self.onNavigateNext = onNavigateNext
+ super.init(nibName: nil, bundle: nil)
+ title = screenName
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .systemBackground
+
+ let container = UIView()
+ container.translatesAutoresizingMaskIntoConstraints = false
+ container.accessibilityIdentifier = "navigation-view-test-\(suffix)"
+ view.addSubview(container)
+
+ lastLabel.accessibilityIdentifier = "last-screen-event"
+ lastLabel.numberOfLines = 0
+ logLabel.accessibilityIdentifier = "screen-event-log"
+ logLabel.numberOfLines = 0
+
+ let stack = UIStackView(arrangedSubviews: [lastLabel, logLabel])
+ stack.axis = .vertical
+ stack.alignment = .center
+ stack.spacing = 12
+ stack.translatesAutoresizingMaskIntoConstraints = false
+
+ if let onNavigateNext {
+ let nextButton = UIButton(type: .system)
+ nextButton.setTitle(nextTitle, for: .normal)
+ nextButton.accessibilityIdentifier = nextIdentifier
+ nextButton.addAction(UIAction { _ in onNavigateNext() }, for: .touchUpInside)
+ stack.addArrangedSubview(nextButton)
+ }
+
+ container.addSubview(stack)
+ NSLayoutConstraint.activate([
+ container.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
+ container.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
+ container.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
+
+ stack.topAnchor.constraint(equalTo: container.topAnchor),
+ stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+ stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+ stack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+ ])
+
+ log.$names
+ .sink { [weak self] names in
+ let last = names.last ?? ""
+ self?.lastLabel.text = last
+ self?.lastLabel.accessibilityLabel = last
+ let joined = names.joined(separator: ",")
+ self?.logLabel.text = joined
+ self?.logLabel.accessibilityLabel = joined
+ }
+ .store(in: &cancellables)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ Task { @MainActor in
+ _ = try? await client.screen(name: screenName)
+ }
+ }
+}
+
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Support/TestHelpers.swift b/implementations/ios-sdk/uitests/Support/TestHelpers.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Support/TestHelpers.swift
rename to implementations/ios-sdk/uitests/Support/TestHelpers.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Support/XCTestExtensions.swift b/implementations/ios-sdk/uitests/Support/XCTestExtensions.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Support/XCTestExtensions.swift
rename to implementations/ios-sdk/uitests/Support/XCTestExtensions.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/AnalyticsTests.swift b/implementations/ios-sdk/uitests/Tests/AnalyticsTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/AnalyticsTests.swift
rename to implementations/ios-sdk/uitests/Tests/AnalyticsTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/ExtendedViewTrackingTests.swift b/implementations/ios-sdk/uitests/Tests/ExtendedViewTrackingTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/ExtendedViewTrackingTests.swift
rename to implementations/ios-sdk/uitests/Tests/ExtendedViewTrackingTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/FlagViewTrackingTests.swift b/implementations/ios-sdk/uitests/Tests/FlagViewTrackingTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/FlagViewTrackingTests.swift
rename to implementations/ios-sdk/uitests/Tests/FlagViewTrackingTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/IdentifiedVariantsTests.swift b/implementations/ios-sdk/uitests/Tests/IdentifiedVariantsTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/IdentifiedVariantsTests.swift
rename to implementations/ios-sdk/uitests/Tests/IdentifiedVariantsTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/LiveUpdatesTests.swift b/implementations/ios-sdk/uitests/Tests/LiveUpdatesTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/LiveUpdatesTests.swift
rename to implementations/ios-sdk/uitests/Tests/LiveUpdatesTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/OfflineBehaviorTests.swift b/implementations/ios-sdk/uitests/Tests/OfflineBehaviorTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/OfflineBehaviorTests.swift
rename to implementations/ios-sdk/uitests/Tests/OfflineBehaviorTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/PreviewPanelOverridesTests.swift b/implementations/ios-sdk/uitests/Tests/PreviewPanelOverridesTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/PreviewPanelOverridesTests.swift
rename to implementations/ios-sdk/uitests/Tests/PreviewPanelOverridesTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/PreviewPanelTests.swift b/implementations/ios-sdk/uitests/Tests/PreviewPanelTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/PreviewPanelTests.swift
rename to implementations/ios-sdk/uitests/Tests/PreviewPanelTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/ScreenTrackingTests.swift b/implementations/ios-sdk/uitests/Tests/ScreenTrackingTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/ScreenTrackingTests.swift
rename to implementations/ios-sdk/uitests/Tests/ScreenTrackingTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/TapTrackingTests.swift b/implementations/ios-sdk/uitests/Tests/TapTrackingTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/TapTrackingTests.swift
rename to implementations/ios-sdk/uitests/Tests/TapTrackingTests.swift
diff --git a/implementations/ios-sdk/OptimizationAppUITests/Tests/UnidentifiedVariantsTests.swift b/implementations/ios-sdk/uitests/Tests/UnidentifiedVariantsTests.swift
similarity index 100%
rename from implementations/ios-sdk/OptimizationAppUITests/Tests/UnidentifiedVariantsTests.swift
rename to implementations/ios-sdk/uitests/Tests/UnidentifiedVariantsTests.swift