TogglableSecureInput.swift 3.5 KB

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