Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: manually mask swiftui views #202

Merged
merged 12 commits into from
Oct 14, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Next
- add `postHogMask` view modifier to manually mask a SwiftUI view ([#202](https://github.com/PostHog/posthog-ios/pull/202))
ioannisj marked this conversation as resolved.
Show resolved Hide resolved

## 3.12.7 - 2024-10-09

Expand Down
8 changes: 8 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
693E977D2C6257F9004B1030 /* ExampleSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E977C2C6257F9004B1030 /* ExampleSanitizer.swift */; };
6955CB732C517651008EFD8D /* CGSize+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6955CB722C517651008EFD8D /* CGSize+Util.swift */; };
69779BEC2AE68E6900D7A48E /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69779BEB2AE68E6900D7A48E /* UIViewController.swift */; };
6984765C2CA1BE50001AA3FD /* PostHogScreenshotMasker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6984765B2CA1BE50001AA3FD /* PostHogScreenshotMasker.swift */; };
6992AA832AFE51A000087600 /* PostHogExampleTvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6992AA822AFE51A000087600 /* PostHogExampleTvOSApp.swift */; };
6992AA852AFE51A000087600 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6992AA842AFE51A000087600 /* ContentView.swift */; };
6992AA872AFE51A100087600 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6992AA862AFE51A100087600 /* Assets.xcassets */; };
Expand Down Expand Up @@ -119,6 +120,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 */
Expand Down Expand Up @@ -327,6 +329,7 @@
693E977C2C6257F9004B1030 /* ExampleSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleSanitizer.swift; sourceTree = "<group>"; };
6955CB722C517651008EFD8D /* CGSize+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+Util.swift"; sourceTree = "<group>"; };
69779BEB2AE68E6900D7A48E /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
6984765B2CA1BE50001AA3FD /* PostHogScreenshotMasker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogScreenshotMasker.swift; sourceTree = "<group>"; };
6992AA802AFE51A000087600 /* PostHogExampleTvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PostHogExampleTvOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
6992AA822AFE51A000087600 /* PostHogExampleTvOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogExampleTvOSApp.swift; sourceTree = "<group>"; };
6992AA842AFE51A000087600 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -375,6 +378,7 @@
69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInputTraits+Util.swift"; sourceTree = "<group>"; };
69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwizzler.swift; sourceTree = "<group>"; };
69F518392BB2BA8300F52C14 /* UIApplicationTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationTracker.swift; sourceTree = "<group>"; };
DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogMaskViewModifier.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -557,6 +561,7 @@
69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */,
693E977A2C625208004B1030 /* PostHogPropertiesSanitizer.swift */,
69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */,
DAD5DD072CB6DEE70087387B /* PostHogMaskViewModifier.swift */,
69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */,
69ED1A9E2C8F451B00FE7A91 /* PostHogPersonProfiles.swift */,
);
Expand Down Expand Up @@ -693,6 +698,7 @@
69EE82B82BA9C4DA00EB9542 /* Replay */ = {
isa = PBXGroup;
children = (
6984765B2CA1BE50001AA3FD /* PostHogScreenshotMasker.swift */,
69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */,
69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */,
69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */,
Expand Down Expand Up @@ -1098,6 +1104,7 @@
69F23A742BB3088E001194F6 /* URLSessionSwizzler.swift in Sources */,
69F518142BAC7F4300F52C14 /* Date+Util.swift in Sources */,
69F5183A2BB2BA8300F52C14 /* UIApplicationTracker.swift in Sources */,
6984765C2CA1BE50001AA3FD /* PostHogScreenshotMasker.swift in Sources */,
69F518182BAC80A300F52C14 /* String+Util.swift in Sources */,
69ED1A882C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift in Sources */,
69F5181A2BAC81FC00F52C14 /* UITextInputTraits+Util.swift in Sources */,
Expand All @@ -1108,6 +1115,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 */,
Expand Down
68 changes: 68 additions & 0 deletions PostHog/PostHogMaskViewModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// PostHogMaskViewModifier.swift
// PostHog
//
// Created by Yiannis Josephides on 09/10/2024.
//

#if os(iOS) && canImport(SwiftUI)

import SwiftUI

public extension View {
func postHogMask(_ value: Bool = true) -> some View {
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
modifier(PostHogMaskViewModifier(enabled: value))
}
}

private struct PostHogMaskViewTagger: UIViewRepresentable {
func makeUIView(context _: Context) -> PostHogMaskViewTaggerView {
PostHogMaskViewTaggerView()
}

func updateUIView(_: PostHogMaskViewTaggerView, context _: Context) {
// nothing
}
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
}

private struct PostHogMaskViewModifier: ViewModifier {
let enabled: Bool
@State private var visible = false

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() {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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
14 changes: 9 additions & 5 deletions PostHog/Replay/PostHogReplayIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

var data: [String: Any] = ["width": width, "height": height]

if let screenName = screenName {
if let screenName {
data["href"] = screenName
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -529,7 +534,6 @@
private protocol AnyObjectUIHostingViewController: AnyObject {}

extension UIHostingController: AnyObjectUIHostingViewController {}

#endif

// swiftlint:enable cyclomatic_complexity
39 changes: 39 additions & 0 deletions PostHog/Replay/PostHogScreenshotMasker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// PostHogScreenshotMasker.swift
// PostHog
//
// Created by Manoel Aranda Neto on 23.09.24.
//

#if os(iOS) && canImport(SwiftUI)
import Foundation
import SwiftUI

class PostHogScreenshotMasker {
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
public static let shared = PostHogScreenshotMasker()

private var maskedViews: [UIView] = []

// Private initializer to prevent multiple instances
private init() {}

func addView(_ view: UIView) {
if !maskedViews.contains(view) {
maskedViews.append(view)
}
}

func removeView(_ view: UIView) {
maskedViews.removeAll { $0 == view }
}

func getAllMaskedViews() -> [UIView] {
maskedViews
}

func clearMaskedViews() {
maskedViews.removeAll()
}
}

#endif
4 changes: 2 additions & 2 deletions PostHogExample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
config.sendFeatureFlagEvent = false
config.sessionReplay = true
config.sessionReplayConfig.screenshotMode = true
config.sessionReplayConfig.maskAllTextInputs = true
config.sessionReplayConfig.maskAllImages = true
config.sessionReplayConfig.maskAllTextInputs = false
config.sessionReplayConfig.maskAllImages = false
ioannisj marked this conversation as resolved.
Show resolved Hide resolved

PostHogSDK.shared.setup(config)
// PostHogSDK.shared.debug()
Expand Down
2 changes: 1 addition & 1 deletion PostHogExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ struct ContentView: View {
RepresentedExampleUIView()
}

Text("Sensitive text!!").accessibilityIdentifier("ph-no-capture")
Text("Sensitive text!!").postHogMask()
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
Button(action: incCounter) {
Text(String(counter))
}.accessibilityIdentifier("ph-no-capture-id").accessibilityLabel("ph-no-capture")
Expand Down
Loading