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