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! 🚀
Sharing is caring!