Neofox
Neofox

Reputation: 423

FocusState changes in SwiftUI cause the keyboard to bounce

I'm making a sign-in interface for iOS in SwiftUI. The user should be able to easily switch from the username text field to the password text field by tapping the "next" button on the software keyboard. It's working well but the keyboard always bounces a little when switching between the two text fields for some reason.

Edit: As suggested in this answer I've added a Spacer into the VStack to make it fill the available space. The text fields aren't bouncing anymore but the keyboard unfortunately still is. I've updated the code and the GIF to reflect my changes.

recording of the issue

After googling a little it seemed like this wasn't a very common issue. This question seemed to be similar to what happens to me but following the answer and wrapping the text fields in a ScrollView or a GeometryReader did not change anything at all. This is my code:

struct AuthenticationView: View {
  @State var userName: String = ""
  @State var userAuth: String = ""
  
  @FocusState var currentFocus: FocusObject?
  enum FocusObject: Hashable { case name, auth }
  
  var body: some View {
    VStack(spacing: 8) {
      TextField("Username", text: $userName)
        .focused($currentFocus, equals: .name)
        .padding(8).background(Color.lightGray)
        .cornerRadius(8).padding(.bottom, 8)
        .textInputAutocapitalization(.never)
        .onSubmit { currentFocus = .auth }
        .autocorrectionDisabled(true)
        .keyboardType(.asciiCapable)
        .textContentType(.username)
        .submitLabel(.next)
      
      SecureField("Password", text: $userAuth)
        .focused($currentFocus, equals: .auth)
        .padding(8).background(Color.lightGray)
        .cornerRadius(8).padding(.bottom, 16)
        .textInputAutocapitalization(.never)
        .onSubmit { currentFocus = nil }
        .autocorrectionDisabled(true)
        .keyboardType(.asciiCapable)
        .textContentType(.password)
        .submitLabel(.done)
      
      Spacer() // This fixes the text fields
      // But it does not fix the keyboard
    }.padding(32)
  }
}

Upvotes: 28

Views: 2569

Answers (3)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 120113

💡 iOS 16+ workaround

The (ugly / but working) workaround for this ridicules bug with pure SwiftUI is to use .vertical axes:

TextField("First", text: $firstText, axis: .vertical) // 👈 Vertical axis here
    .onChange(of: firstText) {
        guard firstText.hasSuffix("\n") else { return } // 👈 Watch for the new line 
        firstText = firstText.replacingOccurrences(of: "\n", with: "") // 👈 Cleanup unintended new lines
        focusedField = .second // 👈 Change the focus here
    }

⚠️ Note that this method is just a workaround and may have some side-effects. Use with caution.

Upvotes: 2

Bhavin Ranpara
Bhavin Ranpara

Reputation: 278

check this below code

authentication

import SwiftUI
import UIKit

struct ContentView: View {
   @State private var firstName: String = ""
   @State private var password: String = ""
   @State private var activeField: Field?

   enum Field {
     case firstName, password
   }

var body: some View {
    VStack(spacing: 50) {
        Spacer()
            .frame(height: 50)
        CustomTextField(placeholder: "First Name", text: $firstName, field: .firstName, activeField: $activeField, isSecure: false)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
            .submitLabel(.next)
            .onSubmit {
                activeField = .password
            }
            .background(Color.gray)
            .frame(height: 30)
        
        CustomTextField(placeholder: "Password", text: $password, field: .password, activeField: $activeField, isSecure: true)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
            .submitLabel(.done)
            .onSubmit {
                activeField = nil // Done editing, dismiss keyboard
            }
            .background(Color.gray)
            .frame(height: 30)
        Spacer()
    }
    .padding()
  }
}

struct CustomTextField: UIViewRepresentable {
   var placeholder: String
   @Binding var text: String
   var field: ContentView.Field
   @Binding var activeField: ContentView.Field?
   var isSecure: Bool

func makeUIView(context: Context) -> UITextField {
    let textField: UITextField
    
    if isSecure {
        textField = UITextField()
        textField.isSecureTextEntry = true
    } else {
        textField = UITextField()
    }
    
    textField.placeholder = placeholder
    textField.delegate = context.coordinator
    textField.returnKeyType = (field == .password) ? .done : .next
    return textField
}

func updateUIView(_ uiView: UITextField, context: Context) {
    uiView.text = text
    
    // Check focus
    if activeField == field {
        uiView.becomeFirstResponder() // Show keyboard if this field is active
    }
}

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

class Coordinator: NSObject, UITextFieldDelegate {
    var parent: CustomTextField
    
    init(_ parent: CustomTextField) {
        self.parent = parent
    }
    
    func textFieldDidChangeSelection(_ textField: UITextField) {
        parent.text = textField.text ?? ""
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        
        if parent.field == .firstName {
            // Move focus to the next field
            if parent.activeField != nil {
                parent.activeField = nil
            }
            parent.activeField = .password
        } else {
            // Done pressed, dismiss the keyboard
            textField.resignFirstResponder()
            parent.activeField = nil // Clear active field to dismiss keyboard
        }
        return true
    }
  }
}

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
      ContentView()
   }
}

Upvotes: -2

CouchDeveloper
CouchDeveloper

Reputation: 19174

Your current layout says:

Put the edit fields into a VStack. Layout the VStack in the parent view by centering it in the available space. Note, that the VStack only uses a minimum size.

Now, when the keyboard appears, the available space of the parent view, i.e. its height, will be reduced accordingly.

Because the VStack is layout in the center, the text fields bounce up and down.

There are a couple of options:

Ensure the VStack extends its height and the text fields are aligned at the top. For example using a Spacer:

    VStack(spacing: 8) {
      TextField("Username", text: $userName)
        ...   
    
      SecureField("Password", text: $userAuth)
        ...
    
      Spacer()
    }.padding(32)

Using a ScrollView:

    ScrollView {

        Spacer(minLength: 80) // give some space at the top

        VStack(spacing: 8) {
          TextField("Username", text: $userName)
          ...   
    
         SecureField("Password", text: $userAuth)
         ...
    
        }.padding(32)
    }

It may not look pretty, but it should give you an idea, where to work on this issue (you may want to use a GeometryReader and a possibly a ScrollView to perfect your layout).

Another option is to use a Form. Put your fields into there, and with a Form you get also a head start which looks pretty nice. The reason why a Form works is because the same reasons why it works with a Spacer (aligns fields on top) and because of a ScrollView.

The fact that the keyboard disappears temporarily when you tap "Next" is unfortunate. I have no solution for this, so far.

Upvotes: 0

Related Questions