diff --git a/CHANGELOG.md b/CHANGELOG.md index c436e0d96..dbf8d576f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Next - recording: session replay respect feature flag variants ([#209](https://github.com/PostHog/posthog-ios/pull/209)) +- add `postHogMask` view modifier to manually mask a SwiftUI view ([#202](https://github.com/PostHog/posthog-ios/pull/202)) ## 3.12.7 - 2024-10-09 diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 65c8955e0..8fbf97cae 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 69F5181A2BAC81FC00F52C14 /* UITextInputTraits+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */; }; 69F518382BB2BA0100F52C14 /* PostHogSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */; }; 69F5183A2BB2BA8300F52C14 /* UIApplicationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */; }; + DAD5DD0C2CB6DEF30087387B /* PostHogMaskViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -375,6 +376,7 @@ 69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInputTraits+Util.swift"; sourceTree = ""; }; 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwizzler.swift; sourceTree = ""; }; 69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationTracker.swift; sourceTree = ""; }; + DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogMaskViewModifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -557,6 +559,7 @@ 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */, 693E977A2C625208004B1030 /* PostHogPropertiesSanitizer.swift */, 69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */, + DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */, 69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */, 69ED1A9E2C8F451B00FE7A91 /* PostHogPersonProfiles.swift */, ); @@ -1108,6 +1111,7 @@ 69F517F32BAC734300F52C14 /* UIColor+Util.swift in Sources */, 3AE3FB3F29924F4F00AFFC18 /* PostHogConfig.swift in Sources */, 69F518382BB2BA0100F52C14 /* PostHogSwizzler.swift in Sources */, + DAD5DD0C2CB6DEF30087387B /* PostHogMaskViewModifier.swift in Sources */, 690FF0C52AEFAE8200A0B06B /* PostHogLegacyQueue.swift in Sources */, 3AE3FB332991388500AFFC18 /* PostHogQueue.swift in Sources */, 690FF0B52AEBBD3C00A0B06B /* DictUtils.swift in Sources */, diff --git a/PostHog/PostHogMaskViewModifier.swift b/PostHog/PostHogMaskViewModifier.swift new file mode 100644 index 000000000..30e8cd127 --- /dev/null +++ b/PostHog/PostHogMaskViewModifier.swift @@ -0,0 +1,67 @@ +// +// PostHogMaskViewModifier.swift +// PostHog +// +// Created by Yiannis Josephides on 09/10/2024. +// + +#if os(iOS) && canImport(SwiftUI) + + import SwiftUI + + public extension View { + func postHogMask(_ isEnabled: Bool = true) -> some View { + modifier(PostHogMaskViewModifier(enabled: isEnabled)) + } + } + + private struct PostHogMaskViewTagger: UIViewRepresentable { + func makeUIView(context _: Context) -> PostHogMaskViewTaggerView { + PostHogMaskViewTaggerView() + } + + func updateUIView(_: PostHogMaskViewTaggerView, context _: Context) { + // nothing + } + } + + private struct PostHogMaskViewModifier: ViewModifier { + let enabled: Bool + + func body(content: Content) -> some View { + content.background(viewTagger) + } + + @ViewBuilder + private var viewTagger: some View { + if enabled { + PostHogMaskViewTagger() + } + } + } + + private class PostHogMaskViewTaggerView: UIView { + override func didMoveToSuperview() { + super.didMoveToSuperview() + superview?.phIsManuallyMasked = true + } + } + + private var phIsManuallyMaskedKey: UInt8 = 0 + extension UIView { + var phIsManuallyMasked: Bool { + get { + objc_getAssociatedObject(self, &phIsManuallyMaskedKey) as? Bool ?? false + } + + set { + objc_setAssociatedObject( + self, + &phIsManuallyMaskedKey, + newValue as Bool?, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + } +#endif diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 2aa81fdc2..356003722 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -97,7 +97,7 @@ var data: [String: Any] = ["width": width, "height": height] - if let screenName = screenName { + if let screenName { data["href"] = screenName } @@ -232,6 +232,11 @@ } } + // manually masked views through view modifier `PostHogMaskViewModifier` + if view.phIsManuallyMasked { + maskableWidgets.append(view.toAbsoluteRect(parent)) + } + if !view.subviews.isEmpty { for child in view.subviews { if !child.isVisible() { @@ -306,11 +311,11 @@ } private func hasText(_ text: String?) -> Bool { - if let text = text, !text.isEmpty { - return true + if let text, !text.isEmpty { + true } else { // if there's no text, there's nothing to mask - return false + false } } @@ -529,7 +534,6 @@ private protocol AnyObjectUIHostingViewController: AnyObject {} extension UIHostingController: AnyObjectUIHostingViewController {} - #endif // swiftlint:enable cyclomatic_complexity diff --git a/PostHogExample/ContentView.swift b/PostHogExample/ContentView.swift index 58dd894f4..584c2e566 100644 --- a/PostHogExample/ContentView.swift +++ b/PostHogExample/ContentView.swift @@ -97,7 +97,8 @@ struct ContentView: View { ContentView() } label: { Text("Infinite navigation") - }.accessibilityIdentifier("ph-no-capture") + } + .postHogMask() Button("Show Sheet") { showingSheet.toggle() @@ -113,12 +114,14 @@ struct ContentView: View { RepresentedExampleUIView() } - Text("Sensitive text!!").accessibilityIdentifier("ph-no-capture") + Text("Sensitive text!!").postHogMask() Button(action: incCounter) { Text(String(counter)) - }.accessibilityIdentifier("ph-no-capture-id").accessibilityLabel("ph-no-capture") + } + .postHogMask() - TextField("Enter your name", text: $name).accessibilityLabel("ph-no-capture") + TextField("Enter your name", text: $name) + .postHogMask() Text("Hello, \(name)!") Button(action: triggerAuthentication) { Text("Trigger fake authentication!")