| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- // LoopFollow
- // TogglableSecureInput.swift
- import SwiftUI
- struct TogglableSecureInput: View {
- enum Style { case singleLine, multiLine }
- let placeholder: String
- @Binding var text: String
- let style: Style
- var textContentType: UITextContentType? = nil
- @State private var isVisible = false
- @FocusState private var isFocused: Bool
- @FocusState private var isMultilineFocused: Bool
- var body: some View {
- HStack(alignment: .top) {
- Group {
- switch style {
- case .singleLine:
- ZStack(alignment: .trailing) {
- if isVisible {
- TextField(placeholder, text: $text)
- .multilineTextAlignment(.trailing)
- .textContentType(textContentType)
- .submitLabel(.done)
- .focused($isFocused)
- } else {
- // A real (masked) SecureField, not static text, so the
- // value stays editable while hidden and — crucially —
- // iOS AutoFill/keychain has a secure field to fill.
- SecureField(placeholder, text: $text)
- .multilineTextAlignment(.trailing)
- .textContentType(textContentType)
- .submitLabel(.done)
- .focused($isFocused)
- }
- }
- case .multiLine:
- ZStack(alignment: .topLeading) {
- TextEditor(text: $text)
- .focused($isMultilineFocused)
- .frame(minHeight: 100)
- .opacity(isVisible ? 1 : 0)
- .disabled(!isVisible)
- .toolbar {
- ToolbarItemGroup(placement: .keyboard) {
- if isMultilineFocused {
- Spacer()
- Button("Done") {
- isMultilineFocused = false
- }
- }
- }
- }
- if !isVisible {
- HStack {
- Spacer()
- Text(maskString)
- .font(.body.monospaced())
- .foregroundColor(.primary)
- .allowsHitTesting(false)
- }
- }
- }
- .frame(minHeight: 100)
- }
- }
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .privacySensitive()
- Button { isVisible.toggle() } label: {
- Image(systemName: isVisible ? "eye.slash" : "eye")
- .foregroundColor(.secondary)
- }
- .buttonStyle(.plain)
- .padding(.leading, 4)
- }
- .contentShape(Rectangle())
- .onTapGesture {
- switch style {
- case .singleLine:
- // The hidden state is already an editable SecureField, so tapping
- // just focuses it — no need to reveal the plaintext to type.
- isFocused = true
- case .multiLine:
- // The multi-line editor is only editable once revealed.
- if !isVisible { isVisible = true }
- isMultilineFocused = true
- }
- }
- }
- private var maskString: String {
- text.map { $0.isNewline ? "\n" : "•" }.joined()
- }
- }
|