TogglableSecureInput.swift 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. // LoopFollow
  2. // TogglableSecureInput.swift
  3. import SwiftUI
  4. struct TogglableSecureInput: View {
  5. enum Style { case singleLine, multiLine }
  6. let placeholder: String
  7. @Binding var text: String
  8. let style: Style
  9. var textContentType: UITextContentType? = nil
  10. @State private var isVisible = false
  11. @FocusState private var isFocused: Bool
  12. @FocusState private var isMultilineFocused: Bool
  13. var body: some View {
  14. HStack(alignment: .top) {
  15. Group {
  16. switch style {
  17. case .singleLine:
  18. ZStack(alignment: .trailing) {
  19. if isVisible {
  20. TextField(placeholder, text: $text)
  21. .multilineTextAlignment(.trailing)
  22. .textContentType(textContentType)
  23. .submitLabel(.done)
  24. .focused($isFocused)
  25. } else {
  26. HStack {
  27. Spacer()
  28. Text(maskString)
  29. .font(.body.monospaced())
  30. .foregroundColor(.primary)
  31. .allowsHitTesting(false)
  32. }
  33. }
  34. }
  35. case .multiLine:
  36. ZStack(alignment: .topLeading) {
  37. TextEditor(text: $text)
  38. .focused($isMultilineFocused)
  39. .frame(minHeight: 100)
  40. .opacity(isVisible ? 1 : 0)
  41. .disabled(!isVisible)
  42. .toolbar {
  43. ToolbarItemGroup(placement: .keyboard) {
  44. if isMultilineFocused {
  45. Spacer()
  46. Button("Done") {
  47. isMultilineFocused = false
  48. }
  49. }
  50. }
  51. }
  52. if !isVisible {
  53. HStack {
  54. Spacer()
  55. Text(maskString)
  56. .font(.body.monospaced())
  57. .foregroundColor(.primary)
  58. .allowsHitTesting(false)
  59. }
  60. }
  61. }
  62. .frame(minHeight: 100)
  63. }
  64. }
  65. .textInputAutocapitalization(.never)
  66. .autocorrectionDisabled()
  67. .privacySensitive()
  68. Button { isVisible.toggle() } label: {
  69. Image(systemName: isVisible ? "eye.slash" : "eye")
  70. .foregroundColor(.secondary)
  71. }
  72. .buttonStyle(.plain)
  73. .padding(.leading, 4)
  74. }
  75. .contentShape(Rectangle())
  76. .onTapGesture {
  77. if !isVisible {
  78. isVisible = true
  79. if style == .singleLine {
  80. isFocused = true
  81. } else if style == .multiLine {
  82. isMultilineFocused = true
  83. }
  84. } else {
  85. if style == .singleLine {
  86. isFocused = true
  87. } else if style == .multiLine {
  88. isMultilineFocused = true
  89. }
  90. }
  91. }
  92. }
  93. private var maskString: String {
  94. text.map { $0.isNewline ? "\n" : "•" }.joined()
  95. }
  96. }