From 6443ff17e16b2684fa1da3da826a196559453b26 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 20 Feb 2023 17:22:49 +0100 Subject: [PATCH 1/2] RUMM-2972 Add support to `UISegmentedControl` recording --- .../SRHost/Fixtures/Fixtures.swift | 6 + .../SRHost/Fixtures/InputElements.storyboard | 118 ++++++++++++++++++ .../SRSnapshotTests.xcodeproj/project.pbxproj | 2 +- .../SRSnapshotTests/SRSnapshotTests.swift | 18 +++ .../Utils/ImageRendering.swift | 19 ++- .../WireframesBuilder.swift | 3 +- .../Recorder/Utilities/SystemColors.swift | 54 ++++++++ .../NodeRecorders/UISegmentRecorder.swift | 110 ++++++++++++++++ .../ViewTreeSnapshotBuilder.swift | 1 + .../UISegmentRecorderTests.swift | 57 +++++++++ 10 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift create mode 100644 DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorderTests.swift diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift index 32e72bc62b..25877f547c 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift @@ -10,6 +10,7 @@ internal enum Fixture: CaseIterable { case basicShapes case basicTexts case sliders + case segments var menuItemTitle: String { switch self { @@ -19,6 +20,8 @@ internal enum Fixture: CaseIterable { return "Basic Texts" case .sliders: return "Sliders" + case .segments: + return "Segments" } } @@ -30,6 +33,9 @@ internal enum Fixture: CaseIterable { return UIStoryboard.basic.instantiateViewController(withIdentifier: "Texts") case .sliders: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Sliders") + case .segments: + return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Segments") + } } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard index 1f61cb608a..eba75aebb0 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard @@ -3,6 +3,7 @@ + @@ -86,8 +87,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -100,5 +212,11 @@ + + + + + + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index b494e2ac28..969fef59ed 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -549,7 +549,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ncreated/Framing"; requirement = { - branch = framer; + branch = "ship-framer"; kind = branch; }; }; diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index 9f67c96aa4..9e576a4c4f 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -52,4 +52,22 @@ final class SRSnapshotTests: SnapshotTestCase { record: recordingMode ) } + + func testSegments() throws { + show(fixture: .segments) + + var image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-maskAll-privacy"), + record: recordingMode + ) + } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift index ba039f9348..8eff2de301 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift @@ -62,7 +62,7 @@ private extension SRTextWireframe { width: CGFloat(width), height: CGFloat(height), style: frameStyle(border: border, style: shapeStyle), - content: frameContent(text: text, textStyle: textStyle) + content: frameContent(text: text, textStyle: textStyle, textPosition: textPosition) ) } } @@ -103,7 +103,7 @@ private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> Bluepri return fs } -private func frameContent(text: String, textStyle: SRTextStyle?) -> BlueprintFrameContent { +private func frameContent(text: String, textStyle: SRTextStyle?, textPosition: SRTextPosition?) -> BlueprintFrameContent { var fc = BlueprintFrameContent( text: text, textColor: .clear, @@ -115,6 +115,21 @@ private func frameContent(text: String, textStyle: SRTextStyle?) -> BlueprintFra fc.font = .systemFont(ofSize: CGFloat(textStyle.size)) } + if let textPosition = textPosition { + switch textPosition.alignment?.horizontal { + case .left?: fc.horizontalAlignment = .leading + case .center?: fc.horizontalAlignment = .center + case .right?: fc.horizontalAlignment = .trailing + default: break + } + switch textPosition.alignment?.vertical { + case .top?: fc.verticalAlignment = .leading + case .center?: fc.verticalAlignment = .center + case .bottom?: fc.verticalAlignment = .trailing + default: break + } + } + return fc } diff --git a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift index 2b020c4c63..98b7d272d2 100644 --- a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift @@ -87,6 +87,7 @@ internal class WireframesBuilder { frame: CGRect, text: String, textFrame: CGRect? = nil, + textAlignment: SRTextPosition.Alignment? = nil, clip: SRContentClip? = nil, textColor: CGColor? = nil, font: UIFont? = nil, @@ -101,7 +102,7 @@ internal class WireframesBuilder { if let textFrame = textFrame { textPosition = .init( - alignment: nil, // TODO: RUMM-2452 Improve text rendering + alignment: textAlignment, padding: .init( bottom: Int64(withNoOverflow: frame.maxY - textFrame.maxY), left: Int64(withNoOverflow: textFrame.minX - frame.minX), diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift new file mode 100644 index 0000000000..711813ff63 --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.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 UIKit + +/// Collection of system colors. +/// +/// Contextual colors are light- and dark-mode sensitive and must be implemented as computed variables, +/// so they return different values upon `UIUserInterfaceStyle` change. +/// +/// For older iOS versions that do not support `UIUserInterfaceStyle`, approximate fallbacks are provided. +/// See https://gist.github.com/ncreated/35bf4d69d83d1a5ab408ff29a77fc9ff for reference when updating this collection. +internal enum SystemColors { + static let clear: CGColor = UIColor.clear.cgColor + + static var tertiarySystemFill: CGColor { + if #available(iOS 13.0, *) { + return UIColor.tertiarySystemFill.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 118 / 255, green: 118 / 255, blue: 128 / 255, alpha: 1).cgColor + } + } + + static var tertiarySystemBackground: CGColor { + if #available(iOS 13.0, *) { + return UIColor.tertiarySystemBackground.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 255 / 255, green: 255 / 255, blue: 255 / 255, alpha: 1).cgColor + } + } + + static var secondarySystemFill: CGColor { + if #available(iOS 13.0, *) { + return UIColor.secondarySystemFill.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 120 / 255, green: 120 / 255, blue: 128 / 255, alpha: 1).cgColor + } + } + + static var label: CGColor { + if #available(iOS 13.0, *) { + return UIColor.label.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1).cgColor + } + } +} diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift new file mode 100644 index 0000000000..36e667bb32 --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift @@ -0,0 +1,110 @@ +/* + * 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 UIKit + +internal struct UISegmentRecorder: NodeRecorder { + func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeSnapshotBuilder.Context) -> NodeSemantics? { + guard let segment = view as? UISegmentedControl else { + return nil + } + + guard attributes.isVisible else { + return InvisibleElement.constant + } + + let ids = context.ids.nodeIDs(1 + segment.numberOfSegments, for: segment) + + let builder = UISegmentWireframesBuilder( + wireframeRect: attributes.frame, + attributes: attributes, + textObfuscator: context.recorder.privacy == .maskAll ? context.textObfuscator : nopTextObfuscator, + backgroundWireframeID: ids[0], + segmentWireframeIDs: Array(ids[1.. [SRWireframe] { + let numberOfSegments = segmentWireframeIDs.count + guard numberOfSegments > 0, segmentTitles.count == numberOfSegments, (selectedSegmentIndex ?? 0) < numberOfSegments else { + return [] // illegal, should not happen + } + + // Create background wireframe: + let background = builder.createShapeWireframe( + id: backgroundWireframeID, + frame: wireframeRect, + borderColor: nil, + borderWidth: nil, + backgroundColor: attributes.backgroundColor ?? SystemColors.tertiarySystemFill, + cornerRadius: 8, + opacity: attributes.alpha + ) + + // Create segment wireframes: + let segmentSize = CGSize( + width: wireframeRect.width / CGFloat(numberOfSegments), + height: wireframeRect.height * 0.96 + ) + + var segmentRects: [CGRect] = [] // rects for succeeding segments + var dividedRect = wireframeRect + for _ in (0.. Date: Mon, 20 Feb 2023 18:23:47 +0100 Subject: [PATCH 2/2] RUMM-2972 Reuse `SystemColors` for `UISlider` --- .../Sources/Recorder/Utilities/SystemColors.swift | 9 +++++++++ .../NodeRecorders/UISliderRecorder.swift | 15 ++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift index 711813ff63..cf23de8716 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift @@ -43,6 +43,15 @@ internal enum SystemColors { } } + static var tintColor: CGColor { + if #available(iOS 15.0, *) { + return UIColor.tintColor.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 0 / 255, green: 122 / 255, blue: 255 / 255, alpha: 1).cgColor + } + } + static var label: CGColor { if #available(iOS 13.0, *) { return UIColor.label.cgColor diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift index 948f8106df..42ac3becab 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift @@ -36,13 +36,6 @@ internal struct UISliderRecorder: NodeRecorder { } internal struct UISliderWireframesBuilder: NodeWireframesBuilder { - struct SystemDefaults { - static let thumbTintColor = UIColor.white.cgColor - static let thumbBorderColor = UIColor.lightGray.cgColor - static let minTrackColor = UIColor.systemBlue.cgColor - static let maxTrackColor = UIColor.lightGray.cgColor - } - var wireframeRect: CGRect let attributes: ViewAttributes @@ -73,9 +66,9 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { let thumb = builder.createShapeWireframe( id: thumbWireframeID, frame: thumbFrame, - borderColor: isEnabled ? SystemDefaults.thumbBorderColor : SystemDefaults.thumbBorderColor.copy(alpha: 0.5), + borderColor: isEnabled ? SystemColors.secondarySystemFill : SystemColors.tertiarySystemFill, borderWidth: 1, - backgroundColor: thumbTintColor ?? SystemDefaults.thumbTintColor, + backgroundColor: isEnabled ? (thumbTintColor ?? UIColor.white.cgColor) : SystemColors.tertiarySystemBackground, cornerRadius: radius, opacity: attributes.alpha ) @@ -90,7 +83,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { frame: leftTrackFrame, borderColor: nil, borderWidth: nil, - backgroundColor: minTrackTintColor ?? SystemDefaults.minTrackColor, + backgroundColor: minTrackTintColor ?? SystemColors.tintColor, cornerRadius: 0, opacity: isEnabled ? attributes.alpha : 0.5 ) @@ -105,7 +98,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { frame: rightTrackFrame, borderColor: nil, borderWidth: nil, - backgroundColor: maxTrackTintColor ?? SystemDefaults.maxTrackColor, + backgroundColor: maxTrackTintColor ?? SystemColors.tertiarySystemFill, cornerRadius: 0, opacity: isEnabled ? attributes.alpha : 0.5 )