TogglableSecureInput.swift 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. // LoopFollow
  2. // TogglableSecureInput.swift
  3. // Created by Jonas Björkert on 2025-05-28.
  4. import SwiftUI
  5. struct TogglableSecureInput: View {
  6. enum Style { case singleLine, multiLine }
  7. let placeholder: String
  8. @Binding var text: String
  9. let style: Style
  10. @State private var isVisible = false
  11. @FocusState private var isFocused: Bool
  12. var body: some View {
  13. HStack(alignment: .top) {
  14. Group {
  15. switch style {
  16. case .singleLine:
  17. if isVisible {
  18. TextField(placeholder, text: $text).multilineTextAlignment(.trailing)
  19. } else {
  20. SecureField(placeholder, text: $text).multilineTextAlignment(.trailing)
  21. }
  22. case .multiLine:
  23. ZStack(alignment: .topLeading) {
  24. TextEditor(text: $text)
  25. .opacity(isVisible ? 1 : 0)
  26. .focused($isFocused)
  27. .frame(minHeight: 100)
  28. if !isVisible {
  29. Text(maskString)
  30. .font(.body.monospaced())
  31. .foregroundColor(.primary)
  32. .frame(maxWidth: .infinity,
  33. maxHeight: .infinity,
  34. alignment: .topLeading)
  35. .padding(.top, 8)
  36. .padding(.leading, 5)
  37. }
  38. }
  39. }
  40. }
  41. .textInputAutocapitalization(.never)
  42. .autocorrectionDisabled()
  43. .privacySensitive()
  44. .submitLabel(.done)
  45. Button { isVisible.toggle() } label: {
  46. Image(systemName: isVisible ? "eye.slash" : "eye")
  47. .foregroundColor(.secondary)
  48. }
  49. .buttonStyle(.plain)
  50. .padding(.leading, 4)
  51. }
  52. .contentShape(Rectangle())
  53. .onTapGesture { isFocused = true }
  54. }
  55. private var maskString: String {
  56. text.map { $0.isNewline ? "\n" : "•" }.joined()
  57. }
  58. }