270 lines
10 KiB
Swift
270 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
|
|
struct UIKitTextField: UIViewRepresentable {
|
|
|
|
static private var NextTagId = 234567 // start at a high, very unlikely number to not interfere with manually set tags
|
|
|
|
|
|
@Binding private var text: String
|
|
|
|
private var placeholder: String
|
|
|
|
private var keyboardType: UIKeyboardType = .default
|
|
private var autocapitalizationType: UITextAutocapitalizationType = .sentences
|
|
private var addDoneButton: Bool = false
|
|
|
|
private var isPasswordField: Bool = false
|
|
|
|
@State private var focusOnStart = false
|
|
private var focusNextTextFieldOnReturnKeyPress = false
|
|
@Binding private var focusTextField: Bool
|
|
|
|
private var isFocusedChanged: ((Bool) -> Void)? = nil
|
|
|
|
private var textAlignment: NSTextAlignment = .natural
|
|
private var isUserInputEnabled: Bool = true
|
|
|
|
private var actionOnReturnKeyPress: (() -> Bool)? = nil
|
|
|
|
private var textChanged: ((String) -> Void)? = nil
|
|
|
|
|
|
init(_ titleKey: String, text: Binding<String>, keyboardType: UIKeyboardType = .default, autocapitalizationType: UITextAutocapitalizationType = .sentences, addDoneButton: Bool = false,
|
|
isPasswordField: Bool = false, focusOnStart: Bool = false, focusNextTextFieldOnReturnKeyPress: Bool = false, focusTextField: Binding<Bool> = .constant(false),
|
|
isFocusedChanged: ((Bool) -> Void)? = nil,
|
|
textAlignment: NSTextAlignment = .natural, isUserInputEnabled: Bool = true,
|
|
actionOnReturnKeyPress: (() -> Bool)? = nil, textChanged: ((String) -> Void)? = nil) {
|
|
|
|
self.placeholder = titleKey
|
|
_text = text
|
|
|
|
self.keyboardType = keyboardType
|
|
self.autocapitalizationType = autocapitalizationType
|
|
self.addDoneButton = addDoneButton
|
|
|
|
self.isPasswordField = isPasswordField
|
|
|
|
self._focusOnStart = State(initialValue: focusOnStart)
|
|
self.focusNextTextFieldOnReturnKeyPress = focusNextTextFieldOnReturnKeyPress
|
|
self._focusTextField = focusTextField
|
|
self.isFocusedChanged = isFocusedChanged
|
|
|
|
self.textAlignment = textAlignment
|
|
self.isUserInputEnabled = isUserInputEnabled
|
|
|
|
self.actionOnReturnKeyPress = actionOnReturnKeyPress
|
|
self.textChanged = textChanged
|
|
}
|
|
|
|
|
|
func makeUIView(context: UIViewRepresentableContext<UIKitTextField>) -> UITextField {
|
|
let textField = UITextField(frame: .zero)
|
|
|
|
textField.placeholder = placeholder.localize()
|
|
|
|
textField.isSecureTextEntry = isPasswordField
|
|
|
|
textField.keyboardType = keyboardType
|
|
textField.autocapitalizationType = autocapitalizationType
|
|
|
|
if addDoneButton {
|
|
addDoneButtonToKeyboard(textField, context.coordinator)
|
|
}
|
|
|
|
textField.delegate = context.coordinator
|
|
|
|
if isPasswordField {
|
|
addTogglePasswordVisibilityButton(textField, context.coordinator)
|
|
}
|
|
|
|
// set tag on all TextFields to be able to focus next view (= next tag). See Coordinator for more details
|
|
Self.NextTagId = Self.NextTagId + 1 // unbelievable, there's no ++ operator
|
|
textField.tag = Self.NextTagId
|
|
|
|
textField.textAlignment = textAlignment
|
|
|
|
return textField
|
|
}
|
|
|
|
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<UIKitTextField>) {
|
|
uiView.text = text
|
|
|
|
if focusOnStart {
|
|
// on iOS 14 calling .focus() in makeUIView() doesn't work -> do it here and reset focusOnStart property
|
|
DispatchQueue.main.async {
|
|
focusOnStart = false
|
|
}
|
|
uiView.focus()
|
|
}
|
|
|
|
if focusTextField {
|
|
DispatchQueue.main.async { // in very few cases focusTextField gets called during view update resulting in 'undefined behavior' -> async() fixes this
|
|
uiView.focus()
|
|
|
|
if isPasswordField { // TODO: currently it works but focusTextField can in general also be set in other ways then by tapping on label
|
|
context.coordinator.togglePasswordVisibility()
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.focusTextField = false // reset value so that it can be set again (otherwise it may never gets resetted and then updateUIView() requests focus even though already another view got the focus in the meantime)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func addTogglePasswordVisibilityButton(_ textField: UITextField, _ coordinator: Coordinator) {
|
|
coordinator.textField = textField
|
|
|
|
let togglePasswordVisiblityView = UIButton()
|
|
togglePasswordVisiblityView.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
|
|
togglePasswordVisiblityView.tintColor = UIColor.secondaryLabel
|
|
togglePasswordVisiblityView.addTarget(coordinator, action:#selector(coordinator.togglePasswordVisibility), for: .touchUpInside)
|
|
|
|
textField.leftView = togglePasswordVisiblityView
|
|
textField.leftViewMode = .always
|
|
}
|
|
|
|
private func addDoneButtonToKeyboard(_ textField: UITextField, _ coordinator: Coordinator) {
|
|
coordinator.textField = textField
|
|
|
|
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
|
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: coordinator, action: #selector(coordinator.doneButtonTapped))
|
|
|
|
let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
|
|
doneToolbar.barStyle = .default
|
|
|
|
doneToolbar.items = [spacer, doneButton]
|
|
doneToolbar.sizeToFit()
|
|
|
|
textField.inputAccessoryView = doneToolbar
|
|
}
|
|
|
|
|
|
func makeCoordinator() -> UIKitTextField.Coordinator {
|
|
return Coordinator(text: $text, focusNextTextFieldOnReturnKeyPress: focusNextTextFieldOnReturnKeyPress, isFocusedChanged: isFocusedChanged,
|
|
isUserInputEnabled: isUserInputEnabled, actionOnReturnKeyPress: actionOnReturnKeyPress, textChanged: textChanged)
|
|
}
|
|
|
|
|
|
class Coordinator: NSObject, UITextFieldDelegate {
|
|
|
|
@Binding private var text: String
|
|
|
|
private var focusNextTextFieldOnReturnKeyPress: Bool
|
|
|
|
private var isFocusedChanged: ((Bool) -> Void)? = nil
|
|
|
|
private var isUserInputEnabled: Bool
|
|
|
|
private var actionOnReturnKeyPress: (() -> Bool)?
|
|
|
|
private var textChanged: ((String) -> Void)?
|
|
|
|
var textField: UITextField? = nil
|
|
|
|
|
|
init(text: Binding<String>, focusNextTextFieldOnReturnKeyPress: Bool, isFocusedChanged: ((Bool) -> Void)? = nil, isUserInputEnabled: Bool,
|
|
actionOnReturnKeyPress: (() -> Bool)? = nil, textChanged: ((String) -> Void)? = nil) {
|
|
_text = text
|
|
|
|
self.focusNextTextFieldOnReturnKeyPress = focusNextTextFieldOnReturnKeyPress
|
|
self.isFocusedChanged = isFocusedChanged
|
|
self.isUserInputEnabled = isUserInputEnabled
|
|
|
|
self.actionOnReturnKeyPress = actionOnReturnKeyPress
|
|
|
|
self.textChanged = textChanged
|
|
}
|
|
|
|
|
|
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
|
if isUserInputEnabled {
|
|
if textField.isFirstResponder {
|
|
isFocusedChanged?(true)
|
|
}
|
|
}
|
|
|
|
return isUserInputEnabled
|
|
}
|
|
|
|
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
|
|
isFocusedChanged?(false)
|
|
}
|
|
|
|
func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
let newText = textField.text ?? ""
|
|
let didTextChange = newText != text // e.g. if just the cursor has been placed to another position then textFieldDidChangeSelection() gets called but text didn't change
|
|
|
|
DispatchQueue.main.async { // to not update state during view update
|
|
self.text = newText
|
|
|
|
if didTextChange {
|
|
self.textChanged?(newText)
|
|
}
|
|
}
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
return handleReturnKeyPress(textField)
|
|
}
|
|
|
|
@discardableResult
|
|
func handleReturnKeyPress(_ textField: UITextField) -> Bool {
|
|
var didHandleReturnKey = actionOnReturnKeyPress?() ?? false
|
|
|
|
if didHandleReturnKey == false && focusNextTextFieldOnReturnKeyPress == true {
|
|
let nextViewTag = textField.tag + 1
|
|
|
|
let nextView = textField.superview?.superview?.superview?.viewWithTag(nextViewTag)
|
|
?? textField.superview?.superview?.superview?.superview?.superview?.viewWithTag(nextViewTag) // for text fields in Lists (tables)
|
|
?? textField.superview?.superview?.superview?.viewWithTag(nextViewTag + 1) // iOS 14 often creates the same TextField twice but displays it once -> to select next view use nextViewTag + 1
|
|
?? textField.superview?.superview?.superview?.superview?.superview?.viewWithTag(nextViewTag + 1)
|
|
|
|
didHandleReturnKey = nextView?.focus() ?? false
|
|
}
|
|
|
|
if didHandleReturnKey == false {
|
|
textField.clearFocus() // default behaviour
|
|
}
|
|
|
|
return didHandleReturnKey
|
|
}
|
|
|
|
@objc
|
|
func doneButtonTapped() {
|
|
if let textField = self.textField {
|
|
handleReturnKeyPress(textField)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func togglePasswordVisibility() {
|
|
if let textField = self.textField {
|
|
textField.isSecureTextEntry.toggle()
|
|
|
|
if let togglePasswordVisiblityButton = textField.leftView as? UIButton {
|
|
if textField.isSecureTextEntry {
|
|
togglePasswordVisiblityButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
|
|
}
|
|
else {
|
|
togglePasswordVisiblityButton.setImage(UIImage(systemName: "eye.fill"), for: .normal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
struct UIKitTextView_Previews: PreviewProvider {
|
|
|
|
@State static private var text = ""
|
|
|
|
static var previews: some View {
|
|
UIKitTextField("Test label", text: $text)
|
|
}
|
|
|
|
}
|