uikit-textfield
offers UIKitTextField
which makes using UITextField
in SwiftUI a breeze.
From data binding, focus binding, handling callbacks from UITextFieldDelegate
,
custom UITextField
subclass, horizontal/vertical stretching, inputView
/inputAccessoryView
,
to extra configuration, UIKitTextField
is the complete solution.
To install through Xcode, follow the official guide to add the following your Xcode project
https://github.com/vinceplusplus/uikit-textfield.git
To install through Swift Package Manager, add the following as package dependency and target dependency respectively
.package(url: "https://github.com/vinceplusplus/uikit-textfield.git", from: "2.0")
.product(name: "UIKitTextField", package: "uikit-textfield")
All configurations are done using UIKitTextField.Configuration
and are in turn passed to
UIKitTextField
's .init(config: )
func value(text: Binding<String>) -> Self
@State var name: String = ""
var body: some View {
VStack(alignment: .leading) {
UIKitTextField(
config: .init()
.value(text: $name)
)
.border(Color.black)
Text("\(name.isEmpty ? "Please enter your name above" : "Hello \(name)")")
}
.padding()
}
@available(iOS 15.0, *)
func value<F>(value: Binding<F.FormatInput>, format: F) -> Self
where
F: ParseableFormatStyle,
F.FormatOutput == String
@available(iOS 15.0, *)
func value<F>(value: Binding<F.FormatInput?>, format: F) -> Self
where
F: ParseableFormatStyle,
F.FormatOutput == String
func value<V>(value: Binding<V>, formatter: Formatter) -> Self
func value<V>(value: Binding<V?>, formatter: Formatter) -> Self
When the text field is not the first responder, it will take value from the binding and display in the specified formatted way
When the text field is the first responder, every change will try to update the binding. If it's a binding of a non optional value,
an invalid input will preserve the last value. If it's a binding of an optional value, an invalid input will set the value to nil
.
@State var value: Int = 0
// ...
Text("Enter a number:")
UIKitTextField(
config: .init()
.value(value: $value, format: .number)
//.value(value: $value, formatter: NumberFormatter())
)
.border(Color.black)
// NOTE: avoiding the formatting behavior which comes from Text()
Text("Your input: \("\(value)")")
func value(
updateViewValue: @escaping (_ textField: UITextFieldType) -> Void,
onViewValueChanged: @escaping (_ textField: UITextFieldType) -> Void
) -> Self
func placeholder(_ placeholder: String?) -> Self
UIKitTextField(
config: .init()
.placeholder("some placeholder...")
)
func font(_ font: UIFont?) -> Self
Note that since there are no ways to convert back from a Font
to UIFont
, the configuration
can only take a UIFont
UIKitTextField(
config: .init()
.font(.systemFont(ofSize: 16))
)
func textColor(_ color: Color?) -> Self
UIKitTextField(
config: .init()
.textColor(.red)
)
func textAlignment(_ textAlignment: NSTextAlignment?) -> Self
UIKitTextField(
config: .init()
.textAlignment(.center)
)
func clearsOnBeginEditing(_ clearsOnBeginEditing: Bool?) -> Self
UIKitTextField(
config: .init()
.clearsOnBeginEditing(true)
)
func clearsOnInsertion(_ clearsOnInsertion: Bool?) -> Self
UIKitTextField(
config: .init()
.clearsOnInsertion(true)
)
func clearButtonMode(_ clearButtonMode: UITextField.ViewMode?) -> Self
UIKitTextField(
config: .init()
.clearButtonMode(.always)
)
Similar to @FocusState
, we could use an orindary @State
to do a 2 way focus binding
func focused(_ binding: Binding<Bool>) -> Self
@State var name = ""
@State var isFocused = false
VStack {
Text("Your name:")
UIKitTextField(
config: .init()
.value(text: $name)
.focused($isFocused)
)
Button {
if name.isEmpty {
isFocused = true
} else {
isFocused = false
}
} label: {
Text("Submit")
}
}
func focused<Value>(_ binding: Binding<Value?>, equals value: Value?) -> Self where Value: Hashable
enum Field {
case firstName
case lastName
}
@State var firstName = ""
@State var lastName = ""
@State var focusedField: Field?
VStack {
Text("First Name:")
UIKitTextField(
config: .init()
.value(text: $firstName)
.focused(focusedField, equals: .firstName)
)
Text("Last Name:")
UIKitTextField(
config: .init()
.value(text: $lastName)
.focused(focusedField, equals: .lastName)
)
Button {
if firstName.isEmpty {
focusedField = .firstName
} else if lastName.isEmpty {
focusedField = .lastName
} else {
focusedField = nil
}
} label: {
Text("Submit")
}
}
By default, UIKitTextField
will stretch horizontally but not vertically
func stretches(horizontal: Bool, vertical: Bool) -> Self
UIKitTextField(
config: .init {
PaddedTextField()
}
.stretches(horizontal: true, vertical: false)
)
.border(Color.black)
Note that PaddedTextField
is just a simple internally padded UITextField
, see more in custom init
UIKitTextField(
config: .init {
PaddedTextField()
}
.stretches(horizontal: false, vertical: false)
)
.border(Color.black)
Supporting UITextField.inputView
and UITextField.inputAccessoryView
by accepting a user defined SwiftUI
view for each of them
func inputView(content: InputViewContent<UITextFieldType>) -> Self
func inputAccessoryView(content: InputViewContent<UITextFieldType>) -> Self
VStack(alignment: .leading) {
UIKitTextField(
config: .init {
PaddedTextField()
}
.placeholder("Enter your expression")
.value(text: $expression)
.focused($isFocused)
.inputView(content: .view { uiTextField in
KeyPad(uiTextField: uiTextField, onEvaluate: onEvaluate)
})
.shouldReturn { _ in
onEvaluate()
return false
}
)
.padding(4)
.border(Color.black)
Text("Result: \(result)")
Divider()
Button {
isFocused = false
} label: {
Text("Dismiss")
}
}
.padding()
Implementation of KeyPad
can be found in InputViewPage
from the example code. But the idea is to accept a UITextField
parameter and render some buttons that do uiTextField.insertText("...")
or uiTextField.deleteBackward()
, like the
following:
struct CustomKeyboard: View {
let uiTextField: UITextField
var body: some View {
VStack {
HStack {
Button {
uiTextField.insertText("1")
} label: {
Text("1")
}
Button {
uiTextField.insertText("2")
} label: {
Text("2")
}
Button {
uiTextField.insertText("3")
} label: {
Text("3")
}
}
HStack { /* ... */ }
// ...
}
}
}
init(_ makeUITextField: @escaping () -> UITextFieldType)
A common use case of a UITextField
subclass is to provide some internal padding which is also tappable. The following
example demonstrates some extra leading padding to accomodate even an icon image
class CustomTextField: BaseUITextField {
let padding = UIEdgeInsets(top: 4, left: 8 + 32 + 8, bottom: 4, right: 8)
public override func textRect(forBounds bounds: CGRect) -> CGRect {
super.textRect(forBounds: bounds).inset(by: padding)
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
super.editingRect(forBounds: bounds).inset(by: padding)
}
}
UIKitTextField(
config: .init {
CustomTextField()
}
.focused($isFocused)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalizationType(UITextAutocapitalizationType.none)
.autocorrectionType(.no)
)
.background(alignment: .leading) {
HStack(spacing: 0) {
Color.clear.frame(width: 8)
ZStack {
Image(systemName: "mail")
}
.frame(width: 32)
}
}
.border(Color.black)
UITextFieldType
needs to conform to UITextFieldProtocol
which is shown below:
public protocol UITextFieldProtocol: UITextField {
var inputViewController: UIInputViewController? { get set }
var inputAccessoryViewController: UIInputViewController? { get set }
}
Basically, it needs have inputViewController
and inputAccessoryViewController
writable so the support
for custom input view and custom input accessory view will work
For most use cases, BaseUITextField
, which provides baseline implementation of UITextFieldProtocol
,
can be subclassed to add more user defined behavior
func configure(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self
If there are configurations that UIKitTextField
doesn't support out of the box, this is the place where we could add them.
The extra configuration will be executed at the end of updateUIView()
after applying all supported configuration (like data binding, etc)
class PaddedTextField: BaseUITextField {
var padding = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) {
didSet {
setNeedsLayout()
}
}
public override func textRect(forBounds bounds: CGRect) -> CGRect {
super.textRect(forBounds: bounds).inset(by: padding)
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
super.editingRect(forBounds: bounds).inset(by: padding)
}
}
@State var text = "some text..."
@State var pads = true
var body: some View {
VStack {
Toggle("Padding", isOn: $pads)
UIKitTextField(
config: .init {
PaddedTextField()
}
.value(text: $text)
.configure { uiTextField in
uiTextField.padding = pads ? .init(top: 4, left: 8, bottom: 4, right: 8) : .zero
}
)
.border(Color.black)
}
.padding()
}
The above example provides a button to toggle internal padding
UITextFieldDelegate
is fully supported
func shouldBeginEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self
func onBeganEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self
func shouldEndEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self
func onEndedEditing(handler: @escaping (_ uiTextField: UITextFieldType, _ reason: UITextField.DidEndEditingReason) -> Void) -> Self
func shouldChangeCharacters(handler: @escaping (_ uiTextField: UITextFieldType, _ range: NSRange, _ replacementString: String) -> Bool) -> Self
func onChangedSelection(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self
func shouldClear(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self
func shouldReturn(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self
Most of the commonly used parts of UITextInputTraits
are supported
func keyboardType(_ keyboardType: UIKeyboardType?) -> Self
func keyboardAppearance(_ keyboardAppearance: UIKeyboardAppearance?) -> Self
func returnKeyType(_ returnKeyType: UIReturnKeyType?) -> Self
func textContentType(_ textContentType: UITextContentType?) -> Self
func isSecureTextEntry(_ isSecureTextEntry: Bool?) -> Self
func enablesReturnKeyAutomatically(_ enablesReturnKeyAutomatically: Bool?) -> Self
func autocapitalizationType(_ autocapitalizationType: UITextAutocapitalizationType?) -> Self
func autocorrectionType(_ autocorrectionType: UITextAutocorrectionType?) -> Self
func spellCheckingType(_ spellCheckingType: UITextSpellCheckingType?) -> Self
func smartQuotesType(_ smartQuotesType: UITextSmartQuotesType?) -> Self
func smartDashesType(_ smartDashesType: UITextSmartDashesType?) -> Self
func smartInsertDeleteType(_ smartInsertDeleteType: UITextSmartInsertDeleteType?) -> Self
Examples/Example
is an example app that demonstrate how to use UIKitTextField
UITextField.isEnabled
is actually supported by the vanilla .disabled(/* ... */)
which might not be very obvious