SwiftUI Custom TextField with UIViewRepresentable Issue with ObservableObject and pushed View - ios

I created a UIViewRepresentable to wrap UITextField for SwiftUI, so I can e.g. change the first responder when the enter key was tapped by the user.
This is my UIViewRepresentable (I removed the first responder code to keep it simple)
struct CustomUIKitTextField: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<CustomUIKitTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUIKitTextField>) {
uiView.text = text
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentCompressionResistancePriority(.required, for: .vertical)
}
func makeCoordinator() -> CustomUIKitTextField.Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomUIKitTextField
init(parent: CustomUIKitTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
The first screen of the app has a "Sign in with email" button which pushes MailView that displays a CustomUIKitTextField and uses a #Published property of an ObservableObject view model as the TextField's text.
struct MailView: View {
#ObservedObject var viewModel: MailSignUpViewModel
var body: some View {
VStack {
CustomUIKitTextField(placeholder: self.viewModel.placeholder,
text: self.$viewModel.mailAddress)
.padding(.top, 30)
.padding(.bottom, 10)
NavigationLink(destination: UsernameView(viewModel: UsernameSignUpViewModel())) {
Text("Next")
}
Spacer()
}
}
}
Everything works fine until I push another view like MailView, say e.g. UsernameView. It is implemented exactly in the same way, but somehow the CustomUIKitTextField gets an updateUIView call with an empty string once I finish typing.
There is additional weird behavior like when I wrap MailView and UsernameView in another NavigationView, everything works fine. But that is obviously not the way to fix it, since I would have multiple NavigationViews then.
It also works when using an #State property instead of a #Published property inside a view model. But I do not want to use #State since I really want to keep the model code outside the view.
Is there anybody who faced the same issue or a similar one?

It looks like you’re using the wrong delegate method. textFieldDidChangeSelection will produce some inconsistent results (which is what it sounds like you’re dealing with). Instead, I recommend using textFieldDidEndEditing which will also give you access to the passed in control, but it guarantees that you’re getting the object as it is resigning the first responder. This is important because it means you’re getting the object after the properties have been changed and it’s releasing the responder object.
So, I would change your delegate method as follows:
func textFieldDidEndEditing(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
For more info, see this link for the textFieldDidEndEditing method:
https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619591-textfielddidendediting
And this link for info on the UITextFieldDelegate object:
https://developer.apple.com/documentation/uikit/uitextfielddelegate
EDIT
Based on the comment, if you're looking to examine the text everytime it changes by one character, you should implement this delegate function:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// User pressed the delete key
if string.isEmpty {
// If you want to update your query string here after the delete key is pressed, you can rebuild it like we are below
return true
}
//this will build the full string the user intends so we can use it to build our search
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
// You can set your parent.text here if you like, or you can fire the search function as well
// When you're done though, return true to indicate that the textField should display the changes from the user
return true
}

I also needed a UITextField representation in SwiftUI, which reacts to every character change, so I went with the answer by binaryPilot84. While the UITextFieldDelegate method textField(_:shouldChangeCharactersIn:replacementString:) is great, it has one caveat -- every time we update the text with this method, the cursor moves to the end of the text. It might be desired behavior. However, it was not for me, so I implemented target-action like so:
public func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChanged), for: .editingChanged)
return textField
}
public final class Coordinator: NSObject {
#Binding private var text: String
public init(text: Binding<String>) {
self._text = text
}
#objc func textChanged(_ sender: UITextField) {
guard let text = sender.text else { return }
self.text = text
}
}

Related

UITextView Delayed AttributedString Link SwiftUI

I have a Text view thal look like this:
class StudyText: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
override var canBecomeFirstResponder: Bool {
return false
}
}
and this is the struct:
struct ClickableText: UIViewRepresentable {
#Binding var text: NSMutableAttributedString
func makeUIView(context: Context) -> StudyText {
let view = StudyText()
view.dataDetectorTypes = .all
view.isEditable = false
view.isSelectable = true
view.delegate = view
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: StudyText, context: Context) {
uiView.attributedText = text
}
}
And I am using the attributed links.
Every solution I tried doesn't make the links respond to a quick tap. immediately. It takes a bit of delay until the print statement is presented.
I tried this:
view.delaysContentTouches = false
And I tried this:
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tappedTextView(tapGesture:)))
self.addGestureRecognizer(tapRecognizer)
#objc func tappedTextView(tapGesture: UIGestureRecognizer) {
let textView = tapGesture.view as! UITextView
let tapLocation = tapGesture.location(in: textView)
let textPosition = textView.closestPosition(to: tapLocation)
let attr = textView.textStyling(at: textPosition!, in: .forward)!
if let url: URL = attr[NSAttributedString.Key(rawValue: NSAttributedString.Key.link.rawValue)] as? URL {
print("clicking here: \(url)")
}
}
But none of them worked. It always responds with a delay
How can I fix this?
UITextView responds to both single tap gestures (which let you follow a link) and double tap gestures (which let you select text). Immediately after you tap the link once, it's unclear whether you've completed your gesture or whether a second tap is coming. Only after a short delay with no second tap can it be sure that you were in fact doing a single tap at which point textView(_:shouldInteractWith:in:interaction:) is called.
Unfortunately there's no standard way to make UITextView allow following links without allowing text selection. You might be able to hunt through the gesture recognizers registered on the view and find the one responsible for recognizing double taps and disable it, but doing so could have unintended side effects.

shouldChangeText not called for UITextField when using hardware keyboard

I have a sub class of UITextField and I want the sub class to control which input values are valid. That I have solved by overriding the shouldChangeText(in:, replacementText:) -> Bool method in the sub class. But this is only called when using the on-screen keyboard. If I use a hardware keyboard it doesn't get called.
The textField(_ textField:, shouldChangeCharactersIn:, replacementString:) -> Bool is called on the UITextFieldDelegate, when using a hardware keyboard. But I do not want to assign the delegate to the text field itself, since I need the delegate in some view controllers for other purposes. So I need an alternative method to validate the input values, like the shouldChangeText(in:, replacementText:) -> Bool gives me for the on-screen keyboard.
I can see on the stack trace from the delegate method, that the system has called a [UITextField keyboardInput:shouldInsertText:isMarkedText:], but I can't override that.
Is there any way to solve this, without assigning the delegate?
Here's an alternate solution that allows both your "real" text field delegate and your custom text field class to implement one more delegate methods.
The code below allows this custom text field to implement any of the needed UITextField delegate methods while still allowing the real delegate to implement any that it needs. The only requirement is that for any delegate method implemented inside the custom text field, you must check to see if the real delegate also implements it and call it as needed. Any delegate methods implemented in the real delegate class should be written normally.
This simple custom text field example is setup to only allow numbers. But it leaves the real delegate of the text field to do other validations such as only allowing a certain length of numbers or a specific range or whatever.
import UIKit
class MyTextField: UITextField, UITextFieldDelegate {
private var realDelegate: UITextFieldDelegate?
// Keep track of the text field's real delegate
override var delegate: UITextFieldDelegate? {
get {
return realDelegate
}
set {
realDelegate = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
// Make the text field its own delegate
super.delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Make the text field its own delegate
super.delegate = self
}
// This is one third of the magic
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return realDelegate
} else {
return super.forwardingTarget(for: aSelector)
}
}
// This is another third of the magic
override func responds(to aSelector: Selector!) -> Bool {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return true
} else {
return super.responds(to: aSelector)
}
}
// And the last third
// This only allows numbers to be typed into the text field.
// Of course this can be changed to do whatever validation you need in this custom text field
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) != nil {
return false // Not a number - fail
} else {
// The string is valid, now let the real delegate decide
if let delegate = realDelegate, delegate.responds(to: #selector(textField(_:shouldChangeCharactersIn:replacementString:))) {
return delegate.textField!(textField, shouldChangeCharactersIn: range, replacementString: string)
} else {
return true
}
}
}
}
The only other complication I leave as an exercise is a case where the custom text field wants to modify the changing text and then allow the real delegate to determine if that modified string/range should be allowed.

How to prevent an object from being de-initialized when it is currently being a delegate

I have created a "utility class" whose sole purpose is to be a UITextFieldDelegate.
Basically, it is supposed to enable a UIAlertAction only when there is text in a textfield, otherwise the action is disabled.
class ActionDisabler: NSObject, UITextFieldDelegate {
let action: UIAlertAction
init(action: UIAlertAction, textField: UITextField) {
self.action = action
super.init()
textField.delegate = self
// Initialize it as enabled or disabled
if let text = textField.text {
action.isEnabled = !text.isEmpty
} else {
action.isEnabled = false
}
}
deinit {
print("Too early?")
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text {
action.isEnabled = text.utf8.count - string.utf8.count > 0
} else {
action.isEnabled = false
}
return true
}
}
As you can see, it takes an alert action and a textfield in its constructor and makes itself the delegate of the text field. Simple, right?
Well, this is how delegate of UITextField is defined:
weak open var delegate: UITextFieldDelegate?
Due to this, ActionDisabler is de-initialized after the closure (a configuration handler for a textfield being added to a UIAlertController it is defined in is no longer in scope.
alert.addTextField(configurationHandler: { textField in
_ = ActionDisabler(action: join, textField: textField)
})
Subclassing UIAlertController and having each alert controller be the delegate of its own textfield is not an option, as explained by this answer.
Is there any way to make ActionDisabler not be de-initialized until the textfield that it is a delegate of is no longer alive?
The by far best solution is to keep a reference to your ActionDisabler where the reference to your UITextField is kept, so both will get deallocated at the same time.
If that is not possible or would make your code ugly, you could subclass UITextField to hold the delegate twice. Once with a strong reference:
class UIStrongTextField: UITextField {
// The delegate will get dealocated when the text field itself gets deallocated
var strongDelegate: UITextFieldDelegate? {
didSet {
self.delegate = strongDelegate
}
}
}
Now you set textField.strongDelegate = self and ActionDisabler will get deallocated together with the text field.
If you can't hold a reference and you can't subclass UITextField. We have to get creative. The ActionDisabler can hold a reference to itself, and set that reference to nil, to release itself. (aka manual memory management)
class ActionDisabler: NSObject, UITextFieldDelegate {
let action: UIAlertAction
var deallocPreventionAnchor: ActionDisabler?
init(action: UIAlertAction, textField: UITextField) {
self.deallocPreventionAnchor = self
self.action = action
When you don't need the ActionDisabler anymore you set self.deallocPreventionAnchor = nil and it will get deallocated. If you forget to set it to nil, you will leak the memory, because this is indeed manual memory management by exploiting the reference counter. And I can already see people screaming in the comments at me because that is kinda hacky :) (This also only makes sense when the deallocation will be triggered by a delegate function, otherwise you would have to keep a reference somewhere anyway)

Change 'Return' button function to 'Done' in swift in UITextView

I would like to get rid of the "return" function of the keyboard while the user is typing, so there are no new lines, so instead I would like the 'return' key to function as 'Done' so it would hide the keyboard.
I am using a UITextView, that is editable, so the user is able to type their post, and post it to the main timeline, but since I have fixed cells, I don't want the user to be able to press 'return' and their post would be out of range of the timeline.
I found this that works with UITextField, but not with UITextView:
func textFieldShouldReturn(textField: UITextField!) -> Bool {
textField.resignFirstResponder() //if desired
return true
}
So I just wanted to know if there is a way to do that in a UITextView, or at least to be able to hide the keyboard if pressed return, instead of creating a new line.
You can set the return key type of the text field:
textField.returnKeyType = UIReturnKeyType.done
Update
You can definitely use the same approach to set the return key to "Done", as mentioned above. However, UITextView doesn't provide a callback when user hits the return key. As a workaround, you can try to handle the textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) delegate call, and dismiss the keyboard when you detect the input of a new line character:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
I have tried many codes and finally this worked for me in Swift 3.0 Latest [April 2019] this achieved using UITextFields
The "ViewController" class should be inherited the "UITextFieldDelegate" for making this code working.
class ViewController: UIViewController,UITextFieldDelegate
Add the Text field with the Proper Tag number and this tag number is used to take the control to appropriate text field based on incremental tag number assigned to it.
override func viewDidLoad() {
userNameTextField.delegate = self
userNameTextField.tag = 0
userNameTextField.returnKeyType = .next
passwordTextField.delegate = self
passwordTextField.tag = 1
passwordTextField.returnKeyType = .go
}
In the above code, the "returnKeyType = UIReturnKeyType.next" where will make the Key pad return key to display as "Next" you also have other options as "Join/Go" etc, based on your application change the values.
This "textFieldShouldReturn" is a method of UITextFieldDelegate controlled and here we have next field selection based on the Tag value incrementation.
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
return true;
}
return false
}
If you're working with a storyboard or xib, you can change the UITextView's Return button to 'Done' (or various other options) within Interface Builder, without the need for any setup code. Just look for this option in the Attributes inspector:
From there, you just pair it up with the UITextViewDelegate code that others have already provided here.
Swift v5:
extension ExampleViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
}
And then, in your viewDidLoad() method:
exampleTextView.delegate = self
Working in Swift 4
Add this in viewDidLoad().
textField.returnKeyType = UIReturnKeyType.Done
Add this anywhere you like.
extension UITextView: UITextViewDelegate {
public func textViewDidChange(_ textView: UITextView) {
if text.last == "\n" { //Check if last char is newline
text.removeLast() //Remove newline
textView.resignFirstResponder() //Dismiss keyboard
}
}
}

How to use the keyboard "Go" to submit UITextFields like a UIButton in Swift 2

Ok so I have a function that allows my user to use the keyboard to go to the next field (Which I got the code from SO) It works perfect. My problem is, once my user gets to the final text field, in which, I've selected "GO" as the return button, and I want to use the go as the ideal submit button. As the flow through the form goes, its presentably right, but the functionality at the end isn't there. I found an answer here, but it looks like its calling the same functions as the "next field" code. So they don't work well together. So here's what the Next Field code looks like:
override func viewDidLoad() {
super.viewDidLoad()
// Keyboard Next Field & Delegate
enterEmailTextField.delegate = self
enterPasswordTextField.delegate = self
self.enterEmailTextField.nextField = self.enterPasswordTextField
self.enterPasswordTextField.nextField = self.enterNameTextField
// ...
}
And the next block is displayed below override (which i'm sure you know) func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
// Next Field
func textFieldShouldReturn(textField: UITextField) -> Bool {
if let nextField = textField.nextField {
nextField.becomeFirstResponder()
}
return true
}
Ok that works just fine down to the last text field, But since the last field is "Go" I'm trying to mimic what I would do with say an #IBAction for a button, but instead the go button. Here's where I got the idea/code for the Go to work:
Action of the "Go" button of the ios keyboard
Any Ideas on how to implement these? Or maybe just to work with the next function, and implement a "keyboard go" action similar to an #IBaction? Maybe just an all around better way to implement both of these so they coincide with each other? Any help is much appreciated!
EDIT!
I forgot the actual NextField.swift File which I'm sure is important (sorry)
import Foundation
import UIKit
private var kAssociationKeyNextField: UInt8 = 0
extension UITextField {
#IBOutlet var nextField: UITextField? {
get {
return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField
}
set(newField) {
objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, UInt(OBJC_ASSOCIATION_RETAIN))
}
}
}
this would help a lot I'm assuming
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
if (textField.returnKeyType == UIReturnKeyNext) {
// tab forward logic here
return YES;
}
else if (textField.returnKeyType == UIReturnKeyGo) {
// submit action here
return YES;
}
return NO;
}
On a cleaner way to handle tab order and form submitting, read my answer here.
First set return key to Go of your TextField, then implement the following method:
func textFieldShouldReturn(textField: UITextField) -> Bool {
if (textField.returnKeyType == UIReturnKeyType.Go)
{
// Implement your IBAction method here
}
return true
}
then see in below image:
Available for iOS 15 in SwiftUI. You can use .submitLabel(.go) to do it.
#available(iOS 15.0, *)
struct TextFieldSubmitView: View {
#Binding var name: String
var body: some View {
VStack(alignment: .leading) {
TextField("Type text", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
#if os(iOS)
.submitLabel(.go)
#endif
}
}
}
#available(iOS 15.0, *)
struct TextFieldSubmitView_Previews: PreviewProvider {
static var previews: some View {
TextFieldSubmitView(name: .constant("Name"))
.previewLayout(.sizeThatFits)
}
}

Resources