Last active
October 28, 2021 12:53
-
-
Save MaxenceMottard/9950a23ab2a721c16cc8ef59cc1aa5f7 to your computer and use it in GitHub Desktop.
UITextField implementation in SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct GenericTextField: UIViewRepresentable { | |
// MARK: States | |
@Binding private var text: String | |
@Binding private var isEditing: Bool | |
@Binding private var isFirstResponder: Bool | |
private var formatText: (String, String) -> (String) = { _, newValue in | |
return newValue | |
} | |
private var tapReturnKey: () -> () = {} | |
// MARK: TextField Configurations | |
private var isDisabled: Bool = false | |
private var isSecureTextEntry: Bool = false | |
private var font: UIFont? | |
private var textColor: UIColor? | |
private var tintColor: UIColor? | |
private var textAlignment: NSTextAlignment = .left | |
private var horizontalHuggingPriority: UILayoutPriority = .fittingSizeLevel | |
private var verticalHuggingPriority: UILayoutPriority = .required | |
private var textConfiguration: TextConfiguration = TextConfiguration() | |
private var passwordRules: UITextInputPasswordRules? | |
init(_ text: Binding<String>, isEditing: Binding<Bool> = .constant(false), isFirstResponder: Binding<Bool> = .constant(false)) { | |
self._text = text | |
self._isEditing = isEditing | |
self._isFirstResponder = isFirstResponder | |
} | |
func makeUIView(context: Context) -> UITextField { | |
let textField = UITextField() | |
textField.delegate = context.coordinator | |
textField.backgroundColor = .clear | |
setTextField(textField: textField) | |
return textField | |
} | |
func updateUIView(_ uiView: UITextField, context: Context) { | |
setTextField(textField: uiView) | |
} | |
private func setTextField(textField: UITextField) { | |
if self.text != textField.text { | |
textField.text = text | |
} | |
textField.isEnabled = !isDisabled | |
textField.isSecureTextEntry = isSecureTextEntry | |
textField.textColor = textColor ?? UIColor.black | |
textField.tintColor = tintColor | |
textField.font = font | |
textField.passwordRules = passwordRules | |
textField.keyboardType = textConfiguration.keyboardType | |
textField.returnKeyType = textConfiguration.returnKeyType | |
textField.textContentType = textConfiguration.textContentType | |
textField.autocorrectionType = textConfiguration.autoCorrectionType | |
textField.autocapitalizationType = textConfiguration.autocapitalizationType | |
textField.setContentHuggingPriority(horizontalHuggingPriority, for: .horizontal) | |
textField.setContentHuggingPriority(verticalHuggingPriority, for: .vertical) | |
if isFirstResponder { | |
DispatchQueue.main.async { | |
textField.becomeFirstResponder() | |
isFirstResponder = false | |
} | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
return Coordinator(text: $text, isEditing: $isEditing, formatText: formatText, tapReturnKey: tapReturnKey) | |
} | |
class Coordinator: NSObject, UITextFieldDelegate { | |
private var formatText: (String, String) -> (String) | |
private var tapReturnKey: () -> () | |
@Binding private var text: String | |
@Binding private var isEditing: Bool | |
init(text: Binding<String>, isEditing: Binding<Bool>, | |
formatText: @escaping (String, String) -> (String), | |
tapReturnKey: @escaping () -> ()) { | |
self._text = text | |
self._isEditing = isEditing | |
self.formatText = formatText | |
self.tapReturnKey = tapReturnKey | |
} | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
if (range == NSRange(location: 0, length: 0) && (string == "" || string == " ")) { | |
return true | |
} | |
withAnimation { [weak self] in | |
guard let self = self else { return } | |
self.text = self.setCursorAndText(for: string, with: range, in: textField) | |
} | |
return false | |
} | |
private func setCursorAndText(for string: String, with range: NSRange, in textField: UITextField) -> String { | |
// Detect Autofill | |
let didAutofillTextfield = (range == NSRange(location: 0, length: 0) && (string.count > 1)) | |
if didAutofillTextfield { | |
return formatText(text, string) | |
} | |
guard let start = textField.position(from: textField.beginningOfDocument, offset: range.location), | |
let textFieldText = textField.text, | |
let textRange = Range(range, in: textFieldText) else { | |
return string | |
} | |
let newText = textFieldText.replacingCharacters(in: textRange, with: string) | |
let formattedText = formatText(text, newText) | |
textField.text = formattedText | |
let diffCount = formattedText.count - text.count < -1 | |
? abs(formattedText.count - text.count) | |
: max(formattedText.count - text.count, 0) | |
let cursorOffset = textField.offset(from: textField.beginningOfDocument, to: start) + diffCount | |
guard let cursorPosition = textField.position(from: textField.beginningOfDocument, offset: cursorOffset) else { | |
return formattedText | |
} | |
textField.selectedTextRange = textField.textRange(from: cursorPosition, to: cursorPosition) | |
return formattedText | |
} | |
func textFieldShouldReturn(_ textField: UITextField) -> Bool { | |
tapReturnKey() | |
return false | |
} | |
func textFieldDidBeginEditing(_ textField: UITextField) { | |
withAnimation { | |
isEditing = true | |
} | |
} | |
func textFieldDidEndEditing(_ textField: UITextField) { | |
withAnimation { | |
isEditing = false | |
} | |
} | |
} | |
struct TextConfiguration { | |
let keyboardType: UIKeyboardType | |
let returnKeyType: UIReturnKeyType | |
let textContentType: UITextContentType? | |
let autoCorrectionType: UITextAutocorrectionType | |
let autocapitalizationType: UITextAutocapitalizationType | |
init(keyboardType: UIKeyboardType = .default, | |
textContentType: UITextContentType? = nil, | |
autoCorrectionType: UITextAutocorrectionType = .default, | |
autocapitalizationType: UITextAutocapitalizationType = .sentences, | |
returnKeyType: UIReturnKeyType = .default) { | |
self.keyboardType = keyboardType | |
self.textContentType = textContentType | |
self.autoCorrectionType = autoCorrectionType | |
self.autocapitalizationType = autocapitalizationType | |
self.returnKeyType = returnKeyType | |
} | |
} | |
} | |
// MARK: Modifiers | |
extension GenericTextField { | |
func isDisabled(_ isDisabled: Bool) -> GenericTextField { | |
var view = self | |
view.isDisabled = isDisabled | |
return view | |
} | |
func isSecureTextEntry(_ isSecureTextEntry: Bool) -> GenericTextField { | |
var view = self | |
view.isSecureTextEntry = isSecureTextEntry | |
return view | |
} | |
func textContentType(_ textContentType: UITextContentType) -> GenericTextField { | |
var view = self | |
view.textConfiguration = TextConfiguration(keyboardType: textConfiguration.keyboardType, textContentType: textContentType, autoCorrectionType: textConfiguration.autoCorrectionType, autocapitalizationType: textConfiguration.autocapitalizationType) | |
return view | |
} | |
func formatOnTextChange(_ closure: @escaping (String, String) -> (String)) -> GenericTextField { | |
var view = self | |
view.formatText = closure | |
return view | |
} | |
func tintColor(_ tintColor: Color?) -> GenericTextField { | |
var view = self | |
if let cgColor = tintColor?.cgColor { | |
view.tintColor = UIColor(cgColor: cgColor) | |
} | |
return view | |
} | |
func textColor(_ textColor: UIColor?) -> GenericTextField { | |
var view = self | |
if let cgColor = textColor?.cgColor { | |
view.textColor = UIColor(cgColor: cgColor) | |
} | |
return view | |
} | |
func textAlignment(_ textAlignment: NSTextAlignment) -> GenericTextField { | |
var view = self | |
view.textAlignment = textAlignment | |
return view | |
} | |
func font(_ font: UIFont?) -> GenericTextField { | |
var view = self | |
view.font = font | |
return view | |
} | |
func dontFillWidth() -> GenericTextField { | |
var view = self | |
view.horizontalHuggingPriority = .required | |
return view | |
} | |
func textConfiguration(_ configuration: TextConfiguration) -> GenericTextField { | |
var view = self | |
view.textConfiguration = configuration | |
return view | |
} | |
func keyboardType(_ keyboardType: UIKeyboardType) -> GenericTextField { | |
var view = self | |
view.textConfiguration = TextConfiguration(keyboardType: keyboardType, textContentType: textConfiguration.textContentType, autoCorrectionType: textConfiguration.autoCorrectionType, autocapitalizationType: textConfiguration.autocapitalizationType) | |
return view | |
} | |
func passwordRules(_ descriptor: String?) -> GenericTextField { | |
var view = self | |
if let descriptor = descriptor { | |
view.passwordRules = UITextInputPasswordRules(descriptor: descriptor) | |
} | |
return view | |
} | |
func onTapReturnKey(_ closure: @escaping () -> ()) -> GenericTextField { | |
var view = self | |
view.tapReturnKey = closure | |
return view | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment