Swifty Ways to Handle Data Redaction in the UI

👍
· Dec 23, 2024 · 4 mins read
Swifty Ways to Handle Data Redaction in the UI

The Problem: Redacting Sensitive Info

We’ve all been there — we need to display some sensitive info on the UI, but can’t just show it all. The naive approach? Implementing redaction logic every single time when need it. Yawn. 😴 One step further, we can wrap the logic into some common methods or objects. You know what, we can do even better! There are some seriously cool Swift techniques that can make our life easier.

Method 1: Computed Properties

Let’s kick things off with a classic approach — computed properties. Check this out:

struct BankAccount {  
    var accountNumber: String  
    var balance: Double  
}  
  
extension BankAccount {  
    var redactedAccountNumber: String {  
        let digitsToShow = 4  
        let length = accountNumber.count  
        guard length > digitsToShow else { return accountNumber }  
        return "\(String(repeating: "*", count: length - digitsToShow))\(accountNumber.suffix(digitsToShow))"  
    }  
  
    var redactedBalance: String {  
        "\(balance)".map { char in  
            char == "." ? "." : "*"  
        }.joined()  
    }  
}  
  
let ac = BankAccount(accountNumber: "1234567890", balance: 123.45)  
print(ac.redactedAccountNumber) // ******7890  
print(ac.redactedBalance) // ***.**

With this setup, we can just use redactedAccountNumber and redactedBalance whenever we need the masked versions. Simple and effective!

Method 2: Property Wrappers

Now, if we want to level up our Swift game, property wrappers are where it’s at. Check out this implementation below:

protocol RedactableType {  
    var redacted: String { get }  
}  
  
extension String: RedactableType {  
    var redacted: String {  
        let digitsToShow = 4  
        let length = count  
        guard length > digitsToShow else { return self }  
        return "\(String(repeating: "*", count: length - digitsToShow))\(suffix(digitsToShow))"  
    }  
}  
  
extension Double: RedactableType {  
    var redacted: String {  
        "\(self)".map { char in  
            char == "." ? "." : "*"  
        }.joined()  
    }  
}  
  
@propertyWrapper  
struct Redactable<Value: RedactableType> {  
    var wrappedValue: Value  
  
    var projectedValue: String {  
        wrappedValue.redacted  
    }  
}  
  
struct BankAccount {  
    @Redactable var accountNumber: String  
    @Redactable var balance: Double  
}  
  
let ac = BankAccount(accountNumber: "1234567890", balance: 123.45)  
  
print(ac.$accountNumber) // ******7890  
print(ac.$balance) //***.**

In this example, we defined a RedactableType protocol to make our Redactable property wrapper can handle different type of values in a generic way. We extended String and Double types to conform to this protocol. Finally, we return the redacted as the projected value of the wrapper.

Also, how cool is that $ syntax? It’s like magic! 🪄

String interpolation

Last but not least, let’s talk about extending DefaultStringInterpolation. This method is like a ninja – it sneaks the redaction right into our string interpolation:

extension DefaultStringInterpolation {  
    mutating func appendInterpolation(toBeRedacted value: String) {  
        let digitsToShow = 4  
        let length = value.count  
        if length <= digitsToShow {  
            appendLiteral(value)  
        } else {  
            let redacted = "\(String(repeating: "*", count: length - digitsToShow))\(value.suffix(digitsToShow))"  
            appendLiteral(redacted)  
        }  
    }  
  
    mutating func appendInterpolation(toBeRedacted value: Double) {  
        let redacted = "\(value)".map { char in  
            char == "." ? "." : "*"  
        }.joined()  
        appendLiteral(redacted)  
    }  
}  
  
print("Account Number: \(toBeRedacted: "1234567890")") // Account Number: ******7890  
print("Account balance: \(toBeRedacted: 123.45)") // Account balance: ***.**

Now that’s what I call smooth! 😎

Swift logger’s way

SwiftLog’s privacy annotations (.public, .private, .sensitive) ensure that sensitive data is redacted in outputs where privacy is enforced, like console logs or remote logging platforms.

import os  
  
let logger = Logger(subsystem: "my system", category: "my category")  
  
// Configure privacy settings if needed (default behavior):  
// Sensitive data can be marked with .private; other values with .public  
  
func trackEvent(_ name: String, properties: [String: String]) {  
    // Log event name as public  
    logger.debug("Event: \(name, privacy: .public)")  
    // Iterate through properties, marking privacy appropriately  
    for (key, value) in properties {  
        if key == "userId" {  
            // Mark userId as private  
            logger.debug("Property: \(key) = \(value), privacy: .private)")  
        } else {  
            // Non-sensitive properties can be public  
            logger.debug("Property: \(key) = \(value), privacy: .public)")  
        }  
    }  
}

Wrapping Up

Whether you’re a fan of computed properties, property wrappers, or string interpolation, you’ve got options. Each method has its own flavour, so pick the one that vibes with your coding style.

Happy coding! 🚀

Comments

Sharing is caring!