Luis Ramirez
Luis Ramirez

Reputation: 1604

Passing down @FocusState to another view

I was wondering how would I be able to pass down a @FocusState to another view. Here is some example code:

struct View1: View {
    enum Field {
        case username, password
    }
    
    @State var passwordText: String = ""
    @FocusState var focusedField: Field?
    
    var body: some View {
        // How would I be able to pass the focusedField here?
        View2(text: $passwordText, placeholder: "Password")
        
        //TextField("Password", text: $passwordText)
        //.frame(minHeight: 44)
        //.padding(.leading, 8)
        //.focused($focusedField, equals: .password)
        
        // How would I be able to add the commented code above to View2?
    }
}

struct View2: View {
    @Binding var text: String
    let placeholder: String
    
    var body: some View {
        HStack {
            TextField(placeholder, text: $text)
                .frame(minHeight: 44)
                .padding(.leading, 8)
            // How would I be able to add this
            //.focused(binding: , equals: )
            if text.count > 0 {
                Image(systemName: "xmark.circle.fill")
                    .font(.headline)
                    .foregroundColor(.secondary)
                    .padding(.trailing, 8)
            }
        }
    }
}

How would I be able to pass it down to View2?
Or is there a better way to reuse a custom text field?

Upvotes: 35

Views: 12044

Answers (4)

Rico Crescenzio
Rico Crescenzio

Reputation: 4226

It looks like no one (including me) was aware of this:

struct ParentView: View {
    @State private var text: String = ""
    @FocusState private var focusedField: FocusedField?
    
    enum FocusedField {
        case textField
    }
    
    var body: some View {
        MyCustomTextField(text: $text)
            .focused($focusedField, equals: .textField)
    }
}

struct MyCustomTextField: View {
    @Binding var text: String
    @FocusState var focused: Bool
    
    var body: some View {
        TextField("Placeholder", text: $text)
            .focused($focused)
    }
}

Basically the focus state is somehow propagated to the child view, so we don't really need to pass it manually; this is not only easier to implement but way more flexible.

I'm not sure if this was not working before (i.e on iOS 15-16) but I tried on iOS 17 simulator and iOS 18 device and it works.

Upvotes: 4

Mark
Mark

Reputation: 18194

This is how it can be done with a generic type.

struct ParentView: View {
    @State private var text: String = ""
    @FocusState private var focusedField: FocusedField?
    
    enum FocusedField {
        case textField
    }
    
    var body: some View {
        ChildView(text: $text, focused: $focusedField, equals: .textField)
    }
}

struct ChildView<Value: Hashable>: View {
    @Binding var text: String
    @FocusState.Binding var focused: Value?
    let equals: Value?
    
    var body: some View {
        TextField("Placeholder", text: $text)
            .focused($focused, equals: equals)
    }
}

Upvotes: 4

Xaxxus
Xaxxus

Reputation: 1809

Storing FocusState<Value>.Binding does not seem to work for me

I got it working like this, which seems to behave just like how regular Binding works:

struct ParentView: View {
    @State var text: String = ""
    @FocusState var isFocused: Bool
    
    var body: some View {
        ChildView(text: $text, isFocused: $isFocused)
    }
}

struct ChildView: View {
    @Binding var text: String
    @FocusState.Binding var isFocused: Bool

    var body: some View {
        TextField($text)
            .focused($isFocused)
    }
}

Upvotes: 40

Asperi
Asperi

Reputation: 257543

You can pass its binding as argument, like

struct View1: View {
  enum Field {
    case username, password
  }

  @State var passwordText: String = ""
  @FocusState var focusedField: Field?

  var body: some View {
    View2(text: $passwordText, placeholder: "Password", focused: $focusedField)
  }
}

struct View2: View {
  @Binding var text: String
  let placeholder: String
  var focused: FocusState<View1.Field?>.Binding     // << here !!

  var body: some View {
    HStack {
        TextField(placeholder, text: $text)
            .frame(minHeight: 44)
            .padding(.leading, 8)
            .focused(focused, equals: .password)     // << here !!
        if text.count > 0 {
            Image(systemName: "xmark.circle.fill")
                .font(.headline)
                .foregroundColor(.secondary)
                .padding(.trailing, 8)
        }

    }
  }
}

Upvotes: 28

Related Questions