6

I trying to make a SwiftUI app where after entering one letter in a TextField the cursor automatically moves to the next TextField. The UI is pretty much like this.

In Swift/IB, it looks like this was done with delegates and adding a target like in this post: How to move to the next UITextField automatically in Swift

But can't find any documentation for using delegates/targets in SwiftUI.

I tried following this post: SwiftUI TextField max length But this has not worked for me. Setting the .prefix(1) does not seem to make a difference. The TextField still accepts any amount of characters and when moved to the next TextField does not reduce the characters entered to only the first character.

In SwiftUI's current state, is it possible to automatically move to the next TextField after 1 character is entered?

Thanks for any help!

dcvonb
  • 315
  • 3
  • 10
  • I think you already mentioned your first bet - use `UIKit`. Instead of looking *only* at `UITextFields`, create a `UIViewController` with all your textfields (and logic) and use `UIViewControllerRepresentable`. Yeah, that's a lot of work. The only other thought I have is to use `Compose` and add logic into it to "point" to which textbook should be next. Not sure about focus, but again, probably as much work as using a view controller representable. –  Jul 24 '19 at 18:11
  • 1
    Given how things stand, if you need *"advanced features"* on textfields, you are probably better off wrapping UITextField inside a Representable. After all, that is what the "pure" SwiftUI TextField seem to be doing anyway. You can see that is the case by simply observing notification `UITextField.textDidChangeNotification`. You'll see that it also fires for `TextField` and as its object, it will provide you with its related `UITextField`. – kontiki Jul 24 '19 at 18:29
  • Thank you both for the responses. Hopefully SwiftUI will solve this in the future! – dcvonb Jul 25 '19 at 06:33
  • This might be helpful. https://www.hackingwithswift.com/forums/100-days-of-swiftui/jump-focus-between-a-series-of-textfields-pin-code-style-entry-widget/765 – Mikael Weiss Nov 21 '20 at 00:10

1 Answers1

3

It can be done in iOS 15 with FocusState

import SwiftUI
///Sample usage
@available(iOS 15.0, *)
struct PinParentView: View {
    @State var pin: Int = 12356
    var body: some View {
        VStack{
            Text(pin.description)
            PinView(pin: $pin)
        }
    }
}
@available(iOS 15.0, *)
struct PinView: View {
    @Binding var pin: Int
    @State var pinDict: [UniqueCharacter] = []
    @FocusState private var focusedField: UniqueCharacter?
    var body: some View{
        HStack{
            ForEach($pinDict, id: \.id, content: { $char in
                TextField("pin digit", text:
                            Binding(get: {
                    char.char.description
                }, set: { newValue in
                    let newest: Character = newValue.last  ?? "0"
                    //This check is only needed if you only want numbers
                    if Int(newest.description) != nil{
                        char.char = newest
                    }
                    //Set the new focus
                    DispatchQueue.main.async {
                        setFocus()
                    }
                })
                ).textFieldStyle(.roundedBorder)
                    .focused($focusedField, equals: char)
            })
            
        }.onAppear(perform: {
            //Set the initial value of the text fields
            //By using unique characters you can keep the order
            pinDict = pin.description.uniqueCharacters()
            
        })
    }
    func setFocus(){
        //Default to the first box when focus is not set or the user reaches the last box
        if focusedField == nil || focusedField == pinDict.last{
            focusedField = pinDict.first
        }else{
            //find the index of the current character
            let idx = pinDict.firstIndex(of: focusedField!)
            //Another safety check for the index
            if idx == nil || pinDict.last == pinDict[idx!]{
                focusedField = pinDict.first
            }else{
                focusedField = pinDict[idx! + 1]
            }
        }
        //Update the Binding that came from the parent
        setPinBinding()
    }
    ///Updates the binding from the parent
    func setPinBinding(){
        var newPinInt = 0
        for n in pinDict{
            if n == pinDict.first{
                newPinInt = Int(n.char.description) ?? 0
            }else{
                newPinInt = Int(String(newPinInt) + n.char.description) ?? 0
            }
        }
        pin = newPinInt
    }
}


//Convert String to Unique characers
extension String{
    func uniqueCharacters() -> [UniqueCharacter]{
        let array: [Character] = Array(self)
        return array.uniqueCharacters()
    }
    func numberOnly() -> String {
        self.trimmingCharacters(in: CharacterSet(charactersIn: "-0123456789.").inverted)
    }
    
}
extension Array where Element == Character {
    
    func uniqueCharacters() -> [UniqueCharacter]{
        var array: [UniqueCharacter] = []
        
        for char in self{
            array.append(UniqueCharacter(char: char))
        }
        return array
    }
    
}

//String/Characters can be repeating so yu have to make them a unique value
struct UniqueCharacter: Identifiable, Equatable, Hashable{
    var char: Character
    var id: UUID = UUID()
}

@available(iOS 15.0, *)
struct PinView_Previews: PreviewProvider {
    static var previews: some View {
        PinParentView()
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48