diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 143c90cb0e..f13d2bfcb9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,12 +57,18 @@ default: rules: - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' -.release-pipeline-delayed-job: +.release-pipeline-20m-delayed-job: rules: - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' when: delayed start_in: 20 minutes +.release-pipeline-40m-delayed-job: + rules: + - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' + when: delayed + start_in: 40 minutes + ENV check: stage: pre rules: @@ -96,7 +102,7 @@ Unit Tests (iOS): script: - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" - make clean repo-setup ENV=ci - - make test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + - make test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 Unit Tests (tvOS): stage: test @@ -109,7 +115,7 @@ Unit Tests (tvOS): script: - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" - make clean repo-setup ENV=ci - - make test-tvos-all OS="$DEFAULT_TVOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + - make test-tvos-all OS="$DEFAULT_TVOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 UI Tests: stage: ui-test @@ -164,6 +170,17 @@ Tools Tests: - make clean repo-setup ENV=ci - make tools-test +Benchmark Build: + stage: smoke-test + rules: + - if: '$CI_COMMIT_BRANCH' # when on branch with following changes compared to develop + changes: + paths: + - "BenchmarkTests/**/*" + compare_to: 'develop' + script: + - make benchmark-build + Smoke Tests (iOS): stage: smoke-test rules: @@ -265,7 +282,7 @@ E2E Test (upload to s8s): - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --datadog-ci - make clean - export DRY_RUN=${DRY_RUN:-0} # default to 0 if not specified - - make e2e-build-upload ARTIFACTS_PATH="artifacts/e2e" + - make e2e-upload ARTIFACTS_PATH="artifacts/e2e" # ┌────────────────────────────┐ # │ Benchmark Test app upload: │ @@ -275,6 +292,7 @@ Benchmark Test (upload to s8s): stage: benchmark-test rules: - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + allow_failure: true artifacts: paths: - artifacts @@ -283,7 +301,7 @@ Benchmark Test (upload to s8s): - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --datadog-ci - make clean - export DRY_RUN=${DRY_RUN:-0} # default to 0 if not specified - - make benchmark-build-upload ARTIFACTS_PATH="artifacts/benchmark" + - make benchmark-upload ARTIFACTS_PATH="artifacts/benchmark" # ┌─────────────────┐ # │ SDK dogfooding: │ @@ -363,7 +381,7 @@ Publish CP podspecs (internal): Publish CP podspecs (dependent): stage: release-publish rules: - - !reference [.release-pipeline-delayed-job, rules] + - !reference [.release-pipeline-20m-delayed-job, rules] before_script: - *export_MAKE_release_params script: @@ -375,7 +393,7 @@ Publish CP podspecs (dependent): Publish CP podspecs (legacy): stage: release-publish rules: - - !reference [.release-pipeline-delayed-job, rules] + - !reference [.release-pipeline-40m-delayed-job, rules] before_script: - *export_MAKE_release_params script: diff --git a/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj b/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj index 4cb16fa4c4..70b3105e82 100644 --- a/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj @@ -7,11 +7,80 @@ objects = { /* Begin PBXBuildFile section */ + D231DC372C73355800F3F66C /* UIKitCatalog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; }; + D231DC382C73355800F3F66C /* UIKitCatalog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D231DCAF2C73356E00F3F66C /* ActivityIndicatorViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */; }; + D231DCB02C73356E00F3F66C /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */; }; + D231DCB12C73356E00F3F66C /* AlertControllerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */; }; + D231DCB22C73356E00F3F66C /* AlertControllerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */; }; + D231DCB42C73356E00F3F66C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D231DC472C73356D00F3F66C /* Assets.xcassets */; }; + D231DCB52C73356E00F3F66C /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC482C73356D00F3F66C /* BaseTableViewController.swift */; }; + D231DCB62C73356E00F3F66C /* ButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */; }; + D231DCB72C73356E00F3F66C /* ButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */; }; + D231DCB82C73356E00F3F66C /* ButtonViewController+Configs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */; }; + D231DCB92C73356E00F3F66C /* CaseElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4D2C73356D00F3F66C /* CaseElement.swift */; }; + D231DCBA2C73356E00F3F66C /* ColorPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */; }; + D231DCBB2C73356E00F3F66C /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */; }; + D231DCBC2C73356E00F3F66C /* content.html in Resources */ = {isa = PBXBuildFile; fileRef = D231DC522C73356D00F3F66C /* content.html */; }; + D231DCBD2C73356E00F3F66C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = D231DC542C73356D00F3F66C /* Credits.rtf */; }; + D231DCBE2C73356E00F3F66C /* CustomPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */; }; + D231DCBF2C73356E00F3F66C /* CustomPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */; }; + D231DCC02C73356E00F3F66C /* CustomSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */; }; + D231DCC12C73356E00F3F66C /* CustomSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */; }; + D231DCC22C73356E00F3F66C /* CustomToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */; }; + D231DCC32C73356E00F3F66C /* CustomToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */; }; + D231DCC42C73356E00F3F66C /* DatePickerController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */; }; + D231DCC52C73356E00F3F66C /* DatePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC602C73356D00F3F66C /* DatePickerController.swift */; }; + D231DCC62C73356E00F3F66C /* DefaultPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */; }; + D231DCC72C73356E00F3F66C /* DefaultPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */; }; + D231DCC82C73356E00F3F66C /* DefaultSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */; }; + D231DCC92C73356E00F3F66C /* DefaultSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */; }; + D231DCCA2C73356E00F3F66C /* DefaultToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */; }; + D231DCCB2C73356E00F3F66C /* DefaultToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */; }; + D231DCCC2C73356E00F3F66C /* FontPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */; }; + D231DCCD2C73356E00F3F66C /* FontPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */; }; + D231DCCE2C73356E00F3F66C /* ImagePickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */; }; + D231DCCF2C73356E00F3F66C /* ImagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */; }; + D231DCD02C73356E00F3F66C /* ImageViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC712C73356D00F3F66C /* ImageViewController.storyboard */; }; + D231DCD12C73356E00F3F66C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC722C73356D00F3F66C /* ImageViewController.swift */; }; + D231DCD42C73356E00F3F66C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D231DC782C73356D00F3F66C /* Localizable.strings */; }; + D231DCD52C73356E00F3F66C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC7A2C73356D00F3F66C /* Main.storyboard */; }; + D231DCD62C73356E00F3F66C /* MenuButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */; }; + D231DCD72C73356E00F3F66C /* MenuButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */; }; + D231DCD82C73356E00F3F66C /* OutlineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */; }; + D231DCD92C73356E00F3F66C /* PickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC802C73356D00F3F66C /* PickerViewController.storyboard */; }; + D231DCDA2C73356E00F3F66C /* PickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC812C73356D00F3F66C /* PickerViewController.swift */; }; + D231DCDB2C73356E00F3F66C /* PointerInteractionButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */; }; + D231DCDC2C73356E00F3F66C /* PointerInteractionButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */; }; + D231DCDD2C73356E00F3F66C /* ProgressViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */; }; + D231DCDE2C73356E00F3F66C /* ProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC872C73356D00F3F66C /* ProgressViewController.swift */; }; + D231DCE02C73356E00F3F66C /* SegmentedControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */; }; + D231DCE12C73356E00F3F66C /* SegmentedControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */; }; + D231DCE22C73356E00F3F66C /* SliderViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */; }; + D231DCE32C73356E00F3F66C /* SliderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC8E2C73356D00F3F66C /* SliderViewController.swift */; }; + D231DCE42C73356E00F3F66C /* StackViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC902C73356D00F3F66C /* StackViewController.storyboard */; }; + D231DCE52C73356E00F3F66C /* StackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC912C73356D00F3F66C /* StackViewController.swift */; }; + D231DCE62C73356E00F3F66C /* StepperViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC932C73356D00F3F66C /* StepperViewController.storyboard */; }; + D231DCE72C73356E00F3F66C /* StepperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC942C73356D00F3F66C /* StepperViewController.swift */; }; + D231DCE82C73356E00F3F66C /* SwitchViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */; }; + D231DCE92C73356E00F3F66C /* SwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC972C73356D00F3F66C /* SwitchViewController.swift */; }; + D231DCEA2C73356E00F3F66C /* SymbolViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */; }; + D231DCEB2C73356E00F3F66C /* SymbolViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */; }; + D231DCEC2C73356E00F3F66C /* TextFieldViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */; }; + D231DCED2C73356E00F3F66C /* TextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */; }; + D231DCEE2C73356E00F3F66C /* TextViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */; }; + D231DCEF2C73356E00F3F66C /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA02C73356D00F3F66C /* TextViewController.swift */; }; + D231DCF02C73356E00F3F66C /* TintedToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */; }; + D231DCF12C73356E00F3F66C /* TintedToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */; }; + D231DCF32C73356E00F3F66C /* VisualEffectViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */; }; + D231DCF42C73356E00F3F66C /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */; }; + D231DCF52C73356E00F3F66C /* WebViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */; }; + D231DCF62C73356E00F3F66C /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCAB2C73356D00F3F66C /* WebViewController.swift */; }; + D231DCF92C7342D500F3F66C /* ModuleBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCF82C7342D500F3F66C /* ModuleBundle.swift */; }; D23DD32D2C58D80C00B90C4C /* DatadogBenchmarks in Frameworks */ = {isa = PBXBuildFile; productRef = D23DD32C2C58D80C00B90C4C /* DatadogBenchmarks */; }; - D276069F2C514F37002D2A14 /* SessionReplay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D27606962C514F37002D2A14 /* SessionReplay.storyboard */; }; - D27606A02C514F37002D2A14 /* SessionReplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27606972C514F37002D2A14 /* SessionReplayController.swift */; }; + D24BFD472C6B916B00AB9604 /* SyntheticScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */; }; + D24E15F32C776956005AE4E8 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */; }; D27606A12C514F37002D2A14 /* SessionReplayScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27606982C514F37002D2A14 /* SessionReplayScenario.swift */; }; - D27606A22C514F37002D2A14 /* DefaultScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D276069A2C514F37002D2A14 /* DefaultScenario.swift */; }; D27606A32C514F37002D2A14 /* Scenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D276069B2C514F37002D2A14 /* Scenario.swift */; }; D27606A42C514F37002D2A14 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D276069D2C514F37002D2A14 /* AppConfiguration.swift */; }; D27606A72C514F77002D2A14 /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = D27606A62C514F77002D2A14 /* DatadogCore */; }; @@ -22,6 +91,16 @@ D29F75502C4AA07E00288638 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29F754F2C4AA07E00288638 /* AppDelegate.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + D231DC352C73355800F3F66C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D29F75282C4A9EFA00288638 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D231DC302C73355800F3F66C; + remoteInfo = UIKitCatalog; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ D29F75872C4AA98F00288638 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -29,6 +108,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + D231DC382C73355800F3F66C /* UIKitCatalog.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -36,10 +116,79 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - D27606962C514F37002D2A14 /* SessionReplay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SessionReplay.storyboard; sourceTree = ""; }; - D27606972C514F37002D2A14 /* SessionReplayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayController.swift; sourceTree = ""; }; + D231DC312C73355800F3F66C /* UIKitCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UIKitCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D231DC402C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ActivityIndicatorViewController.storyboard; sourceTree = ""; }; + D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; + D231DC432C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AlertControllerViewController.storyboard; sourceTree = ""; }; + D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertControllerViewController.swift; sourceTree = ""; }; + D231DC472C73356D00F3F66C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D231DC482C73356D00F3F66C /* BaseTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; + D231DC492C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ButtonViewController.storyboard; sourceTree = ""; }; + D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonViewController.swift; sourceTree = ""; }; + D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ButtonViewController+Configs.swift"; sourceTree = ""; }; + D231DC4D2C73356D00F3F66C /* CaseElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseElement.swift; sourceTree = ""; }; + D231DC4E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ColorPickerViewController.storyboard; sourceTree = ""; }; + D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; + D231DC512C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = Base.lproj/content.html; sourceTree = ""; }; + D231DC532C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = ""; }; + D231DC552C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomPageControlViewController.storyboard; sourceTree = ""; }; + D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPageControlViewController.swift; sourceTree = ""; }; + D231DC582C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomSearchBarViewController.storyboard; sourceTree = ""; }; + D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSearchBarViewController.swift; sourceTree = ""; }; + D231DC5B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomToolbarViewController.storyboard; sourceTree = ""; }; + D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomToolbarViewController.swift; sourceTree = ""; }; + D231DC5E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DatePickerController.storyboard; sourceTree = ""; }; + D231DC602C73356D00F3F66C /* DatePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerController.swift; sourceTree = ""; }; + D231DC612C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultPageControlViewController.storyboard; sourceTree = ""; }; + D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPageControlViewController.swift; sourceTree = ""; }; + D231DC642C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultSearchBarViewController.storyboard; sourceTree = ""; }; + D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSearchBarViewController.swift; sourceTree = ""; }; + D231DC672C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultToolbarViewController.storyboard; sourceTree = ""; }; + D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultToolbarViewController.swift; sourceTree = ""; }; + D231DC6A2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/FontPickerViewController.storyboard; sourceTree = ""; }; + D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontPickerViewController.swift; sourceTree = ""; }; + D231DC6D2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImagePickerViewController.storyboard; sourceTree = ""; }; + D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerViewController.swift; sourceTree = ""; }; + D231DC702C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImageViewController.storyboard; sourceTree = ""; }; + D231DC722C73356D00F3F66C /* ImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; + D231DC772C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; + D231DC792C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D231DC7B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MenuButtonViewController.storyboard; sourceTree = ""; }; + D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuButtonViewController.swift; sourceTree = ""; }; + D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineViewController.swift; sourceTree = ""; }; + D231DC7F2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PickerViewController.storyboard; sourceTree = ""; }; + D231DC812C73356D00F3F66C /* PickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerViewController.swift; sourceTree = ""; }; + D231DC822C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PointerInteractionButtonViewController.storyboard; sourceTree = ""; }; + D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointerInteractionButtonViewController.swift; sourceTree = ""; }; + D231DC852C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ProgressViewController.storyboard; sourceTree = ""; }; + D231DC872C73356D00F3F66C /* ProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewController.swift; sourceTree = ""; }; + D231DC892C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SegmentedControlViewController.storyboard; sourceTree = ""; }; + D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlViewController.swift; sourceTree = ""; }; + D231DC8C2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SliderViewController.storyboard; sourceTree = ""; }; + D231DC8E2C73356D00F3F66C /* SliderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderViewController.swift; sourceTree = ""; }; + D231DC8F2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StackViewController.storyboard; sourceTree = ""; }; + D231DC912C73356D00F3F66C /* StackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; }; + D231DC922C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StepperViewController.storyboard; sourceTree = ""; }; + D231DC942C73356D00F3F66C /* StepperViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperViewController.swift; sourceTree = ""; }; + D231DC952C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SwitchViewController.storyboard; sourceTree = ""; }; + D231DC972C73356D00F3F66C /* SwitchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchViewController.swift; sourceTree = ""; }; + D231DC982C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SymbolViewController.storyboard; sourceTree = ""; }; + D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SymbolViewController.swift; sourceTree = ""; }; + D231DC9B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextFieldViewController.storyboard; sourceTree = ""; }; + D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldViewController.swift; sourceTree = ""; }; + D231DC9E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextViewController.storyboard; sourceTree = ""; }; + D231DCA02C73356D00F3F66C /* TextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; + D231DCA12C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TintedToolbarViewController.storyboard; sourceTree = ""; }; + D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TintedToolbarViewController.swift; sourceTree = ""; }; + D231DCA62C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/VisualEffectViewController.storyboard; sourceTree = ""; }; + D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualEffectViewController.swift; sourceTree = ""; }; + D231DCA92C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/WebViewController.storyboard; sourceTree = ""; }; + D231DCAB2C73356D00F3F66C /* WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + D231DCF82C7342D500F3F66C /* ModuleBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleBundle.swift; sourceTree = ""; }; + D231DCFA2C735FC200F3F66C /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; + D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticScenario.swift; sourceTree = ""; }; + D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; D27606982C514F37002D2A14 /* SessionReplayScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayScenario.swift; sourceTree = ""; }; - D276069A2C514F37002D2A14 /* DefaultScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultScenario.swift; sourceTree = ""; }; D276069B2C514F37002D2A14 /* Scenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scenario.swift; sourceTree = ""; }; D276069D2C514F37002D2A14 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; D27606B22C526908002D2A14 /* Benchmarks.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Benchmarks.local.xcconfig; sourceTree = ""; }; @@ -50,9 +199,17 @@ D29F754F2C4AA07E00288638 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D29F755D2C4AA08000288638 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D2CA7E862C57F9B800AAB380 /* dd-sdk-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "dd-sdk-ios"; path = ..; sourceTree = ""; }; + D2E60B9F2C732FBB00A18F1C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D231DC2E2C73355800F3F66C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29F754A2C4AA07E00288638 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -61,6 +218,7 @@ D27606AF2C514F77002D2A14 /* DatadogTrace in Frameworks */, D27606AD2C514F77002D2A14 /* DatadogSessionReplay in Frameworks */, D23DD32D2C58D80C00B90C4C /* DatadogBenchmarks in Frameworks */, + D231DC372C73355800F3F66C /* UIKitCatalog.framework in Frameworks */, D27606AB2C514F77002D2A14 /* DatadogRUM in Frameworks */, D27606A72C514F77002D2A14 /* DatadogCore in Frameworks */, ); @@ -69,11 +227,93 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D231DC322C73355800F3F66C /* UIKitCatalog */ = { + isa = PBXGroup; + children = ( + D256FB522C737F5800377260 /* LICENSE */, + D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */, + D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */, + D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */, + D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */, + D231DC472C73356D00F3F66C /* Assets.xcassets */, + D231DC482C73356D00F3F66C /* BaseTableViewController.swift */, + D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */, + D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */, + D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */, + D231DC4D2C73356D00F3F66C /* CaseElement.swift */, + D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */, + D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */, + D231DC522C73356D00F3F66C /* content.html */, + D231DC542C73356D00F3F66C /* Credits.rtf */, + D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */, + D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */, + D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */, + D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */, + D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */, + D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */, + D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */, + D231DC602C73356D00F3F66C /* DatePickerController.swift */, + D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */, + D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */, + D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */, + D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */, + D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */, + D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */, + D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */, + D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */, + D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */, + D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */, + D231DC712C73356D00F3F66C /* ImageViewController.storyboard */, + D231DC722C73356D00F3F66C /* ImageViewController.swift */, + D231DC782C73356D00F3F66C /* Localizable.strings */, + D231DC7A2C73356D00F3F66C /* Main.storyboard */, + D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */, + D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */, + D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */, + D231DC802C73356D00F3F66C /* PickerViewController.storyboard */, + D231DC812C73356D00F3F66C /* PickerViewController.swift */, + D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */, + D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */, + D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */, + D231DC872C73356D00F3F66C /* ProgressViewController.swift */, + D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */, + D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */, + D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */, + D231DC8E2C73356D00F3F66C /* SliderViewController.swift */, + D231DC902C73356D00F3F66C /* StackViewController.storyboard */, + D231DC912C73356D00F3F66C /* StackViewController.swift */, + D231DC932C73356D00F3F66C /* StepperViewController.storyboard */, + D231DC942C73356D00F3F66C /* StepperViewController.swift */, + D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */, + D231DC972C73356D00F3F66C /* SwitchViewController.swift */, + D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */, + D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */, + D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */, + D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */, + D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */, + D231DCA02C73356D00F3F66C /* TextViewController.swift */, + D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */, + D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */, + D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */, + D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */, + D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */, + D231DCAB2C73356D00F3F66C /* WebViewController.swift */, + D231DCF82C7342D500F3F66C /* ModuleBundle.swift */, + ); + path = UIKitCatalog; + sourceTree = ""; + }; + D256FB522C737F5800377260 /* LICENSE */ = { + isa = PBXGroup; + children = ( + D231DCFA2C735FC200F3F66C /* LICENSE.txt */, + ); + path = LICENSE; + sourceTree = ""; + }; D27606992C514F37002D2A14 /* SessionReplay */ = { isa = PBXGroup; children = ( - D27606962C514F37002D2A14 /* SessionReplay.storyboard */, - D27606972C514F37002D2A14 /* SessionReplayController.swift */, D27606982C514F37002D2A14 /* SessionReplayScenario.swift */, ); path = SessionReplay; @@ -82,9 +322,9 @@ D276069C2C514F37002D2A14 /* Scenarios */ = { isa = PBXGroup; children = ( - D27606992C514F37002D2A14 /* SessionReplay */, - D276069A2C514F37002D2A14 /* DefaultScenario.swift */, D276069B2C514F37002D2A14 /* Scenario.swift */, + D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */, + D27606992C514F37002D2A14 /* SessionReplay */, ); path = Scenarios; sourceTree = ""; @@ -102,8 +342,10 @@ D29F75272C4A9EFA00288638 = { isa = PBXGroup; children = ( + D2E60B9F2C732FBB00A18F1C /* README.md */, D27606B52C526908002D2A14 /* xcconfigs */, D29F754E2C4AA07E00288638 /* Runner */, + D231DC322C73355800F3F66C /* UIKitCatalog */, D29F75482C4A9F9500288638 /* Frameworks */, D29F75312C4A9EFA00288638 /* Products */, ); @@ -113,6 +355,7 @@ isa = PBXGroup; children = ( D29F754D2C4AA07E00288638 /* Runner.app */, + D231DC312C73355800F3F66C /* UIKitCatalog.framework */, ); name = Products; sourceTree = ""; @@ -131,6 +374,7 @@ children = ( D29F754F2C4AA07E00288638 /* AppDelegate.swift */, D276069D2C514F37002D2A14 /* AppConfiguration.swift */, + D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */, D276069C2C514F37002D2A14 /* Scenarios */, D29F755D2C4AA08000288638 /* Info.plist */, ); @@ -139,7 +383,35 @@ }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + D231DC2C2C73355800F3F66C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ + D231DC302C73355800F3F66C /* UIKitCatalog */ = { + isa = PBXNativeTarget; + buildConfigurationList = D231DC3C2C73355800F3F66C /* Build configuration list for PBXNativeTarget "UIKitCatalog" */; + buildPhases = ( + D231DC2C2C73355800F3F66C /* Headers */, + D231DC2D2C73355800F3F66C /* Sources */, + D231DC2E2C73355800F3F66C /* Frameworks */, + D231DC2F2C73355800F3F66C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIKitCatalog; + productName = UIKitCatalog; + productReference = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; + productType = "com.apple.product-type.framework"; + }; D29F754C2C4AA07E00288638 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = D29F75602C4AA08000288638 /* Build configuration list for PBXNativeTarget "Runner" */; @@ -152,6 +424,7 @@ buildRules = ( ); dependencies = ( + D231DC362C73355800F3F66C /* PBXTargetDependency */, ); name = Runner; packageProductDependencies = ( @@ -176,6 +449,9 @@ LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1540; TargetAttributes = { + D231DC302C73355800F3F66C = { + CreatedOnToolsVersion = 15.4; + }; D29F754C2C4AA07E00288638 = { CreatedOnToolsVersion = 15.4; }; @@ -197,38 +473,482 @@ projectRoot = ""; targets = ( D29F754C2C4AA07E00288638 /* Runner */, + D231DC302C73355800F3F66C /* UIKitCatalog */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D231DC2F2C73355800F3F66C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D231DCD62C73356E00F3F66C /* MenuButtonViewController.storyboard in Resources */, + D231DCAF2C73356E00F3F66C /* ActivityIndicatorViewController.storyboard in Resources */, + D231DCDB2C73356E00F3F66C /* PointerInteractionButtonViewController.storyboard in Resources */, + D231DCBE2C73356E00F3F66C /* CustomPageControlViewController.storyboard in Resources */, + D231DCD92C73356E00F3F66C /* PickerViewController.storyboard in Resources */, + D231DCF32C73356E00F3F66C /* VisualEffectViewController.storyboard in Resources */, + D231DCC42C73356E00F3F66C /* DatePickerController.storyboard in Resources */, + D231DCE62C73356E00F3F66C /* StepperViewController.storyboard in Resources */, + D231DCBC2C73356E00F3F66C /* content.html in Resources */, + D231DCD52C73356E00F3F66C /* Main.storyboard in Resources */, + D231DCC62C73356E00F3F66C /* DefaultPageControlViewController.storyboard in Resources */, + D231DCEA2C73356E00F3F66C /* SymbolViewController.storyboard in Resources */, + D231DCEC2C73356E00F3F66C /* TextFieldViewController.storyboard in Resources */, + D231DCCE2C73356E00F3F66C /* ImagePickerViewController.storyboard in Resources */, + D231DCC22C73356E00F3F66C /* CustomToolbarViewController.storyboard in Resources */, + D231DCC02C73356E00F3F66C /* CustomSearchBarViewController.storyboard in Resources */, + D231DCBD2C73356E00F3F66C /* Credits.rtf in Resources */, + D231DCD42C73356E00F3F66C /* Localizable.strings in Resources */, + D231DCE02C73356E00F3F66C /* SegmentedControlViewController.storyboard in Resources */, + D231DCF02C73356E00F3F66C /* TintedToolbarViewController.storyboard in Resources */, + D231DCDD2C73356E00F3F66C /* ProgressViewController.storyboard in Resources */, + D231DCB42C73356E00F3F66C /* Assets.xcassets in Resources */, + D231DCE82C73356E00F3F66C /* SwitchViewController.storyboard in Resources */, + D231DCB12C73356E00F3F66C /* AlertControllerViewController.storyboard in Resources */, + D231DCEE2C73356E00F3F66C /* TextViewController.storyboard in Resources */, + D231DCB62C73356E00F3F66C /* ButtonViewController.storyboard in Resources */, + D231DCBA2C73356E00F3F66C /* ColorPickerViewController.storyboard in Resources */, + D231DCE42C73356E00F3F66C /* StackViewController.storyboard in Resources */, + D231DCCA2C73356E00F3F66C /* DefaultToolbarViewController.storyboard in Resources */, + D231DCE22C73356E00F3F66C /* SliderViewController.storyboard in Resources */, + D231DCCC2C73356E00F3F66C /* FontPickerViewController.storyboard in Resources */, + D231DCC82C73356E00F3F66C /* DefaultSearchBarViewController.storyboard in Resources */, + D231DCF52C73356E00F3F66C /* WebViewController.storyboard in Resources */, + D231DCD02C73356E00F3F66C /* ImageViewController.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29F754B2C4AA07E00288638 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D276069F2C514F37002D2A14 /* SessionReplay.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D231DC2D2C73355800F3F66C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D231DCDE2C73356E00F3F66C /* ProgressViewController.swift in Sources */, + D231DCF42C73356E00F3F66C /* VisualEffectViewController.swift in Sources */, + D231DCB02C73356E00F3F66C /* ActivityIndicatorViewController.swift in Sources */, + D231DCE12C73356E00F3F66C /* SegmentedControlViewController.swift in Sources */, + D231DCE72C73356E00F3F66C /* StepperViewController.swift in Sources */, + D231DCC32C73356E00F3F66C /* CustomToolbarViewController.swift in Sources */, + D231DCCF2C73356E00F3F66C /* ImagePickerViewController.swift in Sources */, + D231DCB82C73356E00F3F66C /* ButtonViewController+Configs.swift in Sources */, + D231DCB52C73356E00F3F66C /* BaseTableViewController.swift in Sources */, + D231DCB72C73356E00F3F66C /* ButtonViewController.swift in Sources */, + D231DCF12C73356E00F3F66C /* TintedToolbarViewController.swift in Sources */, + D231DCD72C73356E00F3F66C /* MenuButtonViewController.swift in Sources */, + D231DCB92C73356E00F3F66C /* CaseElement.swift in Sources */, + D231DCF92C7342D500F3F66C /* ModuleBundle.swift in Sources */, + D231DCDC2C73356E00F3F66C /* PointerInteractionButtonViewController.swift in Sources */, + D231DCBB2C73356E00F3F66C /* ColorPickerViewController.swift in Sources */, + D231DCBF2C73356E00F3F66C /* CustomPageControlViewController.swift in Sources */, + D231DCD12C73356E00F3F66C /* ImageViewController.swift in Sources */, + D231DCF62C73356E00F3F66C /* WebViewController.swift in Sources */, + D231DCE32C73356E00F3F66C /* SliderViewController.swift in Sources */, + D231DCE92C73356E00F3F66C /* SwitchViewController.swift in Sources */, + D231DCED2C73356E00F3F66C /* TextFieldViewController.swift in Sources */, + D231DCDA2C73356E00F3F66C /* PickerViewController.swift in Sources */, + D231DCC52C73356E00F3F66C /* DatePickerController.swift in Sources */, + D231DCD82C73356E00F3F66C /* OutlineViewController.swift in Sources */, + D231DCC92C73356E00F3F66C /* DefaultSearchBarViewController.swift in Sources */, + D231DCEF2C73356E00F3F66C /* TextViewController.swift in Sources */, + D231DCC72C73356E00F3F66C /* DefaultPageControlViewController.swift in Sources */, + D231DCB22C73356E00F3F66C /* AlertControllerViewController.swift in Sources */, + D231DCCD2C73356E00F3F66C /* FontPickerViewController.swift in Sources */, + D231DCC12C73356E00F3F66C /* CustomSearchBarViewController.swift in Sources */, + D231DCEB2C73356E00F3F66C /* SymbolViewController.swift in Sources */, + D231DCE52C73356E00F3F66C /* StackViewController.swift in Sources */, + D231DCCB2C73356E00F3F66C /* DefaultToolbarViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29F75492C4AA07E00288638 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D27606A42C514F37002D2A14 /* AppConfiguration.swift in Sources */, D29F75502C4AA07E00288638 /* AppDelegate.swift in Sources */, - D27606A22C514F37002D2A14 /* DefaultScenario.swift in Sources */, D27606A12C514F37002D2A14 /* SessionReplayScenario.swift in Sources */, + D24E15F32C776956005AE4E8 /* BenchmarkProfiler.swift in Sources */, + D24BFD472C6B916B00AB9604 /* SyntheticScenario.swift in Sources */, D27606A32C514F37002D2A14 /* Scenario.swift in Sources */, - D27606A02C514F37002D2A14 /* SessionReplayController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + D231DC362C73355800F3F66C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D231DC302C73355800F3F66C /* UIKitCatalog */; + targetProxy = D231DC352C73355800F3F66C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC402C73356D00F3F66C /* Base */, + ); + name = ActivityIndicatorViewController.storyboard; + sourceTree = ""; + }; + D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC432C73356D00F3F66C /* Base */, + ); + name = AlertControllerViewController.storyboard; + sourceTree = ""; + }; + D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC492C73356D00F3F66C /* Base */, + ); + name = ButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC4E2C73356D00F3F66C /* Base */, + ); + name = ColorPickerViewController.storyboard; + sourceTree = ""; + }; + D231DC522C73356D00F3F66C /* content.html */ = { + isa = PBXVariantGroup; + children = ( + D231DC512C73356D00F3F66C /* Base */, + ); + name = content.html; + sourceTree = ""; + }; + D231DC542C73356D00F3F66C /* Credits.rtf */ = { + isa = PBXVariantGroup; + children = ( + D231DC532C73356D00F3F66C /* Base */, + ); + name = Credits.rtf; + sourceTree = ""; + }; + D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC552C73356D00F3F66C /* Base */, + ); + name = CustomPageControlViewController.storyboard; + sourceTree = ""; + }; + D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC582C73356D00F3F66C /* Base */, + ); + name = CustomSearchBarViewController.storyboard; + sourceTree = ""; + }; + D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC5B2C73356D00F3F66C /* Base */, + ); + name = CustomToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC5E2C73356D00F3F66C /* Base */, + ); + name = DatePickerController.storyboard; + sourceTree = ""; + }; + D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC612C73356D00F3F66C /* Base */, + ); + name = DefaultPageControlViewController.storyboard; + sourceTree = ""; + }; + D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC642C73356D00F3F66C /* Base */, + ); + name = DefaultSearchBarViewController.storyboard; + sourceTree = ""; + }; + D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC672C73356D00F3F66C /* Base */, + ); + name = DefaultToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC6A2C73356D00F3F66C /* Base */, + ); + name = FontPickerViewController.storyboard; + sourceTree = ""; + }; + D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC6D2C73356D00F3F66C /* Base */, + ); + name = ImagePickerViewController.storyboard; + sourceTree = ""; + }; + D231DC712C73356D00F3F66C /* ImageViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC702C73356D00F3F66C /* Base */, + ); + name = ImageViewController.storyboard; + sourceTree = ""; + }; + D231DC782C73356D00F3F66C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D231DC772C73356D00F3F66C /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D231DC7A2C73356D00F3F66C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC792C73356D00F3F66C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC7B2C73356D00F3F66C /* Base */, + ); + name = MenuButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC802C73356D00F3F66C /* PickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC7F2C73356D00F3F66C /* Base */, + ); + name = PickerViewController.storyboard; + sourceTree = ""; + }; + D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC822C73356D00F3F66C /* Base */, + ); + name = PointerInteractionButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC852C73356D00F3F66C /* Base */, + ); + name = ProgressViewController.storyboard; + sourceTree = ""; + }; + D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC892C73356D00F3F66C /* Base */, + ); + name = SegmentedControlViewController.storyboard; + sourceTree = ""; + }; + D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC8C2C73356D00F3F66C /* Base */, + ); + name = SliderViewController.storyboard; + sourceTree = ""; + }; + D231DC902C73356D00F3F66C /* StackViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC8F2C73356D00F3F66C /* Base */, + ); + name = StackViewController.storyboard; + sourceTree = ""; + }; + D231DC932C73356D00F3F66C /* StepperViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC922C73356D00F3F66C /* Base */, + ); + name = StepperViewController.storyboard; + sourceTree = ""; + }; + D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC952C73356D00F3F66C /* Base */, + ); + name = SwitchViewController.storyboard; + sourceTree = ""; + }; + D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC982C73356D00F3F66C /* Base */, + ); + name = SymbolViewController.storyboard; + sourceTree = ""; + }; + D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC9B2C73356D00F3F66C /* Base */, + ); + name = TextFieldViewController.storyboard; + sourceTree = ""; + }; + D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC9E2C73356D00F3F66C /* Base */, + ); + name = TextViewController.storyboard; + sourceTree = ""; + }; + D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA12C73356D00F3F66C /* Base */, + ); + name = TintedToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA62C73356D00F3F66C /* Base */, + ); + name = VisualEffectViewController.storyboard; + sourceTree = ""; + }; + D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA92C73356D00F3F66C /* Base */, + ); + name = WebViewController.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + D231DC392C73355800F3F66C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + D231DC3A2C73355800F3F66C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + D231DC3B2C73355800F3F66C /* Synthetics */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Synthetics; + }; D27606B62C526925002D2A14 /* Synthetics */ = { isa = XCBuildConfiguration; buildSettings = { @@ -292,9 +1012,10 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CURRENT_PROJECT_VERSION = f34790fea; + CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -439,9 +1160,10 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CURRENT_PROJECT_VERSION = f34790fea; + CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -467,9 +1189,10 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CURRENT_PROJECT_VERSION = f34790fea; + CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -491,6 +1214,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D231DC3C2C73355800F3F66C /* Build configuration list for PBXNativeTarget "UIKitCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D231DC392C73355800F3F66C /* Debug */, + D231DC3A2C73355800F3F66C /* Release */, + D231DC3B2C73355800F3F66C /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D29F752B2C4A9EFA00288638 /* Build configuration list for PBXProject "BenchmarkTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 47396a3dc5..4795e49e7a 100644 --- a/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -50,6 +50,18 @@ ReferencedContainer = "container:BenchmarkTests.xcodeproj"> + + + + + + SpanExporterResultCode { - return .success - } - - public func export(metrics: [Metric], shouldCancel: (() -> Bool)?) -> MetricExporterResultCode { - return .success - } - - public func flush() -> SpanExporterResultCode { - return .success - } - - public func shutdown() { - - } -} - -#endif diff --git a/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift b/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift new file mode 100644 index 0000000000..598ce60239 --- /dev/null +++ b/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift @@ -0,0 +1,162 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import OpenTelemetrySdk + +enum MetricExporterError: Error { + case unsupportedMetric(aggregation: AggregationType, dataType: Any.Type) +} + +/// Replacement of otel `DatadogExporter` for metrics. +/// +/// This version does not store data to disk, it uploads to the intake directly. +/// Additionally, it does not crash. +final class MetricExporter: OpenTelemetrySdk.MetricExporter { + struct Configuration { + let apiKey: String + let version: String + } + + /// The type of metric. The available types are 0 (unspecified), 1 (count), 2 (rate), and 3 (gauge). Allowed enum values: 0,1,2,3 + enum MetricType: Int, Codable { + case unspecified = 0 + case count = 1 + case rate = 2 + case gauge = 3 + } + + /// https://docs.datadoghq.com/api/latest/metrics/#submit-metrics + internal struct Serie: Codable { + struct Point: Codable { + let timestamp: Int64 + let value: Double + } + + struct Resource: Codable { + let name: String + let type: String + } + + let type: MetricType + let interval: Int64? + let metric: String + let unit: String? + let points: [Point] + let resources: [Resource] + let tags: [String] + } + + let session: URLSession + let encoder = JSONEncoder() + let configuration: Configuration + + // swiftlint:disable force_unwrapping + let intake = URL(string: "https://api.datadoghq.com/api/v2/series")! + let prefix = "{ \"series\": [".data(using: .utf8)! + let separator = ",".data(using: .utf8)! + let suffix = "]}".data(using: .utf8)! + // swiftlint:enable force_unwrapping + + required init(configuration: Configuration) { + let sessionConfiguration: URLSessionConfiguration = .ephemeral + sessionConfiguration.urlCache = nil + self.session = URLSession(configuration: sessionConfiguration) + self.configuration = configuration + } + + func export(metrics: [Metric], shouldCancel: (() -> Bool)?) -> MetricExporterResultCode { + do { + let series = try metrics.map(transform) + try submit(series: series) + return.success + } catch { + return .failureNotRetryable + } + } + + /// Transforms otel `Metric` to Datadog `serie`. + /// + /// - Parameter metric: The otel metric + /// - Returns: The timeserie. + func transform(_ metric: Metric) throws -> Serie { + var tags: Set = [] + + let points: [Serie.Point] = try metric.data.map { data in + let timestamp = Int64(data.timestamp.timeIntervalSince1970) + + data.labels.forEach { tags.insert("\($0):\($1)") } + + switch data { + case let data as SumData: + return Serie.Point(timestamp: timestamp, value: data.sum) + case let data as SumData: + return Serie.Point(timestamp: timestamp, value: Double(data.sum)) + case let data as SummaryData: + return Serie.Point(timestamp: timestamp, value: data.sum) + case let data as SummaryData: + return Serie.Point(timestamp: timestamp, value: Double(data.sum)) +// case let data as HistogramData: +// return Serie.Point(timestamp: timestamp, value: Double(data.sum)) +// case let data as HistogramData: +// return Serie.Point(timestamp: timestamp, value: data.sum) + default: + throw MetricExporterError.unsupportedMetric( + aggregation: metric.aggregationType, + dataType: type(of: data) + ) + } + } + + return Serie( + type: MetricType(metric.aggregationType), + interval: nil, + metric: metric.name, + unit: nil, + points: points, + resources: [], + tags: Array(tags) + ) + } + + /// Submit timeseries to the Metrics intake. + /// + /// - Parameter series: The timeseries. + func submit(series: [Serie]) throws { + var data = try series.reduce(Data()) { data, serie in + try data + encoder.encode(serie) + separator + } + + // remove last separator + data.removeLast(separator.count) + + var request = URLRequest(url: intake) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ + "Content-Type": "application/json", + "DD-API-KEY": configuration.apiKey, + "DD-EVP-ORIGIN": "ios", + "DD-EVP-ORIGIN-VERSION": configuration.version, + "DD-REQUEST-ID": UUID().uuidString, + ] + + request.httpBody = prefix + data + suffix + session.dataTask(with: request).resume() + } +} + +private extension MetricExporter.MetricType { + init(_ type: OpenTelemetrySdk.AggregationType) { + switch type { + case .doubleSum, .intSum: + self = .count + case .intGauge, .doubleGauge: + self = .gauge + case .doubleSummary, .intSummary, .doubleHistogram, .intHistogram: + self = .unspecified + } + } +} diff --git a/BenchmarkTests/Benchmarks/Sources/Metrics.swift b/BenchmarkTests/Benchmarks/Sources/Metrics.swift new file mode 100644 index 0000000000..886329b7fa --- /dev/null +++ b/BenchmarkTests/Benchmarks/Sources/Metrics.swift @@ -0,0 +1,274 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import QuartzCore + +// The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too +// complex for the Swift C importer, so we have to define them ourselves. +let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) +let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(MemoryLayout.offset(of: \task_vm_info_data_t.min_address)! / MemoryLayout.size) + +internal enum MachError: Error { + case task_info(return: kern_return_t) + case task_threads(return: kern_return_t) + case thread_info(return: kern_return_t) +} + +/// Aggregate metric values and compute `min`, `max`, `sum`, `avg`, and `count`. +internal class MetricAggregator where T: Numeric { + internal struct Aggregation { + let min: T + let max: T + let sum: T + let count: Int + let avg: Double + } + + private var mutex = pthread_mutex_t() + private var _aggregation: Aggregation? + + var aggregation: Aggregation? { + pthread_mutex_lock(&mutex) + defer { pthread_mutex_unlock(&mutex) } + return _aggregation + } + + /// Resets the minimum frame rate to `nil`. + func reset() { + pthread_mutex_lock(&mutex) + _aggregation = nil + pthread_mutex_unlock(&mutex) + } + + deinit { + pthread_mutex_destroy(&mutex) + } +} + +extension MetricAggregator where T: BinaryInteger { + /// Records a `BinaryInteger` value. + /// + /// - Parameter value: The value to record. + func record(value: T) { + pthread_mutex_lock(&mutex) + _aggregation = _aggregation.map { + let sum = $0.sum + value + let count = $0.count + 1 + return Aggregation( + min: Swift.min($0.min, value), + max: Swift.max($0.max, value), + sum: sum, + count: count, + avg: Double(sum) / Double(count) + ) + } ?? Aggregation(min: value, max: value, sum: value, count: 1, avg: Double(value)) + pthread_mutex_unlock(&mutex) + } +} + +extension MetricAggregator where T: BinaryFloatingPoint { + /// Records a `BinaryFloatingPoint` value. + /// + /// - Parameter value: The value to record. + func record(value: T) { + pthread_mutex_lock(&mutex) + _aggregation = _aggregation.map { + let sum = $0.sum + value + let count = $0.count + 1 + return Aggregation( + min: Swift.min($0.min, value), + max: Swift.max($0.max, value), + sum: sum, + count: count, + avg: Double(sum) / Double(count) + ) + } ?? Aggregation(min: value, max: value, sum: value, count: 1, avg: Double(value)) + pthread_mutex_unlock(&mutex) + } +} + +/// Collect Memory footprint metric. +/// +/// Based on a timer, the `Memory` aggregator will periodically record the memory footprint. +internal final class Memory: MetricAggregator { + /// Dispatch source object for monitoring timer events. + private let timer: DispatchSourceTimer + + /// Create a `Memory` aggregator to periodically record the memory footprint on the + /// provided queue. + /// + /// By default, the timer is scheduled with 100 ms interval with 10 ms leeway. + /// + /// - Parameters: + /// - queue: The queue on which to execute the timer handler. + /// - interval: The timer interval, default to 100 ms. + /// - leeway: The timer leeway, default to 10 ms. + required init( + queue: DispatchQueue, + every interval: DispatchTimeInterval = .milliseconds(100), + leeway: DispatchTimeInterval = .milliseconds(10) + ) { + timer = DispatchSource.makeTimerSource(queue: queue) + super.init() + + timer.setEventHandler { [weak self] in + guard let self, let footprint = try? self.footprint() else { + return + } + + self.record(value: footprint) + } + + timer.schedule(deadline: .now(), repeating: interval, leeway: leeway) + timer.activate() + } + + deinit { + timer.cancel() + } + + /// Collects single sample of current memory footprint. + /// + /// The computation is based on https://developer.apple.com/forums/thread/105088 + /// It leverages recommended `phys_footprint` value, which returns values that are close to Xcode's _Memory Use_ + /// gauge and _Allocations Instrument_. + /// + /// - Returns: Current memory footprint in bytes, `throws` if failed to read. + private func footprint() throws -> Double { + var info = task_vm_info_data_t() + var count = TASK_VM_INFO_COUNT + let kr = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) + } + } + + guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else { + throw MachError.task_info(return: kr) + } + + return Double(info.phys_footprint) + } +} + +/// Collect CPU usage metric. +/// +/// Based on a timer, the `CPU` aggregator will periodically record the CPU usage. +internal final class CPU: MetricAggregator { + /// Dispatch source object for monitoring timer events. + private let timer: DispatchSourceTimer + + /// Create a `CPU` aggregator to periodically record the CPU usage on the + /// provided queue. + /// + /// By default, the timer is scheduled with 100 ms interval with 10 ms leeway. + /// + /// - Parameters: + /// - queue: The queue on which to execute the timer handler. + /// - interval: The timer interval, default to 100 ms. + /// - leeway: The timer leeway, default to 10 ms. + init( + queue: DispatchQueue, + every interval: DispatchTimeInterval = .milliseconds(100), + leeway: DispatchTimeInterval = .milliseconds(10) + ) { + self.timer = DispatchSource.makeTimerSource(queue: queue) + super.init() + + timer.setEventHandler { [weak self] in + guard let self, let usage = try? self.usage() else { + return + } + + self.record(value: usage) + } + + timer.schedule(deadline: .now(), repeating: interval, leeway: leeway) + timer.activate() + } + + deinit { + timer.cancel() + } + + /// Collect single sample of current cpu usage. + /// + /// The computation is based on https://gist.github.com/hisui/10004131#file-cpu-usage-cpp + /// It reads the `cpu_usage` from all thread to compute the application usage percentage. + /// + /// - Returns: The cpu usage of all threads. + private func usage() throws -> Double { + var threads_list: thread_act_array_t? + var threads_count = mach_msg_type_number_t() + let kr = withUnsafeMutablePointer(to: &threads_list) { + $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) { + task_threads(mach_task_self_, $0, &threads_count) + } + } + + guard kr == KERN_SUCCESS, let threads_list = threads_list else { + throw MachError.task_threads(return: kr) + } + + defer { + vm_deallocate(mach_task_self_, vm_address_t(bitPattern: threads_list), vm_size_t(Int(threads_count) * MemoryLayout.stride)) + } + + return try (0.. { + private class CADisplayLinker { + weak var fps: FPS? + + init() { } + + @objc func tick(link: CADisplayLink) { + guard let fps else { + return + } + + let rate = 1 / (link.targetTimestamp - link.timestamp) + fps.record(value: lround(rate)) + } + } + + private var displayLink: CADisplayLink + + override init() { + let linker = CADisplayLinker() + displayLink = CADisplayLink(target: linker, selector: #selector(CADisplayLinker.tick(link:))) + super.init() + + linker.fps = self + displayLink.add(to: RunLoop.main, forMode: .common) + } + + deinit { + displayLink.invalidate() + } +} diff --git a/BenchmarkTests/Makefile b/BenchmarkTests/Makefile index 9606e4187f..e0478c0f52 100644 --- a/BenchmarkTests/Makefile +++ b/BenchmarkTests/Makefile @@ -14,12 +14,24 @@ ifdef ARTIFACTS_PATH rm -rf "$(IPA_PATH)" endif +build: + @$(ECHO_SUBTITLE2) "make build" + set -eo pipefail; \ + DD_BENCHMARK=1 OTEL_SWIFT=1 xcodebuild \ + -project BenchmarkTests.xcodeproj \ + -scheme Runner \ + -sdk iphonesimulator \ + -configuration Release \ + -destination generic/platform=iOS\ Simulator \ + | xcbeautify + @$(ECHO_SUCCESS) "BenchmarkTests compiles" + archive: @:$(eval VERSION ?= $(CURRENT_GIT_COMMIT_SHORT)) @$(ECHO_SUBTITLE2) "make archive VERSION='$(VERSION)'" @xcrun agvtool new-version "$(VERSION)" set -eo pipefail; \ - OTEL_SWIFT=1 xcodebuild \ + DD_BENCHMARK=1 OTEL_SWIFT=1 xcodebuild \ -project BenchmarkTests.xcodeproj \ -scheme Runner \ -sdk iphoneos \ @@ -58,4 +70,4 @@ upload: open: @$(ECHO_SUBTITLE2) "make open" - @open --new --env OTEL_SWIFT BenchmarkTests.xcodeproj + @open --env DD_BENCHMARK --env OTEL_SWIFT --new BenchmarkTests.xcodeproj diff --git a/BenchmarkTests/README.md b/BenchmarkTests/README.md index 3d40e49f35..38c1e6fcbc 100644 --- a/BenchmarkTests/README.md +++ b/BenchmarkTests/README.md @@ -5,7 +5,7 @@ ## CI -CI continuously builds, signs, and uploads a runner application to Synthetics which runs predefined tests. +CI continuously builds, signs, and uploads a runner application to Synthetics, which runs predefined tests. ### Build @@ -52,7 +52,15 @@ import DatadogLogs struct LogsScenario: Scenario { - func start(info: TestInfo) -> UIViewController { + /// The initial view-controller of the scenario + let initialViewController: UIViewController = LoggerViewController() + + /// Start instrumenting the application by enabling the Datadog SDK and + /// its Features. + /// + /// - Parameter info: The application information to use during SDK + /// initialisation. + func instrument(with info: AppInfo) { Datadog.initialize( with: .benchmark(info: info), // SDK init with the benchmark configuration @@ -60,13 +68,11 @@ struct LogsScenario: Scenario { ) Logs.enable() - - return LoggerViewController() } } ``` -Add the test to the [`SyntheticScenario`](Runner/Scenarios/Scenario.swift) enumeration so it can be selected, either manually or by setting the `BENCHMARK_SCENARIO` environment variable. +Add the test to the [`SyntheticScenario`](Runner/Scenarios/SyntheticScenario.swift#L12) object so it can be selected by setting the `BENCHMARK_SCENARIO` environment variable. ### Synthetics Configuration diff --git a/BenchmarkTests/Runner/AppConfiguration.swift b/BenchmarkTests/Runner/AppConfiguration.swift index d6e13ccdec..be251360c6 100644 --- a/BenchmarkTests/Runner/AppConfiguration.swift +++ b/BenchmarkTests/Runner/AppConfiguration.swift @@ -8,7 +8,7 @@ import Foundation import DatadogInternal import DatadogCore -/// Test info reads configuration from `Info.plist`. +/// Application info reads configuration from `Info.plist`. /// /// The expected format is as follow: /// @@ -27,7 +27,7 @@ import DatadogCore /// $(DD_SITE) /// /// -struct TestInfo: Decodable { +struct AppInfo: Decodable { let clientToken: String let applicationID: String let apiKey: String @@ -43,7 +43,7 @@ struct TestInfo: Decodable { } } -extension TestInfo { +extension AppInfo { init(bundle: Bundle = .main) throws { let decoder = AnyDecoder() let obj = bundle.object(forInfoDictionaryKey: "DatadogConfiguration") @@ -51,7 +51,7 @@ extension TestInfo { } } -extension TestInfo { +extension AppInfo { static var empty: Self { .init( clientToken: "", @@ -66,7 +66,7 @@ extension TestInfo { extension DatadogSite: Decodable {} extension Datadog.Configuration { - static func benchmark(info: TestInfo) -> Self { + static func benchmark(info: AppInfo) -> Self { .init( clientToken: info.clientToken, env: info.env, diff --git a/BenchmarkTests/Runner/AppDelegate.swift b/BenchmarkTests/Runner/AppDelegate.swift index 212ec75bad..88ea3657af 100644 --- a/BenchmarkTests/Runner/AppDelegate.swift +++ b/BenchmarkTests/Runner/AppDelegate.swift @@ -5,20 +5,85 @@ */ import UIKit +import DatadogInternal +import DatadogBenchmarks @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let info = try! TestInfo() // crash if test info are missing or malformed + guard + let scenario = SyntheticScenario(), + let run = SyntheticRun() + else { + return false + } - let scenario: Scenario = SyntheticScenario() ?? DefaultScenario() + let applicationInfo = try! AppInfo() // crash if info are missing or malformed + + switch run { + case .baseline, .instrumented: + // measure metrics during baseline and metrics runs + Benchmarks.enableMetrics( + with: Benchmarks.Configuration( + info: applicationInfo, + scenario: scenario, + run: run + ) + ) + case .profiling: + // Collect traces during profiling run + Benchmarks.enableTracer( + with: Benchmarks.Configuration( + info: applicationInfo, + scenario: scenario, + run: run + ) + ) + + DatadogInternal.profiler = Profiler() + break + } + + if run != .baseline { + // instrument the application with Datadog SDK + // when not in baseline run + scenario.instrument(with: applicationInfo) + } window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = scenario.start(info: info) + window?.rootViewController = scenario.initialViewController window?.makeKeyAndVisible() return true } } + +extension Benchmarks.Configuration { + init( + info: AppInfo, + scenario: SyntheticScenario, + run: SyntheticRun, + bundle: Bundle = .main, + sysctl: SysctlProviding = Sysctl(), + device: UIDevice = .current + ) { + self.init( + clientToken: info.clientToken, + apiKey: info.apiKey, + context: Benchmarks.Configuration.Context( + applicationIdentifier: bundle.bundleIdentifier!, + applicationName: bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as! String, + applicationVersion: bundle.object(forInfoDictionaryKey: "CFBundleVersion") as! String, + sdkVersion: "", + deviceModel: try! sysctl.model(), + osName: device.systemName, + osVersion: device.systemVersion, + run: run.rawValue, + scenario: scenario.name.rawValue, + branch: "" + ) + ) + } +} diff --git a/BenchmarkTests/Runner/BenchmarkProfiler.swift b/BenchmarkTests/Runner/BenchmarkProfiler.swift new file mode 100644 index 0000000000..8030f5b506 --- /dev/null +++ b/BenchmarkTests/Runner/BenchmarkProfiler.swift @@ -0,0 +1,26 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +import DatadogInternal +import DatadogBenchmarks + +internal final class Profiler: DatadogInternal.BenchmarkProfiler { + func tracer(operation: @autoclosure () -> String) -> any DatadogInternal.BenchmarkTracer { + DummyTracer() + } +} + +internal final class DummyTracer: DatadogInternal.BenchmarkTracer { + func startSpan(named: @autoclosure () -> String) -> any DatadogInternal.BenchmarkSpan { + DummySpan() + } +} + +internal final class DummySpan: DatadogInternal.BenchmarkSpan { + func stop() { } +} diff --git a/BenchmarkTests/Runner/Scenarios/DefaultScenario.swift b/BenchmarkTests/Runner/Scenarios/DefaultScenario.swift deleted file mode 100644 index d6760fed82..0000000000 --- a/BenchmarkTests/Runner/Scenarios/DefaultScenario.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation -import UIKit -import SwiftUI - -/// The default scenario will present the list of Synthetic scenarios to run in development mode. -/// To skip this screen, you can set the `E2E_SCENARIO` environment variable with the name -/// the desired scenario. -struct DefaultScenario: Scenario { - func start(info: TestInfo) -> UIViewController { - UIHostingController(rootView: ContentView(info: info)) - } - - struct ContentView: View { - let info: TestInfo - - var body: some View { - NavigationView { - List(SyntheticScenario.allCases, id: \.rawValue) { scenario in - NavigationLink { - ScenarioView(info: info, scenario: scenario) - } label: { - Text(scenario.rawValue) - } - } - .navigationBarTitle("Scenarios") - } - } - } - - struct ScenarioView: UIViewControllerRepresentable { - let info: TestInfo - let scenario: Scenario - - func makeUIViewController(context: Context) -> UIViewController { - scenario.start(info: info) - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - } -} - -#if DEBUG -struct DefaultScenario_Previews: PreviewProvider { - static var previews: some View { - DefaultScenario.ContentView(info: .empty) - } -} -#endif diff --git a/BenchmarkTests/Runner/Scenarios/Scenario.swift b/BenchmarkTests/Runner/Scenarios/Scenario.swift index 7a5abd9f85..b844e8ce07 100644 --- a/BenchmarkTests/Runner/Scenarios/Scenario.swift +++ b/BenchmarkTests/Runner/Scenarios/Scenario.swift @@ -7,61 +7,18 @@ import Foundation import UIKit -/// A `Scenario` is the entry-point of the E2E runner application. +/// A `Scenario` is the entry-point of the Benchmark Runner Application. /// /// The compliant objects are responsible for initializing the SDK, enabling -/// Features, and create the root view-controller. +/// Features, and create the initial view-controller. protocol Scenario { - /// Starts the scenario. - /// - /// Starting the scenario should intialize the SDK and enable Features based on - /// the provided ``TestInfo`` and scenario's needs. - /// - /// The returned view-controller will be used as the root view controller of the - /// application window. - /// - /// - Parameter info: The test info for configuring the SDK. - /// - Returns: The root view-controller. - func start(info: TestInfo) -> UIViewController -} - -/// A Synthetic scenario can be initialized by defining a Synthetic Test Process Argument -/// named `BENCHMARK_SCENARIO`. -/// -/// Note: The raw value of enum case must match the test name defined in Synthetics. -enum SyntheticScenario: String, CaseIterable { - case sessionReplay - - /// Creates the scenario defined by the`BENCHMARK_SCENARIO` environment variable. - /// - /// - Parameter processInfo: The process info holding the environment variables. - init?(processInfo: ProcessInfo = .processInfo) { - guard - processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == nil, // skip SwiftUI preview - let rawValue = processInfo.environment["BENCHMARK_SCENARIO"], - let scenario = Self(rawValue: rawValue) - else { - return nil - } - - self = scenario - } - - /// Returns the scenario defined by the environment variable. - var scenario: Scenario { - switch self { - case .sessionReplay: - return SessionReplayScenario() - } - } -} + /// The initial view-controller of the scenario + var initialViewController: UIViewController { get } -extension SyntheticScenario: Scenario { - /// Starts the underlying scenario. + /// Start instrumenting the application by enabling the Datadog SDK and + /// its Features. /// - /// - Parameter info: The test info for configuring the SDK. - /// - Returns: The root view-controller. - func start(info: TestInfo) -> UIViewController { - scenario.start(info: info) - } + /// - Parameter info: The application information to use during SDK + /// initialisation. + func instrument(with info: AppInfo) } diff --git a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplay.storyboard b/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplay.storyboard deleted file mode 100644 index 53add6285f..0000000000 --- a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplay.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift b/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift index 6b3b33ff96..65d116c8ff 100644 --- a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift +++ b/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift @@ -11,8 +11,15 @@ import DatadogCore import DatadogRUM import DatadogSessionReplay +import UIKitCatalog + struct SessionReplayScenario: Scenario { - func start(info: TestInfo) -> UIViewController { + var initialViewController: UIViewController { + let storyboard = UIStoryboard(name: "Main", bundle: UIKitCatalog.bundle) + return storyboard.instantiateInitialViewController()! + } + + func instrument(with info: AppInfo) { Datadog.initialize( with: .benchmark(info: info), trackingConsent: .granted @@ -34,8 +41,5 @@ struct SessionReplayScenario: Scenario { ) RUMMonitor.shared().addAttribute(forKey: "scenario", value: "SessionReplay") - - let storyboard = UIStoryboard(name: "SessionReplay", bundle: nil) - return storyboard.instantiateInitialViewController()! } } diff --git a/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift b/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift new file mode 100644 index 0000000000..488da6328f --- /dev/null +++ b/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import UIKit + +/// The Synthetics Scenario reads the `BENCHMARK_SCENARIO` environment +/// variable to instantiate a `Scenario` compliant object. +internal struct SyntheticScenario: Scenario { + /// The Synthetics benchmark scenario value. + internal enum Name: String { + case sessionReplay + } + /// The scenario's name. + let name: Name + + /// The underlying scenario. + private let _scenario: Scenario + + /// Creates the scenario by reading the `BENCHMARK_SCENARIO` value from the + /// environment variables. + /// + /// - Parameter processInfo: The `ProcessInfo` with environment variables + /// configured + init?(processInfo: ProcessInfo = .processInfo) { + guard + let rawValue = processInfo.environment["BENCHMARK_SCENARIO"], + let name = Name(rawValue: rawValue) + else { + return nil + } + + switch name { + case .sessionReplay: + _scenario = SessionReplayScenario() + } + + self.name = name + } + + var initialViewController: UIViewController { + _scenario.initialViewController + } + + func instrument(with info: AppInfo) { + _scenario.instrument(with: info) + } +} + +/// The Synthetics benchmark run. +/// +/// The run specifies the execution context of a benchmark scenrio. +/// Each execution will collect different type of benchmarking data: +/// - The `baseline` run collects various metrics during the scenario execution **without** +/// the Datadog SDK being initialised. +/// - The `instrumented` run collects the same metrics as `baseline` but **with** the +/// Datadog SDK initialised. Comparing the `baseline` and `instrumented` runs will provide +/// the overhead of the SDK for each metric. +/// - The `profiling` run will only collect traces of the SDK internal processes. +internal enum SyntheticRun: String { + case baseline + case instrumented + case profiling + + /// Creates the scenario by reading the `BENCHMARK_RUN` value from the + /// environment variables. + /// + /// - Parameter processInfo: The `ProcessInfo` with environment variables + /// configured + init?(processInfo: ProcessInfo = .processInfo) { + guard + let rawValue = processInfo.environment["BENCHMARK_RUN"], + let run = Self(rawValue: rawValue) + else { + return nil + } + + self = run + } +} diff --git a/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift b/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift new file mode 100755 index 0000000000..dfa876cbc1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift @@ -0,0 +1,81 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIActivityIndicatorView`. +*/ + +import UIKit + +class ActivityIndicatorViewController: BaseTableViewController { + + // Cell identifier for each activity indicator table view cell. + enum ActivityIndicatorKind: String, CaseIterable { + case mediumIndicator + case largeIndicator + case mediumTintedIndicator + case largeTintedIndicator + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.mediumIndicator.rawValue, + configHandler: configureMediumActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.largeIndicator.rawValue, + configHandler: configureLargeActivityIndicatorView) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted activity indicators available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumTintedIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.mediumTintedIndicator.rawValue, + configHandler: configureMediumTintedActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeTintedIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.largeTintedIndicator.rawValue, + configHandler: configureLargeTintedActivityIndicatorView) + ]) + } + } + + // MARK: - Configuration + + func configureMediumActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureMediumTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + +} diff --git a/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift b/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift new file mode 100755 index 0000000000..40ae167374 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift @@ -0,0 +1,317 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +The view controller that demonstrates how to use `UIAlertController`. +*/ + +import UIKit + +class AlertControllerViewController: UITableViewController { + // MARK: - Properties + + weak var secureTextAlertAction: UIAlertAction? + + private enum StyleSections: Int { + case alertStyleSection = 0 + case actionStyleSection + } + + private enum AlertStyleTest: Int { + // Alert style alerts. + case showSimpleAlert = 0 + case showOkayCancelAlert + case showOtherAlert + case showTextEntryAlert + case showSecureTextEntryAlert + } + + private enum ActionSheetStyleTest: Int { + // Action sheet style alerts. + case showOkayCancelActionSheet = 0 + case howOtherActionSheet + } + + private var textDidChangeObserver: Any? = nil + + // MARK: - UIAlertControllerStyleAlert Style Alerts + + /// Show an alert with an "OK" button. + func showSimpleAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the action. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The simple alert's cancel action occurred.") + } + + // Add the action. + alertController.addAction(cancelAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show an alert with an "OK" and "Cancel" button. + func showOkayCancelAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertCotroller = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's other action occurred.") + } + + // Add the actions. + alertCotroller.addAction(cancelAction) + alertCotroller.addAction(otherAction) + + present(alertCotroller, animated: true, completion: nil) + } + + /// Show an alert with two custom buttons. + func showOtherAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitleOne = NSLocalizedString("Choice One", bundle: .module, comment: "") + let otherButtonTitleTwo = NSLocalizedString("Choice Two", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Other\" alert's cancel action occurred.") + } + + let otherButtonOneAction = UIAlertAction(title: otherButtonTitleOne, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button one action occurred.") + } + + let otherButtonTwoAction = UIAlertAction(title: otherButtonTitleTwo, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button two action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherButtonOneAction) + alertController.addAction(otherButtonTwoAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a text entry alert with two custom buttons. + func showTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for text entry. + alertController.addTextField { _ in + // If you need to customize the text field, you can do so here. + } + + // Create the actions. + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Text Entry\" alert's cancel action occurred.") + } + + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Text Entry\" alert's other action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a secure text entry alert with two custom buttons. + func showSecureTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for the secure text entry. + alertController.addTextField { textField in + if let observer = self.textDidChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + /** Listen for changes to the text field's text so that we can toggle the current + action's enabled property based on whether the user has entered a sufficiently + secure entry. + */ + self.textDidChangeObserver = + NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, + object: textField, + queue: OperationQueue.main, + using: { (notification) in + if let textField = notification.object as? UITextField { + // Enforce a minimum length of >= 5 characters for secure text alerts. + if let alertAction = self.secureTextAlertAction { + if let text = textField.text { + alertAction.isEnabled = text.count >= 5 + } else { + alertAction.isEnabled = false + } + } + } + }) + + textField.isSecureTextEntry = true + } + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's other action occurred.") + } + + /** The text field initially has no text in the text field, so we'll disable it for now. + It will be re-enabled when the first character is typed. + */ + otherAction.isEnabled = false + + /** Hold onto the secure text alert action to toggle the enabled / disabled + state when the text changed. + */ + secureTextAlertAction = otherAction + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + // MARK: - UIAlertControllerStyleActionSheet Style Alerts + + // Show a dialog with an "OK" and "Cancel" button. + func showOkayCancelActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let destructiveButtonTitle = NSLocalizedString("Confirm", bundle: .module, comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert action sheet's cancel action occurred.") + } + + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Confirm\" alert action sheet's destructive action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(destructiveAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } + + // Show a dialog with two custom buttons. + func showOtherActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let destructiveButtonTitle = NSLocalizedString("Destructive Choice", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("Safe Choice", bundle: .module, comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .destructive) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's destructive action occurred.") + } + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's other action occurred.") + } + + // Add the actions. + alertController.addAction(destructiveAction) + alertController.addAction(otherAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } + +} + +// MARK: - UITableViewDelegate + +extension AlertControllerViewController { + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case StyleSections.alertStyleSection.rawValue: + // Alert style. + switch indexPath.row { + case AlertStyleTest.showSimpleAlert.rawValue: + showSimpleAlert() + case AlertStyleTest.showOkayCancelAlert.rawValue: + showOkayCancelAlert() + case AlertStyleTest.showOtherAlert.rawValue: + showOtherAlert() + case AlertStyleTest.showTextEntryAlert.rawValue: + showTextEntryAlert() + case AlertStyleTest.showSecureTextEntryAlert.rawValue: + showSecureTextEntryAlert() + default: break + } + case StyleSections.actionStyleSection.rawValue: + switch indexPath.row { + // Action sheet style. + case ActionSheetStyleTest.showOkayCancelActionSheet.rawValue: + showOkayCancelActionSheet(indexPath) + case ActionSheetStyleTest.howOtherActionSheet.rawValue: + showOtherActionSheet(indexPath) + default: break + } + default: break + } + + tableView.deselectRow(at: indexPath, animated: true) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000000..d8db8d65fd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json new file mode 100755 index 0000000000..73c00596a7 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json new file mode 100755 index 0000000000..4e892e1870 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png new file mode 100755 index 0000000000..b4b3b382c4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json new file mode 100755 index 0000000000..f58b0f113b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png new file mode 100755 index 0000000000..149520fb4d Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json new file mode 100755 index 0000000000..5e6240639e --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png new file mode 100755 index 0000000000..c65e3961d8 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png new file mode 100755 index 0000000000..6e68c5bd05 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png new file mode 100755 index 0000000000..be149037da Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json new file mode 100755 index 0000000000..fdb1b66722 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png new file mode 100755 index 0000000000..7abdc2bcb4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png new file mode 100755 index 0000000000..0580445308 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png new file mode 100755 index 0000000000..29805f326e Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json new file mode 100755 index 0000000000..bca57e87be --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png new file mode 100755 index 0000000000..c623650ddc Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png new file mode 100755 index 0000000000..2a9ee5c1c4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png new file mode 100755 index 0000000000..cf0a17a548 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json new file mode 100755 index 0000000000..68464e93ab --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "search_bar_bg_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_bg_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png new file mode 100755 index 0000000000..486f5413bb Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png new file mode 100755 index 0000000000..d20a0bb6e7 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png new file mode 100755 index 0000000000..88ecb2f12d Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json new file mode 100755 index 0000000000..ea6fe64740 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_blue_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png new file mode 100755 index 0000000000..3f10475947 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png new file mode 100755 index 0000000000..7ba3616579 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png new file mode 100755 index 0000000000..7f47c6e305 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json new file mode 100755 index 0000000000..bad86401df --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_green_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png new file mode 100755 index 0000000000..dd6087d24a Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png new file mode 100755 index 0000000000..5c6cd69e86 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png new file mode 100755 index 0000000000..75a6915a89 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json new file mode 100644 index 0000000000..86976ae85a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stepper_and_segment_segment_divider_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stepper_and_segment_segment_divider_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stepper_and_segment_divider_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png new file mode 100644 index 0000000000..1aabd6a584 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png new file mode 100644 index 0000000000..2d092bd7a4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png new file mode 100644 index 0000000000..168bdfd472 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json new file mode 100755 index 0000000000..7162851034 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json @@ -0,0 +1,45 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_1x.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png new file mode 100755 index 0000000000..5c3c3cf6a5 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png new file mode 100755 index 0000000000..abf9f0a012 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png new file mode 100755 index 0000000000..b121f9db65 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json new file mode 100755 index 0000000000..64a5b15a81 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png new file mode 100755 index 0000000000..c450af9689 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png new file mode 100755 index 0000000000..e81719e878 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png new file mode 100755 index 0000000000..2957cbb6d3 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json new file mode 100755 index 0000000000..fb8876d7a1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Sunset_5.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png new file mode 100755 index 0000000000..3ce67dff32 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json new file mode 100755 index 0000000000..e36b88e424 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "0.000", + "blue" : "0.000", + "green" : "0.000" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json new file mode 100755 index 0000000000..479569c484 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json new file mode 100755 index 0000000000..479569c484 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json new file mode 100755 index 0000000000..1756a035cc --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "toolbar_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png new file mode 100755 index 0000000000..f37907ff93 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png new file mode 100755 index 0000000000..a271d28de7 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png new file mode 100755 index 0000000000..486f5413bb Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard new file mode 100755 index 0000000000..40c0d74348 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard new file mode 100755 index 0000000000..e52293c5a6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard new file mode 100755 index 0000000000..0076e70c5c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard new file mode 100755 index 0000000000..55e9ee6d73 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf b/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf new file mode 100755 index 0000000000..c9f3ebb74f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf @@ -0,0 +1,10 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2617 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} +{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} +\vieww9000\viewh8400\viewkind0 +\pard\tx960\tx1920\tx2880\tx3840\tx4800\tx5760\tx6720\tx7680\tx8640\tx9600\qc\partightenfactor0 + +\f0\fs20 \cf2 Demonstrates how to use {\field{\*\fldinst{HYPERLINK "https://developer.apple.com/documentation/uikit"}}{\fldrslt UIKit}}\ +views, controls and pickers.\ +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard new file mode 100755 index 0000000000..6b60d8eb4b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard new file mode 100755 index 0000000000..bd83634c34 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard new file mode 100755 index 0000000000..80366b55e4 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard new file mode 100755 index 0000000000..2d752ae3c4 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard new file mode 100755 index 0000000000..aac4dffef5 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard new file mode 100755 index 0000000000..3053676020 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + Title + Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard new file mode 100755 index 0000000000..248ff5042a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard new file mode 100755 index 0000000000..b28c89f0a7 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard new file mode 100755 index 0000000000..c0c74769df --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard new file mode 100755 index 0000000000..886c071308 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings b/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings new file mode 100755 index 0000000000..78c04666cf --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings @@ -0,0 +1,173 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Strings used across the application via the NSLocalizedString API. +*/ + +"OK" = "OK"; +"Cancel" = "Cancel"; +"Confirm" = "Confirm"; +"Destructive Choice" = "Destructive Choice"; +"Safe Choice" = "Safe Choice"; +"A Short Title Is Best" = "A Short Title Is Best"; +"A message needs to be a short, complete sentence." = "A message needs to be a short, complete sentence."; +"Choice One" = "Choice One"; +"Choice Two" = "Choice Two"; +"Button" = "Button"; +"Pressed" = "Pressed"; +"X Button" = "X Button"; +"Image" = "Image"; +"bold" = "bold"; +"highlighted" = "highlighted"; +"underlined" = "underlined"; +"tinted" = "tinted"; +"Placeholder text" = "Placeholder text"; +"Enter search text" = "Enter search text"; +"Red color component value" = "Red color component value"; +"Green color component value" = "Green color component value"; +"Blue color component value" = "Blue color component value"; +"Animated" = "A slide show of images"; + +"Airplane" = "Airplane"; +"Gift" = "Gift"; +"Burst" = "Burst"; + +"An error occurred:" = "An error occurred:"; + +"ButtonsTitle" = "Buttons"; +"MenuButtonsTitle" = "Menu Buttons"; +"PointerInteractionButtonsTitle" = "Pointer Interaction"; +"PageControlTitle" = "Page Controls"; +"SearchBarsTitle" = "Search Bars"; +"SegmentedControlsTitle" = "Segmented Controls"; +"SlidersTitle" = "Sliders"; +"SteppersTitle" = "Steppers"; +"SwitchesTitle" = "Switches"; +"TextFieldsTitle" = "Text Fields"; + +"ActivityIndicatorsTitle" = "Activity Indicators"; +"AlertControllersTitle" = "Alert Controllers"; + +"ImagesTitle" = "Image Views"; +"ImageViewTitle" = "Image View"; +"SymbolsTitle" = "SF Symbol"; + +"ProgressViewsTitle" = "Progress Views"; +"StackViewsTitle" = "Stack Views"; +"TextViewTitle" = "Text View"; +"ToolbarsTitle" = "Toolbars"; +"VisualEffectTitle" = "Visual Effect"; +"WebViewTitle" = "Web View"; + +"DatePickerTitle" = "Date Picker"; +"PickerViewTitle" = "Picker View"; +"ColorPickerTitle" = "Color Picker"; +"FontPickerTitle" = "Font Picker"; +"ImagePickerTitle" = "Image Picker"; + +"DefaultSearchBarTitle" = "Default Search Bar"; +"CustomSearchBarTitle" = "Custom Search Bar"; + +"DefaultToolBarTitle" = "Default Toolbar"; +"TintedToolbarTitle" = "Tinted Toolbar"; +"CustomToolbarBarTitle" = "Custom Toolbar"; + +"ChooseItemTitle" = "Choose an item:"; +"ItemTitle" = "Item %@"; + +"SampleFontTitle" = "Sample Font"; + +"CheckTitle" = "Check"; +"SearchTitle" = "Search"; +"ToolsTitle" = "Tools"; + +"DefaultPageControlTitle" = "Page Control"; +"CustomPageControlTitle" = "Custom Page Control"; + +"SwitchTitle" = "Title"; + +"DefaultSwitchTitle" = "Default"; +"CheckboxSwitchTitle" = "Checkbox"; +"TintedSwitchTitle" = "Tinted"; + +"ImageToolTipTitle" = "This is a list of flower photos obtained from the sample's asset library."; +"GrayStyleButtonToolTipTitle" = "This is a gray-style system button."; +"TintedStyleButtonToolTipTitle" = "This is a tinted-style system button."; +"FilledStyleButtonToolTipTitle" = "This is a filled-style system button."; +"CapsuleStyleButtonToolTipTitle" = "This is a capsule-style system button."; +"CartFilledButtonToolTipTitle" = "Button cart is filled"; +"CartEmptyButtonToolTipTitle" = "Button cart is empty"; +"XButtonToolTipTitle" = "X Button"; +"PersonButtonToolTipTitle" = "Person Button"; +"VisualEffectToolTipTitle" = "This demonstrates how to use a UIVisualEffectView on top of an UIImageView and underneath a UITextView."; + +"VisualEffectTextContent" = "This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView."; + +"DefaultTitle" = "Default"; +"DetailDisclosureTitle" = "Detail Disclosure"; +"AddContactTitle" = "Add Contact"; +"CloseTitle" = "Close"; +"GrayTitle" = "Gray"; +"TintedTitle" = "Tinted"; +"FilledTitle" = "Filled"; +"CornerStyleTitle" = "Corner Style"; +"ToggleTitle" = "Toggle"; +"ButtonColorTitle" = "Colored Title"; + +"ImageTitle" = "Image"; +"AttributedStringTitle" = "Attributed String"; +"SymbolTitle" = "Symbol"; + +"LargeSymbolTitle" = "Large Symbol"; +"SymbolStringTitle" = "Symbol + String"; +"StringSymbolTitle" = "String + Symbol"; +"MultiTitleTitle" = "Multi-Title"; +"BackgroundTitle" = "Background"; + +"UpdateActivityHandlerTitle" = "Update Activity Handler"; +"UpdateHandlerTitle" = "Update Handler"; +"UpdateImageHandlerTitle" = "Update Handler (Button Image)"; + +"AddToCartTitle" = "Add to Cart"; + +"DropDownTitle" = "Drop Down"; +"DropDownProgTitle" = "Drop Down Programmatic"; +"DropDownMultiActionTitle" = "Drop Down Multi-Action"; +"DropDownButtonSubMenuTitle" = "Drop Down Submenu"; +"PopupSelection" = "Popup Selection"; +"PopupMenuTitle" = "Popup Menu"; + +"CustomSegmentsTitle" = "Custom Segments"; +"CustomBackgroundTitle" = "Custom Background"; +"ActionBasedTitle" = "Action Based"; + +"CustomTitle" = "Custom"; +"MinMaxImagesTitle" = "Min and Max Images"; + +"DefaultStepperTitle" = "Default Stepper"; +"TintedStepperTitle" = "Tinted Stepper"; +"CustomStepperTitle" = "Custom Stepper"; + +"PlainSymbolTitle" = "Default"; +"TintedSymbolTitle" = "Tinted"; +"LargeSymbolTitle" = "Large"; +"HierarchicalSymbolTitle" = "Hierarchical Color"; +"PaletteSymbolTitle" = "Palette Color"; +"PreferringMultiColorSymbolTitle" = "Preferring Multi-Color"; + +"DefaultTextFieldTitle" = "Default"; +"TintedTextFieldTitle" = "Tinted"; +"SecuretTextFieldTitle" = "Secure"; +"SpecificKeyboardTextFieldTitle" = "Specific Keyboard"; +"CustomTextFieldTitle" = "Custom"; +"SearchTextFieldTitle" = "Search"; + +"MediumIndicatorTitle" = "Medium"; +"LargeIndicatorTitle" = "Large"; +"MediumTintedIndicatorTitle" = "Medium Tinted"; +"LargeTintedIndicatorTitle" = "Large Tinted"; + +"ProgressDefaultTitle" = "Default"; +"ProgressBarTitle" = "Bar"; +"ProgressTintedTitle" = "Tinted"; diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard new file mode 100755 index 0000000000..afda7f7f21 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard new file mode 100755 index 0000000000..6e7ecf37c8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard new file mode 100755 index 0000000000..f5209519a6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard new file mode 100755 index 0000000000..664719dc74 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard new file mode 100755 index 0000000000..efc642095c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard new file mode 100755 index 0000000000..4166c5b05e --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard new file mode 100755 index 0000000000..4420a0f00a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard new file mode 100755 index 0000000000..a3b3d88723 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard new file mode 100755 index 0000000000..a28bc8f7d3 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard new file mode 100755 index 0000000000..69655b053b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard new file mode 100755 index 0000000000..cecdae8104 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard new file mode 100755 index 0000000000..3e2676b938 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard new file mode 100755 index 0000000000..1161aae7b8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + This is a UITextView that uses attributed text. You can programmatically modify the display of the text by making it bold, highlighted, underlined, tinted, symbols, and more. These attributes are defined in NSAttributedString.h. You can even embed attachments in an NSAttributedString! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard new file mode 100755 index 0000000000..b5b460b356 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard new file mode 100755 index 0000000000..12d43a517f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard new file mode 100755 index 0000000000..d335aaaa16 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/content.html b/BenchmarkTests/UIKitCatalog/Base.lproj/content.html new file mode 100755 index 0000000000..c2dc89958f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/content.html @@ -0,0 +1,16 @@ + + + + WKWebView + + + +
+

This is HTML content inside a WKWebView.

+ For more information refer to developer.apple.com + + diff --git a/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift b/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift new file mode 100644 index 0000000000..9320cdd193 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift @@ -0,0 +1,52 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A base class used for all UITableViewControllers in this sample app. +*/ + +import UIKit + +class BaseTableViewController: UITableViewController { + // List of table view cell test cases. + var testCells = [CaseElement]() + + func centeredHeaderView(_ title: String) -> UITableViewHeaderFooterView { + // Set the header title and make it centered. + let headerView: UITableViewHeaderFooterView = UITableViewHeaderFooterView() + var content = UIListContentConfiguration.groupedHeader() + content.text = title + content.textProperties.alignment = .center + headerView.contentConfiguration = content + return headerView + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return centeredHeaderView(testCells[section].title) + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return testCells[section].title + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return testCells.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let view = cellTest.targetView(cell) { + cellTest.configHandler(view) + } + return cell + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift b/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift new file mode 100755 index 0000000000..2de5fb0d6c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift @@ -0,0 +1,470 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Configuration functions for all the UIButtons found in ButtonViewController. +*/ + +import UIKit + +extension ButtonViewController: UIToolTipInteractionDelegate { + + func configureSystemTextButton(_ button: UIButton) { + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: []) + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + func configureSystemDetailDisclosureButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + func configureSystemContactAddButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureCloseButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleGrayButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + let config = UIButton.Configuration.gray() + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("GrayStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleTintedButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + + var config = UIButton.Configuration.tinted() + + /** To keep the look the same betwen iOS and macOS: + For tinted color to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + // The following will make the button title red and background a lighter red. + config.baseBackgroundColor = .systemRed + config.baseForegroundColor = .systemRed + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("TintedStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.configuration = config + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleFilledButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + var config = UIButton.Configuration.filled() + config.background.backgroundColor = .systemRed + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("FilledStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureCornerStyleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For cornerStyle to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + var config = UIButton.Configuration.gray() + config.cornerStyle = .capsule + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("CapsuleStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureImageButton(_ button: UIButton) { + // To create this button in code you can use `UIButton.init(type: .system)`. + + // Set the tint color to the button's image. + if let image = UIImage(systemName: "xmark") { + let imageButtonNormalImage = image.withTintColor(.systemPurple) + button.setImage(imageButtonNormalImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("X", bundle: .module, comment: "") + + if #available(iOS 15, *) { + button.toolTip = NSLocalizedString("XButtonToolTipTitle", bundle: .module, comment: "") + } + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureAttributedTextSystemButton(_ button: UIButton) { + let buttonTitle = NSLocalizedString("Button", bundle: .module, comment: "") + + // Set the button's title for normal state. + let normalTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue + ] + + let normalAttributedTitle = NSAttributedString(string: buttonTitle, attributes: normalTitleAttributes) + button.setAttributedTitle(normalAttributedTitle, for: .normal) + + // Set the button's title for highlighted state (note this is not supported in Mac Catalyst). + let highlightedTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.thick.rawValue + ] + let highlightedAttributedTitle = NSAttributedString(string: buttonTitle, attributes: highlightedTitleAttributes) + button.setAttributedTitle(highlightedAttributedTitle, for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = buttonImage + button.configuration = buttonConfig + + button.toolTip = NSLocalizedString("PersonButtonToolTipTitle", bundle: .module, comment: "") + } else { + button.setImage(buttonImage, for: .normal) + } + + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .large) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureLargeSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to change the size. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + buttonConfig.image = buttonImage + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolTextButton(_ button: UIButton) { + // Button with image to the left of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + buttonConfig.image = buttonImage + + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + + // Set up the symbol image size to match that of the title font size. + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .small) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureTextSymbolButton(_ button: UIButton) { + // Button with image to the right of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + + buttonConfig.image = buttonImage + + // Set the image placement to the right of the title. + /** For image placement to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + buttonConfig.imagePlacement = .trailing + + button.configuration = buttonConfig + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureMultiTitleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For setTitle(.highlighted) to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.setTitle(NSLocalizedString("Pressed", bundle: .module, comment: ""), for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureToggleButton(button: UIButton) { + button.changesSelectionAsPrimaryAction = true // This makes the button style a "toggle button". + } + + func configureTitleTextButton(_ button: UIButton) { + // Note: Only for iOS the title's color can be changed. + button.setTitleColor(UIColor.systemGreen, for: [.normal]) + button.setTitleColor(UIColor.systemRed, for: [.highlighted]) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureBackgroundButton(_ button: UIButton) { + if #available(iOS 15, *) { + /** To keep the look the same betwen iOS and macOS: + For setBackgroundImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + } + + button.setBackgroundImage(UIImage(named: "background", in: .module, compatibleWith: nil), for: .normal) + button.setBackgroundImage(UIImage(named: "background_highlighted", in: .module, compatibleWith: nil), for: .highlighted) + button.setBackgroundImage(UIImage(named: "background_disabled", in: .module, compatibleWith: nil), for: .disabled) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + // This handler is called when this button needs updating. + @available(iOS 15.0, *) + func configureUpdateActivityHandlerButton(_ button: UIButton) { + let activityUpdateHandler: (UIButton) -> Void = { button in + /// Shows an activity indicator in place of an image. Its placement is controlled by the `imagePlacement` property. + + // Start with the current button's configuration. + var config = button.configuration + config?.showsActivityIndicator = button.isSelected ? false : true + button.configuration = config + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "tray") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + button.configuration = buttonConfig + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = activityUpdateHandler + + // For this button to include an activity indicator next to the title, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.baseBackgroundColor = button.isSelected + ? UIColor.systemPink.withAlphaComponent(0.4) + : UIColor.systemPink + } + + let buttonConfig = UIButton.Configuration.filled() + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use baseBackgroundColor for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateImageHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.image = + button.isSelected ? UIImage(systemName: "cart.fill") : UIImage(systemName: "cart") + button.toolTip = + button.isSelected ? + NSLocalizedString("CartFilledButtonToolTipTitle", bundle: .module, comment: "") : + NSLocalizedString("CartEmptyButtonToolTipTitle", bundle: .module, comment: "") + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "cart") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use the updateHandler to change it's icon for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle("", for: []) // No title, just an image. + button.isSelected = false + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + // MARK: - Add To Cart Button + + @available(iOS 15.0, *) + func toolTipInteraction(_ interaction: UIToolTipInteraction, configurationAt point: CGPoint) -> UIToolTipConfiguration? { + let formatString = NSLocalizedString("Cart Tooltip String", + bundle: .module, + comment: "Cart Tooltip String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + return UIToolTipConfiguration(toolTip: resultString) + } + + @available(iOS 15.0, *) + func addToCart(action: UIAction) { + cartItemCount = cartItemCount > 0 ? 0 : 12 + if let button = action.sender as? UIButton { + button.setNeedsUpdateConfiguration() + } + } + + @available(iOS 15.0, *) + func configureAddToCartButton(_ button: UIButton) { + var config = UIButton.Configuration.filled() + config.buttonSize = .large + config.image = UIImage(systemName: "cart.fill") + config.title = "Add to Cart" + config.cornerStyle = .capsule + config.baseBackgroundColor = UIColor.systemTeal + button.configuration = config + + button.toolTip = "" // The value will be determined in its delegate. + button.toolTipInteraction?.delegate = self + + button.addAction(UIAction(handler: addToCart(action:)), for: .touchUpInside) + + // For this button to include subtitle and larger size, the behavioral style needs to be set to ".pad". + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + + // This handler is called when this button needs updating. + button.configurationUpdateHandler = { + [unowned self] button in + + // Start with the current button's configuration. + var newConfig = button.configuration + + if button.isSelected { + // The button was clicked or tapped. + newConfig?.image = cartItemCount > 0 + ? UIImage(systemName: "cart.fill.badge.plus") + : UIImage(systemName: "cart.badge.plus") + + let formatString = NSLocalizedString("Cart Items String", + bundle: .module, + comment: "Cart Items String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + newConfig?.subtitle = resultString + } else { + // As the button is highlighted (pressed), apply a temporary image and subtitle. + newConfig?.image = UIImage(systemName: "cart.fill") + newConfig?.subtitle = "" + } + + newConfig?.imagePadding = 8 // Add a litle more space between the icon and button title. + + // Note: To change the padding between the title and subtitle, set "titlePadding". + // Note: To change the padding around the perimeter of the button, set "contentInsets". + + button.configuration = newConfig + } + } + + // MARK: - Button Actions + + @objc + func buttonClicked(_ sender: UIButton) { + Swift.debugPrint("Button was clicked.") + } + + @objc + func toggleButtonClicked(_ sender: UIButton) { + Swift.debugPrint("Toggle action: \(sender)") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ButtonViewController.swift b/BenchmarkTests/UIKitCatalog/ButtonViewController.swift new file mode 100755 index 0000000000..0c9f0e1e48 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ButtonViewController.swift @@ -0,0 +1,156 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIButton`. + The buttons are created using storyboards, but each of the system buttons can be created in code by + using the UIButton.init(type buttonType: UIButtonType) initializer. + + See the UIButton interface for a comprehensive list of the various UIButtonType values. +*/ + +import UIKit + +class ButtonViewController: BaseTableViewController { + + // Cell identifier for each button table view cell. + enum ButtonKind: String, CaseIterable { + case buttonSystem + case buttonDetailDisclosure + case buttonSystemAddContact + case buttonClose + case buttonStyleGray + case buttonStyleTinted + case buttonStyleFilled + case buttonCornerStyle + case buttonToggle + case buttonTitleColor + case buttonImage + case buttonAttrText + case buttonSymbol + case buttonLargeSymbol + case buttonTextSymbol + case buttonSymbolText + case buttonMultiTitle + case buttonBackground + case addToCartButton + case buttonUpdateActivityHandler + case buttonUpdateHandler + case buttonImageUpdateHandler + } + + // MARK: - Properties + + // "Add to Cart" Button + var cartItemCount: Int = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSystem.rawValue, + configHandler: configureSystemTextButton), + CaseElement(title: NSLocalizedString("DetailDisclosureTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonDetailDisclosure.rawValue, + configHandler: configureSystemDetailDisclosureButton), + CaseElement(title: NSLocalizedString("AddContactTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSystemAddContact.rawValue, + configHandler: configureSystemContactAddButton), + CaseElement(title: NSLocalizedString("CloseTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonClose.rawValue, + configHandler: configureCloseButton) + ]) + + if #available(iOS 15, *) { + // These button styles are available on iOS 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("GrayTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleGray.rawValue, + configHandler: configureStyleGrayButton), + CaseElement(title: NSLocalizedString("TintedTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleTinted.rawValue, + configHandler: configureStyleTintedButton), + CaseElement(title: NSLocalizedString("FilledTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleFilled.rawValue, + configHandler: configureStyleFilledButton), + CaseElement(title: NSLocalizedString("CornerStyleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonCornerStyle.rawValue, + configHandler: configureCornerStyleButton), + CaseElement(title: NSLocalizedString("ToggleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonToggle.rawValue, + configHandler: configureToggleButton) + ]) + } + + if traitCollection.userInterfaceIdiom != .mac { + // Colored button titles only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ButtonColorTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonTitleColor.rawValue, + configHandler: configureTitleTextButton) + ]) + } + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ImageTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonImage.rawValue, + configHandler: configureImageButton), + CaseElement(title: NSLocalizedString("AttributedStringTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonAttrText.rawValue, + configHandler: configureAttributedTextSystemButton), + CaseElement(title: NSLocalizedString("SymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSymbol.rawValue, + configHandler: configureSymbolButton) + ]) + + if #available(iOS 15, *) { + // This case uses UIButtonConfiguration which is available on iOS 15 or later. + if traitCollection.userInterfaceIdiom != .mac { + // UIButtonConfiguration for large images available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("LargeSymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonLargeSymbol.rawValue, + configHandler: configureLargeSymbolButton) + ]) + } + } + + if #available(iOS 15, *) { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("StringSymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonTextSymbol.rawValue, + configHandler: configureTextSymbolButton), + CaseElement(title: NSLocalizedString("SymbolStringTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSymbolText.rawValue, + configHandler: configureSymbolTextButton), + + CaseElement(title: NSLocalizedString("BackgroundTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonBackground.rawValue, + configHandler: configureBackgroundButton), + + // Multi-title button: title for normal and highlight state, setTitle(.highlighted) is for iOS 15 and later. + CaseElement(title: NSLocalizedString("MultiTitleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonMultiTitle.rawValue, + configHandler: configureMultiTitleButton), + + // Various button effects done to the addToCartButton are available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("AddToCartTitle", bundle: .module, comment: ""), + cellID: ButtonKind.addToCartButton.rawValue, + configHandler: configureAddToCartButton), + + // UIButtonConfiguration with updateHandlers is available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("UpdateActivityHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonUpdateActivityHandler.rawValue, + configHandler: configureUpdateActivityHandlerButton), + CaseElement(title: NSLocalizedString("UpdateHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonUpdateHandler.rawValue, + configHandler: configureUpdateHandlerButton), + CaseElement(title: NSLocalizedString("UpdateImageHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonImageUpdateHandler.rawValue, + configHandler: configureUpdateImageHandlerButton) + ]) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CaseElement.swift b/BenchmarkTests/UIKitCatalog/CaseElement.swift new file mode 100644 index 0000000000..54f0ebdf74 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CaseElement.swift @@ -0,0 +1,29 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Test case element that serves our UITableViewCells. +*/ + +import UIKit + +struct CaseElement { + var title: String // Visual title of the cell (table section header title) + var cellID: String // Table view cell's identifier for searching for the cell within the nib file. + + typealias ConfigurationClosure = (UIView) -> Void + var configHandler: ConfigurationClosure // Configuration handler for setting up the cell's subview. + + init(title: String, cellID: String, configHandler: @escaping (V) -> Void) { + self.title = title + self.cellID = cellID + self.configHandler = { view in + guard let view = view as? V else { fatalError("Impossible") } + configHandler(view) + } + } + + func targetView(_ cell: UITableViewCell?) -> UIView? { + return cell != nil ? cell!.contentView.subviews[0] : nil + } +} diff --git a/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift b/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift new file mode 100755 index 0000000000..77838319a0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift @@ -0,0 +1,144 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIColorPickerViewController`. +*/ + +import UIKit + +class ColorPickerViewController: UIViewController, UIColorPickerViewControllerDelegate { + + // MARK: - Properties + + var colorWell: UIColorWell! + var colorPicker: UIColorPickerViewController! + + @IBOutlet var pickerButton: UIButton! // UIButton to present the picker. + @IBOutlet var pickerWellView: UIView! // UIView placeholder to hold the UIColorWell. + + @IBOutlet var colorView: UIView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureColorPicker() + configureColorWell() + + // For iOS, the picker button in the main view is not used, the color picker is presented from the navigation bar. + if navigationController?.traitCollection.userInterfaceIdiom != .mac { + pickerButton.isHidden = true + } + } + + // MARK: - UIColorWell + + // Update the color view from the color well chosen action. + func colorWellHandler(action: UIAction) { + if let colorWell = action.sender as? UIColorWell { + colorView.backgroundColor = colorWell.selectedColor + } + } + + func configureColorWell() { + + /** Note: Both color well and picker buttons achieve the same thing, presenting the color picker. + But one presents it with a color well control, the other by a bar button item. + */ + let colorWellAction = UIAction(title: "", handler: colorWellHandler) + colorWell = + UIColorWell(frame: CGRect(x: 0, y: 0, width: 32, height: 32), primaryAction: colorWellAction) + + // For Mac Catalyst, the UIColorWell is placed in the main view. + if navigationController?.traitCollection.userInterfaceIdiom == .mac { + pickerWellView.addSubview(colorWell) + } else { + // For iOS, the UIColorWell is placed inside the navigation bar as a UIBarButtonItem. + let colorWellBarItem = UIBarButtonItem(customView: colorWell) + let fixedBarItem = UIBarButtonItem.fixedSpace(20.0) + navigationItem.rightBarButtonItems!.append(fixedBarItem) + navigationItem.rightBarButtonItems!.append(colorWellBarItem) + } + } + + // MARK: - UIColorPickerViewController + + func configureColorPicker() { + colorPicker = UIColorPickerViewController() + colorPicker.supportsAlpha = true + colorPicker.selectedColor = UIColor.blue + colorPicker.delegate = self + } + + // Present the color picker from the UIBarButtonItem, iOS only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByBarButton(_ sender: UIBarButtonItem) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover // will display as popover for iPad or sheet for compact screens. + let popover: UIPopoverPresentationController = colorPicker.popoverPresentationController! + popover.barButtonItem = sender + present(colorPicker, animated: true, completion: nil) + } + + // Present the color picker from the UIButton, Mac Catalyst only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByButton(_ sender: UIButton) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover + if let popover = colorPicker.popoverPresentationController { + popover.sourceView = sender + present(colorPicker, animated: true, completion: nil) + } + } + + // MARK: - UIColorPickerViewControllerDelegate + + // Color returned from the color picker via UIBarButtonItem - iOS 15.0 + @available(iOS 15.0, *) + func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Dismiss the color picker if the conditions are right: + // 1) User is not doing a continous pick (tap and drag across multiple colors). + // 2) Picker is presented on a non-compact device. + // + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if !continuously { + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + } + + // Color returned from the color picker - iOS 14.x and earlier. + func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + + func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { + /** In presentations (except popovers) the color picker shows a close button. If the close button is tapped, + the view controller is dismissed and `colorPickerViewControllerDidFinish:` is called. Can be used to + animate alongside the dismissal. + */ + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift b/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift new file mode 100755 index 0000000000..8111b05dea --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift @@ -0,0 +1,92 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a customized `UIPageControl`. +*/ + +import UIKit + +class CustomPageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var pageControl: UIPageControl! + + @IBOutlet weak var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple, + UIColor.systemGray2, + UIColor.systemGray3, + UIColor.systemGray4, + UIColor.systemGray5 + ] + + let images = [ + UIImage(systemName: "square.fill"), + UIImage(systemName: "square"), + UIImage(systemName: "triangle.fill"), + UIImage(systemName: "triangle"), + UIImage(systemName: "circle.fill"), + UIImage(systemName: "circle"), + UIImage(systemName: "star.fill"), + UIImage(systemName: "star"), + UIImage(systemName: "staroflife"), + UIImage(systemName: "staroflife.fill"), + UIImage(systemName: "heart.fill"), + UIImage(systemName: "heart"), + UIImage(systemName: "moon"), + UIImage(systemName: "moon.fill") + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + // Prominent background style. + pageControl.backgroundStyle = .prominent + + // Set custom indicator images. + for (index, image) in images.enumerated() { + pageControl.setIndicatorImage(image, forPage: index) + } + + pageControl.addTarget(self, + action: #selector(PageControlViewController.pageControlValueDidChange), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift b/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift new file mode 100755 index 0000000000..bfd7738144 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift @@ -0,0 +1,61 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UISearchBar`. +*/ + +import UIKit + +class CustomSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsBookmarkButton = true + + searchBar.tintColor = UIColor.systemPurple + + searchBar.backgroundImage = UIImage(named: "search_bar_background", in: .module, compatibleWith: nil) + + // Set the bookmark image for both normal and highlighted states. + let bookImage = UIImage(systemName: "bookmark") + searchBar.setImage(bookImage, for: .bookmark, state: .normal) + + let bookFillImage = UIImage(systemName: "bookmark.fill") + searchBar.setImage(bookFillImage, for: .bookmark, state: .highlighted) + } +} + +// MARK: - UISearchBarDelegate + +extension CustomSearchBarViewController: UISearchBarDelegate { + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar keyboard \"Search\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar \"Cancel\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom \"bookmark button\" inside the search bar was tapped.") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift new file mode 100755 index 0000000000..df91bffc4d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift @@ -0,0 +1,72 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UIToolbar`. +*/ + +import UIKit + +class CustomToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarBackgroundImage = UIImage(named: "toolbar_background", in: .module, compatibleWith: nil) + toolbar.setBackgroundImage(toolbarBackgroundImage, forToolbarPosition: .bottom, barMetrics: .default) + + let toolbarButtonItems = [ + customImageBarButtonItem, + flexibleSpaceBarButtonItem, + customBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var customImageBarButtonItem: UIBarButtonItem { + let customBarButtonItemImage = UIImage(systemName: "exclamationmark.triangle") + + let customImageBarButtonItem = UIBarButtonItem(image: customBarButtonItemImage, + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) + + customImageBarButtonItem.tintColor = UIColor.systemPurple + + return customImageBarButtonItem + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + } + + var customBarButtonItem: UIBarButtonItem { + let barButtonItem = UIBarButtonItem(title: NSLocalizedString("Button", bundle: .module, comment: ""), + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked)) + + let attributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple + ] + barButtonItem.setTitleTextAttributes(attributes, for: []) + + return barButtonItem + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the custom toolbar was clicked: \(barButtonItem).") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/DatePickerController.swift b/BenchmarkTests/UIKitCatalog/DatePickerController.swift new file mode 100755 index 0000000000..464e479a83 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DatePickerController.swift @@ -0,0 +1,82 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIDatePicker`. +*/ + +import UIKit + +class DatePickerController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var datePicker: UIDatePicker! + + @IBOutlet weak var dateLabel: UILabel! + + // A date formatter to format the `date` property of `datePicker`. + lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + return dateFormatter + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 15, *) { + // In case the label's content is too large to fit inside the label (causing truncation), + // use this to reveal the label's full text drawn as a tool tip. + dateLabel.showsExpansionTextWhenTruncated = true + } + + configureDatePicker() + } + + // MARK: - Configuration + + func configureDatePicker() { + datePicker.datePickerMode = .dateAndTime + + /** Set min/max date for the date picker. As an example we will limit the date between + now and 7 days from now. + */ + let now = Date() + datePicker.minimumDate = now + + // Decide the best date picker style based on the trait collection's vertical size. + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + + var dateComponents = DateComponents() + dateComponents.day = 7 + + let sevenDaysFromNow = Calendar.current.date(byAdding: .day, value: 7, to: now) + datePicker.maximumDate = sevenDaysFromNow + + datePicker.minuteInterval = 2 + + datePicker.addTarget(self, action: #selector(DatePickerController.updateDatePickerLabel), for: .valueChanged) + + updateDatePickerLabel() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + // Adjust the date picker style due to the trait collection's vertical size. + super.traitCollectionDidChange(previousTraitCollection) + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + } + + // MARK: - Actions + + @objc + func updateDatePickerLabel() { + dateLabel.text = dateFormatter.string(from: datePicker.date) + + Swift.debugPrint("Chosen date: \(dateFormatter.string(from: datePicker.date))") + } +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift new file mode 100755 index 0000000000..42f66cf414 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift @@ -0,0 +1,62 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIPageControl`. +*/ + +import UIKit + +class PageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var pageControl: UIPageControl! + + @IBOutlet weak var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.pageIndicatorTintColor = UIColor.systemGreen + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + pageControl.addTarget(self, action: #selector(PageControlViewController.pageControlValueDidChange), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift new file mode 100755 index 0000000000..cd0d9be1c1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift @@ -0,0 +1,56 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a default `UISearchBar`. +*/ + +import UIKit + +class DefaultSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsScopeBar = true + + searchBar.scopeButtonTitles = [ + NSLocalizedString("Scope One", bundle: .module, comment: ""), + NSLocalizedString("Scope Two", bundle: .module, comment: "") + ] + } + +} + +// MARK: - UISearchBarDelegate + +extension DefaultSearchBarViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + Swift.debugPrint("The default search selected scope button index changed to \(selectedScope).") + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar keyboard search button was tapped: \(String(describing: searchBar.text)).") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar cancel button was tapped.") + + searchBar.resignFirstResponder() + } + +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift new file mode 100755 index 0000000000..5b8717f57a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift @@ -0,0 +1,60 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a default `UIToolbar`. +*/ + +import UIKit + +class DefaultToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarButtonItems = [ + trashBarButtonItem, + flexibleSpaceBarButtonItem, + customTitleBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var trashBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .trash, + target: self, + action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + func menuHandler(action: UIAction) { + Swift.debugPrint("Menu Action '\(action.title)'") + } + + var customTitleBarButtonItem: UIBarButtonItem { + let buttonMenu = UIMenu(title: "", + children: (1...5).map { + UIAction(title: "Option \($0)", handler: menuHandler) + }) + return UIBarButtonItem(image: UIImage(systemName: "list.number"), menu: buttonMenu) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the default toolbar was clicked: \(barButtonItem).") + } +} diff --git a/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift b/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift new file mode 100755 index 0000000000..8294fe784d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift @@ -0,0 +1,108 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIFontPickerViewController`. +*/ + +import UIKit + +class FontPickerViewController: UIViewController { + + // MARK: - Properties + + var fontPicker: UIFontPickerViewController! + var textFormatter: UITextFormattingCoordinator! + + @IBOutlet var fontLabel: UILabel! + @IBOutlet var textFormatterButton: UIButton! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + fontLabel.text = NSLocalizedString("SampleFontTitle", bundle: .module, comment: "") + + configureFontPicker() + + if traitCollection.userInterfaceIdiom != .mac { + // UITextFormattingCoordinator's toggleFontPanel is available only for macOS. + textFormatterButton.isHidden = true + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + configureTextFormatter() + } + + func configureFontPicker() { + let configuration = UIFontPickerViewController.Configuration() + configuration.includeFaces = true + configuration.displayUsingSystemFont = false + configuration.filteredTraits = [.classModernSerifs] + + fontPicker = UIFontPickerViewController(configuration: configuration) + fontPicker.delegate = self + fontPicker.modalPresentationStyle = UIModalPresentationStyle.popover + } + + func configureTextFormatter() { + if textFormatter == nil { + guard let scene = self.view.window?.windowScene else { return } + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: fontLabel.font as Any] + textFormatter = UITextFormattingCoordinator(for: scene) + textFormatter.delegate = self + textFormatter.setSelectedAttributes(attributes, isMultiple: true) + } + } + + @IBAction func presentFontPicker(_ sender: Any) { + if let button = sender as? UIButton { + let popover: UIPopoverPresentationController = fontPicker.popoverPresentationController! + popover.sourceView = button + present(fontPicker, animated: true, completion: nil) + } + } + + @IBAction func presentTextFormattingCoordinator(_ sender: Any) { + if !UITextFormattingCoordinator.isFontPanelVisible { + UITextFormattingCoordinator.toggleFontPanel(sender) + } + } + +} + +// MARK: - UIFontPickerViewControllerDelegate + +extension FontPickerViewController: UIFontPickerViewControllerDelegate { + + func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) { + //.. + } + + func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) { + guard let fontDescriptor = viewController.selectedFontDescriptor else { return } + let font = UIFont(descriptor: fontDescriptor, size: 28.0) + fontLabel.font = font + } + +} + +// MARK: - UITextFormattingCoordinatorDelegate + +extension FontPickerViewController: UITextFormattingCoordinatorDelegate { + + override func updateTextAttributes(conversionHandler: ([NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any]) { + guard let oldLabelText = fontLabel.attributedText else { return } + let newString = NSMutableAttributedString(string: oldLabelText.string) + oldLabelText.enumerateAttributes(in: NSRange(location: 0, length: oldLabelText.length), + options: []) { (attributeDictionary, range, stop) in + newString.setAttributes(conversionHandler(attributeDictionary), range: range) + } + fontLabel.attributedText = newString + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift b/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift new file mode 100755 index 0000000000..b2bb197f23 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift @@ -0,0 +1,45 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIFontPickerViewController`. +*/ + +import UIKit + +class ImagePickerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + // MARK: - Properties + var imagePicker: UIImagePickerController! + @IBOutlet var imageView: UIImageView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImagePicker() + } + + func configureImagePicker() { + imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.mediaTypes = ["public.image"] + imagePicker.sourceType = .photoLibrary + } + + @IBAction func presentImagePicker(_: AnyObject) { + present(imagePicker, animated: true) + } + + // MARK: - UIImagePickerControllerDelegate + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + imageView.image = image + } + picker.dismiss(animated: true, completion: nil) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ImageViewController.swift b/BenchmarkTests/UIKitCatalog/ImageViewController.swift new file mode 100755 index 0000000000..4abc247509 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ImageViewController.swift @@ -0,0 +1,44 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIImageView`. +*/ + +import UIKit + +class ImageViewController: UIViewController { + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImageView() + } + + // MARK: - Configuration + + func configureImageView() { + // The root view of the view controller is set in Interface Builder and is an UIImageView. + if let imageView = view as? UIImageView { + // Fetch the images (each image is of the format Flowers_number). + imageView.animationImages = (1...2).map { UIImage(named: "Flowers_\($0)", in: .module, compatibleWith: nil)! } + + // We want the image to be scaled to the correct aspect ratio within imageView's bounds. + imageView.contentMode = .scaleAspectFit + + imageView.animationDuration = 5 + imageView.startAnimating() + + imageView.isAccessibilityElement = true + imageView.accessibilityLabel = NSLocalizedString("Animated", bundle: .module, comment: "") + + if #available(iOS 15, *) { + // This case uses UIToolTipInteraction which is available on iOS 15 or later. + let interaction = + UIToolTipInteraction(defaultToolTip: NSLocalizedString("ImageToolTipTitle", bundle: .module, comment: "")) + imageView.addInteraction(interaction) + } + } + } +} diff --git a/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt b/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt new file mode 100755 index 0000000000..1f0d0578f9 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt @@ -0,0 +1,8 @@ +Copyright © 2021 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift b/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift new file mode 100755 index 0000000000..35c3b10e37 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift @@ -0,0 +1,184 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to attach menus to `UIButton`. +*/ + +import UIKit + +class MenuButtonViewController: BaseTableViewController { + + // Cell identifier for each menu button table view cell. + enum MenuButtonKind: String, CaseIterable { + case buttonMenuProgrammatic + case buttonMenuMultiAction + case buttonSubMenu + case buttonMenuSelection + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DropDownProgTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuProgrammatic.rawValue, + configHandler: configureDropDownProgrammaticButton), + CaseElement(title: NSLocalizedString("DropDownMultiActionTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuMultiAction.rawValue, + configHandler: configureDropdownMultiActionButton), + CaseElement(title: NSLocalizedString("DropDownButtonSubMenuTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonSubMenu.rawValue, + configHandler: configureDropdownSubMenuButton), + CaseElement(title: NSLocalizedString("PopupSelection", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuSelection.rawValue, + configHandler: configureSelectionPopupButton) + ]) + } + + // MARK: - Handlers + + enum ButtonMenuActionIdentifiers: String { + case item1 + case item2 + case item3 + } + func menuHandler(action: UIAction) { + switch action.identifier.rawValue { + case ButtonMenuActionIdentifiers.item1.rawValue: + Swift.debugPrint("Menu Action: item 1") + case ButtonMenuActionIdentifiers.item2.rawValue: + Swift.debugPrint("Menu Action: item 2") + case ButtonMenuActionIdentifiers.item3.rawValue: + Swift.debugPrint("Menu Action: item 3") + default: break + } + } + + func item4Handler(action: UIAction) { + Swift.debugPrint("Menu Action: \(action.title)") + } + + // MARK: - Drop Down Menu Buttons + + func configureDropDownProgrammaticButton(button: UIButton) { + button.menu = UIMenu(children: [ + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "1"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "2"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler) + ]) + + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownMultiActionButton(button: UIButton) { + let buttonMenu = UIMenu(children: [ + // Share a single handler for the first 3 actions. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "1"), + image: UIImage(systemName: "1.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + attributes: [], + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "2"), + image: UIImage(systemName: "2.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "3"), + image: UIImage(systemName: "3.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item3.rawValue), + handler: menuHandler), + + // Use a separate handler for this 4th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "4"), + image: UIImage(systemName: "4.circle"), + identifier: nil, + handler: item4Handler(action:)), + + // Use a closure for the 5th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "5"), + image: UIImage(systemName: "5.circle"), + identifier: nil) { action in + Swift.debugPrint("Menu Action: \(action.title)") + }, + + // Use attributes to make the 6th action disabled. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "6"), + image: UIImage(systemName: "6.circle"), + identifier: nil, + attributes: [UIMenuElement.Attributes.disabled]) { action in + Swift.debugPrint("Menu Action: \(action.title)") + } + ]) + button.menu = buttonMenu + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownSubMenuButton(button: UIButton) { + let sortClosure = { (action: UIAction) in + Swift.debugPrint("Sort by: \(action.title)") + } + let refreshClosure = { (action: UIAction) in + Swift.debugPrint("Refresh handler") + } + let accountHandler = { (action: UIAction) in + Swift.debugPrint("Account handler") + } + + var sortMenu: UIMenu + if #available(iOS 15, *) { // .singleSelection option only on iOS 15 or later + // The sort sub menu supports a selection. + sortMenu = UIMenu(title: "Sort By", options: .singleSelection, children: [ + UIAction(title: "Date", state: .on, handler: sortClosure), + UIAction(title: "Size", handler: sortClosure) + ]) + } else { + sortMenu = UIMenu(title: "Sort By", children: [ + UIAction(title: "Date", handler: sortClosure), + UIAction(title: "Size", handler: sortClosure) + ]) + } + + let topMenu = UIMenu(children: [ + UIAction(title: "Refresh", handler: refreshClosure), + UIAction(title: "Account", handler: accountHandler), + sortMenu + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + button.menu = topMenu + } + + // MARK: - Selection Popup Menu Button + + func updateColor(_ title: String) { + Swift.debugPrint("Color selected: \(title)") + } + + func configureSelectionPopupButton(button: UIButton) { + let colorClosure = { [unowned self] (action: UIAction) in + self.updateColor(action.title) + } + + button.menu = UIMenu(children: [ + UIAction(title: "Red", handler: colorClosure), + UIAction(title: "Green", state: .on, handler: colorClosure), // The default selected item (green). + UIAction(title: "Blue", handler: colorClosure) + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + + if #available(iOS 15, *) { + button.changesSelectionAsPrimaryAction = true + // Select the default menu item (green). + updateColor((button.menu?.selectedElements.first!.title)!) + } + } + +} diff --git a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayController.swift b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift similarity index 60% rename from BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayController.swift rename to BenchmarkTests/UIKitCatalog/ModuleBundle.swift index 8b7ebbad0c..9821bc2283 100644 --- a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayController.swift +++ b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift @@ -4,7 +4,12 @@ * Copyright 2019-Present Datadog, Inc. */ -import UIKit +import Foundation -class SessionReplayController: UIViewController { +private class ModuleClass { } + +extension Bundle { + static var module: Bundle { Bundle(for: ModuleClass.self) } } + +public let bundle: Bundle = .module diff --git a/BenchmarkTests/UIKitCatalog/OutlineViewController.swift b/BenchmarkTests/UIKitCatalog/OutlineViewController.swift new file mode 100755 index 0000000000..41801645e8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/OutlineViewController.swift @@ -0,0 +1,336 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A simple outline view for the sample app's main UI +*/ + +import UIKit + +class OutlineViewController: UIViewController { + + enum Section { + case main + } + + class OutlineItem: Identifiable, Hashable { + let title: String + let subitems: [OutlineItem] + let storyboardName: String? + let imageName: String? + + init(title: String, imageName: String?, storyboardName: String? = nil, subitems: [OutlineItem] = []) { + self.title = title + self.subitems = subitems + self.storyboardName = storyboardName + self.imageName = imageName + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool { + return lhs.id == rhs.id + } + + } + + var dataSource: UICollectionViewDiffableDataSource! = nil + var outlineCollectionView: UICollectionView! = nil + + private var detailTargetChangeObserver: Any? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + configureCollectionView() + configureDataSource() + + // Add a translucent background to the primary view controller for the Mac. + splitViewController!.primaryBackgroundStyle = .sidebar + view.backgroundColor = UIColor.clear + + // Listen for when the split view controller is expanded or collapsed for iPad multi-tasking, + // and on device rotate (iPhones that support regular size class). + detailTargetChangeObserver = + NotificationCenter.default.addObserver(forName: UIViewController.showDetailTargetDidChangeNotification, + object: nil, + queue: OperationQueue.main, + using: { _ in + // Posted when a split view controller is expanded or collapsed. + + // Re-load the data source, the disclosure indicators need to change (push vs. present on a cell). + var snapshot = self.dataSource.snapshot() + snapshot.reloadItems(self.menuItems) + self.dataSource.apply(snapshot, animatingDifferences: false) + }) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navigationController!.navigationBar.isHidden = true + } + } + + deinit { + if let observer = detailTargetChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + lazy var controlsOutlineItem: OutlineItem = { + + // Determine the content of the UIButton grouping. + var buttonItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", bundle: .module, comment: ""), imageName: "rectangle", + storyboardName: "ButtonViewController"), + OutlineItem(title: NSLocalizedString("MenuButtonsTitle", bundle: .module, comment: ""), imageName: "list.bullet.rectangle", + storyboardName: "MenuButtonViewController") + ] + // UIPointerInteraction to UIButtons is applied for iPad. + if navigationController!.traitCollection.userInterfaceIdiom == .pad { + buttonItems.append(contentsOf: + [OutlineItem(title: NSLocalizedString("PointerInteractionButtonsTitle", bundle: .module, comment: ""), + imageName: "cursorarrow.rays", + storyboardName: "PointerInteractionButtonViewController") ]) + } + + var controlsSubItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", bundle: .module, comment: ""), imageName: "rectangle.on.rectangle", subitems: buttonItems), + + OutlineItem(title: NSLocalizedString("PageControlTitle", bundle: .module, comment: ""), imageName: "photo.on.rectangle", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultPageControlTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultPageControlViewController"), + OutlineItem(title: NSLocalizedString("CustomPageControlTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomPageControlViewController") + ]), + + OutlineItem(title: NSLocalizedString("SearchBarsTitle", bundle: .module, comment: ""), imageName: "magnifyingglass", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultSearchBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultSearchBarViewController"), + OutlineItem(title: NSLocalizedString("CustomSearchBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomSearchBarViewController") + ]), + + OutlineItem(title: NSLocalizedString("SegmentedControlsTitle", bundle: .module, comment: ""), imageName: "square.split.3x1", + storyboardName: "SegmentedControlViewController"), + OutlineItem(title: NSLocalizedString("SlidersTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SliderViewController"), + OutlineItem(title: NSLocalizedString("SwitchesTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SwitchViewController"), + OutlineItem(title: NSLocalizedString("TextFieldsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TextFieldViewController") + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIStepper class is not supported when running Mac Catalyst apps in the Mac idiom. + let stepperItem = + OutlineItem(title: NSLocalizedString("SteppersTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "StepperViewController") + controlsSubItems.append(stepperItem) + } + + return OutlineItem(title: "Controls", imageName: "slider.horizontal.3", subitems: controlsSubItems) + }() + + lazy var pickersOutlineItem: OutlineItem = { + var pickerSubItems = [ + OutlineItem(title: NSLocalizedString("DatePickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DatePickerController"), + OutlineItem(title: NSLocalizedString("ColorPickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ColorPickerViewController"), + OutlineItem(title: NSLocalizedString("FontPickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "FontPickerViewController"), + OutlineItem(title: NSLocalizedString("ImagePickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ImagePickerViewController") + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIPickerView class is not supported when running Mac Catalyst apps in the Mac idiom. + // To use a picker in macOS, use UIButton with changesSelectionAsPrimaryAction set to "true". + let pickerViewItem = + OutlineItem(title: NSLocalizedString("PickerViewTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "PickerViewController") + pickerSubItems.append(pickerViewItem) + } + + return OutlineItem(title: "Pickers", imageName: "list.bullet", subitems: pickerSubItems) + }() + + lazy var viewsOutlineItem: OutlineItem = { + OutlineItem(title: "Views", imageName: "rectangle.stack.person.crop", subitems: [ + OutlineItem(title: NSLocalizedString("ActivityIndicatorsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ActivityIndicatorViewController"), + OutlineItem(title: NSLocalizedString("AlertControllersTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "AlertControllerViewController"), + OutlineItem(title: NSLocalizedString("TextViewTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TextViewController"), + + OutlineItem(title: NSLocalizedString("ImagesTitle", bundle: .module, comment: ""), imageName: "photo", subitems: [ + OutlineItem(title: NSLocalizedString("ImageViewTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ImageViewController"), + OutlineItem(title: NSLocalizedString("SymbolsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SymbolViewController") + ]), + + OutlineItem(title: NSLocalizedString("ProgressViewsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ProgressViewController"), + OutlineItem(title: NSLocalizedString("StackViewsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "StackViewController"), + + OutlineItem(title: NSLocalizedString("ToolbarsTitle", bundle: .module, comment: ""), imageName: "hammer", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultToolBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultToolbarViewController"), + OutlineItem(title: NSLocalizedString("TintedToolbarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TintedToolbarViewController"), + OutlineItem(title: NSLocalizedString("CustomToolbarBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomToolbarViewController") + ]), + + OutlineItem(title: NSLocalizedString("VisualEffectTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "VisualEffectViewController"), + + OutlineItem(title: NSLocalizedString("WebViewTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "WebViewController") + ]) + }() + + private lazy var menuItems: [OutlineItem] = { + return [ + controlsOutlineItem, + viewsOutlineItem, + pickersOutlineItem + ] + }() + +} + +// MARK: - UICollectionViewDiffableDataSource + +extension OutlineViewController { + + private func configureCollectionView() { + let collectionView = + UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout()) + view.addSubview(collectionView) + collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + self.outlineCollectionView = collectionView + collectionView.delegate = self + } + + private func configureDataSource() { + + let containerCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, menuItem) in + + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline) + cell.contentConfiguration = contentConfiguration + + let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header) + cell.accessories = [.outlineDisclosure(options: disclosureOptions)] + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + } + + let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, menuItem in + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + cell.contentConfiguration = contentConfiguration + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + + cell.accessories = self.splitViewWantsToShowDetail() ? [] : [.disclosureIndicator()] + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: outlineCollectionView) { + (collectionView: UICollectionView, indexPath: IndexPath, item: OutlineItem) -> UICollectionViewCell? in + // Return the cell. + if item.subitems.isEmpty { + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } else { + return collectionView.dequeueConfiguredReusableCell(using: containerCellRegistration, for: indexPath, item: item) + } + } + + // Load our initial data. + let snapshot = initialSnapshot() + self.dataSource.apply(snapshot, to: .main, animatingDifferences: false) + } + + private func generateLayout() -> UICollectionViewLayout { + let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar) + let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration) + return layout + } + + private func initialSnapshot() -> NSDiffableDataSourceSectionSnapshot { + var snapshot = NSDiffableDataSourceSectionSnapshot() + + func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) { + snapshot.append(menuItems, to: parent) + for menuItem in menuItems where !menuItem.subitems.isEmpty { + addItems(menuItem.subitems, to: menuItem) + } + } + + addItems(menuItems, to: nil) + return snapshot + } + +} + +// MARK: - UICollectionViewDelegate + +extension OutlineViewController: UICollectionViewDelegate { + + private func splitViewWantsToShowDetail() -> Bool { + return splitViewController?.traitCollection.horizontalSizeClass == .regular + } + + private func pushOrPresentViewController(viewController: UIViewController) { + if splitViewWantsToShowDetail() { + let navVC = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(navVC, sender: navVC) // Replace the detail view controller. + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navVC.navigationBar.isHidden = true + } + } else { + navigationController?.pushViewController(viewController, animated: true) // Just push instead of replace. + } + } + + private func pushOrPresentStoryboard(storyboardName: String) { + let exampleStoryboard = UIStoryboard(name: storyboardName, bundle: .module) + if let exampleViewController = exampleStoryboard.instantiateInitialViewController() { + pushOrPresentViewController(viewController: exampleViewController) + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let menuItem = self.dataSource.itemIdentifier(for: indexPath) else { return } + + collectionView.deselectItem(at: indexPath, animated: true) + + if let storyboardName = menuItem.storyboardName { + pushOrPresentStoryboard(storyboardName: storyboardName) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + if let windowScene = view.window?.windowScene { + if #available(iOS 15, *) { + windowScene.subtitle = menuItem.title + } + } + } + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/PickerViewController.swift b/BenchmarkTests/UIKitCatalog/PickerViewController.swift new file mode 100755 index 0000000000..a4bd6bffcd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/PickerViewController.swift @@ -0,0 +1,171 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIPickerView`. +*/ + +import UIKit + +class PickerViewController: UIViewController { + // MARK: - Types + + enum ColorComponent: Int { + case red = 0, green, blue + + static var count: Int { + return ColorComponent.blue.rawValue + 1 + } + } + + struct RGB { + static let max: CGFloat = 255.0 + static let min: CGFloat = 0.0 + static let offset: CGFloat = 5.0 + } + + // MARK: - Properties + + @IBOutlet weak var pickerView: UIPickerView! + @IBOutlet weak var colorSwatchView: UIView! + + lazy var numberOfColorValuesPerComponent: Int = (Int(RGB.max) / Int(RGB.offset)) + 1 + + var redColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var greenColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var blueColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePickerView() + } + + func updateColorSwatchViewBackgroundColor() { + colorSwatchView.backgroundColor = UIColor(red: redColor, green: greenColor, blue: blueColor, alpha: 1) + } + + func configurePickerView() { + // Set the default selected rows (the desired rows to initially select will vary from app to app). + let selectedRows: [ColorComponent: Int] = [.red: 13, .green: 41, .blue: 24] + + for (colorComponent, selectedRow) in selectedRows { + /** Note that the delegate method on `UIPickerViewDelegate` is not triggered + when manually calling `selectRow(_:inComponent:animated:)`. To do + this, we fire off delegate method manually. + */ + pickerView.selectRow(selectedRow, inComponent: colorComponent.rawValue, animated: true) + pickerView(pickerView, didSelectRow: selectedRow, inComponent: colorComponent.rawValue) + } + } + +} + +// MARK: - UIPickerViewDataSource + +extension PickerViewController: UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return ColorComponent.count + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return numberOfColorValuesPerComponent + } +} + +// MARK: - UIPickerViewDelegate + +extension PickerViewController: UIPickerViewDelegate { + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + let colorValue = CGFloat(row) * RGB.offset + + // Set the initial colors for each picker segment. + let value = CGFloat(colorValue) / RGB.max + var redColorComponent = RGB.min + var greenColorComponent = RGB.min + var blueColorComponent = RGB.min + + switch ColorComponent(rawValue: component)! { + case .red: + redColorComponent = value + + case .green: + greenColorComponent = value + + case .blue: + blueColorComponent = value + } + + if redColorComponent < 0.5 { + redColorComponent = 0.5 + } + if blueColorComponent < 0.5 { + blueColorComponent = 0.5 + } + if greenColorComponent < 0.5 { + greenColorComponent = 0.5 + } + let foregroundColor = UIColor(red: redColorComponent, green: greenColorComponent, blue: blueColorComponent, alpha: 1.0) + + // Set the foreground color for the entire attributed string. + let attributes = [ + NSAttributedString.Key.foregroundColor: foregroundColor + ] + + let title = NSMutableAttributedString(string: "\(Int(colorValue))", attributes: attributes) + + return title + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + let colorComponentValue = RGB.offset * CGFloat(row) / RGB.max + + switch ColorComponent(rawValue: component)! { + case .red: + redColor = colorComponentValue + + case .green: + greenColor = colorComponentValue + + case .blue: + blueColor = colorComponentValue + } + } + +} + +// MARK: - UIPickerViewAccessibilityDelegate + +extension PickerViewController: UIPickerViewAccessibilityDelegate { + + func pickerView(_ pickerView: UIPickerView, accessibilityLabelForComponent component: Int) -> String? { + + switch ColorComponent(rawValue: component)! { + case .red: + return NSLocalizedString("Red color component value", bundle: .module, comment: "") + + case .green: + return NSLocalizedString("Green color component value", bundle: .module, comment: "") + + case .blue: + return NSLocalizedString("Blue color component value", bundle: .module, comment: "") + } + } +} + diff --git a/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift b/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift new file mode 100755 index 0000000000..b9283464c0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift @@ -0,0 +1,168 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to intergrate pointer interactions to `UIButton`. +*/ + +import UIKit + +class PointerInteractionButtonViewController: BaseTableViewController { + + // Cell identifier for each button pointer table view cell. + enum PointerButtonKind: String, CaseIterable { + case buttonPointer + case buttonHighlight + case buttonLift + case buttonHover + case buttonCustom + } + + // The pointer effect kind to use for each button (corresponds to the button's view tag). + enum ButtonPointerEffectKind: Int { + case pointer = 1 + case highlight + case lift + case hover + case custom + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: "UIPointerEffect.automatic", + cellID: PointerButtonKind.buttonPointer.rawValue, + configHandler: configurePointerButton), + CaseElement(title: "UIPointerEffect.highlight", + cellID: PointerButtonKind.buttonHighlight.rawValue, + configHandler: configureHighlightButton), + CaseElement(title: "UIPointerEffect.lift", + cellID: PointerButtonKind.buttonLift.rawValue, + configHandler: configureLiftButton), + CaseElement(title: "UIPointerEffect.hover", + cellID: PointerButtonKind.buttonHover.rawValue, + configHandler: configureHoverButton), + CaseElement(title: "UIPointerEffect (custom)", + cellID: PointerButtonKind.buttonCustom.rawValue, + configHandler: configureCustomButton) + ]) + } + + // MARK: - Configurations + + func configurePointerButton(button: UIButton) { + button.pointerStyleProvider = defaultButtonProvider + } + + func configureHighlightButton(button: UIButton) { + button.pointerStyleProvider = highlightButtonProvider + } + + func configureLiftButton(button: UIButton) { + button.pointerStyleProvider = liftButtonProvider + } + + func configureHoverButton(button: UIButton) { + button.pointerStyleProvider = hoverButtonProvider + } + + func configureCustomButton(button: UIButton) { + button.pointerStyleProvider = customButtonProvider + } + + // MARK: Button Pointer Providers + + func defaultButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** UIPointerEffect.automatic attempts to determine the appropriate effect for the given preview automatically. + The pointer effect has an automatic nature which adapts to the aspects of the button (background color, corner radius, size) + */ + let buttonPointerEffect = UIPointerEffect.automatic(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: pointerShape) + return buttonPointerStyle + } + + func highlightButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + // Pointer slides under the given view and morphs into the view's shape. + let buttonHighlightPointerEffect = UIPointerEffect.highlight(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonHighlightPointerEffect, shape: pointerShape) + + return buttonPointerStyle + } + + func liftButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer slides under the given view and disappears as the view scales up and gains a shadow. + Make the pointer shape’s bounds match the view’s frame so the highlight extends to the edges. + */ + let buttonLiftPointerEffect = UIPointerEffect.lift(targetedPreview) + let customPointerShape = UIPointerShape.path(UIBezierPath(roundedRect: button.bounds, cornerRadius: 6.0)) + buttonPointerStyle = UIPointerStyle(effect: buttonLiftPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + func hoverButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer retains the system shape while over the given view. + Visual changes applied to the view are dictated by the effect's properties. + */ + let buttonHoverPointerEffect = + UIPointerEffect.hover(targetedPreview, preferredTintMode: .none, prefersShadow: true) + buttonPointerStyle = UIPointerStyle(effect: buttonHoverPointerEffect, shape: nil) + + return buttonPointerStyle + } + + func customButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + /** Hover pointer with a custom triangle pointer shape. + Override the default UITargetedPreview with our own, make the visible path outset a little larger. + */ + let parameters = UIPreviewParameters() + parameters.visiblePath = UIBezierPath(rect: button.bounds.insetBy(dx: -15.0, dy: -15.0)) + let newTargetedPreview = UITargetedPreview(view: button, parameters: parameters) + + let buttonPointerEffect = + UIPointerEffect.hover(newTargetedPreview, preferredTintMode: .overlay, prefersShadow: false, prefersScaledContent: false) + + let customPointerShape = UIPointerShape.path(trianglePointerShape()) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + // Return a triangle bezier path for the pointer's shape. + func trianglePointerShape() -> UIBezierPath { + let width = 20.0 + let height = 20.0 + let offset = 10.0 // Coordinate location to match up with the coordinate of default pointer shape. + + let pathView = UIBezierPath() + pathView.move(to: CGPoint(x: (width / 2) - offset, y: -offset)) + pathView.addLine(to: CGPoint(x: -offset, y: height - offset)) + pathView.addLine(to: CGPoint(x: width - offset, y: height - offset)) + pathView.close() + + return pathView + } +} diff --git a/BenchmarkTests/UIKitCatalog/ProgressViewController.swift b/BenchmarkTests/UIKitCatalog/ProgressViewController.swift new file mode 100755 index 0000000000..04b0b9dcbd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ProgressViewController.swift @@ -0,0 +1,132 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIProgressView`. +*/ + +import UIKit + +class ProgressViewController: BaseTableViewController { + // Cell identifier for each progress view table view cell. + enum ProgressViewKind: String, CaseIterable { + case defaultProgress + case barProgress + case tintedProgress + } + + // MARK: - Properties + + var observer: NSKeyValueObservation? + + // An `NSProgress` object whose `fractionCompleted` is observed using KVO to update the `UIProgressView`s' `progress` properties. + let progress = Progress(totalUnitCount: 10) + + // A repeating timer that, when fired, updates the `NSProgress` object's `completedUnitCount` property. + var updateTimer: Timer? + + var progressViews = [UIProgressView]() // Accumulated progress views from all table cells for progress updating. + + // MARK: - Initialization + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + // Register as an observer of the `NSProgress`'s `fractionCompleted` property. + observer = progress.observe(\.fractionCompleted, options: [.new]) { (_, _) in + // Update the progress views. + for progressView in self.progressViews { + progressView.setProgress(Float(self.progress.fractionCompleted), animated: true) + } + } + } + + deinit { + // Unregister as an observer of the `NSProgress`'s `fractionCompleted` property. + observer?.invalidate() + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressDefaultTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.defaultProgress.rawValue, + configHandler: configureDefaultStyleProgressView), + CaseElement(title: NSLocalizedString("ProgressBarTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.barProgress.rawValue, + configHandler: configureBarStyleProgressView) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted progress views available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressTintedTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.tintedProgress.rawValue, + configHandler: configureTintedProgressView) + ]) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + /** Reset the `completedUnitCount` of the `NSProgress` object and create + a repeating timer to increment it over time. + */ + progress.completedUnitCount = 0 + + updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (_) in + /** Update the `completedUnitCount` of the `NSProgress` object if it's + not completed. Otherwise, stop the timer. + */ + if self.progress.completedUnitCount < self.progress.totalUnitCount { + self.progress.completedUnitCount += 1 + } else { + self.updateTimer?.invalidate() + } + }) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + // Stop the timer from firing. + updateTimer?.invalidate() + } + + // MARK: - Configuration + + func configureDefaultStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureBarStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .bar + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureTintedProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + progressView.trackTintColor = UIColor.systemBlue + progressView.progressTintColor = UIColor.systemPurple + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift b/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift new file mode 100755 index 0000000000..c4fe1334bd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift @@ -0,0 +1,189 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISegmentedControl`. +*/ + +import UIKit + +class SegmentedControlViewController: BaseTableViewController { + + // Cell identifier for each segmented control table view cell. + enum SegmentKind: String, CaseIterable { + case segmentDefault + case segmentTinted + case segmentCustom + case segmentCustomBackground + case segmentAction + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentDefault.rawValue, + configHandler: configureDefaultSegmentedControl), + CaseElement(title: NSLocalizedString("CustomSegmentsTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentCustom.rawValue, + configHandler: configureCustomSegmentsSegmentedControl), + CaseElement(title: NSLocalizedString("CustomBackgroundTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentCustomBackground.rawValue, + configHandler: configureCustomBackgroundSegmentedControl), + CaseElement(title: NSLocalizedString("ActionBasedTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentAction.rawValue, + configHandler: configureActionBasedSegmentedControl) + ]) + if self.traitCollection.userInterfaceIdiom != .mac { + // Tinted segmented control is only available on iOS. + testCells.append(contentsOf: [ + CaseElement(title: "Tinted", + cellID: SegmentKind.segmentTinted.rawValue, + configHandler: configureTintedSegmentedControl) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSegmentedControl(_ segmentedControl: UISegmentedControl) { + // As a demonstration, disable the first segment. + segmentedControl.setEnabled(false, forSegmentAt: 0) + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureTintedSegmentedControl(_ segmentedControl: UISegmentedControl) { + // Use a dynamic tinted "green" color (separate one for Light Appearance and separate one for Dark Appearance). + segmentedControl.selectedSegmentTintColor = UIColor(named: "tinted_segmented_control", in: .module, compatibleWith: nil)! + segmentedControl.selectedSegmentIndex = 1 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureCustomSegmentsSegmentedControl(_ segmentedControl: UISegmentedControl) { + let airplaneImage = UIImage(systemName: "airplane") + airplaneImage?.accessibilityLabel = NSLocalizedString("Airplane", bundle: .module, comment: "") + segmentedControl.setImage(airplaneImage, forSegmentAt: 0) + + let giftImage = UIImage(systemName: "gift") + giftImage?.accessibilityLabel = NSLocalizedString("Gift", bundle: .module, comment: "") + segmentedControl.setImage(giftImage, forSegmentAt: 1) + + let burstImage = UIImage(systemName: "burst") + burstImage?.accessibilityLabel = NSLocalizedString("Burst", bundle: .module, comment: "") + segmentedControl.setImage(burstImage, forSegmentAt: 2) + + segmentedControl.selectedSegmentIndex = 0 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + // Utility function to resize an image to a particular size. + func scaledImage(_ image: UIImage, scaledToSize newSize: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + image.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return newImage + } + + // Configure the segmented control with a background image, dividers, and custom font. + // The background image first needs to be sized to match the control's size. + // + func configureCustomBackgroundSegmentedControl(_ placeHolderView: UIView) { + let customBackgroundSegmentedControl = + UISegmentedControl(items: [NSLocalizedString("CheckTitle", bundle: .module, comment: ""), + NSLocalizedString("SearchTitle", bundle: .module, comment: ""), + NSLocalizedString("ToolsTitle", bundle: .module, comment: "")]) + customBackgroundSegmentedControl.selectedSegmentIndex = 2 + + // Place this custom segmented control within the placeholder view. + customBackgroundSegmentedControl.frame.size.width = placeHolderView.frame.size.width + customBackgroundSegmentedControl.frame.origin.y = + (placeHolderView.bounds.size.height - customBackgroundSegmentedControl.bounds.size.height) / 2 + placeHolderView.addSubview(customBackgroundSegmentedControl) + + // Set the background images for each control state. + let normalSegmentBackgroundImage = UIImage(named: "background", in: .module, compatibleWith: nil) + // Size the background image to match the bounds of the segmented control. + let backgroundImageSize = customBackgroundSegmentedControl.bounds.size + let newBackgroundImageSize = scaledImage(normalSegmentBackgroundImage!, scaledToSize: backgroundImageSize) + customBackgroundSegmentedControl.setBackgroundImage(newBackgroundImageSize, for: .normal, barMetrics: .default) + + let disabledSegmentBackgroundImage = UIImage(named: "background_disabled", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setBackgroundImage(disabledSegmentBackgroundImage, for: .disabled, barMetrics: .default) + + let highlightedSegmentBackgroundImage = UIImage(named: "background_highlighted", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setBackgroundImage(highlightedSegmentBackgroundImage, for: .highlighted, barMetrics: .default) + + // Set the divider image. + let segmentDividerImage = UIImage(named: "stepper_and_segment_divider", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setDividerImage(segmentDividerImage, + forLeftSegmentState: .normal, + rightSegmentState: .normal, + barMetrics: .default) + + // Create a font to use for the attributed title, for both normal and highlighted states. + let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body), size: 0) + let normalTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple, + NSAttributedString.Key.font: font + ] + customBackgroundSegmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + + let highlightedTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.font: font + ] + customBackgroundSegmentedControl.setTitleTextAttributes(highlightedTextAttributes, for: .highlighted) + + customBackgroundSegmentedControl.addTarget(self, + action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), + for: .valueChanged) + } + + func configureActionBasedSegmentedControl(_ segmentedControl: UISegmentedControl) { + segmentedControl.selectedSegmentIndex = 0 + let firstAction = + UIAction(title: NSLocalizedString("CheckTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(firstAction, forSegmentAt: 0) + let secondAction = + UIAction(title: NSLocalizedString("SearchTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(secondAction, forSegmentAt: 1) + let thirdAction = + UIAction(title: NSLocalizedString("ToolsTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(thirdAction, forSegmentAt: 2) + } + + // MARK: - Actions + + @objc + func selectedSegmentDidChange(_ segmentedControl: UISegmentedControl) { + Swift.debugPrint("The selected segment: \(segmentedControl.selectedSegmentIndex).") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let segementedControl = cellTest.targetView(cell) as? UISegmentedControl { + cellTest.configHandler(segementedControl) + } else if let placeHolderView = cellTest.targetView(cell) { + // The only non-segmented control cell has a placeholder UIView (for adding one as a subview). + cellTest.configHandler(placeHolderView) + } + return cell + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SliderViewController.swift b/BenchmarkTests/UIKitCatalog/SliderViewController.swift new file mode 100755 index 0000000000..5e24fa16e2 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SliderViewController.swift @@ -0,0 +1,145 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISlider`. +*/ + +import UIKit + +class SliderViewController: BaseTableViewController { + // Cell identifier for each slider table view cell. + enum SliderKind: String, CaseIterable { + case sliderDefault + case sliderTinted + case sliderCustom + case sliderMaxMinImage + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderDefault.rawValue, + configHandler: configureDefaultSlider) + ]) + + if #available(iOS 15, *) { + // These cases require iOS 15 or later when running on Mac Catalyst. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CustomTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderCustom.rawValue, + configHandler: configureCustomSlider) + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MinMaxImagesTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderMaxMinImage.rawValue, + configHandler: configureMinMaxImageSlider) + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderTinted.rawValue, + configHandler: configureTintedSlider) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSlider(_ slider: UISlider) { + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.value = 42 + slider.isContinuous = true + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureTintedSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For minimumTrackTintColor, maximumTrackTintColor to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + slider.minimumTrackTintColor = UIColor.systemBlue + slider.maximumTrackTintColor = UIColor.systemPurple + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureCustomSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumTrackImage, setMaximumTrackImage, setThumbImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + let leftTrackImage = UIImage(named: "slider_blue_track", in: .module, compatibleWith: nil) + slider.setMinimumTrackImage(leftTrackImage, for: .normal) + + let rightTrackImage = UIImage(named: "slider_green_track", in: .module, compatibleWith: nil) + slider.setMaximumTrackImage(rightTrackImage, for: .normal) + + // Set the sliding thumb image (normal and highlighted). + // + // For fun, choose a different image symbol configuraton for the thumb's image between macOS and iOS. + var thumbImageConfig: UIImage.SymbolConfiguration + if slider.traitCollection.userInterfaceIdiom == .mac { + thumbImageConfig = UIImage.SymbolConfiguration(scale: .large) + } else { + thumbImageConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .heavy, scale: .large) + } + let thumbImage = UIImage(systemName: "circle.fill", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImage, for: .normal) + + let thumbImageHighlighted = UIImage(systemName: "circle", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImageHighlighted, for: .highlighted) + + // Set the rest of the slider's attributes. + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.isContinuous = false + slider.value = 84 + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + func configureMinMaxImageSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumValueImage, setMaximumValueImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if #available(iOS 15, *) { + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + } + + slider.minimumValueImage = UIImage(systemName: "tortoise") + slider.maximumValueImage = UIImage(systemName: "hare") + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func sliderValueDidChange(_ slider: UISlider) { + let formattedValue = String(format: "%.2f", slider.value) + Swift.debugPrint("Slider changed its value: \(formattedValue)") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/StackViewController.swift b/BenchmarkTests/UIKitCatalog/StackViewController.swift new file mode 100755 index 0000000000..b8859f258b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/StackViewController.swift @@ -0,0 +1,98 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates different options for manipulating `UIStackView` content. +*/ + +import UIKit + +class StackViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var furtherDetailStackView: UIStackView! + @IBOutlet var plusButton: UIButton! + @IBOutlet var addRemoveExampleStackView: UIStackView! + @IBOutlet var addArrangedViewButton: UIButton! + @IBOutlet var removeArrangedViewButton: UIButton! + + let maximumArrangedSubviewCount = 3 + + // MARK: - View Life Cycle + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + furtherDetailStackView.isHidden = true + plusButton.isHidden = false + updateAddRemoveButtons() + } + + // MARK: - Actions + + @IBAction func showFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let showDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeIn, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = false + self?.plusButton.isHidden = true + }) + showDetailAnimator.startAnimation() + } + + @IBAction func hideFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let hideDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = true + self?.plusButton.isHidden = false + }) + hideDetailAnimator.startAnimation() + } + + @IBAction func addArrangedSubviewToStack(_: AnyObject) { + // Create a simple, fixed-size, square view to add to the stack view. + let newViewSize = CGSize(width: 38, height: 38) + let newView = UIView(frame: CGRect(origin: CGPoint.zero, size: newViewSize)) + newView.backgroundColor = randomColor() + newView.widthAnchor.constraint(equalToConstant: newViewSize.width).isActive = true + newView.heightAnchor.constraint(equalToConstant: newViewSize.height).isActive = true + + // Adding an arranged subview automatically adds it as a child of the stack view. + addRemoveExampleStackView.addArrangedSubview(newView) + + updateAddRemoveButtons() + } + + @IBAction func removeArrangedSubviewFromStack(_: AnyObject) { + // Make sure there is an arranged view to remove. + guard let viewToRemove = addRemoveExampleStackView.arrangedSubviews.last else { return } + + addRemoveExampleStackView.removeArrangedSubview(viewToRemove) + + /** Calling `removeArrangedSubview` does not remove the provided view from + the stack view's `subviews` array. Since we no longer want the view + we removed to appear, we have to explicitly remove it from its superview. + */ + viewToRemove.removeFromSuperview() + + updateAddRemoveButtons() + } + + // MARK: - Convenience + + func updateAddRemoveButtons() { + let arrangedSubviewCount = addRemoveExampleStackView.arrangedSubviews.count + + addArrangedViewButton.isEnabled = arrangedSubviewCount < maximumArrangedSubviewCount + removeArrangedViewButton.isEnabled = arrangedSubviewCount > 0 + } + + func randomColor() -> UIColor { + let red = CGFloat(arc4random_uniform(255)) / 255.0 + let green = CGFloat(arc4random_uniform(255)) / 255.0 + let blue = CGFloat(arc4random_uniform(255)) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: 1.0) + } +} diff --git a/BenchmarkTests/UIKitCatalog/StepperViewController.swift b/BenchmarkTests/UIKitCatalog/StepperViewController.swift new file mode 100755 index 0000000000..216fc7e0cf --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/StepperViewController.swift @@ -0,0 +1,97 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIStepper`. +*/ + +import UIKit + +class StepperViewController: BaseTableViewController { + + // Cell identifier for each stepper table view cell. + enum StepperKind: String, CaseIterable { + case defaultStepper + case tintedStepper + case customStepper + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.defaultStepper.rawValue, + configHandler: configureDefaultStepper), + CaseElement(title: NSLocalizedString("TintedStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.tintedStepper.rawValue, + configHandler: configureTintedStepper), + CaseElement(title: NSLocalizedString("CustomStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.customStepper.rawValue, + configHandler: configureCustomStepper) + ]) + } + + // MARK: - Configuration + + func configureDefaultStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 10, initial value 0, increment/decrement factor of 1. + stepper.value = 0 + stepper.minimumValue = 0 + stepper.maximumValue = 10 + stepper.stepValue = 1 + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureTintedStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 20, initial value 20, increment/decrement factor of 1. + stepper.value = 20 + stepper.minimumValue = 0 + stepper.maximumValue = 20 + stepper.stepValue = 1 + + stepper.tintColor = UIColor(named: "tinted_stepper_control", in: .module, compatibleWith: nil)! + stepper.setDecrementImage(stepper.decrementImage(for: .normal), for: .normal) + stepper.setIncrementImage(stepper.incrementImage(for: .normal), for: .normal) + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureCustomStepper(stepper: UIStepper) { + // Set the background image. + let stepperBackgroundImage = UIImage(named: "background", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperBackgroundImage, for: .normal) + + let stepperHighlightedBackgroundImage = UIImage(named: "background_highlighted", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperHighlightedBackgroundImage, for: .highlighted) + + let stepperDisabledBackgroundImage = UIImage(named: "background_disabled", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperDisabledBackgroundImage, for: .disabled) + + // Set the image which will be painted in between the two stepper segments. It depends on the states of both segments. + let stepperSegmentDividerImage = UIImage(named: "stepper_and_segment_divider", in: .module, compatibleWith: nil) + stepper.setDividerImage(stepperSegmentDividerImage, forLeftSegmentState: .normal, rightSegmentState: .normal) + + // Set the image for the + button. + let stepperIncrementImage = UIImage(systemName: "plus") + stepper.setIncrementImage(stepperIncrementImage, for: .normal) + + // Set the image for the - button. + let stepperDecrementImage = UIImage(systemName: "minus") + stepper.setDecrementImage(stepperDecrementImage, for: .normal) + + stepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func stepperValueDidChange(_ stepper: UIStepper) { + Swift.debugPrint("A stepper changed its value: \(stepper.value).") + } +} diff --git a/BenchmarkTests/UIKitCatalog/SwitchViewController.swift b/BenchmarkTests/UIKitCatalog/SwitchViewController.swift new file mode 100755 index 0000000000..fddd6494f5 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SwitchViewController.swift @@ -0,0 +1,91 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISwitch`. +*/ + +import UIKit + +class SwitchViewController: BaseTableViewController { + + // Cell identifier for each switch table view cell. + enum SwitchKind: String, CaseIterable { + case defaultSwitch + case checkBoxSwitch + case tintedSwitch + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.defaultSwitch.rawValue, + configHandler: configureDefaultSwitch) + ]) + + // Checkbox switch is available only when running on macOS. + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CheckboxSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.checkBoxSwitch.rawValue, + configHandler: configureCheckboxSwitch) + ]) + } + + // Tinted switch is available only when running on iOS. + if navigationController!.traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.tintedSwitch.rawValue, + configHandler: configureTintedSwitch) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + switchControl.preferredStyle = .sliding + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + func configureCheckboxSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + + // On the Mac, make sure this control take on the apperance of a checkbox with a title. + if traitCollection.userInterfaceIdiom == .mac { + switchControl.preferredStyle = .checkbox + + // Title on a UISwitch is only supported when running Catalyst apps in the Mac Idiom. + switchControl.title = NSLocalizedString("SwitchTitle", bundle: .module, comment: "") + } + } + + func configureTintedSwitch(_ switchControl: UISwitch) { + switchControl.tintColor = UIColor.systemBlue + switchControl.onTintColor = UIColor.systemGreen + switchControl.thumbTintColor = UIColor.systemPurple + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func switchValueDidChange(_ aSwitch: UISwitch) { + Swift.debugPrint("A switch changed its value: \(aSwitch.isOn).") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SymbolViewController.swift b/BenchmarkTests/UIKitCatalog/SymbolViewController.swift new file mode 100755 index 0000000000..70c4ea030c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SymbolViewController.swift @@ -0,0 +1,106 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use SF Symbols. +*/ + +import UIKit + +class SymbolViewController: BaseTableViewController { + + // Cell identifier for each SF Symbol table view cell. + enum SymbolKind: String, CaseIterable { + case plainSymbol + case tintedSymbol + case largeSizeSymbol + case hierarchicalColorSymbol + case paletteColorsSymbol + case preferringMultiColorSymbol + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("PlainSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.plainSymbol.rawValue, + configHandler: configurePlainSymbol), + CaseElement(title: NSLocalizedString("TintedSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.tintedSymbol.rawValue, + configHandler: configureTintedSymbol), + CaseElement(title: NSLocalizedString("LargeSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.largeSizeSymbol.rawValue, + configHandler: configureLargeSizeSymbol) + ]) + + if #available(iOS 15, *) { + // These type SF Sybols, and variants are available on iOS 15, Mac Catalyst 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("HierarchicalSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.hierarchicalColorSymbol.rawValue, + configHandler: configureHierarchicalSymbol), + CaseElement(title: NSLocalizedString("PaletteSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.paletteColorsSymbol.rawValue, + configHandler: configurePaletteColorsSymbol), + CaseElement(title: NSLocalizedString("PreferringMultiColorSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.preferringMultiColorSymbol.rawValue, + configHandler: configurePreferringMultiColorSymbol) + ]) + } + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID) + return cell!.contentView.bounds.size.height + } + + // MARK: - Configuration + + func configurePlainSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + } + + func configureTintedSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + imageView.tintColor = .systemPurple + } + + func configureLargeSizeSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 32, weight: .heavy, scale: .large) + imageView.preferredSymbolConfiguration = symbolConfig + } + + @available(iOS 15.0, *) + func configureHierarchicalSymbol(_ imageView: UIImageView) { + let imageConfig = UIImage.SymbolConfiguration(hierarchicalColor: UIColor.systemRed) + let hierarchicalSymbol = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = hierarchicalSymbol + imageView.preferredSymbolConfiguration = imageConfig + } + + @available(iOS 15.0, *) + func configurePaletteColorsSymbol(_ imageView: UIImageView) { + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemRed, UIColor.systemOrange, UIColor.systemYellow]) + let palleteSymbol = UIImage(systemName: "battery.100.bolt") + imageView.image = palleteSymbol + imageView.backgroundColor = UIColor.darkText + imageView.preferredSymbolConfiguration = palleteSymbolConfig + } + + @available(iOS 15.0, *) + func configurePreferringMultiColorSymbol(_ imageView: UIImageView) { + let preferredSymbolConfig = UIImage.SymbolConfiguration.preferringMulticolor() + let preferredSymbol = UIImage(systemName: "circle.hexagongrid.fill") + imageView.image = preferredSymbol + imageView.preferredSymbolConfiguration = preferredSymbolConfig + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift b/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift new file mode 100755 index 0000000000..23d2a4153d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift @@ -0,0 +1,181 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UITextField`. +*/ + +import UIKit + +class TextFieldViewController: BaseTableViewController { + + // Cell identifier for each text field table view cell. + enum TextFieldKind: String, CaseIterable { + case textField + case tintedTextField + case secureTextField + case specificKeyboardTextField + case customTextField + case searchTextField + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.textField.rawValue, + configHandler: configureTextField), + CaseElement(title: NSLocalizedString("TintedTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.tintedTextField.rawValue, + configHandler: configureTintedTextField), + CaseElement(title: NSLocalizedString("SecuretTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.secureTextField.rawValue, + configHandler: configureSecureTextField), + CaseElement(title: NSLocalizedString("SearchTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.searchTextField.rawValue, + configHandler: configureSearchTextField) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + // Show text field with specific kind of keyboard for iOS only. + CaseElement(title: NSLocalizedString("SpecificKeyboardTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.specificKeyboardTextField.rawValue, + configHandler: configureSpecificKeyboardTextField), + + // Show text field with custom background for iOS only. + CaseElement(title: NSLocalizedString("CustomTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.customTextField.rawValue, + configHandler: configureCustomTextField) + ]) + } + } + + // MARK: - Configuration + + func configureTextField(_ textField: UITextField) { + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.autocorrectionType = .yes + textField.returnKeyType = .done + textField.clearButtonMode = .whileEditing + } + + func configureTintedTextField(_ textField: UITextField) { + textField.tintColor = UIColor.systemBlue + textField.textColor = UIColor.systemGreen + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .never + } + + func configureSecureTextField(_ textField: UITextField) { + textField.isSecureTextEntry = true + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .always + } + + func configureSearchTextField(_ textField: UITextField) { + if let searchField = textField as? UISearchTextField { + searchField.placeholder = NSLocalizedString("Enter search text", bundle: .module, comment: "") + searchField.returnKeyType = .done + searchField.clearButtonMode = .always + searchField.allowsDeletingTokens = true + + // Setup the left view as a symbol image view. + let searchIcon = UIImageView(image: UIImage(systemName: "magnifyingglass")) + searchIcon.tintColor = UIColor.systemGray + searchField.leftView = searchIcon + searchField.leftViewMode = .always + + let secondToken = UISearchToken(icon: UIImage(systemName: "staroflife"), text: "Token 2") + searchField.insertToken(secondToken, at: 0) + + let firstToken = UISearchToken(icon: UIImage(systemName: "staroflife.fill"), text: "Token 1") + searchField.insertToken(firstToken, at: 0) + } + } + + /** There are many different types of keyboards that you may choose to use. + The different types of keyboards are defined in the `UITextInputTraits` interface. + This example shows how to display a keyboard to help enter email addresses. + */ + func configureSpecificKeyboardTextField(_ textField: UITextField) { + textField.keyboardType = .emailAddress + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + } + + func configureCustomTextField(_ textField: UITextField) { + // Text fields with custom image backgrounds must have no border. + textField.borderStyle = .none + + textField.background = UIImage(named: "text_field_background", in: .module, compatibleWith: nil) + + // Create a purple button to be used as the right view of the custom text field. + let purpleImage = UIImage(named: "text_field_purple_right_view", in: .module, compatibleWith: nil)! + let purpleImageButton = UIButton(type: .custom) + purpleImageButton.bounds = CGRect(x: 0, y: 0, width: purpleImage.size.width, height: purpleImage.size.height) + purpleImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) + purpleImageButton.setImage(purpleImage, for: .normal) + purpleImageButton.addTarget(self, action: #selector(TextFieldViewController.customTextFieldPurpleButtonClicked), for: .touchUpInside) + textField.rightView = purpleImageButton + textField.rightViewMode = .always + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.autocorrectionType = .no + textField.clearButtonMode = .never + textField.returnKeyType = .done + } + + // MARK: - Actions + + @objc + func customTextFieldPurpleButtonClicked() { + Swift.debugPrint("The custom text field's purple right view button was clicked.") + } + +} + +// MARK: - UITextFieldDelegate + +extension TextFieldViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + func textFieldDidChangeSelection(_ textField: UITextField) { + // User changed the text selection. + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Return false to not change text. + return true + } +} + +// Custom text field for controlling input text placement. +class CustomTextField: UITextField { + let leftMarginPadding: CGFloat = 12 + let rightMarginPadding: CGFloat = 36 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TextViewController.swift b/BenchmarkTests/UIKitCatalog/TextViewController.swift new file mode 100755 index 0000000000..b1d71f03ef --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TextViewController.swift @@ -0,0 +1,237 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UITextView`. +*/ + +import UIKit + +class TextViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var textView: UITextView! + + /// Used to adjust the text view's height when the keyboard hides and shows. + @IBOutlet weak var textViewBottomLayoutGuideConstraint: NSLayoutConstraint! + + lazy var font = UIFont( + descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body), + size: 0) + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureTextView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Listen for changes to keyboard visibility so that we can adjust the text view's height accordingly. + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil) + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + + // MARK: - Keyboard Event Notifications + + @objc + func handleKeyboardNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo else { return } + + // Get the animation duration. + var animationDuration: TimeInterval = 0 + if let value = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber { + animationDuration = value.doubleValue + } + + // Convert the keyboard frame from screen to view coordinates. + var keyboardScreenBeginFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue) { + keyboardScreenBeginFrame = value.cgRectValue + } + + var keyboardScreenEndFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue) { + keyboardScreenEndFrame = value.cgRectValue + } + + let keyboardViewBeginFrame = view.convert(keyboardScreenBeginFrame, from: view.window) + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) + + let originDelta = keyboardViewEndFrame.origin.y - keyboardViewBeginFrame.origin.y + + // The text view should be adjusted, update the constant for this constraint. + textViewBottomLayoutGuideConstraint.constant -= originDelta + + // Inform the view that its autolayout constraints have changed and the layout should be updated. + view.setNeedsUpdateConstraints() + + // Animate updating the view's layout by calling layoutIfNeeded inside a `UIViewPropertyAnimator` animation block. + let textViewAnimator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeIn, animations: { [weak self] in + self?.view.layoutIfNeeded() + }) + textViewAnimator.startAnimation() + + // Scroll to the selected text once the keyboard frame changes. + let selectedRange = textView.selectedRange + textView.scrollRangeToVisible(selectedRange) + } + + // MARK: - Configuration + + func reflowTextAttributes() { + var entireTextColor = UIColor.black + + // The text should be white in dark mode. + if self.view.traitCollection.userInterfaceStyle == .dark { + entireTextColor = UIColor.white + } + let entireAttributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + let entireRange = NSRange(location: 0, length: entireAttributedText.length) + entireAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: entireTextColor, range: entireRange) + textView.attributedText = entireAttributedText + + /** Modify some of the attributes of the attributed string. + You can modify these attributes yourself to get a better feel for what they do. + Note that the initial text is visible in the storyboard. + */ + let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + + /** Use NSString so the result of rangeOfString is an NSRange, not Range. + This will then be the correct type to then pass to the addAttribute method of NSMutableAttributedString. + */ + let text = textView.text! as NSString + + // Find the range of each element to modify. + let boldRange = text.range(of: NSLocalizedString("bold", bundle: .module, comment: "")) + let highlightedRange = text.range(of: NSLocalizedString("highlighted", bundle: .module, comment: "")) + let underlinedRange = text.range(of: NSLocalizedString("underlined", bundle: .module, comment: "")) + let tintedRange = text.range(of: NSLocalizedString("tinted", bundle: .module, comment: "")) + + // Add bold attribute. Take the current font descriptor and create a new font descriptor with an additional bold trait. + let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) + let boldFont = UIFont(descriptor: boldFontDescriptor!, size: 0) + attributedText.addAttribute(NSAttributedString.Key.font, value: boldFont, range: boldRange) + + // Add highlight attribute. + attributedText.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.systemGreen, range: highlightedRange) + + // Add underline attribute. + attributedText.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: underlinedRange) + + // Add tint color. + attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: tintedRange) + + textView.attributedText = attributedText + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + // With the background change, we need to re-apply the text attributes. + reflowTextAttributes() + } + + func symbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + if let symbolImage = UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + @available(iOS 15.0, *) + func multiColorSymbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemOrange, UIColor.systemRed]) + if let symbolImage = UIImage(systemName: name)?.withConfiguration(palleteSymbolConfig) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + func configureTextView() { + textView.font = font + textView.backgroundColor = UIColor(named: "text_view_background", in: .module, compatibleWith: nil) + + textView.isScrollEnabled = true + + // Apply different attributes to the text (bold, tinted, underline, etc.). + reflowTextAttributes() + + // Insert symbols as image attachments. + let text = textView.text! as NSString + let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + let symbolsSearchRange = text.range(of: NSLocalizedString("symbols", bundle: .module, comment: "")) + var insertPoint = symbolsSearchRange.location + symbolsSearchRange.length + attributedText.insert(symbolAttributedString(name: "heart"), at: insertPoint) + insertPoint += 1 + attributedText.insert(symbolAttributedString(name: "heart.fill"), at: insertPoint) + insertPoint += 1 + attributedText.insert(symbolAttributedString(name: "heart.slash"), at: insertPoint) + + // Multi-color SF Symbols only in iOS 15 or later. + if #available(iOS 15, *) { + insertPoint += 1 + attributedText.insert(multiColorSymbolAttributedString(name: "arrow.up.heart.fill"), at: insertPoint) + } + + // Add the image as an attachment. + if let image = UIImage(named: "text_view_attachment", in: .module, compatibleWith: nil) { + let textAttachment = NSTextAttachment() + textAttachment.image = image + textAttachment.bounds = CGRect(origin: CGPoint.zero, size: image.size) + let textAttachmentString = NSAttributedString(attachment: textAttachment) + attributedText.append(textAttachmentString) + textView.attributedText = attributedText + } + + /** When turned on, this changes the rendering scale of the text to match the standard text scaling + and preserves the original font point sizes when the contents of the text view are copied to the pasteboard. + Apps that show a lot of text content, such as a text viewer or editor, should turn this on and use the standard text scaling. + */ + textView.usesStandardTextScaling = true + } + + // MARK: - Actions + + @objc + func doneBarButtonItemClicked() { + // Dismiss the keyboard by removing it as the first responder. + textView.resignFirstResponder() + + navigationItem.setRightBarButton(nil, animated: true) + } +} + +// MARK: - UITextViewDelegate + +extension TextViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + // Provide a "Done" button for the user to end text editing. + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, + target: self, + action: #selector(TextViewController.doneBarButtonItemClicked)) + + navigationItem.setRightBarButton(doneBarButtonItem, animated: true) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift new file mode 100755 index 0000000000..430ac755ee --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift @@ -0,0 +1,76 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UIToolbar`. +*/ + +import UIKit + +class TintedToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // See the `UIBarStyle` enum for more styles, including `.Default`. + toolbar.barStyle = .black + toolbar.isTranslucent = false + + toolbar.tintColor = UIColor.systemGreen + toolbar.backgroundColor = UIColor.systemBlue + + let toolbarButtonItems = [ + refreshBarButtonItem, + flexibleSpaceBarButtonItem, + actionBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - `UIBarButtonItem` Creation and Configuration + + var refreshBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .refresh, + target: self, + action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + var actionBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .action, + target: self, + action: #selector(TintedToolbarViewController.actionBarButtonItemClicked(_:))) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the tinted toolbar was clicked: \(barButtonItem).") + } + + @objc + func actionBarButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + if let image = UIImage(named: "Flowers_1", in: .module, compatibleWith: nil) { + let activityItems = ["Shared piece of text", image] as [Any] + + let activityViewController = + UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + + activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + present(activityViewController, animated: true, completion: nil) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements b/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements new file mode 100755 index 0000000000..ee95ab7e58 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift b/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift new file mode 100755 index 0000000000..521604f4e0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift @@ -0,0 +1,68 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIVisualEffectView`. +*/ + +import UIKit + +class VisualEffectViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var imageView: UIImageView! + + private var visualEffect: UIVisualEffectView = { + let vev = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + vev.translatesAutoresizingMaskIntoConstraints = false + return vev + }() + + private var textView: UITextView = { + let textView = UITextView(frame: CGRect()) + textView.font = UIFont.systemFont(ofSize: 14) + textView.text = NSLocalizedString("VisualEffectTextContent", bundle: .module, comment: "") + + textView.translatesAutoresizingMaskIntoConstraints = false + textView.backgroundColor = UIColor.clear + if let fontDescriptor = UIFontDescriptor + .preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body) + .withSymbolicTraits(UIFontDescriptor.SymbolicTraits.traitLooseLeading) { + let looseLeadingFont = UIFont(descriptor: fontDescriptor, size: 0) + textView.font = looseLeadingFont + } + return textView + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Add the visual effect view in the same area covering the image view. + view.addSubview(visualEffect) + NSLayoutConstraint.activate([ + visualEffect.topAnchor.constraint(equalTo: imageView.topAnchor), + visualEffect.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + visualEffect.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + visualEffect.bottomAnchor.constraint(equalTo: imageView.bottomAnchor) + ]) + + // Add a text view as a subview to the visual effect view. + visualEffect.contentView.addSubview(textView) + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.topAnchor), + textView.leadingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.bottomAnchor) + ]) + + if #available(iOS 15, *) { + // Use UIToolTipInteraction which is available on iOS 15 or later, add it to the image view. + let toolTipString = NSLocalizedString("VisualEffectToolTipTitle", bundle: .module, comment: "") + let interaction = UIToolTipInteraction(defaultToolTip: toolTipString) + imageView.addInteraction(interaction) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/WebViewController.swift b/BenchmarkTests/UIKitCatalog/WebViewController.swift new file mode 100755 index 0000000000..2b462a81f6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/WebViewController.swift @@ -0,0 +1,59 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `WKWebView`. +*/ + +import UIKit +import WebKit + +/** NOTE: + If your app customizes, interacts with, or controls the display of web content, use the WKWebView class. + If you want to view a website from anywhere on the Internet, use the SFSafariViewController class. + */ + +class WebViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var webView: WKWebView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // So we can capture failures in "didFailProvisionalNavigation". + webView.navigationDelegate = self + loadAddressURL() + } + + // MARK: - Loading + + func loadAddressURL() { + // Set the content to local html in our app bundle. + if let url = Bundle.module.url(forResource: "content", withExtension: "html") { + webView.loadFileURL(url, allowingReadAccessTo: url) + } + } + +} + +// MARK: - WKNavigationDelegate + +extension WebViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + let webKitError = error as NSError + if webKitError.code == NSURLErrorNotConnectedToInternet { + // Report the error inside the web view. + let localizedErrorMessage = NSLocalizedString("An error occurred:", bundle: .module, comment: "") + + let message = "\(localizedErrorMessage) \(error.localizedDescription)" + let errorHTML = + "
\(message)
" + + webView.loadHTMLString(errorHTML, baseURL: nil) + } + } + +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c78431b0d..fd28a73186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ -# Unreleased +# 2.17.0 / 11-09-2024 + +- [FEATURE] Add support for view loading API (addViewLoadingTime). See [#2026][] +- [IMPROVEMENT] Drop support for deprecated cocoapod specs. See [#1998][] +- [FIX] Propagate global Tracer tags to OpenTelemetry span attributes. See [#2000][] +- [FEATURE] Add Logs event mapper to ObjC API. See [#2008][] +- [IMPROVEMENT] Send retry information with network requests (eg. retry_count, last_failure_status and idempotency key). See [#1991][] +- [IMPROVEMENT] Enable app launch time on mac, macCatalyst and visionOS. See [#1888][] (Thanks [@Hengyu][]) +- [FIX] Ignore network reachability on watchOS . See [#2005][] (Thanks [@jfiser-paylocity][]) +- [FEATURE] Add Start / Stop API to Session Replay (start/stopRecording). See [#1986][] # 2.16.0 / 20-08-2024 +- [IMPROVEMENT] Deprecate Alamofire extension pod. See [#1966][] - [FIX] Refresh rate vital for variable refresh rate displays when over performing. See [#1973][] - [FIX] Alamofire extension types are deprecated now. See [#1988][] @@ -745,6 +755,15 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1967]: https://github.com/DataDog/dd-sdk-ios/pull/1967 [#1973]: https://github.com/DataDog/dd-sdk-ios/pull/1973 [#1988]: https://github.com/DataDog/dd-sdk-ios/pull/1988 +[#2000]: https://github.com/DataDog/dd-sdk-ios/pull/2000 +[#1991]: https://github.com/DataDog/dd-sdk-ios/pull/1991 +[#1986]: https://github.com/DataDog/dd-sdk-ios/pull/1986 +[#1888]: https://github.com/DataDog/dd-sdk-ios/pull/1888 +[#2008]: https://github.com/DataDog/dd-sdk-ios/pull/2008 +[#2005]: https://github.com/DataDog/dd-sdk-ios/pull/2005 +[#1998]: https://github.com/DataDog/dd-sdk-ios/pull/1998 +[#1966]: https://github.com/DataDog/dd-sdk-ios/pull/1966 +[#2026]: https://github.com/DataDog/dd-sdk-ios/pull/2026 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu @@ -776,3 +795,4 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [@alexfanatics]: https://github.com/alexfanatics [@changm4n]: https://github.com/changm4n [@jfiser-paylocity]: https://github.com/jfiser-paylocity +[@Hengyu]: https://github.com/Hengyu diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index a33bfa4a6e..dc359ed886 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -112,6 +112,10 @@ 3CCECDB02BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */; }; 3CCECDB22BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; 3CCECDB32BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; + 3CD3A13A2C6C99ED00436A69 /* Data+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */; }; + 3CD3A13B2C6C99ED00436A69 /* Data+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */; }; + 3CD3A13C2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */; }; + 3CD3A13D2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */; }; 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */; }; 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; @@ -404,6 +408,14 @@ 615CC40C2694A56D0005F08C /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */; }; 615CC4102694A64D0005F08C /* SwiftExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */; }; 615CC4132695957C0005F08C /* CrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC4122695957C0005F08C /* CrashReportTests.swift */; }; + 615D52B82C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */; }; + 615D52B92C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */; }; + 615D52BB2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */; }; + 615D52BC2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */; }; + 615D52BE2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */; }; + 615D52BF2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */; }; + 615D52C12C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */; }; + 615D52C22C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */; }; 6167C79326665D6900D4CF07 /* E2EUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167C79226665D6900D4CF07 /* E2EUtils.swift */; }; 6167C7952666622800D4CF07 /* LoggingE2EHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167C7942666622800D4CF07 /* LoggingE2EHelpers.swift */; }; 6167E6D32B7F8B3300C3CA2D /* AppHangsMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D22B7F8B3300C3CA2D /* AppHangsMonitor.swift */; }; @@ -855,6 +867,8 @@ D22743EA29DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */; }; D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; + D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; + D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D22C5BC82A98A0B20024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BC52A989D130024CC1F /* Baggages.swift */; }; D22C5BC92A98A0B30024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BC52A989D130024CC1F /* Baggages.swift */; }; D22C5BCB2A98A5400024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BCA2A98A5400024CC1F /* Baggages.swift */; }; @@ -1691,6 +1705,8 @@ E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */; }; E2AA55EA2C32C76A002FEF28 /* WatchKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */; }; E2AA55EC2C32C78B002FEF28 /* WatchKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */; }; + F6E106542C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */; }; + F6E106552C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2112,6 +2128,8 @@ 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLink.swift; sourceTree = ""; }; 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLinkTests.swift; sourceTree = ""; }; 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.swift; sourceTree = ""; }; + 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Crypto.swift"; sourceTree = ""; }; + 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+CryptoTests.swift"; sourceTree = ""; }; 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReport.swift; sourceTree = ""; }; 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitorTests.swift; sourceTree = ""; }; 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitorTests.swift; sourceTree = ""; }; @@ -2439,6 +2457,10 @@ 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionTests.swift; sourceTree = ""; }; 615CC4122695957C0005F08C /* CrashReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportTests.swift; sourceTree = ""; }; + 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedAttributes.swift; sourceTree = ""; }; + 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTags.swift; sourceTree = ""; }; + 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTagsTests.swift; sourceTree = ""; }; + 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedAttributesTests.swift; sourceTree = ""; }; 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogCrashReporting.xcconfig; sourceTree = ""; }; 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; 6161247825CA9CA6009901BE /* CrashReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = ""; }; @@ -2818,6 +2840,7 @@ D2216EC22A96632F00ADAEC8 /* FeatureBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureBaggageTests.swift; sourceTree = ""; }; D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportReceiverTests.swift; sourceTree = ""; }; D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataModelMocks.swift; sourceTree = ""; }; + D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; D22C5BC52A989D130024CC1F /* Baggages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baggages.swift; sourceTree = ""; }; D22C5BCA2A98A5400024CC1F /* Baggages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baggages.swift; sourceTree = ""; }; @@ -3045,6 +3068,7 @@ E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationNotifications.swift; sourceTree = ""; }; E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchKitExtensions.swift; sourceTree = ""; }; F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMUserActionsPredicate.swift; sourceTree = ""; }; + F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LogsDataModels+objc.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -4213,6 +4237,8 @@ 61133BC22423979B00786299 /* LogEventEncoder.swift */, 61133BC32423979B00786299 /* LogEventBuilder.swift */, 61133BC42423979B00786299 /* LogEventSanitizer.swift */, + 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */, + 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */, ); path = Log; sourceTree = ""; @@ -4411,6 +4437,8 @@ children = ( 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */, 61133C3C2423990D00786299 /* LogSanitizerTests.swift */, + 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */, + 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */, ); path = Log; sourceTree = ""; @@ -5796,6 +5824,14 @@ path = Integrations; sourceTree = ""; }; + D227A0A22C76229400C83324 /* Benchmarks */ = { + isa = PBXGroup; + children = ( + D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */, + ); + path = Benchmarks; + sourceTree = ""; + }; D22C1F5A2714849700922024 /* Scrubbing */ = { isa = PBXGroup; children = ( @@ -5807,6 +5843,7 @@ D23039A6298D513D001A1FA3 /* DatadogInternal */ = { isa = PBXGroup; children = ( + D227A0A22C76229400C83324 /* Benchmarks */, 6167E6DF2B81203A00C3CA2D /* Models */, D23039CA298D5235001A1FA3 /* Attributes */, D23039C3298D5235001A1FA3 /* Codable */, @@ -5922,6 +5959,7 @@ D23039D6298D5235001A1FA3 /* Extensions */ = { isa = PBXGroup; children = ( + 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */, D23039D8298D5235001A1FA3 /* DatadogExtended.swift */, D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */, D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */, @@ -6134,6 +6172,7 @@ D263BCB129DB014900FA0E21 /* Extensions */ = { isa = PBXGroup; children = ( + 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */, D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */, D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */, ); @@ -6271,6 +6310,7 @@ isa = PBXGroup; children = ( 61133C0C2423983800786299 /* Logs+objc.swift */, + F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */, ); path = Logs; sourceTree = ""; @@ -8270,6 +8310,7 @@ 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */, D2A434AA2A8E40A20028E329 /* SessionReplay+objc.swift in Sources */, 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */, + F6E106542C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */, 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8559,7 +8600,9 @@ D207319529A522F600ECBF94 /* LogsFeature.swift in Sources */, D242C29E2A14D6A6004B4980 /* RemoteLogger.swift in Sources */, D20731B529A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, + 615D52BB2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */, D243BBF529A620CC000B9CEC /* MessageReceivers.swift in Sources */, + 615D52B82C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */, D22C5BC92A98A0B30024CC1F /* Baggages.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8570,6 +8613,7 @@ files = ( D2A783E829A53468003B03BB /* ConsoleLoggerTests.swift in Sources */, D2A783EB29A53468003B03BB /* LogSanitizerTests.swift in Sources */, + 615D52BE2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */, D2D30E602A40CD310020C553 /* LogsTests.swift in Sources */, D2A783E729A53468003B03BB /* LogEventBuilderTests.swift in Sources */, D242C2A12A14D747004B4980 /* RemoteLoggerTests.swift in Sources */, @@ -8577,6 +8621,7 @@ D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249972A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C12C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8597,7 +8642,9 @@ D20731AB29A5279D00ECBF94 /* LogsFeature.swift in Sources */, D242C29F2A14D6A7004B4980 /* RemoteLogger.swift in Sources */, D20731B629A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, + 615D52BC2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */, D243BBF629A620CC000B9CEC /* MessageReceivers.swift in Sources */, + 615D52B92C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */, D22C5BC82A98A0B20024CC1F /* Baggages.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8642,6 +8689,7 @@ D21A94F22B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D23039EC298D5236001A1FA3 /* LaunchTime.swift in Sources */, 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */, + D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */, D23039EE298D5236001A1FA3 /* FeatureMessageReceiver.swift in Sources */, D23039DE298D5235001A1FA3 /* Writer.swift in Sources */, D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */, @@ -8677,6 +8725,7 @@ D23039E8298D5236001A1FA3 /* DatadogContext.swift in Sources */, D23039FF298D5236001A1FA3 /* Foundation+Datadog.swift in Sources */, D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */, + 3CD3A13A2C6C99ED00436A69 /* Data+Crypto.swift in Sources */, D2D3199A29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56A2BA2237300D35B08 /* DataStore.swift in Sources */, 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, @@ -9199,6 +9248,7 @@ files = ( D2A783F329A534F9003B03BB /* ConsoleLoggerTests.swift in Sources */, D2A783F429A534F9003B03BB /* LogSanitizerTests.swift in Sources */, + 615D52BF2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */, D2D30E612A40CD310020C553 /* LogsTests.swift in Sources */, D2A783F529A534F9003B03BB /* LogEventBuilderTests.swift in Sources */, D242C2A22A14D747004B4980 /* RemoteLoggerTests.swift in Sources */, @@ -9206,6 +9256,7 @@ D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249982A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C22C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9519,6 +9570,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F6E106552C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */, D2CB6F9927C5217A00A62B57 /* Casting.swift in Sources */, D2CB6F9A27C5217A00A62B57 /* RUMDataModels+objc.swift in Sources */, D2CB6F9B27C5217A00A62B57 /* DDSpanContext+objc.swift in Sources */, @@ -9618,6 +9670,7 @@ D21A94F32B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D2DA2364298D57AA00C6C7E6 /* LaunchTime.swift in Sources */, 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */, + D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */, D2DA2365298D57AA00C6C7E6 /* FeatureMessageReceiver.swift in Sources */, D2DA2366298D57AA00C6C7E6 /* Writer.swift in Sources */, D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */, @@ -9653,6 +9706,7 @@ D2DA2374298D57AA00C6C7E6 /* DatadogContext.swift in Sources */, D2DA2375298D57AA00C6C7E6 /* Foundation+Datadog.swift in Sources */, D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */, + 3CD3A13B2C6C99ED00436A69 /* Data+Crypto.swift in Sources */, D2D3199B29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56B2BA2237300D35B08 /* DataStore.swift in Sources */, 3C3EF2B12C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, @@ -9731,6 +9785,7 @@ D2160CD429C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, D263BCB629DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23AA298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, + 3CD3A13D2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */, D2DA23A8298D58F400C6C7E6 /* DeviceInfoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9781,6 +9836,7 @@ D2160CD529C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, D263BCB729DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23B8298D59DC00C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, + 3CD3A13C2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */, D2DA23BA298D59DC00C6C7E6 /* DeviceInfoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -13697,7 +13753,7 @@ repositoryURL = "https://github.com/DataDog/dd-sdk-swift-testing.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.4.0; + minimumVersion = 2.5.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index 8eea0f6703..eaca1e4c15 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -34,20 +34,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - @@ -81,92 +67,137 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme index 3280d3e0e4..fc53609cdd 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme @@ -20,20 +20,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - @@ -67,92 +53,137 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme index 145c6ae2e4..853f578441 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme @@ -33,9 +33,9 @@ @@ -53,27 +53,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme index d1c4d80d64..417294662d 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme @@ -33,9 +33,9 @@ @@ -53,27 +53,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme index b88210c4c2..a4422a9c7e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme index ecd19591ee..251f49560e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme index 8d8c65bf52..96304d7948 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme index c28f7de21f..bb2fb24be0 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme index 6758f81ec1..3749d2bd23 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme @@ -44,98 +44,6 @@ isEnabled = "YES"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme index 1a9cfff2ee..13de3f1975 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme index 440cbbab98..07a31a4557 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme @@ -26,13 +26,15 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> @@ -50,27 +52,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme index 20e8be3204..05898dd9fd 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme index a3a85bc4c8..0ba9e725e5 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme index 977495b8e5..a5fee88576 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme @@ -40,7 +40,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Base.lproj/Main iOS.storyboard b/Datadog/Example/Base.lproj/Main iOS.storyboard index fb7c848fb2..f2f1e7d959 100644 --- a/Datadog/Example/Base.lproj/Main iOS.storyboard +++ b/Datadog/Example/Base.lproj/Main iOS.storyboard @@ -3,7 +3,7 @@ - + @@ -1598,7 +1598,7 @@ - + @@ -1621,21 +1621,24 @@ - + + + + - + - + @@ -1706,7 +1709,7 @@ - + @@ -1799,40 +1802,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + - - + + - - - - + @@ -1849,7 +1945,6 @@ - diff --git a/Datadog/Example/Debugging/DebugRUMViewController.swift b/Datadog/Example/Debugging/DebugRUMViewController.swift index 8e70203e3e..6a3ad798ad 100644 --- a/Datadog/Example/Debugging/DebugRUMViewController.swift +++ b/Datadog/Example/Debugging/DebugRUMViewController.swift @@ -7,10 +7,10 @@ import UIKit import DatadogRUM import DatadogCore +import DatadogInternal class DebugRUMViewController: UIViewController { @IBOutlet weak var rumServiceNameTextField: UITextField! - @IBOutlet weak var consoleTextView: UITextView! private var simulatedViewControllers: [UIViewController] = [] @@ -18,7 +18,6 @@ class DebugRUMViewController: UIViewController { super.viewDidLoad() rumServiceNameTextField.text = serviceName hideKeyboardWhenTapOutside() - startDisplayingDebugInfo(in: consoleTextView) viewURLTextField.placeholder = viewURL actionViewURLTextField.placeholder = actionViewURL @@ -181,6 +180,51 @@ class DebugRUMViewController: UIViewController { simulatedViewControllers.append(viewController) sendErrorEventButton.disableFor(seconds: 0.5) } + + // MARK: - Telemetry Events + + @IBAction func didTapTelemetryEvent(_ sender: Any) { + guard let button = sender as? UIButton, let title = button.currentTitle else { + return + } + button.disableFor(seconds: 0.5) + + let telemetry = CoreRegistry.default.telemetry + + switch title { + case "debug": + telemetry.debug( + id: UUID().uuidString, + message: "DEBUG telemetry message", + attributes: [ + "attribute-foo": "foo", + "attribute-42": 42, + ] + ) + case "error": + telemetry.error( + id: UUID().uuidString, + message: "ERROR telemetry message", + kind: "error.telemetry.kind", + stack: "error.telemetry.stack" + ) + case "metric": + telemetry.metric( + name: "METRIC telemetry", + attributes: [ + "attribute-foo": "foo", + "attribute-42": 42, + ], + sampleRate: 100 + ) + case "usage": + telemetry.send( + telemetry: .usage(.setTrackingConsent(.granted)) + ) + default: + break + } + } } // MARK: - Private Helpers diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift index 6046fd33a1..045d08416e 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -64,6 +64,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { // Enable Trace Trace.enable( with: Trace.Configuration( + tags: ["testing-tag": "my-value"], networkInfoEnabled: true, customEndpoint: Environment.readCustomTraceURL() ) diff --git a/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift index 9a72b80ef5..347de11f40 100644 --- a/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift @@ -26,7 +26,6 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Given var config = RUM.Configuration(applicationID: .mockAny()) config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 RUM.enable(with: config, in: core) // When @@ -34,9 +33,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { #sourceLocation(file: "File.swift", line: 42) core.telemetry.error("Error Telemetry") #sourceLocation() - core.telemetry.metric(name: "Metric Name", attributes: ["metric.attribute": 42]) + core.telemetry.metric(name: "Metric Name", attributes: ["metric.attribute": 42], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) // Then @@ -69,7 +69,6 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Given var config = RUM.Configuration(applicationID: "rum-app-id") config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 RUM.enable(with: config, in: core) // When @@ -79,9 +78,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) @@ -116,7 +116,6 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Given var config = RUM.Configuration(applicationID: "rum-app-id") config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 RUM.enable(with: config, in: core) // When @@ -125,9 +124,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) @@ -162,7 +162,6 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Given var config = RUM.Configuration(applicationID: "rum-app-id") config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 RUM.enable(with: config, in: core) // When @@ -172,9 +171,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 80f8bcdf81..d1f1191426 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -21,7 +21,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { ) rumConfig = RUM.Configuration(applicationID: .mockAny()) rumConfig.telemetrySampleRate = 100 - rumConfig.metricsTelemetrySampleRate = 100 + rumConfig.sessionEndedMetricSampleRate = 100 rumConfig.dateProvider = dateProvider } diff --git a/DatadogAlamofireExtension.podspec b/DatadogAlamofireExtension.podspec index 34021697ff..cd22b6d0b6 100644 --- a/DatadogAlamofireExtension.podspec +++ b/DatadogAlamofireExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogAlamofireExtension" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." s.description = <<-DESC The DatadogAlamofireExtension pod is deprecated and will no longer be maintained. diff --git a/DatadogCore.podspec b/DatadogCore.podspec index 118ce2496a..e25ceabb95 100644 --- a/DatadogCore.podspec +++ b/DatadogCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCore" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCore/Private/ObjcAppLaunchHandler.m b/DatadogCore/Private/ObjcAppLaunchHandler.m index 3239d61391..bea30ced2a 100644 --- a/DatadogCore/Private/ObjcAppLaunchHandler.m +++ b/DatadogCore/Private/ObjcAppLaunchHandler.m @@ -9,8 +9,10 @@ #import "ObjcAppLaunchHandler.h" -#if TARGET_OS_IOS || TARGET_OS_TV +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION #import +#elif TARGET_OS_OSX +#import #endif // A very long application launch time is most-likely the result of a pre-warmed process. @@ -42,9 +44,17 @@ + (void)load { // This is called at the `DatadogPrivate` load time, keep the work minimal _shared = [[self alloc] initWithProcessInfo:NSProcessInfo.processInfo loadTime:CFAbsoluteTimeGetCurrent()]; -#if TARGET_OS_IOS || TARGET_OS_TV + + NSString *notificationName; +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION + notificationName = UIApplicationDidBecomeActiveNotification; +#elif TARGET_OS_OSX + notificationName = NSApplicationDidBecomeActiveNotification; +#endif + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION || TARGET_OS_OSX NSNotificationCenter * __weak center = NSNotificationCenter.defaultCenter; - id __block __unused token = [center addObserverForName:UIApplicationDidBecomeActiveNotification + id __block __unused token = [center addObserverForName:notificationName object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *_){ diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index bbae88954f..e798504e23 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -313,7 +313,7 @@ extension DatadogCore: DatadogCoreProtocol { } } -internal class CoreFeatureScope: FeatureScope where Feature: DatadogFeature { +internal class CoreFeatureScope: @unchecked Sendable, FeatureScope where Feature: DatadogFeature { private weak var core: DatadogCore? private let store: FeatureDataStore diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift index b4b3848b81..d0851d2eeb 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -250,7 +250,8 @@ internal class FilesOrchestrator: FilesOrchestratorType { BatchDeletedMetric.batchAgeKey: batchAge.toMilliseconds, BatchDeletedMetric.batchRemovalReasonKey: deletionReason.toString(), BatchDeletedMetric.inBackgroundKey: false - ] + ], + sampleRate: BatchDeletedMetric.sampleRate ) } @@ -276,7 +277,8 @@ internal class FilesOrchestrator: FilesOrchestratorType { BatchClosedMetric.batchSizeKey: lastWritableFileApproximatedSize, BatchClosedMetric.batchEventsCountKey: lastWritableFileObjectsCount, BatchClosedMetric.batchDurationKey: batchDuration.toMilliseconds - ] + ], + sampleRate: BatchClosedMetric.sampleRate ) } } diff --git a/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift index 9fec6f414c..9e76b72ba3 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift @@ -28,12 +28,17 @@ internal struct DataUploadConditions { } func blockersForUpload(with context: DatadogContext) -> [Blocker] { + var blockers: [Blocker] = [] + #if !os(watchOS) guard let reachability = context.networkConnectionInfo?.reachability else { // when `NetworkConnectionInfo` is not yet available return [.networkReachability(description: "unknown")] } let networkIsReachable = reachability == .yes || reachability == .maybe - var blockers: [Blocker] = networkIsReachable ? [] : [.networkReachability(description: reachability.rawValue)] + if !networkIsReachable { + blockers = [.networkReachability(description: reachability.rawValue)] + } + #endif guard let battery = context.batteryStatus, battery.state != .unknown else { // Note: in RUMS-132 we got the report on `.unknown` battery state reporing `-1` battery level on iPad device diff --git a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift index 333ef7d36c..e1a00db198 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift @@ -72,28 +72,32 @@ internal struct DataUploadStatus { let userDebugDescription: String let error: DataUploadError? + + let attempt: UInt } extension DataUploadStatus { // MARK: - Initialization - init(httpResponse: HTTPURLResponse, ddRequestID: String?) { + init(httpResponse: HTTPURLResponse, ddRequestID: String?, attempt: UInt) { let statusCode = HTTPResponseStatusCode(rawValue: httpResponse.statusCode) ?? .unexpected self.init( needsRetry: statusCode.needsRetry, responseCode: httpResponse.statusCode, - userDebugDescription: "[response code: \(httpResponse.statusCode) (\(statusCode)), request ID: \(ddRequestID ?? "(???)")]", - error: DataUploadError(status: httpResponse.statusCode) + userDebugDescription: "[response code: \(httpResponse.statusCode) (\(statusCode)), request ID: \(ddRequestID ?? "(???)")", + error: DataUploadError(status: httpResponse.statusCode), + attempt: attempt ) } - init(networkError: Error) { + init(networkError: Error, attempt: UInt) { self.init( needsRetry: true, // retry this upload as it failed due to network transport isse responseCode: nil, userDebugDescription: "[error: \(DDError(error: networkError).message)]", // e.g. "[error: A data connection is not currently allowed]" - error: DataUploadError(networkError: networkError) + error: DataUploadError(networkError: networkError), + attempt: attempt ) } } diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index b2d6aba5bc..6e8f722ebd 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -44,6 +44,8 @@ internal class DataUploadWorker: DataUploadWorkerType { /// Background task coordinator responsible for registering and ending background tasks for UIKit targets. private var backgroundTaskCoordinator: BackgroundTaskCoordinator? + private var previousUploadStatus: DataUploadStatus? + init( queue: DispatchQueue, fileReader: Reader, @@ -113,8 +115,11 @@ internal class DataUploadWorker: DataUploadWorkerType { do { let uploadStatus = try self.dataUploader.upload( events: batch.events, - context: context + context: context, + previous: previousUploadStatus ) + previousUploadStatus = uploadStatus + if uploadStatus.needsRetry { DD.logger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") self.delay.increase() @@ -129,6 +134,7 @@ internal class DataUploadWorker: DataUploadWorkerType { batch, reason: .intakeCode(responseCode: uploadStatus.responseCode) ) + previousUploadStatus = nil } if let error = uploadStatus.error { @@ -144,6 +150,7 @@ internal class DataUploadWorker: DataUploadWorkerType { } catch let error { // If upload can't be initiated do not retry, so drop the batch: self.fileReader.markBatchAsRead(batch, reason: .invalid) + previousUploadStatus = nil self.telemetry.error("Failed to initiate '\(self.featureName)' data upload", error: error) } } @@ -173,12 +180,21 @@ internal class DataUploadWorker: DataUploadWorkerType { // metrics or telemetry. This is legitimate as long as `flush()` routine is only available for testing // purposes and never run in production apps. self.fileReader.markBatchAsRead(nextBatch, reason: .flushed) + previousUploadStatus = nil } do { // Try uploading the batch and do one more retry on failure. - _ = try self.dataUploader.upload(events: nextBatch.events, context: self.contextProvider.read()) + previousUploadStatus = try self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) } catch { - _ = try? self.dataUploader.upload(events: nextBatch.events, context: self.contextProvider.read()) + previousUploadStatus = try? self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) } } } diff --git a/DatadogCore/Sources/Core/Upload/DataUploader.swift b/DatadogCore/Sources/Core/Upload/DataUploader.swift index 3178a0923e..0c89d211fc 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploader.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploader.swift @@ -6,10 +6,11 @@ import Foundation import DatadogInternal +import CommonCrypto /// A type that performs data uploads. internal protocol DataUploaderType { - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus + func upload(events: [Event], context: DatadogContext, previous: DataUploadStatus?) throws -> DataUploadStatus } /// Synchronously uploads data to server using `HTTPClient`. @@ -19,7 +20,8 @@ internal final class DataUploader: DataUploaderType { needsRetry: false, responseCode: nil, userDebugDescription: "", - error: nil + error: nil, + attempt: 0 ) private let httpClient: HTTPClient @@ -32,8 +34,17 @@ internal final class DataUploader: DataUploaderType { /// Uploads data synchronously (will block current thread) and returns the upload status. /// Uses timeout configured for `HTTPClient`. - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { - let request = try requestBuilder.request(for: events, with: context) + func upload(events: [Event], context: DatadogContext, previous: DataUploadStatus?) throws -> DataUploadStatus { + let attempt: UInt + if let previous = previous { + attempt = previous.attempt + 1 + } else { + attempt = 0 + } + + let execution: ExecutionContext = .init(previousResponseCode: previous?.responseCode, attempt: attempt) + let request = try requestBuilder.request(for: events, with: context, execution: execution) + let requestID = request.value(forHTTPHeaderField: URLRequestBuilder.HTTPHeader.ddRequestIDHeaderField) var uploadStatus: DataUploadStatus? @@ -43,9 +54,16 @@ internal final class DataUploader: DataUploaderType { httpClient.send(request: request) { result in switch result { case .success(let httpResponse): - uploadStatus = DataUploadStatus(httpResponse: httpResponse, ddRequestID: requestID) + uploadStatus = DataUploadStatus( + httpResponse: httpResponse, + ddRequestID: requestID, + attempt: attempt + ) case .failure(let error): - uploadStatus = DataUploadStatus(networkError: error) + uploadStatus = DataUploadStatus( + networkError: error, + attempt: attempt + ) } semaphore.signal() diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 1ebd818528..0f91d21e0e 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -38,6 +38,9 @@ internal enum BatchDeletedMetric { static let name = "Batch Deleted" /// Metric type value. static let typeValue = "batch deleted" + /// The sample rate for this metric. + /// It is applied in addition to the telemetry sample rate (20% by default). + static let sampleRate: Float = 1.5 // 1.5% /// The key for uploader's delay options. static let uploaderDelayKey = "uploader_delay" /// The min delay of uploads for this track (in ms). @@ -107,6 +110,9 @@ internal enum BatchClosedMetric { static let name = "Batch Closed" /// Metric type value. static let typeValue = "batch closed" + /// The sample rate for this metric. + /// It is applied in addition to the telemetry sample rate (20% by default). + static let sampleRate: Float = 1.5 // 1.5% /// The default duration since last write (in ms) after which the uploader considers the file to be "ready for upload". static let uploaderWindowKey = "uploader_window" diff --git a/DatadogCore/Sources/Versioning.swift b/DatadogCore/Sources/Versioning.swift index 39369d25ed..8ad390190e 100644 --- a/DatadogCore/Sources/Versioning.swift +++ b/DatadogCore/Sources/Versioning.swift @@ -1,3 +1,3 @@ // GENERATED FILE: Do not edit directly -internal let __sdkVersion = "2.16.0" +internal let __sdkVersion = "2.17.0" diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 789f042cce..c6f0e44b09 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -71,6 +71,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "batch_age": expectedBatchAge.toMilliseconds, "batch_removal_reason": "intake-code-202", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } func testWhenObsoleteFileIsDeleted_itSendsBatchDeletedMetric() throws { @@ -100,6 +101,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "batch_age": (storage.maxFileAgeForRead + 1).toMilliseconds, "batch_removal_reason": "obsolete", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } func testWhenDirectoryIsPurged_itSendsBatchDeletedMetrics() throws { @@ -132,6 +134,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "batch_age": expectedBatchAge.toMilliseconds, "batch_removal_reason": "purged", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } // MARK: - "Batch Closed" Metric @@ -170,5 +173,6 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "batch_events_count": expectedWrites.count, "batch_duration": expectedWriteDelays.reduce(0, +).toMilliseconds ]) + XCTAssertEqual(metric.sampleRate, BatchClosedMetric.sampleRate) } } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift index 91156d8a3d..6e826828df 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift @@ -11,85 +11,75 @@ import TestUtilities class DataUploadStatusTests: XCTestCase { // MARK: - Test `.needsRetry` - private let statusCodesExpectingNoRetry = [ - 202, // ACCEPTED - 400, // BAD REQUEST - 401, // UNAUTHORIZED - 403, // FORBIDDEN - 413, // PAYLOAD TOO LARGE + private let statusCodesExpectingNoRetry: [Int: String] = [ + 202: "accepted", + 400: "badRequest", + 401: "unauthorized", + 403: "forbidden", + 413: "payloadTooLarge", ] - private let statusCodesExpectingRetry = [ - 408, // REQUEST TIMEOUT - 429, // TOO MANY REQUESTS - 500, // INTERNAL SERVER ERROR - 502, // BAD GATEWAY - 503, // SERVICE UNAVAILABLE - 504, // GATEWAY TIMEOUT - 507, // INSUFFICIENT STORAGE + private let statusCodesExpectingRetry: [Int: String] = [ + 408: "requestTimeout", + 429: "tooManyRequests", + 500: "internalServerError", + 502: "badGateway", + 503: "serviceUnavailable", + 504: "gatewayTimeout", + 507: "insufficientStorage", ] private lazy var expectedStatusCodes = statusCodesExpectingNoRetry + statusCodesExpectingRetry func testWhenUploadFinishesWithResponse_andStatusCodeNeedsNoRetry_itSetsNeedsRetryFlagToFalse() { - statusCodesExpectingNoRetry.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + statusCodesExpectingNoRetry.forEach { statusCode, _ in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andStatusCodeNeedsRetry_itSetsNeedsRetryFlagToTrue() { - statusCodesExpectingRetry.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + statusCodesExpectingRetry.forEach { statusCode, _ in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertTrue(status.needsRetry, "Upload should be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andStatusCodeIsUnexpected_itSetsNeedsRetryFlagToFalse() { let allStatusCodes = Set((100...599)) - let unexpectedStatusCodes = allStatusCodes.subtracting(Set(expectedStatusCodes)) + let unexpectedStatusCodes = allStatusCodes.subtracting(Set(expectedStatusCodes.keys)) unexpectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithError_itSetsNeedsRetryFlagToTrue() { - let status = DataUploadStatus(networkError: ErrorMock()) + let status = DataUploadStatus(networkError: ErrorMock(), attempt: 0) XCTAssertTrue(status.needsRetry, "Upload should be retried if it finished with error") } // MARK: - Test `.userDebugDescription` func testWhenUploadFinishesWithResponse_andRequestIDIsAvailable_itCreatesUserDebugDescription() { - expectedStatusCodes.forEach { statusCode in + expectedStatusCodes.forEach { statusCode, message in let requestID: String = .mockRandom(among: .alphanumerics) - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID) - XCTAssertTrue( - status.userDebugDescription.matches( - regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \(requestID)\\]" - ), - "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and request id '\(requestID)'" - ) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID, attempt: 0) + XCTAssertEqual(status.userDebugDescription, "[response code: \(statusCode) (\(message)), request ID: \(requestID)") } } func testWhenUploadFinishesWithResponse_andRequestIDIsNotAvailable_itCreatesUserDebugDescription() { - expectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) - XCTAssertTrue( - status.userDebugDescription.matches( - regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \\(\\?\\?\\?\\)\\]" - ), - "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and no request id" - ) + expectedStatusCodes.forEach { statusCode, message in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) + XCTAssertEqual(status.userDebugDescription, "[response code: \(statusCode) (\(message)), request ID: (???)") } } func testWhenUploadFinishesWithError_itCreatesUserDebugDescription() { let randomErrorDescription: String = .mockRandom() - let status = DataUploadStatus(networkError: ErrorMock(randomErrorDescription)) + let status = DataUploadStatus(networkError: ErrorMock(randomErrorDescription), attempt: 0) XCTAssertEqual(status.userDebugDescription, "[error: \(randomErrorDescription)]") } @@ -105,20 +95,20 @@ class DataUploadStatusTests: XCTestCase { ] func testWhenUploadFinishesWithResponse_andStatusCodeIs401_itCreatesError() { - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 401), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 401), ddRequestID: nil, attempt: 0) XCTAssertEqual(status.error, .unauthorized) } func testWhenUploadFinishesWithResponse_andStatusCodeIsDifferentThan401_itDoesNotCreateAnyError() { Set((100...599)).subtracting(alertingStatusCodes).forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error) } } func testWhenUploadFinishesWithResponse_andStatusCodeMeansSDKIssue_itCreatesHTTPError() { alertingStatusCodes.subtracting([401, 403]).forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockRandom()) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockRandom(), attempt: 01) guard case let .httpError(statusCode: receivedStatusCode) = status.error else { return XCTFail("Upload status error should be created for status code: \(statusCode)") @@ -129,24 +119,24 @@ class DataUploadStatusTests: XCTestCase { } func testWhenUploadFinishesWithResponse_andStatusCodeMeansClientIssue_itDoesNotCreateHTTPError() { - let clientIssueStatusCodes = Set(expectedStatusCodes).subtracting(Set(alertingStatusCodes)) + let clientIssueStatusCodes = Set(expectedStatusCodes.keys).subtracting(Set(alertingStatusCodes)) clientIssueStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error, "Upload status error should not be created for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andUnexpectedStatusCodeMeansClientIssue_itDoesNotCreateHTTPError() { - let unexpectedStatusCodes = Set((100...599)).subtracting(Set(expectedStatusCodes)) + let unexpectedStatusCodes = Set((100...599)).subtracting(Set(expectedStatusCodes.keys)) unexpectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error) } } func testWhenUploadFinishesWithError_andErrorCodeMeansSDKIssue_itCreatesNetworkError() throws { let alertingNSURLErrorCode = NSURLErrorBadURL - let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: alertingNSURLErrorCode, userInfo: nil)) + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: alertingNSURLErrorCode, userInfo: nil), attempt: 0) guard case let .networkError(error: nserror) = status.error else { return XCTFail("Upload status error should be created for NSURLError code: \(alertingNSURLErrorCode)") @@ -157,7 +147,7 @@ class DataUploadStatusTests: XCTestCase { func testWhenUploadFinishesWithError_andErrorCodeMeansExternalFactors_itDoesNotCreateNetworkError() { let notAlertingNSURLErrorCode = NSURLErrorNetworkConnectionLost - let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: notAlertingNSURLErrorCode, userInfo: nil)) + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: notAlertingNSURLErrorCode, userInfo: nil), attempt: 0) XCTAssertNil(status.error, "Upload status error should not be created for NSURLError code: \(notAlertingNSURLErrorCode)") } @@ -165,7 +155,7 @@ class DataUploadStatusTests: XCTestCase { func testWhenUploadFinishesWithResponse_itSetsResponseCode() { let randomCode: Int = .mockRandom(min: 1, max: 999) - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: randomCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: randomCode), ddRequestID: nil, attempt: 0) XCTAssertEqual(status.responseCode, randomCode) } } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index d87f82975d..f7ae3a6f8e 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -47,8 +47,15 @@ class DataUploadWorkerTests: XCTestCase { uploadExpectation.expectedFulfillmentCount = 3 let dataUploader = DataUploaderMock( - uploadStatus: DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 200), ddRequestID: nil), - onUpload: uploadExpectation.fulfill + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) // Given @@ -84,8 +91,15 @@ class DataUploadWorkerTests: XCTestCase { uploadExpectation.expectedFulfillmentCount = 2 let dataUploader = DataUploaderMock( - uploadStatus: DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 200), ddRequestID: nil), - onUpload: uploadExpectation.fulfill + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) // Given @@ -120,7 +134,10 @@ class DataUploadWorkerTests: XCTestCase { let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: false)) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } // Given writer.write(value: ["key": "value"]) @@ -150,7 +167,8 @@ class DataUploadWorkerTests: XCTestCase { let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) - mockDataUploader.onUpload = { + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) initiatingUploadExpectation.fulfill() throw ErrorMock("Failed to prepare upload") } @@ -183,7 +201,10 @@ class DataUploadWorkerTests: XCTestCase { let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: true)) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } // Given writer.write(value: ["key": "value"]) @@ -209,6 +230,55 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 1, "When upload finishes with `needsRetry: true`, data should be preserved") } + func testGivenDataToUpload_whenUploadFinishesAndNeedsToBeRetried_thenPreviousUploadStatusIsNotNil() { + let startUploadExpectation = self.expectation(description: "Upload has started") + startUploadExpectation.expectedFulfillmentCount = 3 + + let mockDataUploader = DataUploaderMock( + uploadStatuses: [ + .mockWith(needsRetry: true, attempt: 0), + .mockWith(needsRetry: true, attempt: 1), + .mockWith(needsRetry: false, attempt: 2) + ] + ) + + var attempt: UInt = 0 + mockDataUploader.onUpload = { previousUploadStatus in + if attempt == 0 { + XCTAssertNil(previousUploadStatus) + } else { + XCTAssertNotNil(previousUploadStatus) + XCTAssertEqual(previousUploadStatus?.attempt, attempt - 1) + } + + attempt += 1 + startUploadExpectation.fulfill() + } + + // Given + writer.write(value: ["key": "value"]) + XCTAssertEqual(try orchestrator.directory.files().count, 1) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try orchestrator.directory.files().count, 0) + } + // MARK: - Upload Interval Changes func testWhenThereIsNoBatch_thenIntervalIncreases() { @@ -275,7 +345,8 @@ class DataUploadWorkerTests: XCTestCase { needsRetry: true, error: .httpError(statusCode: 500) ) - ) { + ) { previousUploadStatus in + XCTAssertNil(previousUploadStatus) uploadAttemptExpectation.fulfill() } @@ -352,7 +423,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -398,7 +472,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -433,7 +510,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -467,7 +547,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -500,7 +583,8 @@ class DataUploadWorkerTests: XCTestCase { // When let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) - mockDataUploader.onUpload = { + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) initiatingUploadExpectation.fulfill() throw ErrorMock("Failed to prepare upload") } @@ -565,7 +649,10 @@ class DataUploadWorkerTests: XCTestCase { let dataUploader = DataUploaderMock( uploadStatus: .mockWith(needsRetry: false), - onUpload: uploadExpectation.fulfill + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) let worker = DataUploadWorker( queue: uploaderQueue, diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift index 341f18aeab..d8884c70c7 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift @@ -27,13 +27,15 @@ class DataUploaderTests: XCTestCase { // When let uploadStatus = try uploader.upload( events: .mockAny(), - context: .mockAny() + context: .mockAny(), + previous: nil ) // Then let expectedUploadStatus = DataUploadStatus( httpResponse: randomResponse, - ddRequestID: randomRequest.value(forHTTPHeaderField: "DD-REQUEST-ID") + ddRequestID: randomRequest.value(forHTTPHeaderField: "DD-REQUEST-ID"), + attempt: 0 ) DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) @@ -54,11 +56,12 @@ class DataUploaderTests: XCTestCase { // When let uploadStatus = try uploader.upload( events: .mockAny(), - context: .mockAny() + context: .mockAny(), + previous: nil ) // Then - let expectedUploadStatus = DataUploadStatus(networkError: randomError) + let expectedUploadStatus = DataUploadStatus(networkError: randomError, attempt: 0) DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) } @@ -73,7 +76,7 @@ class DataUploaderTests: XCTestCase { ) // When & Then - XCTAssertThrowsError(try uploader.upload(events: .mockAny(), context: .mockAny())) { error in + XCTAssertThrowsError(try uploader.upload(events: .mockAny(), context: .mockAny(), previous: nil)) { error in XCTAssertTrue(error is ErrorMock) } } diff --git a/DatadogCore/Tests/Datadog/LoggerTests.swift b/DatadogCore/Tests/Datadog/LoggerTests.swift index 2e0caaf7eb..87b7858556 100644 --- a/DatadogCore/Tests/Datadog/LoggerTests.swift +++ b/DatadogCore/Tests/Datadog/LoggerTests.swift @@ -986,7 +986,7 @@ class LoggerTests: XCTestCase { ) XCTAssertEqual( dd.logger.criticalLog?.error?.message, - "🔥 Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.builder.build()`." + "🔥 Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.create()`." ) XCTAssertTrue(logger is NOPLogger) } @@ -1009,7 +1009,7 @@ class LoggerTests: XCTestCase { ) XCTAssertEqual( dd.logger.criticalLog?.error?.message, - "🔥 Datadog SDK usage error: `Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + "🔥 Datadog SDK usage error: `Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." ) XCTAssertTrue(logger is NOPLogger) } diff --git a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift index 77ff5ae105..d374ccb3e9 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift @@ -297,23 +297,36 @@ class NOPDataUploadWorker: DataUploadWorkerType { } internal class DataUploaderMock: DataUploaderType { - let uploadStatus: DataUploadStatus + let uploadStatuses: [DataUploadStatus] /// Notifies on each started upload. - var onUpload: (() throws -> Void)? + var onUpload: ((DataUploadStatus?) throws -> Void)? /// Tracks uploaded events. private(set) var uploadedEvents: [Event] = [] - init(uploadStatus: DataUploadStatus, onUpload: (() -> Void)? = nil) { - self.uploadStatus = uploadStatus + convenience init(uploadStatus: DataUploadStatus, onUpload: ((DataUploadStatus?) -> Void)? = nil) { + self.init(uploadStatuses: [uploadStatus], onUpload: onUpload) + } + + init(uploadStatuses: [DataUploadStatus], onUpload: ((DataUploadStatus?) -> Void)? = nil) { + self.uploadStatuses = uploadStatuses self.onUpload = onUpload } - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { - uploadedEvents += events - try onUpload?() - return uploadStatus + func upload( + events: [DatadogInternal.Event], + context: DatadogInternal.DatadogContext, + previous: DataUploadStatus?) throws -> DataUploadStatus { + uploadedEvents += events + try onUpload?(previous) + let attempt: UInt + if let previous = previous { + attempt = previous.attempt + 1 + } else { + attempt = 0 + } + return uploadStatuses[Int(attempt)] } } @@ -323,7 +336,8 @@ extension DataUploadStatus: RandomMockable { needsRetry: .random(), responseCode: .mockRandom(), userDebugDescription: .mockRandom(), - error: nil + error: nil, + attempt: .mockRandom() ) } @@ -331,13 +345,15 @@ extension DataUploadStatus: RandomMockable { needsRetry: Bool = .mockAny(), responseCode: Int = .mockAny(), userDebugDescription: String = .mockAny(), - error: DataUploadError? = nil + error: DataUploadError? = nil, + attempt: UInt = 0 ) -> DataUploadStatus { return DataUploadStatus( needsRetry: needsRetry, responseCode: responseCode, userDebugDescription: userDebugDescription, - error: error + error: error, + attempt: attempt ) } } diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index f947e7b863..5e857d5e0c 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -146,7 +146,7 @@ private struct FeatureScopeProxy: FeatureScope { } } -private class FeatureScopeInterceptor { +private final class FeatureScopeInterceptor: @unchecked Sendable { struct InterceptingWriter: Writer { static let jsonEncoder = JSONEncoder.dd.default() diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift index 5009e59f55..bc20a3704a 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift @@ -19,7 +19,11 @@ internal class FeatureRequestBuilderMock: FeatureRequestBuilder { self.init(factory: { _, _ in request }) } - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { return try factory(events, context) } } @@ -28,7 +32,11 @@ internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { /// Records parameters passed to `requet(for:with:)` private(set) var requestParameters: [(events: [Event], context: DatadogContext)] = [] - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { requestParameters.append((events: events, context: context)) return .mockAny() } @@ -37,7 +45,11 @@ internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { internal struct FailingRequestBuilderMock: FeatureRequestBuilder { let error: Error - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { throw error } } diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 3505464f39..30392a2d28 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -469,7 +469,17 @@ extension RUMLongTaskEvent: RandomMockable { date: .mockRandom(), device: .mockRandom(), display: nil, - longTask: .init(duration: .mockRandom(), id: .mockRandom(), isFrozenFrame: .mockRandom()), + longTask: .init( + blockingDuration: nil, + duration: .mockRandom(), + entryType: nil, + firstUiEventTimestamp: nil, + id: .mockRandom(), + isFrozenFrame: .mockRandom(), + renderStart: nil, + scripts: nil, + styleAndLayoutStart: nil + ), os: .mockRandom(), service: .mockRandom(), session: .init( diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index 7c3e56acb1..2e37d1b439 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -729,7 +729,7 @@ extension RUMScopeDependencies { onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), viewCache: ViewCache = ViewCache(), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), - sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()), + sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), watchdogTermination: WatchdogTerminationMonitor? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( diff --git a/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift index f1d36ec394..df66086bea 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift @@ -91,7 +91,7 @@ class RUMFeatureTests: XCTestCase { XCTAssertEqual( requestURL.query, """ - ddsource=\(randomSource)&ddtags=service:\(randomServiceName),version:\(randomApplicationVersion),sdk_version:\(randomSDKVersion),env:\(randomEnvironmentName) + ddsource=\(randomSource)&ddtags=service:\(randomServiceName),version:\(randomApplicationVersion),sdk_version:\(randomSDKVersion),env:\(randomEnvironmentName),retry_count:1 """ ) XCTAssertEqual( diff --git a/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift index 2c163c9c5f..ca87c74455 100644 --- a/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift @@ -84,7 +84,8 @@ class DatadogTraceFeatureTests: XCTestCase { let request = server.waitAndReturnRequests(count: 1)[0] let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(requestURL.absoluteString, randomUploadURL.absoluteString) + XCTAssertEqual(requestURL.host, randomUploadURL.host) + XCTAssertEqual(requestURL.path, randomUploadURL.path) XCTAssertNil(requestURL.query) XCTAssertEqual( request.allHTTPHeaderFields?["User-Agent"], diff --git a/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift b/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift index 8eb14f2f6e..f4a5eac7a7 100644 --- a/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift @@ -271,6 +271,24 @@ class DDLogsTests: XCTestCase { XCTAssertEqual(objcConfig.configuration.remoteSampleRate, 50) XCTAssertNotNil(objcConfig.configuration.consoleLogFormat) } + + func testEventMapping() throws { + let logsConfiguration = DDLogsConfiguration() + logsConfiguration.setEventMapper { logEvent in + logEvent.message = "custom-log-message" + logEvent.attributes.userAttributes["custom-attribute"] = "custom-value" + return logEvent + } + DDLogs.enable(with: logsConfiguration) + + let objcLogger = DDLogger.create() + + objcLogger.debug("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertMessage(equals: "custom-log-message") + logMatchers[0].assertAttributes(equal: ["custom-attribute": "custom-value"]) + } } // swiftlint:enable multiline_arguments_brackets // swiftlint:enable compiler_protocol_init diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec index 46ebe77e2f..002fb29c56 100644 --- a/DatadogCrashReporting.podspec +++ b/DatadogCrashReporting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCrashReporting" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift index f9c2f76646..8e99395844 100644 --- a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift @@ -6,7 +6,7 @@ import Foundation import DatadogInternal -import CrashReporter +@preconcurrency import CrashReporter internal extension PLCrashReporterConfig { /// `PLCR` configuration used for `DatadogCrashReporting` diff --git a/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift index 7f98f22381..8d7109f512 100644 --- a/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift +++ b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift @@ -8,7 +8,7 @@ import Foundation import DatadogInternal /// An interface of 3rd party crash reporter used by the DatadogCrashReporting. -internal protocol ThirdPartyCrashReporter { +internal protocol ThirdPartyCrashReporter: Sendable { /// Initializes and enables the crash reporter. init() throws diff --git a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift index 0ab2e26187..c2535470c7 100644 --- a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift +++ b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift @@ -100,12 +100,14 @@ class CrashReportingPluginTests: XCTestCase { // MARK: - Handling Errors + private let printFunction = PrintFunctionMock() + func testGivenPendingCrashReport_whenItsLoadingFails_itPrintsError() throws { let expectation = self.expectation(description: "No Crash Report was delivered to the caller.") - var errorPrinted: String? - consolePrint = { message, _ in errorPrinted = message } - defer { consolePrint = { message, _ in print(message) } } + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } let crashReporter = try ThirdPartyCrashReporterMock() let plugin = PLCrashReporterPlugin { crashReporter } @@ -126,16 +128,15 @@ class CrashReportingPluginTests: XCTestCase { waitForExpectations(timeout: 0.5, handler: nil) XCTAssertFalse(crashReporter.hasPurgedPendingCrashReport) XCTAssertEqual( - errorPrinted, + printFunction.printedMessage, "🔥 DatadogCrashReporting error: failed to load crash report: Reading error" ) } func testWhenCrashReporterCannotBeEnabled_itPrintsError() { - var errorPrinted: String? - - consolePrint = { message, _ in errorPrinted = message } - defer { consolePrint = { message, _ in print(message) } } + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } // When ThirdPartyCrashReporterMock.initializationError = ErrorMock("Initialization error") @@ -145,7 +146,7 @@ class CrashReportingPluginTests: XCTestCase { _ = PLCrashReporterPlugin { try ThirdPartyCrashReporterMock() } XCTAssertEqual( - errorPrinted, + printFunction.printedMessage, "🔥 DatadogCrashReporting error: failed to enable crash reporter: Initialization error" ) } diff --git a/DatadogCrashReporting/Tests/Mocks.swift b/DatadogCrashReporting/Tests/Mocks.swift index 32a4ffdf40..6a9d33863a 100644 --- a/DatadogCrashReporting/Tests/Mocks.swift +++ b/DatadogCrashReporting/Tests/Mocks.swift @@ -6,10 +6,9 @@ import DatadogInternal import CrashReporter - @testable import DatadogCrashReporting -internal class ThirdPartyCrashReporterMock: ThirdPartyCrashReporter { +internal final class ThirdPartyCrashReporterMock: ThirdPartyCrashReporter, @unchecked Sendable { static var initializationError: Error? var pendingCrashReport: DDCrashReport? diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index a8c8e87dc2..a58ccf215f 100644 --- a/DatadogInternal.podspec +++ b/DatadogInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogInternal" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "Datadog Internal Package. This module is not for public use." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift index f0ee4e5288..ea5af300a9 100644 --- a/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift +++ b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift @@ -17,7 +17,7 @@ public extension Thread { } /// A protocol for types capable of generating backtrace reports. -public protocol BacktraceReporting { +public protocol BacktraceReporting: Sendable { /// Generates a backtrace report for given thread ID. /// /// The thread given by `threadID` will be promoted in the main stack of returned `BacktraceReport` (`report.stack`). @@ -41,7 +41,7 @@ public extension BacktraceReporting { } } -internal struct CoreBacktraceReporter: BacktraceReporting { +internal struct CoreBacktraceReporter: BacktraceReporting, @unchecked Sendable { /// A weak core reference. private weak var core: DatadogCoreProtocol? diff --git a/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift b/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift new file mode 100644 index 0000000000..d9149f8889 --- /dev/null +++ b/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift @@ -0,0 +1,63 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +#if DD_BENCHMARK +/// The profiler endpoint to collect data for benchmarking. +public var profiler: BenchmarkProfiler = NOPBenchmarkProfiler() +#else +/// The profiler endpoint to collect data for benchmarking. This static variable can only +/// be mutated in the benchmark environment. +public let profiler: BenchmarkProfiler = NOPBenchmarkProfiler() +#endif + +/// The Benchmark Profiler provides interfaces to collect data in a benchmark +/// environment. +/// +/// During benchmarking, a concrete implementation of the profiler will be +/// injected to collect data during execution of the SDK. +/// +/// In production, the profiler is no-op and immutable. +public protocol BenchmarkProfiler { + /// Returns a `BenchmarkTracer` instance for the given operation. + /// + /// The profiler must return the same instance of a tracer for the same operation. + /// + /// - Parameter operation: The tracer operation name. The parameter is an auto-closure + /// to not intialise the value if the profiler is no-op. + /// - Returns: The tracer instance. + func tracer(operation: @autoclosure () -> String) -> BenchmarkTracer +} + +/// The Benchmark Tracer will create and start spans in a benchmark environment. +/// This tracer can be used to measure CPU Time of inner operation of the SDK. +/// In production, the Benchmark Tracer is no-op. +public protocol BenchmarkTracer { + /// Creates and starts a span at the current time. + /// + /// The span will be activated automatically and linked to its parent in this tracer context. + /// + /// - Parameter named: The span name. The parameter is an auto-closure + /// to not intialise the value if the profiler is no-op. + /// - Returns: The started span. + func startSpan(named: @autoclosure () -> String) -> BenchmarkSpan +} + +/// A timespan of an operation in a benchmark environment. +public protocol BenchmarkSpan { + /// Stops the span at the current time. + func stop() +} + +private final class NOPBenchmarkProfiler: BenchmarkProfiler, BenchmarkTracer, BenchmarkSpan { + /// no-op + func tracer(operation: @autoclosure () -> String) -> BenchmarkTracer { self } + /// no-op + func startSpan(named: @autoclosure () -> String) -> BenchmarkSpan { self } + /// no-op + func stop() {} +} diff --git a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift index 5093712c7f..78586b83c5 100644 --- a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift +++ b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift @@ -9,8 +9,8 @@ import Foundation /// A property wrapper using a fair, POSIX conforming reader-writer lock for atomic /// access to the value. It is optimised for concurrent reads and exclusive writes. /// -/// The wrapper is a class to prevent copying the lock, it creates and initilaizes a `pthread_rwlock_t`. -/// An additional method `mutate` allow to safely mutate the value in-place (to read it +/// The wrapper is a class to prevent copying the lock, it creates and initializes a `pthread_rwlock_t`. +/// An additional method `mutate` allows to safely mutate the value in-place (to read it /// and write it while obtaining the lock only once). @propertyWrapper public final class ReadWriteLock: @unchecked Sendable { diff --git a/DatadogInternal/Sources/Context/DateProvider.swift b/DatadogInternal/Sources/Context/DateProvider.swift index 38c3c9be79..ac17720e37 100644 --- a/DatadogInternal/Sources/Context/DateProvider.swift +++ b/DatadogInternal/Sources/Context/DateProvider.swift @@ -7,7 +7,7 @@ import Foundation /// Provides current device time information. -public protocol DateProvider { +public protocol DateProvider: Sendable { /// Current device time. /// /// A specific point in time, independent of any calendar or time zone. diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index fd10e21633..67262e3f4f 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -30,7 +30,7 @@ import OSLog #endif /// Function printing `String` content to console. -public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in +public var consolePrint: @Sendable (String, CoreLoggerLevel) -> Void = { message, level in #if canImport(OSLog) if #available(iOS 14.0, tvOS 14.0, *) { switch level { diff --git a/DatadogInternal/Sources/DatadogCoreProtocol.swift b/DatadogInternal/Sources/DatadogCoreProtocol.swift index 6a2c8bd9a5..486ad1ab97 100644 --- a/DatadogInternal/Sources/DatadogCoreProtocol.swift +++ b/DatadogInternal/Sources/DatadogCoreProtocol.swift @@ -222,7 +222,7 @@ extension BaggageSharing { } /// Feature scope provides a context and a writer to build a record event. -public protocol FeatureScope: MessageSending, BaggageSharing { +public protocol FeatureScope: MessageSending, BaggageSharing, Sendable { /// Retrieve the core context and event writer. /// /// The Feature scope provides the current Datadog context and event writer for building and recording events. diff --git a/DatadogInternal/Sources/Extensions/Data+Crypto.swift b/DatadogInternal/Sources/Extensions/Data+Crypto.swift new file mode 100644 index 0000000000..a4a2d81713 --- /dev/null +++ b/DatadogInternal/Sources/Extensions/Data+Crypto.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import CommonCrypto + +extension Data { + public func sha1() -> String { + let hash = withUnsafeBytes { bytes -> [UInt8] in + var hash: [UInt8] = Array(repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) + CC_SHA1(bytes.baseAddress, CC_LONG(count), &hash) + return hash + } + + return hash.map { String(format: "%02x", $0) }.joined() + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift index 25c5da94d8..84ecd506f0 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift @@ -195,12 +195,11 @@ public protocol TraceIDGenerator { func generate() -> TraceID } -/// A Default `TraceID` genarator. +/// A Default `TraceID` generator. +/// TraceId are 128 bit and follows a specific format: +/// <32-bit unix seconds> <32 bits of zero> <64 random bits> public struct DefaultTraceIDGenerator: TraceIDGenerator { - /// Describes the lower and upper boundary of tracing ID generation. - /// - /// * Lower: starts with `1` as `0` is reserved for historical reason: 0 == "unset", ref: dd-trace-java:DDId.java. - /// * Upper: equals to `2 ^ 63 - 1` as some tracers can't handle the `2 ^ 64 -1` range, ref: dd-trace-java:DDId.java. + /// Describes the lower and upper boundary of lower part of the trace ID. public static let defaultGenerationRange = (1...UInt64.max) /// The generator's range. diff --git a/DatadogInternal/Sources/Telemetry/Telemetry.swift b/DatadogInternal/Sources/Telemetry/Telemetry.swift index e45a978995..9d6b96f636 100644 --- a/DatadogInternal/Sources/Telemetry/Telemetry.swift +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -6,6 +6,7 @@ import Foundation +/// Defines the type of configuration telemetry events supported by the SDK. public struct ConfigurationTelemetry: Equatable { public let actionNameAttribute: String? public let allowFallbackToLocalStorage: Bool? @@ -25,6 +26,7 @@ public struct ConfigurationTelemetry: Equatable { public let sessionReplaySampleRate: Int64? public let sessionSampleRate: Int64? public let silentMultipleInit: Bool? + public let startRecordingImmediately: Bool? public let startSessionReplayRecordingManually: Bool? public let telemetryConfigurationSampleRate: Int64? public let telemetrySampleRate: Int64? @@ -57,11 +59,69 @@ public struct ConfigurationTelemetry: Equatable { public let useWorkerUrl: Bool? } +public struct MetricTelemetry { + /// The default sample rate for metric events (15%), applied in addition to the telemetry sample rate (20% by default). + public static let defaultSampleRate: Float = 15 + + /// The name of the metric. + public let name: String + + /// The attributes associated with this metric. + public let attributes: [String: Encodable] + + /// The sample rate for this metric, applied in addition to the telemetry sample rate. + /// + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public let sampleRate: Float +} + +/// Describes the type of the usage telemetry events supported by the SDK. +public enum UsageTelemetry { + /// setTrackingConsent API + case setTrackingConsent(TrackingConsent) + /// stopSession API + case stopSession + /// startView API + case startView + /// addAction API + case addAction + /// addError API + case addError + /// setGlobalContext, setGlobalContextProperty, addAttribute APIs + case setGlobalContext + /// setUser, setUserProperty, setUserInfo APIs + case setUser + /// addFeatureFlagEvaluation API + case addFeatureFlagEvaluation + /// addFeatureFlagEvaluation API + case addViewLoadingTime(ViewLoadingTime) + + /// Describes the properties of `addViewLoadingTime` usage telemetry. + public struct ViewLoadingTime { + /// Whether the available view is not active + public let noActiveView: Bool + /// Whether the view is not available + public let noView: Bool + /// Whether the loading time was overwritten + public let overwritten: Bool + } +} + +/// Defines different types of telemetry messages supported by the SDK. public enum TelemetryMessage { + /// A debug log message. case debug(id: String, message: String, attributes: [String: Encodable]?) + /// An execution error. case error(id: String, message: String, kind: String, stack: String) + /// A configuration telemetry. case configuration(ConfigurationTelemetry) - case metric(name: String, attributes: [String: Encodable]) + case metric(MetricTelemetry) + case usage(UsageTelemetry) } /// The `Telemetry` protocol defines methods to collect debug information @@ -74,20 +134,28 @@ public protocol Telemetry { } public extension Telemetry { - /// Starts a method call. + /// Starts timing a method call using the "Method Called" metric. /// /// - Parameters: - /// - operationName: Platform agnostic name of the operation. - /// - callerClass: The name of the class that calls the method. - /// - samplingRate: The sampling rate of the method call. Value between `0.0` and `100.0`, where `0.0` means NO event will be processed and `100.0` means ALL events will be processed. Note that this value is multiplicated by telemetry sampling (by default 20%) and metric events sampling (hardcoded to 15%). Making it effectively 3% sampling rate for sending events, when this value is set to `100`. + /// - operationName: A platform-agnostic name for the operation. + /// - callerClass: The name of the class that invokes the method. + /// - headSampleRate: The sample rate for **head-based** sampling of the method call metric. Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: The head sample rate is compounded with the tail sample rate, which is configured in `stopMethodCalled()`. Both are applied + /// in addition to the telemetry sample rate. For example, if the telemetry sample rate is 20% (default), the head sample rate is 1%, and the tail sample + /// rate is 15% (default), the effective sample rate will be 20% x 1% x 15% = 0.03%. /// - /// - Returns: A `MethodCalledTrace` instance to be used to stop the method call and measure it's execution time. It can be `nil` if the method call is not sampled. + /// Unlike the telemetry sample rate and tail-based sampling in `stopMethodCalled()`, this sample rate is applied at the start of the method call timing. + /// This head-based sampling reduces the impact of processing high-frequency metrics in the SDK core, as most samples can be dropped + /// before being passed to the message bus. + /// + /// - Returns: A `MethodCalledTrace` instance for stopping the method call and measuring its execution time, or `nil` if the method call is not sampled. func startMethodCalled( operationName: String, callerClass: String, - samplingRate: Float = 100.0 + headSampleRate: Float ) -> MethodCalledTrace? { - if Sampler(samplingRate: samplingRate).sample() { + if Sampler(samplingRate: headSampleRate).sample() { return MethodCalledTrace( operationName: operationName, callerClass: callerClass @@ -97,15 +165,38 @@ public extension Telemetry { } } - /// Stops a method call, transforms method call metric to telemetry message, - /// and transmits on the message-bus of the core. + /// Stops timing a method call and posts a value for the "Method Called" metric. + /// + /// This method applies tail-based sampling in addition to the head-based sampling applied in `startMethodCalled()`. + /// The tail sample rate is compounded with the head sample rate and the telemetry sample rate to determine the effective sample rate. /// - /// - Parameters + /// - Parameters: /// - metric: The `MethodCalledTrace` instance. - /// - isSuccessful: A flag indicating if the method call was successful. - func stopMethodCalled(_ metric: MethodCalledTrace?, isSuccessful: Bool = true) { + /// - isSuccessful: A flag indicating whether the method call was successful. + /// - tailSampleRate: The sample rate for **tail-based** sampling of the metric, applied in telemetry receiver after the metric is processed by the SDK core. + /// Defaults to `MetricTelemetry.defaultSampleRate` (15%). + func stopMethodCalled( + _ metric: MethodCalledTrace?, + isSuccessful: Bool = true, + tailSampleRate: Float = MetricTelemetry.defaultSampleRate + ) { if let metric = metric { - send(telemetry: metric.asTelemetryMetric(isSuccessful: isSuccessful)) + let executionTime = -metric.startTime.timeIntervalSinceNow.toInt64Nanoseconds + send( + telemetry: .metric( + MetricTelemetry( + name: MethodCalledMetric.name, + attributes: [ + MethodCalledMetric.executionTime: executionTime, + MethodCalledMetric.operationName: metric.operationName, + MethodCalledMetric.callerClass: metric.callerClass, + MethodCalledMetric.isSuccessful: isSuccessful, + SDKMetricFields.typeKey: MethodCalledMetric.typeValue + ], + sampleRate: tailSampleRate + ) + ) + ) } } } @@ -115,23 +206,6 @@ public struct MethodCalledTrace { let operationName: String let callerClass: String let startTime = Date() - - var exectutionTime: Int64 { - return -startTime.timeIntervalSinceNow.toInt64Nanoseconds - } - - func asTelemetryMetric(isSuccessful: Bool) -> TelemetryMessage { - return .metric( - name: MethodCalledMetric.name, - attributes: [ - MethodCalledMetric.executionTime: exectutionTime, - MethodCalledMetric.operationName: operationName, - MethodCalledMetric.callerClass: callerClass, - MethodCalledMetric.isSuccessful: isSuccessful, - SDKMetricFields.typeKey: MethodCalledMetric.typeValue - ] - ) - } } extension Telemetry { @@ -259,6 +333,7 @@ extension Telemetry { sessionReplaySampleRate: Int64? = nil, sessionSampleRate: Int64? = nil, silentMultipleInit: Bool? = nil, + startRecordingImmediately: Bool? = nil, startSessionReplayRecordingManually: Bool? = nil, telemetryConfigurationSampleRate: Int64? = nil, telemetrySampleRate: Int64? = nil, @@ -309,6 +384,7 @@ extension Telemetry { sessionReplaySampleRate: sessionReplaySampleRate, sessionSampleRate: sessionSampleRate, silentMultipleInit: silentMultipleInit, + startRecordingImmediately: startRecordingImmediately, startSessionReplayRecordingManually: startSessionReplayRecordingManually, telemetryConfigurationSampleRate: telemetryConfigurationSampleRate, telemetrySampleRate: telemetrySampleRate, @@ -342,16 +418,23 @@ extension Telemetry { )) } - /// Collect metric value. + /// Collects a metric value. /// - /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicates filtering and - /// are get sampled with a different rate. Metric attributes are used to create facets for later querying and graphing. + /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicate filtering and + /// are sampled at a different rate. Metric attributes are used to create facets for later querying and graphing. /// /// - Parameters: - /// - name: The name of this metric. - /// - attributes: Parameters associated with this metric. - public func metric(name: String, attributes: [String: Encodable]) { - send(telemetry: .metric(name: name, attributes: attributes)) + /// - name: The name of the metric. + /// - attributes: The attributes associated with this metric. + /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate (15% by default). + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public func metric(name: String, attributes: [String: Encodable], sampleRate: Float = MetricTelemetry.defaultSampleRate) { + send(telemetry: .metric(MetricTelemetry(name: name, attributes: attributes, sampleRate: sampleRate))) } } @@ -422,6 +505,7 @@ extension ConfigurationTelemetry { sessionReplaySampleRate: other.sessionReplaySampleRate ?? sessionReplaySampleRate, sessionSampleRate: other.sessionSampleRate ?? sessionSampleRate, silentMultipleInit: other.silentMultipleInit ?? silentMultipleInit, + startRecordingImmediately: other.startRecordingImmediately ?? startRecordingImmediately, startSessionReplayRecordingManually: other.startSessionReplayRecordingManually ?? startSessionReplayRecordingManually, telemetryConfigurationSampleRate: other.telemetryConfigurationSampleRate ?? telemetryConfigurationSampleRate, telemetrySampleRate: other.telemetrySampleRate ?? telemetrySampleRate, diff --git a/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift index 80ac415d77..c184ec3d31 100644 --- a/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift +++ b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift @@ -25,5 +25,30 @@ public protocol FeatureRequestBuilder { /// - context: The current core context. /// - events: The events data to be uploaded. /// - Returns: The URL request. - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest +} + +/// Represents the context in which the request is being executed. +public struct ExecutionContext { + /// HTTP status code of the previous response. + public let previousResponseCode: Int? + + /// The current attempt number. + public let attempt: UInt + + /// Initializes the execution context. + /// - Parameters: + /// - previousResponseCode: Previous HTTP status code, if available. + /// - attempt: The current attempt number. + public init( + previousResponseCode: Int?, + attempt: UInt + ) { + self.previousResponseCode = previousResponseCode + self.attempt = attempt + } } diff --git a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift index 38f6625fa5..8104417bc0 100644 --- a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift +++ b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift @@ -23,6 +23,7 @@ public struct URLRequestBuilder { public static let ddEVPOriginHeaderField = "DD-EVP-ORIGIN" public static let ddEVPOriginVersionHeaderField = "DD-EVP-ORIGIN-VERSION" public static let ddRequestIDHeaderField = "DD-REQUEST-ID" + public static let ddIdempotencyKeyHeaderField = "DD-IDEMPOTENCY-KEY" public enum ContentType { case applicationJSON @@ -91,6 +92,13 @@ public struct URLRequestBuilder { public static func ddRequestIDHeader() -> HTTPHeader { return HTTPHeader(field: ddRequestIDHeaderField, value: { UUID().uuidString }) } + + /// An optional Datadog header for ensuring idempotent requests. + /// - Parameter key: The idempotency key. + /// - Returns: Header with the idempotency key. + public static func ddIdempotencyKeyHeader(key: String) -> HTTPHeader { + return HTTPHeader(field: ddIdempotencyKeyHeaderField, value: { key }) + } } /// Upload `URL`. private let url: URL diff --git a/DatadogInternal/Sources/Utils/DateFormatting.swift b/DatadogInternal/Sources/Utils/DateFormatting.swift index 7dc4f94cf5..ec32ed44d5 100644 --- a/DatadogInternal/Sources/Utils/DateFormatting.swift +++ b/DatadogInternal/Sources/Utils/DateFormatting.swift @@ -6,31 +6,20 @@ import Foundation -public protocol DateFormatterType { +public protocol DateFormatterType: Sendable { func string(from date: Date) -> String func date(from string: String) -> Date? } -extension ISO8601DateFormatter: DateFormatterType {} -extension DateFormatter: DateFormatterType {} +extension ISO8601DateFormatter: DateFormatterType, @unchecked Sendable {} +extension DateFormatter: DateFormatterType, @unchecked Sendable {} /// Date formatter producing `ISO8601` string representation of a given date. /// Should be used to encode dates in messages send to the server. public let iso8601DateFormatter: DateFormatterType = { - // As there is a known crash in iOS 11.0 and 11.1 when using `.withFractionalSeconds` option in `ISO8601DateFormatter`, - // we use different `DateFormatterType` implementation depending on the OS version. The problem was fixed by Apple in iOS 11.2. - if #available(iOS 11.2, *) { - let formatter = ISO8601DateFormatter() - formatter.formatOptions.insert(.withFractionalSeconds) - return formatter - } else { - let iso8601Formatter = DateFormatter() - iso8601Formatter.locale = Locale(identifier: "en_US_POSIX") - iso8601Formatter.timeZone = TimeZone(abbreviation: "UTC")! // swiftlint:disable:this force_unwrapping - iso8601Formatter.calendar = Calendar(identifier: .gregorian) - iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" // ISO8601 format - return iso8601Formatter - } + let formatter = ISO8601DateFormatter() + formatter.formatOptions.insert(.withFractionalSeconds) + return formatter }() /// Date formatter producing string representation of a given date for user-facing features (like console output). diff --git a/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift b/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift new file mode 100644 index 0000000000..a7dc318b49 --- /dev/null +++ b/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest + +final class DataCryptoTests: XCTestCase { + func testSha1() throws { + let str1 = "The quick brown fox jumps over the lazy dog" + let data1 = str1.data(using: .utf8)! + let sha1 = data1.sha1() + XCTAssertEqual(sha1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + + let str2 = "The quick brown fox jumps over the lazy cog" + let data2 = str2.data(using: .utf8)! + let sha2 = data2.sha1() + XCTAssertEqual(sha2, "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3") + } + + func testSha1_emptyString() throws { + let str = "" + let data = str.data(using: .utf8)! + let sha = data.sha1() + XCTAssertEqual(sha, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + } +} diff --git a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift index 3b804681c3..ac82227934 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift @@ -29,6 +29,7 @@ extension ConfigurationTelemetry { sessionReplaySampleRate: .mockRandom(), sessionSampleRate: .mockRandom(), silentMultipleInit: .mockRandom(), + startRecordingImmediately: .mockRandom(), startSessionReplayRecordingManually: .mockRandom(), telemetryConfigurationSampleRate: .mockRandom(), telemetrySampleRate: .mockRandom(), diff --git a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift index f61db878ac..d10b51b4c1 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift @@ -121,20 +121,43 @@ class TelemetryTests: XCTestCase { func testSendingMetricTelemetry() throws { // When - telemetry.metric(name: "metric name", attributes: ["attribute": "value"]) + telemetry.metric(name: "metric name", attributes: ["attribute": "value"], sampleRate: 4.21) // Then let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) XCTAssertEqual(metric.name, "metric name") XCTAssertEqual(metric.attributes as? [String: String], ["attribute": "value"]) + XCTAssertEqual(metric.sampleRate, 4.21) } - func testStartingMethodCalledMetricTrace_whenSampled() throws { - XCTAssertNotNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 100)) + func testMetricTelemetryDefaultSampleRate() throws { + // When + telemetry.metric(name: "metric name", attributes: [:]) + + // Then + let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) + } + + func testHeadSampleRateInMethodCalledMetric() throws { + XCTAssertNotNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100)) + XCTAssertNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 0)) } - func testStartingMethodCalledMetricTrace_whenNotSampled() throws { - XCTAssertNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 0)) + func testDefaultTailSampleRateInMethodCalledMetric() throws { + let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) + telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny()) + + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) + } + + func testTailSampleRateInMethodCalledMetric() throws { + let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) + telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny(), tailSampleRate: 42.5) + + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + XCTAssertEqual(metric.sampleRate, 42.5) } func testTrackingMethodCallMetricTelemetry() throws { @@ -143,7 +166,7 @@ class TelemetryTests: XCTestCase { let isSuccessful: Bool = .random() // When - let metricTrace = telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, samplingRate: 100) + let metricTrace = telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, headSampleRate: 100) Thread.sleep(forTimeInterval: 0.05) telemetry.stopMethodCalled(metricTrace, isSuccessful: isSuccessful) @@ -156,6 +179,7 @@ class TelemetryTests: XCTestCase { let executionTime = try XCTUnwrap(metric.attributes[MethodCalledMetric.executionTime] as? Int64) XCTAssertGreaterThan(executionTime, 0) XCTAssertLessThan(executionTime, TimeInterval(1).toInt64Nanoseconds) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) } // MARK: - Integration with Core @@ -173,10 +197,10 @@ class TelemetryTests: XCTestCase { core.telemetry.configuration(batchSize: 123) XCTAssertEqual(receiver.messages.lastTelemetry?.asConfiguration?.batchSize, 123) - core.telemetry.metric(name: "metric name", attributes: [:]) + core.telemetry.metric(name: "metric name", attributes: [:], sampleRate: 15) XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, "metric name") - let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny()) + let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) core.telemetry.stopMethodCalled(metricTrace) XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, MethodCalledMetric.name) } diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index 79c8c63a55..7e2934b234 100644 --- a/DatadogLogs.podspec +++ b/DatadogLogs.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogLogs" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "Datadog Logs Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogLogs/Sources/ConsoleLogger.swift b/DatadogLogs/Sources/ConsoleLogger.swift index d2a793cea0..4e42d1b0ae 100644 --- a/DatadogLogs/Sources/ConsoleLogger.swift +++ b/DatadogLogs/Sources/ConsoleLogger.swift @@ -23,12 +23,12 @@ internal final class ConsoleLogger: LoggerProtocol { /// The prefix to use when rendering log. private let prefix: String /// The function used to render log. - private let printFunction: (String, CoreLoggerLevel) -> Void + private let printFunction: @Sendable (String, CoreLoggerLevel) -> Void init( configuration: Configuration, dateProvider: DateProvider, - printFunction: @escaping (String, CoreLoggerLevel) -> Void + printFunction: @escaping @Sendable (String, CoreLoggerLevel) -> Void ) { self.dateProvider = dateProvider self.timeFormatter = presentationDateFormatter(withTimeZone: configuration.timeZone) diff --git a/DatadogLogs/Sources/Feature/LogsFeature.swift b/DatadogLogs/Sources/Feature/LogsFeature.swift index ffd1fa1794..fa8159d2a4 100644 --- a/DatadogLogs/Sources/Feature/LogsFeature.swift +++ b/DatadogLogs/Sources/Feature/LogsFeature.swift @@ -18,8 +18,8 @@ internal struct LogsFeature: DatadogRemoteFeature { let backtraceReporter: BacktraceReporting? - @ReadWriteLock - private var attributes: [String: Encodable] = [:] + /// Global attributes attached to every log event. + let attributes: SynchronizedAttributes /// Time provider. let dateProvider: DateProvider @@ -59,17 +59,6 @@ internal struct LogsFeature: DatadogRemoteFeature { self.messageReceiver = messageReceiver self.dateProvider = dateProvider self.backtraceReporter = backtraceReporter - } - - internal func addAttribute(forKey key: AttributeKey, value: AttributeValue) { - _attributes.mutate { $0[key] = value } - } - - internal func removeAttribute(forKey key: AttributeKey) { - _attributes.mutate { $0.removeValue(forKey: key) } - } - - internal func getAttributes() -> [String: Encodable] { - return attributes + self.attributes = SynchronizedAttributes(attributes: [:]) } } diff --git a/DatadogLogs/Sources/Feature/RequestBuilder.swift b/DatadogLogs/Sources/Feature/RequestBuilder.swift index 892f57683a..1444c19425 100644 --- a/DatadogLogs/Sources/Feature/RequestBuilder.swift +++ b/DatadogLogs/Sources/Feature/RequestBuilder.swift @@ -27,11 +27,15 @@ internal struct RequestBuilder: FeatureRequestBuilder { self.telemetry = telemetry } - func request(for events: [Event], with context: DatadogContext) -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) -> URLRequest { let builder = URLRequestBuilder( url: url(with: context), queryItems: [ - .ddsource(source: context.source) + .ddsource(source: context.source), ], headers: [ .contentTypeHeader(contentType: .applicationJSON), diff --git a/DatadogLogs/Sources/Log/SynchronizedAttributes.swift b/DatadogLogs/Sources/Log/SynchronizedAttributes.swift new file mode 100644 index 0000000000..d0270ed7cd --- /dev/null +++ b/DatadogLogs/Sources/Log/SynchronizedAttributes.swift @@ -0,0 +1,47 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// A thread-safe container for managing attributes in a key-value format. +/// This class allows concurrent access and modification of attributes, ensuring data consistency +/// through the use of a `ReadWriteLock`. It is designed to be used in scenarios where attributes +/// need to be safely managed across multiple threads or tasks. +internal final class SynchronizedAttributes: Sendable { + /// The underlying dictionary of attributes, wrapped in a `ReadWriteLock` to ensure thread safety. + private let attributes: ReadWriteLock<[String: Encodable]> + + /// Initializes a new instance of `SynchronizedAttributes` with the provided dictionary. + /// + /// - Parameter attributes: A dictionary of initial attributes. + init(attributes: [String: Encodable]) { + self.attributes = .init(wrappedValue: attributes) + } + + /// Adds or updates an attribute in the container. + /// + /// - Parameters: + /// - key: The key associated with the attribute. + /// - value: The value to associate with the key. + func addAttribute(key: AttributeKey, value: AttributeValue) { + attributes.mutate { $0[key] = value } + } + + /// Removes an attribute from the container. + /// + /// - Parameter key: The key of the attribute to remove. + func removeAttribute(forKey key: AttributeKey) { + attributes.mutate { $0.removeValue(forKey: key) } + } + + /// Retrieves the current dictionary of attributes. + /// + /// - Returns: A dictionary containing all the attributes. + func getAttributes() -> [String: Encodable] { + return attributes.wrappedValue + } +} diff --git a/DatadogLogs/Sources/Log/SynchronizedTags.swift b/DatadogLogs/Sources/Log/SynchronizedTags.swift new file mode 100644 index 0000000000..273b3a4e35 --- /dev/null +++ b/DatadogLogs/Sources/Log/SynchronizedTags.swift @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// A thread-safe container for managing tags in a set. +/// This class allows concurrent access and modification of tags, ensuring data consistency +/// through the use of a `ReadWriteLock`. It is designed to be used in scenarios where tags +/// need to be safely managed across multiple threads or tasks. +internal final class SynchronizedTags: Sendable { + /// The underlying set of tags, wrapped in a `ReadWriteLock` to ensure thread safety. + private let tags: ReadWriteLock> + + /// Initializes a new instance of `SynchronizedTags` with the provided set. + /// + /// - Parameter tags: A set of initial tags. + init(tags: Set) { + self.tags = .init(wrappedValue: tags) + } + + /// Adds a tag to the set. + /// + /// - Parameter tag: The tag to add. + func addTag(_ tag: String) { + tags.mutate { $0.insert(tag) } + } + + /// Removes a tag from the set. + /// + /// - Parameter tag: The tag to remove. + func removeTag(_ tag: String) { + tags.mutate { $0.remove(tag) } + } + + /// Removes tags from the set based on a predicate. + /// + /// - Parameter shouldRemove: A closure that takes a tag and returns `true` if the tag should be removed. + func removeTags(where shouldRemove: (String) -> Bool) { + tags.mutate { $0 = $0.filter { !shouldRemove($0) } } + } + + /// Retrieves the current set of tags. + /// + /// - Returns: A set containing all the tags. + func getTags() -> Set { + return tags.wrappedValue + } +} diff --git a/DatadogLogs/Sources/Logger.swift b/DatadogLogs/Sources/Logger.swift index b96b731d7b..ecaf65d23f 100644 --- a/DatadogLogs/Sources/Logger.swift +++ b/DatadogLogs/Sources/Logger.swift @@ -136,13 +136,13 @@ public struct Logger { private static func createOrThrow(with configuration: Configuration, in core: DatadogCoreProtocol) throws -> LoggerProtocol { if core is NOPDatadogCore { throw ProgrammerError( - description: "`Datadog.initialize()` must be called prior to `Logger.builder.build()`." + description: "`Datadog.initialize()` must be called prior to `Logger.create()`." ) } guard let feature = core.get(feature: LogsFeature.self) else { throw ProgrammerError( - description: "`Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + description: "`Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." ) } @@ -154,7 +154,8 @@ public struct Logger { } return RemoteLogger( - core: core, + featureScope: core.scope(for: LogsFeature.self), + globalAttributes: feature.attributes, configuration: RemoteLogger.Configuration( service: configuration.service, name: configuration.name, @@ -165,7 +166,8 @@ public struct Logger { ), dateProvider: feature.dateProvider, rumContextIntegration: configuration.bundleWithRumEnabled, - activeSpanIntegration: configuration.bundleWithTraceEnabled + activeSpanIntegration: configuration.bundleWithTraceEnabled, + backtraceReporter: feature.backtraceReporter ) }() diff --git a/DatadogLogs/Sources/LoggerProtocol.swift b/DatadogLogs/Sources/LoggerProtocol.swift index f0c7c55cb2..07c28adda0 100644 --- a/DatadogLogs/Sources/LoggerProtocol.swift +++ b/DatadogLogs/Sources/LoggerProtocol.swift @@ -39,7 +39,7 @@ extension CoreLoggerLevel { /// /// // logger reference /// var logger = Logger.create() -public protocol LoggerProtocol { +public protocol LoggerProtocol: Sendable { /// General purpose logging method. /// Sends a log with certain `level`, `message`, `error` and `attributes`. /// diff --git a/DatadogLogs/Sources/Logs.swift b/DatadogLogs/Sources/Logs.swift index c55513bcb1..19534e6ab4 100644 --- a/DatadogLogs/Sources/Logs.swift +++ b/DatadogLogs/Sources/Logs.swift @@ -85,7 +85,7 @@ public enum Logs { guard let feature = core.get(feature: LogsFeature.self) else { return } - feature.addAttribute(forKey: key, value: value) + feature.attributes.addAttribute(key: key, value: value) sendAttributesChanged(for: feature, in: core) } @@ -99,7 +99,7 @@ public enum Logs { guard let feature = core.get(feature: LogsFeature.self) else { return } - feature.removeAttribute(forKey: key) + feature.attributes.removeAttribute(forKey: key) sendAttributesChanged(for: feature, in: core) } @@ -107,7 +107,7 @@ public enum Logs { core.send( message: .baggage( key: GlobalLogAttributes.key, - value: GlobalLogAttributes(attributes: feature.getAttributes()) + value: GlobalLogAttributes(attributes: feature.attributes.getAttributes()) ) ) } diff --git a/DatadogLogs/Sources/RemoteLogger.swift b/DatadogLogs/Sources/RemoteLogger.swift index 2533c54d84..308f9a1077 100644 --- a/DatadogLogs/Sources/RemoteLogger.swift +++ b/DatadogLogs/Sources/RemoteLogger.swift @@ -8,8 +8,8 @@ import Foundation import DatadogInternal /// `Logger` sending logs to Datadog. -internal final class RemoteLogger: LoggerProtocol { - struct Configuration { +internal final class RemoteLogger: LoggerProtocol, Sendable { + struct Configuration: @unchecked Sendable { /// The `service` value for logs. /// See: [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). let service: String? @@ -25,10 +25,10 @@ internal final class RemoteLogger: LoggerProtocol { let sampler: Sampler } - /// `DatadogCore` instance managing this logger. - internal weak var core: DatadogCoreProtocol? + /// Logs feature scope. + let featureScope: FeatureScope /// Configuration specific to this logger. - internal let configuration: Configuration + let configuration: Configuration /// Date provider for logs. private let dateProvider: DateProvider /// Integration with RUM. It is used to correlate Logs with RUM events by injecting RUM context to `LogEvent`. @@ -37,53 +37,61 @@ internal final class RemoteLogger: LoggerProtocol { /// Integration with Tracing. It is used to correlate Logs with Spans by injecting `Span` context to `LogEvent`. /// Can be `false` if the integration is disabled for this logger. internal let activeSpanIntegration: Bool + /// Global attributes shared with all logger instances. + private let globalAttributes: SynchronizedAttributes /// Logger-specific attributes. - @ReadWriteLock - private var attributes: [String: Encodable] = [:] + private let loggerAttributes: SynchronizedAttributes /// Logger-specific tags. - @ReadWriteLock - private var tags: Set = [] + private let loggerTags: SynchronizedTags + /// Backtrace reporter for attaching binary images to cross-platform errors. + private let backtraceReporter: BacktraceReporting? init( - core: DatadogCoreProtocol, + featureScope: FeatureScope, + globalAttributes: SynchronizedAttributes, configuration: Configuration, dateProvider: DateProvider, rumContextIntegration: Bool, - activeSpanIntegration: Bool + activeSpanIntegration: Bool, + backtraceReporter: BacktraceReporting? ) { - self.core = core + self.featureScope = featureScope + self.globalAttributes = globalAttributes + self.loggerAttributes = SynchronizedAttributes(attributes: [:]) + self.loggerTags = SynchronizedTags(tags: []) self.configuration = configuration self.dateProvider = dateProvider self.rumContextIntegration = rumContextIntegration self.activeSpanIntegration = activeSpanIntegration + self.backtraceReporter = backtraceReporter } // MARK: - Attributes func addAttribute(forKey key: AttributeKey, value: AttributeValue) { - _attributes.mutate { $0[key] = value } + loggerAttributes.addAttribute(key: key, value: value) } func removeAttribute(forKey key: AttributeKey) { - _attributes.mutate { $0.removeValue(forKey: key) } + loggerAttributes.removeAttribute(forKey: key) } // MARK: - Tags func addTag(withKey key: String, value: String) { - _tags.mutate { $0.insert("\(key):\(value)") } + loggerTags.addTag("\(key):\(value)") } func removeTag(withKey key: String) { - _tags.mutate { $0 = $0.filter { !$0.hasPrefix("\(key):") } } + loggerTags.removeTags(where: { $0.hasPrefix("\(key):") }) } func add(tag: String) { - _tags.mutate { $0.insert(tag) } + loggerTags.addTag(tag) } func remove(tag: String) { - _tags.mutate { $0.remove(tag) } + loggerTags.removeTag(tag) } // MARK: - Logging @@ -100,32 +108,32 @@ internal final class RemoteLogger: LoggerProtocol { return } - let logsFeature = self.core?.get(feature: LogsFeature.self) - - let globalAttributes = logsFeature?.getAttributes() - // on user thread: let date = dateProvider.now let threadName = Thread.current.dd.name // capture current tags and attributes before opening the write event context - let tags = self.tags + let tags = loggerTags.getTags() + let globalAttributes = globalAttributes.getAttributes() + let loggerAttributes = loggerAttributes.getAttributes() var logAttributes = attributes + let isCrash = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorLogIsCrash)?.dd.decode() ?? false let errorFingerprint: String? = logAttributes?.removeValue(forKey: Logs.Attributes.errorFingerprint)?.dd.decode() let addBinaryImages = logAttributes?.removeValue(forKey: CrossPlatformAttributes.includeBinaryImages)?.dd.decode() ?? false - let userAttributes = self.attributes - .merging(logAttributes ?? [:]) { $1 } // prefer message attributes - let combinedAttributes: [String: any Encodable] - if let globalAttributes = globalAttributes { - combinedAttributes = globalAttributes.merging(userAttributes) { $1 } - } else { - combinedAttributes = userAttributes - } + let userAttributes = loggerAttributes + .merging(logAttributes ?? [:]) { $1 } // prefer `logAttributes`` + + let combinedAttributes: [String: any Encodable] = globalAttributes + .merging(userAttributes) { $1 } // prefer `userAttribute` // SDK context must be requested on the user thread to ensure that it provides values // that are up-to-date for the caller. - core?.scope(for: LogsFeature.self).eventWriteContext { context, writer in + featureScope.eventWriteContext { [weak self] context, writer in + guard let self else { + return + } + var internalAttributes: [String: Encodable] = [:] // When bundle with RUM is enabled, link RUM context (if available): @@ -137,7 +145,7 @@ internal final class RemoteLogger: LoggerProtocol { internalAttributes[LogEvent.Attributes.RUM.viewID] = rum.viewID internalAttributes[LogEvent.Attributes.RUM.actionID] = rum.userActionID } catch { - self.core?.telemetry + self.featureScope.telemetry .error("Fails to decode RUM context from Logs", error: error) } } @@ -149,7 +157,7 @@ internal final class RemoteLogger: LoggerProtocol { internalAttributes[LogEvent.Attributes.Trace.traceID] = trace.traceID?.toString(representation: .hexadecimal) internalAttributes[LogEvent.Attributes.Trace.spanID] = trace.spanID?.toString(representation: .decimal) } catch { - self.core?.telemetry + self.featureScope.telemetry .error("Fails to decode Span context from Logs", error: error) } } @@ -158,7 +166,7 @@ internal final class RemoteLogger: LoggerProtocol { var binaryImages: [BinaryImage]? if addBinaryImages { // TODO: RUM-4072 Replace full backtrace reporter with simpler binary image fetcher - binaryImages = try? logsFeature?.backtraceReporter?.generateBacktrace()?.binaryImages + binaryImages = try? self.backtraceReporter?.generateBacktrace()?.binaryImages } let builder = LogEventBuilder( @@ -189,7 +197,16 @@ internal final class RemoteLogger: LoggerProtocol { return } - self.core?.send( + // Add back in fingerprint and error source type + var busCombinedAttributes = combinedAttributes + if let errorSourcetype = error?.sourceType { + busCombinedAttributes[CrossPlatformAttributes.errorSourceType] = errorSourcetype + } + if let errorFingerprint = errorFingerprint { + busCombinedAttributes[Logs.Attributes.errorFingerprint] = errorFingerprint + } + + self.featureScope.send( message: .baggage( key: ErrorMessage.key, value: ErrorMessage( @@ -197,7 +214,7 @@ internal final class RemoteLogger: LoggerProtocol { message: log.error?.message ?? log.message, type: log.error?.kind, stack: log.error?.stack, - attributes: .init(combinedAttributes), + attributes: .init(busCombinedAttributes), binaryImages: binaryImages ) ) diff --git a/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift b/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift new file mode 100644 index 0000000000..403ff41801 --- /dev/null +++ b/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogLogs + +final class SynchronizedAttributesTests: XCTestCase { + func testAddAttribute() { + let synchronizedAttributes = SynchronizedAttributes(attributes: [:]) + synchronizedAttributes.addAttribute(key: "key1", value: "value1") + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertEqual(attributes["key1"] as? String, "value1") + XCTAssertEqual(attributes.count, 1) + } + + func testRemoveAttribute() { + let synchronizedAttributes = SynchronizedAttributes(attributes: ["key1": "value1", "key2": "value2"]) + synchronizedAttributes.removeAttribute(forKey: "key1") + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertNil(attributes["key1"]) + XCTAssertEqual(attributes["key2"] as? String, "value2") + XCTAssertEqual(attributes.count, 1) + } + + func testGetAttributes() { + let initialAttributes: [String: Encodable] = ["key1": "value1", "key2": "value2"] + let synchronizedAttributes = SynchronizedAttributes(attributes: initialAttributes) + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertEqual(attributes.count, 2) + XCTAssertEqual(attributes["key1"] as? String, "value1") + XCTAssertEqual(attributes["key2"] as? String, "value2") + } + + func testThreadSafety() { + let synchronizedAttributes = SynchronizedAttributes(attributes: [:]) + + callConcurrently( + closures: [ + { idx in synchronizedAttributes.addAttribute(key: "key\(idx)", value: "value\(idx)") }, + { idx in synchronizedAttributes.removeAttribute(forKey: "unknown-key\(idx)") }, + { _ in _ = synchronizedAttributes.getAttributes() }, + ], + iterations: 1_000 + ) + + XCTAssertEqual(synchronizedAttributes.getAttributes().count, 1_000) + } +} diff --git a/DatadogLogs/Tests/Log/SynchronizedTagsTests.swift b/DatadogLogs/Tests/Log/SynchronizedTagsTests.swift new file mode 100644 index 0000000000..95c81ecc4c --- /dev/null +++ b/DatadogLogs/Tests/Log/SynchronizedTagsTests.swift @@ -0,0 +1,65 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogLogs + +final class SynchronizedTagsTests: XCTestCase { + func testAddTag() { + let synchronizedTags = SynchronizedTags(tags: []) + synchronizedTags.addTag("tag1") + + let tags = synchronizedTags.getTags() + XCTAssertTrue(tags.contains("tag1")) + XCTAssertEqual(tags.count, 1) + } + + func testRemoveTag() { + let synchronizedTags = SynchronizedTags(tags: ["tag1", "tag2"]) + synchronizedTags.removeTag("tag1") + + let tags = synchronizedTags.getTags() + XCTAssertFalse(tags.contains("tag1")) + XCTAssertTrue(tags.contains("tag2")) + XCTAssertEqual(tags.count, 1) + } + + func testRemoveTagsWithPredicate() { + let synchronizedTags = SynchronizedTags(tags: ["tag1", "tag2", "tag3", "tag4"]) + synchronizedTags.removeTags { $0.contains("2") || $0.contains("4") } + + let tags = synchronizedTags.getTags() + XCTAssertFalse(tags.contains("tag2")) + XCTAssertFalse(tags.contains("tag4")) + XCTAssertTrue(tags.contains("tag1")) + XCTAssertTrue(tags.contains("tag3")) + XCTAssertEqual(tags.count, 2) + } + + func testGetTags() { + let initialTags: Set = ["tag1", "tag2"] + let synchronizedTags = SynchronizedTags(tags: initialTags) + + let tags = synchronizedTags.getTags() + XCTAssertEqual(tags, initialTags) + } + + func testThreadSafety() { + let synchronizedTags = SynchronizedTags(tags: []) + + callConcurrently( + closures: [ + { idx in synchronizedTags.addTag("tag\(idx)") }, + { idx in synchronizedTags.removeTag("unknown-tag\(idx)") }, + { idx in synchronizedTags.removeTags(where: { _ in false }) }, + { _ in _ = synchronizedTags.getTags() }, + ], + iterations: 1_000 + ) + + XCTAssertEqual(synchronizedTags.getTags().count, 1_000) + } +} diff --git a/DatadogLogs/Tests/LogsTests.swift b/DatadogLogs/Tests/LogsTests.swift index 8e29b2fac7..ba16749304 100644 --- a/DatadogLogs/Tests/LogsTests.swift +++ b/DatadogLogs/Tests/LogsTests.swift @@ -106,7 +106,7 @@ class LogsTests: XCTestCase { // Then let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) - XCTAssertEqual(feature.getAttributes()[attributeKey] as? String, attributeValue) + XCTAssertEqual(feature.attributes.getAttributes()[attributeKey] as? String, attributeValue) } func testLogsRemoveAttributeForwardedToFeature() throws { @@ -123,7 +123,7 @@ class LogsTests: XCTestCase { // Then let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) - XCTAssertNil(feature.getAttributes()[attributeKey]) + XCTAssertNil(feature.attributes.getAttributes()[attributeKey]) } func testItSendsGlobalLogUpdates_whenAddAttribute() throws { diff --git a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift index 8040d4f5e3..275a5837ee 100644 --- a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift +++ b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift @@ -332,3 +332,9 @@ extension LogEvent.Attributes: Equatable { && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) } } + +extension SynchronizedAttributes: AnyMockable { + public static func mockAny() -> SynchronizedAttributes { + return SynchronizedAttributes(attributes: [:]) + } +} diff --git a/DatadogLogs/Tests/RemoteLoggerTests.swift b/DatadogLogs/Tests/RemoteLoggerTests.swift index 9896fb042d..6b98bfd8c3 100644 --- a/DatadogLogs/Tests/RemoteLoggerTests.swift +++ b/DatadogLogs/Tests/RemoteLoggerTests.swift @@ -7,11 +7,14 @@ import XCTest import TestUtilities import DatadogInternal - @testable import DatadogLogs -private class ErrorMessageReceiverMock: FeatureMessageReceiver { - struct ErrorMessage: Decodable { +class RemoteLoggerTests: XCTestCase { + private let featureScope = FeatureScopeMock() + + // MARK: - Sending Error Message over Message Bus + + private struct ExpectedErrorMessage: Decodable { /// The Log error message let message: String /// The Log error type @@ -22,227 +25,317 @@ private class ErrorMessageReceiverMock: FeatureMessageReceiver { let source: String /// The Log attributes let attributes: [String: AnyCodable] + /// Binary images + let binaryImages: [BinaryImage]? } - var errors: [ErrorMessage] = [] - - /// Adds RUM Error with given message and stack to current RUM View. - func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { - guard - let error = try? message.baggage(forKey: "error", type: ErrorMessage.self) - else { - return false - } + func testWhenNonErrorLogged_itDoesNotPostsToMessageBus() throws { + // Given + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() + ) - self.errors.append(error) + // When + logger.info("Info message") - return true + // Then + XCTAssertEqual(featureScope.messagesSent().count, 0) } -} -class RemoteLoggerTests: XCTestCase { - func testItSendsErrorAlongWithErrorLog() throws { - let messageReceiver = ErrorMessageReceiverMock() - - let core = PassthroughCoreMock( - expectation: expectation(description: "Send error"), - messageReceiver: messageReceiver + func testWhenErrorLogged_itPostsToMessageBus() throws { + // Given + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() ) + // When + logger.error("Error message") + + // Then + let errorBaggage = try XCTUnwrap(featureScope.messagesSent().firstBaggage(withKey: "error")) + let error: ExpectedErrorMessage = try errorBaggage.decode() + XCTAssertEqual(error.message, "Error message") + } + + func testWhenCrossPlatformCrashErrorLogged_itDoesNotPostToMessageBus() throws { // Given let logger = RemoteLogger( - core: core, + featureScope: featureScope, + globalAttributes: .mockAny(), configuration: .mockAny(), dateProvider: RelativeDateProvider(), rumContextIntegration: false, - activeSpanIntegration: false + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() ) // When - logger.error("Error message") + logger.error("Error message", error: nil, attributes: [CrossPlatformAttributes.errorLogIsCrash: true]) // Then - waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertEqual(featureScope.messagesSent().count, 0) + } + + func testWhenAttributesContainIncludeBinaryImages_itPostsBinaryImagesToMessageBus() throws { + let stubBacktrace: BacktraceReport = .mockRandom() + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock(backtrace: stubBacktrace) + ) - XCTAssertEqual(messageReceiver.errors.count, 1) - XCTAssertEqual(messageReceiver.errors.first?.message, "Error message") + // When + logger.error("Information message", error: ErrorMock(), attributes: [CrossPlatformAttributes.includeBinaryImages: true]) + + // Then + let errorBaggage = try XCTUnwrap(featureScope.messagesSent().firstBaggage(withKey: "error")) + let error: ExpectedErrorMessage = try errorBaggage.decode() + // This is removed because binary images are sent in the message, so the additional attribute isn't needed + XCTAssertNil(error.attributes[CrossPlatformAttributes.includeBinaryImages]) + XCTAssertEqual(error.binaryImages?.count, stubBacktrace.binaryImages.count) + for i in 0.. DDLogEvent?) { + configuration.eventMapper = { swiftEvent in + let objcEvent = DDLogEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } } @objc diff --git a/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift b/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift new file mode 100644 index 0000000000..7c8333bd24 --- /dev/null +++ b/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift @@ -0,0 +1,486 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogLogs +import DatadogInternal + +@objc +public class DDLogEvent: NSObject { + internal var swiftModel: LogEvent + + internal init(swiftModel: LogEvent) { + self.swiftModel = swiftModel + } + + @objc public var date: Date { + swiftModel.date + } + + @objc public var status: DDLogEventStatus { + .init(swift: swiftModel.status) + } + + @objc public var message: String { + set { swiftModel.message = newValue } + get { swiftModel.message } + } + + @objc public var error: DDLogEventError? { + if swiftModel.error != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var serviceName: String { + swiftModel.serviceName + } + + @objc public var environment: String { + swiftModel.environment + } + + @objc public var loggerName: String { + swiftModel.loggerName + } + + @objc public var loggerVersion: String { + swiftModel.loggerVersion + } + + @objc public var threadName: String? { + swiftModel.threadName + } + + @objc public var applicationVersion: String { + swiftModel.applicationVersion + } + + @objc public var applicationBuildNumber: String { + swiftModel.applicationBuildNumber + } + + @objc public var buildId: String? { + swiftModel.buildId + } + + @objc public var variant: String? { + swiftModel.variant + } + + @objc public var dd: DDLogEventDd { + .init(root: self) + } + + @objc public var os: DDLogEventOperatingSystem { + .init(root: self) + } + + @objc public var userInfo: DDLogEventUserInfo { + .init(root: self) + } + + @objc public var networkConnectionInfo: DDLogEventNetworkConnectionInfo? { + if swiftModel.networkConnectionInfo != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var mobileCarrierInfo: DDLogEventCarrierInfo? { + if swiftModel.mobileCarrierInfo != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var attributes: DDLogEventAttributes { + .init(root: self) + } + + @objc public var tags: [String]? { + set { swiftModel.tags = newValue } + get { swiftModel.tags } + } +} + +@objc +public enum DDLogEventStatus: Int { + internal init(swift: LogEvent.Status) { + switch swift { + case .debug: self = .debug + case .info: self = .info + case .notice: self = .notice + case .warn: self = .warn + case .error: self = .error + case .critical: self = .critical + case .emergency: self = .emergency + } + } + + internal var toSwift: LogEvent.Status { + switch self { + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warn: return .warn + case .error: return .error + case .critical: return .critical + case .emergency: return .emergency + } + } + + case debug + case info + case notice + case warn + case error + case critical + case emergency +} + +@objc +public class DDLogEventAttributes: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var userAttributes: [String: Any] { + set { root.swiftModel.attributes.userAttributes = newValue.dd.swiftAttributes } + get { root.swiftModel.attributes.userAttributes.dd.objCAttributes } + } +} + +@objc +public class DDLogEventUserInfo: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var id: String? { + root.swiftModel.userInfo.id + } + + @objc public var name: String? { + root.swiftModel.userInfo.name + } + + @objc public var email: String? { + root.swiftModel.userInfo.email + } + + @objc public var extraInfo: [String: Any] { + set { root.swiftModel.userInfo.extraInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.userInfo.extraInfo.dd.objCAttributes } + } +} + +@objc +public class DDLogEventError: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var kind: String? { + set { root.swiftModel.error?.kind = newValue } + get { root.swiftModel.error?.kind } + } + + @objc public var message: String? { + set { root.swiftModel.error?.message = newValue } + get { root.swiftModel.error?.message } + } + + @objc public var stack: String? { + set { root.swiftModel.error?.stack = newValue } + get { root.swiftModel.error?.stack } + } + + @objc public var sourceType: String { + // swiftlint:disable force_unwrapping + set { root.swiftModel.error!.sourceType = newValue } + get { root.swiftModel.error!.sourceType } + // swiftlint:enable force_unwrapping + } + + @objc public var fingerprint: String? { + set { root.swiftModel.error?.fingerprint = newValue } + get { root.swiftModel.error?.fingerprint } + } + + @objc public var binaryImages: [DDLogEventBinaryImage]? { + set { root.swiftModel.error?.binaryImages = newValue?.map { $0.swiftModel } } + get { root.swiftModel.error?.binaryImages?.map { DDLogEventBinaryImage(swiftModel: $0) } } + } +} + +@objc +public class DDLogEventBinaryImage: NSObject { + internal let swiftModel: LogEvent.Error.BinaryImage + + internal init(swiftModel: LogEvent.Error.BinaryImage) { + self.swiftModel = swiftModel + } + + @objc public var arch: String? { + swiftModel.arch + } + + @objc public var isSystem: Bool { + swiftModel.isSystem + } + + @objc public var loadAddress: String? { + swiftModel.loadAddress + } + + @objc public var maxAddress: String? { + swiftModel.maxAddress + } + + @objc public var name: String { + swiftModel.name + } + + @objc public var uuid: String { + swiftModel.uuid + } +} + +@objc +public class DDLogEventOperatingSystem: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var name: String { + root.swiftModel.os.name + } + + @objc public var version: String { + root.swiftModel.os.version + } + + @objc public var build: String? { + root.swiftModel.os.build + } +} + +@objc +public class DDLogEventDd: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var device: DDLogEventDeviceInfo { + .init(root: root) + } +} + +@objc +public class DDLogEventDeviceInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var brand: String { + root.swiftModel.dd.device.brand + } + + @objc public var name: String { + root.swiftModel.dd.device.name + } + + @objc public var model: String { + root.swiftModel.dd.device.model + } + + @objc public var architecture: String { + root.swiftModel.dd.device.architecture + } +} + +@objc +public class DDLogEventNetworkConnectionInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var reachability: DDLogEventReachability { + // swiftlint:disable force_unwrapping + .init(swift: root.swiftModel.networkConnectionInfo!.reachability) + // swiftlint:enable force_unwrapping + } + + @objc public var availableInterfaces: [Int]? { + root.swiftModel.networkConnectionInfo?.availableInterfaces?.map { DDLogEventInterface(swift: $0).rawValue } + } + + @objc public var supportsIPv4: NSNumber? { + root.swiftModel.networkConnectionInfo?.supportsIPv4 as NSNumber? + } + + @objc public var supportsIPv6: NSNumber? { + root.swiftModel.networkConnectionInfo?.supportsIPv6 as NSNumber? + } + + @objc public var isExpensive: NSNumber? { + root.swiftModel.networkConnectionInfo?.isExpensive as NSNumber? + } + + @objc public var isConstrained: NSNumber? { + root.swiftModel.networkConnectionInfo?.isConstrained as NSNumber? + } +} + +@objc +public enum DDLogEventReachability: Int { + internal init(swift: NetworkConnectionInfo.Reachability) { + switch swift { + case .yes: self = .yes + case .maybe: self = .maybe + case .no: self = .no + } + } + + internal var toSwift: NetworkConnectionInfo.Reachability { + switch self { + case .yes: return .yes + case .maybe: return .maybe + case .no: return .no + } + } + + case yes + case maybe + case no +} + +@objc +public enum DDLogEventInterface: Int { + internal init(swift: NetworkConnectionInfo.Interface) { + switch swift { + case .wifi: self = .wifi + case .wiredEthernet: self = .wiredEthernet + case .cellular: self = .cellular + case .loopback: self = .loopback + case .other: self = .other + } + } + + internal var toSwift: NetworkConnectionInfo.Interface { + switch self { + case .wifi: return .wifi + case .wiredEthernet: return .wiredEthernet + case .cellular: return .cellular + case .loopback: return .loopback + case .other: return .other + } + } + + case wifi + case wiredEthernet + case cellular + case loopback + case other +} + +@objc +public class DDLogEventCarrierInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.mobileCarrierInfo?.carrierName + } + + @objc public var carrierISOCountryCode: String? { + root.swiftModel.mobileCarrierInfo?.carrierISOCountryCode + } + + @objc public var carrierAllowsVOIP: Bool { + // swiftlint:disable force_unwrapping + root.swiftModel.mobileCarrierInfo!.carrierAllowsVOIP + // swiftlint:enable force_unwrapping + } + + @objc public var radioAccessTechnology: DDLogEventRadioAccessTechnology { + // swiftlint:disable force_unwrapping + .init(swift: root.swiftModel.mobileCarrierInfo!.radioAccessTechnology) + // swiftlint:enable force_unwrapping + } +} + +@objc +public enum DDLogEventRadioAccessTechnology: Int { + internal init(swift: CarrierInfo.RadioAccessTechnology) { + switch swift { + case .GPRS: self = .GPRS + case .Edge: self = .Edge + case .WCDMA: self = .WCDMA + case .HSDPA: self = .HSDPA + case .HSUPA: self = .HSUPA + case .CDMA1x: self = .CDMA1x + case .CDMAEVDORev0: self = .CDMAEVDORev0 + case .CDMAEVDORevA: self = .CDMAEVDORevA + case .CDMAEVDORevB: self = .CDMAEVDORevB + case .eHRPD: self = .eHRPD + case .LTE: self = .LTE + case .unknown: self = .unknown + } + } + + internal var toSwift: CarrierInfo.RadioAccessTechnology { + switch self { + case .GPRS: return .GPRS + case .Edge: return .Edge + case .WCDMA: return .WCDMA + case .HSDPA: return .HSDPA + case .HSUPA: return .HSUPA + case .CDMA1x: return .CDMA1x + case .CDMAEVDORev0: return .CDMAEVDORev0 + case .CDMAEVDORevA: return .CDMAEVDORevA + case .CDMAEVDORevB: return .CDMAEVDORevB + case .eHRPD: return .eHRPD + case .LTE: return .LTE + case .unknown: return .unknown + } + } + + case GPRS + case Edge + case WCDMA + case HSDPA + case HSUPA + case CDMA1x + case CDMAEVDORev0 + case CDMAEVDORevA + case CDMAEVDORevB + case eHRPD + case LTE + case unknown +} diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 0aa46461cb..9c6bf5d2dd 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -2991,10 +2991,22 @@ public class DDRUMLongTaskEventLongTask: NSObject { self.root = root } + @objc public var blockingDuration: NSNumber? { + root.swiftModel.longTask.blockingDuration as NSNumber? + } + @objc public var duration: NSNumber { root.swiftModel.longTask.duration as NSNumber } + @objc public var entryType: DDRUMLongTaskEventLongTaskEntryType { + .init(swift: root.swiftModel.longTask.entryType) + } + + @objc public var firstUiEventTimestamp: NSNumber? { + root.swiftModel.longTask.firstUiEventTimestamp as NSNumber? + } + @objc public var id: String? { root.swiftModel.longTask.id } @@ -3002,6 +3014,130 @@ public class DDRUMLongTaskEventLongTask: NSObject { @objc public var isFrozenFrame: NSNumber? { root.swiftModel.longTask.isFrozenFrame as NSNumber? } + + @objc public var renderStart: NSNumber? { + root.swiftModel.longTask.renderStart as NSNumber? + } + + @objc public var scripts: [DDRUMLongTaskEventLongTaskScripts]? { + root.swiftModel.longTask.scripts?.map { DDRUMLongTaskEventLongTaskScripts(swiftModel: $0) } + } + + @objc public var styleAndLayoutStart: NSNumber? { + root.swiftModel.longTask.styleAndLayoutStart as NSNumber? + } +} + +@objc +public enum DDRUMLongTaskEventLongTaskEntryType: Int { + internal init(swift: RUMLongTaskEvent.LongTask.EntryType?) { + switch swift { + case nil: self = .none + case .longTask?: self = .longTask + case .longAnimationFrame?: self = .longAnimationFrame + } + } + + internal var toSwift: RUMLongTaskEvent.LongTask.EntryType? { + switch self { + case .none: return nil + case .longTask: return .longTask + case .longAnimationFrame: return .longAnimationFrame + } + } + + case none + case longTask + case longAnimationFrame +} + +@objc +public class DDRUMLongTaskEventLongTaskScripts: NSObject { + internal var swiftModel: RUMLongTaskEvent.LongTask.Scripts + internal var root: DDRUMLongTaskEventLongTaskScripts { self } + + internal init(swiftModel: RUMLongTaskEvent.LongTask.Scripts) { + self.swiftModel = swiftModel + } + + @objc public var duration: NSNumber? { + root.swiftModel.duration as NSNumber? + } + + @objc public var executionStart: NSNumber? { + root.swiftModel.executionStart as NSNumber? + } + + @objc public var forcedStyleAndLayoutDuration: NSNumber? { + root.swiftModel.forcedStyleAndLayoutDuration as NSNumber? + } + + @objc public var invoker: String? { + root.swiftModel.invoker + } + + @objc public var invokerType: DDRUMLongTaskEventLongTaskScriptsInvokerType { + .init(swift: root.swiftModel.invokerType) + } + + @objc public var pauseDuration: NSNumber? { + root.swiftModel.pauseDuration as NSNumber? + } + + @objc public var sourceCharPosition: NSNumber? { + root.swiftModel.sourceCharPosition as NSNumber? + } + + @objc public var sourceFunctionName: String? { + root.swiftModel.sourceFunctionName + } + + @objc public var sourceUrl: String? { + root.swiftModel.sourceUrl + } + + @objc public var startTime: NSNumber? { + root.swiftModel.startTime as NSNumber? + } + + @objc public var windowAttribution: String? { + root.swiftModel.windowAttribution + } +} + +@objc +public enum DDRUMLongTaskEventLongTaskScriptsInvokerType: Int { + internal init(swift: RUMLongTaskEvent.LongTask.Scripts.InvokerType?) { + switch swift { + case nil: self = .none + case .userCallback?: self = .userCallback + case .eventListener?: self = .eventListener + case .resolvePromise?: self = .resolvePromise + case .rejectPromise?: self = .rejectPromise + case .classicScript?: self = .classicScript + case .moduleScript?: self = .moduleScript + } + } + + internal var toSwift: RUMLongTaskEvent.LongTask.Scripts.InvokerType? { + switch self { + case .none: return nil + case .userCallback: return .userCallback + case .eventListener: return .eventListener + case .resolvePromise: return .resolvePromise + case .rejectPromise: return .rejectPromise + case .classicScript: return .classicScript + case .moduleScript: return .moduleScript + } + } + + case none + case userCallback + case eventListener + case resolvePromise + case rejectPromise + case classicScript + case moduleScript } @objc @@ -7245,6 +7381,11 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.forwardReports != nil ? DDTelemetryConfigurationEventTelemetryConfigurationForwardReports(root: root) : nil } + @objc public var imagePrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.imagePrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.imagePrivacyLevel } + } + @objc public var initializationType: String? { set { root.swiftModel.telemetry.configuration.initializationType = newValue } get { root.swiftModel.telemetry.configuration.initializationType } @@ -7325,6 +7466,16 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.telemetryUsageSampleRate as NSNumber? } + @objc public var textAndInputPrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.textAndInputPrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.textAndInputPrivacyLevel } + } + + @objc public var touchPrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.touchPrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.touchPrivacyLevel } + } + @objc public var traceContextInjection: DDTelemetryConfigurationEventTelemetryConfigurationTraceContextInjection { set { root.swiftModel.telemetry.configuration.traceContextInjection = newValue.toSwift } get { .init(swift: root.swiftModel.telemetry.configuration.traceContextInjection) } @@ -7715,4 +7866,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/41d2cb901a87fa025843c85568c16d3e199fea4c +// Generated from https://github.com/DataDog/rum-events-format/tree/ec07c062cbbb2f19b49d08f72bc95703b502906d diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec index f21ef0068a..de414f8290 100644 --- a/DatadogRUM.podspec +++ b/DatadogRUM.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogRUM" - s.version = "2.16.0" + s.version = "2.17.0" s.summary = "Datadog Real User Monitoring Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index b013fb5382..3691352a0a 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -1337,19 +1337,108 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Long Task properties public struct LongTask: Codable { - /// Duration in ns of the long task + /// Duration in ns for which the animation frame was being blocked + public let blockingDuration: Int64? + + /// Duration in ns of the long task or long animation frame public let duration: Int64 - /// UUID of the long task + /// Type of the event: long task or long animation frame + public let entryType: EntryType? + + /// Start time of of the first UI event (mouse/keyboard and so on) to be handled during the course of this frame + public let firstUiEventTimestamp: Double? + + /// UUID of the long task or long animation frame public let id: String? /// Whether this long task is considered a frozen frame public let isFrozenFrame: Bool? + /// Start time of the rendering cycle, which includes requestAnimationFrame callbacks, style and layout calculation, resize observer and intersection observer callbacks + public let renderStart: Double? + + /// A list of long scripts that were executed over the course of the long frame + public let scripts: [Scripts]? + + /// Start time of the time period spent in style and layout calculations + public let styleAndLayoutStart: Double? + enum CodingKeys: String, CodingKey { + case blockingDuration = "blocking_duration" case duration = "duration" + case entryType = "entry_type" + case firstUiEventTimestamp = "first_ui_event_timestamp" case id = "id" case isFrozenFrame = "is_frozen_frame" + case renderStart = "render_start" + case scripts = "scripts" + case styleAndLayoutStart = "style_and_layout_start" + } + + /// Type of the event: long task or long animation frame + public enum EntryType: String, Codable { + case longTask = "long-task" + case longAnimationFrame = "long-animation-frame" + } + + public struct Scripts: Codable { + /// Duration in ns between startTime and when the subsequent microtask queue has finished processing + public let duration: Int64? + + /// Time after compilation + public let executionStart: Double? + + /// Duration in ns of the the total time spent processing forced layout and style inside this function + public let forcedStyleAndLayoutDuration: Int64? + + /// Information about the invoker of the script + public let invoker: String? + + /// Type of the invoker of the script + public let invokerType: InvokerType? + + /// Duration in ns of the total time spent in 'pausing' synchronous operations (alert, synchronous XHR) + public let pauseDuration: Int64? + + /// The script character position where available (or -1 if not found) + public let sourceCharPosition: Int64? + + /// The script function name where available (or empty if not found) + public let sourceFunctionName: String? + + /// The script resource name where available (or empty if not found) + public let sourceUrl: String? + + /// Time the entry function was invoked + public let startTime: Double? + + /// The container (the top-level document, or an