SwiftUI XCode 13 TextField shouldChangeText property wiring - swiftui

I'm using XCode 13 and SwiftUI (latest version)
I've got a simple TextField inside of a View.
TextField("enter barcode", value: $scannedBarcode, shouldChangeText : ?)
I cannot figure out how to use the shouldChangeText property of TextField.
I need an example of how to wire up this function.
I'm a newbie to SwiftUI, cannot find an example, and the developer docs are sorely lacking...
Apparently it looks like this maybe:
shouldChangeText(in: <UITextRange>, replacementText: <String>)
I'm very new to XCode (a few weeks) and need a solid example of how to use this to filter out all non whole numbers from a text field (no decimal point or alpha characters)
Thanks!
-----Edit----
With help below found this:
TextField("enter barcode", text: Binding(get: {scannedBarcode}, set: { scannedBarcode = String($0).replacingOccurrences( of:"[^0-9]", with: "", options: .regularExpression)}))
Which is great a filtering out non-digits going into the scannedBarcode variable. How can I prevent them from hitting the screen in the first place?

That is really simple in SwiftUI, you just have to use .onChange. Add this anywhere in your View:
.onChange(of: scannedBarcode) { newValue in
//do anything with newValue
}
It will give you the exact value of what's in the TextField.
For the UIKit one, it's the following function which is part of UITextFieldDelegate:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
}
UIKit with SwiftUI:
struct UTextField: UIViewRepresentable {
let placeHolder: String
#Binding var text: String?
init(_ placeHolder: String, text: Binding<String?>) {
self.placeHolder = placeHolder
self._text = text
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeHolder
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
text = uiView.text
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//do something
//whenever you don't want the text to be entered you return false
return true
}
}
}
Usage: UTextField("", text: $someText)

Related

How do I make a UITextView inside of a UIViewRepresentable update when I add an attribute to an NSMutableAttributedString?

I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable. I am primarily using SwiftUI but am using UIKit here as it seems SwiftUI does not currently support the functionality needed.
My problem is, when I set the NSMutableAttributedString to be already containing a string with attributes, if I then select that text in the UIViewRepresentable before typing any new text and press the underline button in the UIToolBar to add the attribute, the attribute is added to the NSMutableAttributedString but the UIView does not update to show the updated NSMutableAttributedString. However, if I type a single character and then select the text and add the underline attribute, the UIView updates.
Could someone explain why this is and maybe point me towards a solution? Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the string before typing anything new",
attributes: [.foregroundColor: UIColor.blue])
var body: some View {
EditorExample(outerMutableString: $mutableAttributedString)
}
}
struct EditorExample: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
#State private var outerSelectedRange: NSRange = NSRange()
func makeUIView(context: Context) -> some UITextView {
// make UITextView
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = context.coordinator
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: context.coordinator,
action: #selector(context.coordinator.underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.attributedText = outerMutableString
}
func makeCoordinator() -> Coordinator {
Coordinator(innerMutableString: $outerMutableString, selectedRange: $outerSelectedRange)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var innerMutableString: NSMutableAttributedString
#Binding var selectedRange: NSRange
init(innerMutableString: Binding<NSMutableAttributedString>, selectedRange: Binding<NSRange>) {
self._innerMutableString = innerMutableString
self._selectedRange = selectedRange
}
func textViewDidChange(_ textView: UITextView) {
innerMutableString = textView.textStorage
}
func textViewDidChangeSelection(_ textView: UITextView) {
selectedRange = textView.selectedRange
}
#objc func underline() {
if (selectedRange.length > 0) {
innerMutableString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: selectedRange)
}
}
}
}
It's not working because NSAttributedString is a class and #State is for value types like structs. This means the dependency tracking is broken and things won't update correctly.
Also your UIViewRepresentable and Coordinator design is non-standard so I thought I would share an example of the correct way to do it. The binding is change to a string, which is a value type so it's working (minus the underline feature obviously).
struct ContentView: View {
//#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
// string: "this is the string before typing anything new",
// attributes: [.foregroundColor: UIColor.blue])
#State var string = "this is the string before typing anything new"
var body: some View {
VStack {
// EditorExample(outerMutableString: $mutableAttributedString)
// EditorExample(outerMutableString: $mutableAttributedString) // a second to test bindings are working\
//Text(mutableAttributedString.string)
EditorExample(outerMutableString2: $string)
EditorExample(outerMutableString2: $string)
}
}
}
struct EditorExample: UIViewRepresentable {
//#Binding var outerMutableString: NSMutableAttributedString
#Binding var outerMutableString2: String
// this is called first
func makeCoordinator() -> Coordinator {
// we can't pass in any values to the Coordinator because they will be out of date when update is called the second time.
Coordinator()
}
// this is called second
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
// this is called third and then repeatedly every time a let or `#Binding var` that is passed to this struct's init has changed from last time.
func updateUIView(_ uiView: UITextView, context: Context) {
//uiView.attributedText = outerMutableString
uiView.text = outerMutableString2
// we don't usually pass bindings in to the coordinator and instead use closures.
// we have to set a new closure because the binding might be different.
context.coordinator.stringDidChange2 = { string in
outerMutableString2 = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}()
//var stringDidChange: ((NSMutableAttributedString) -> ())?
var stringDidChange2: ((String) -> ())?
func textViewDidChange(_ textView: UITextView) {
//innerMutableString = textView.textStorage
//stringDidChange?(textView.textStorage)
stringDidChange2?(textView.text)
}
func textViewDidChangeSelection(_ textView: UITextView) {
// selectedRange = textView.selectedRange
}
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
textView.textStorage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
// stringDidChange?(textView.textStorage)
}
}
}
}

SwiftUI Show/Dismiss Keyboard

This code below show and hide TextField keyboard perfectly except this warning message keep showing to me when run the code, did anyone can help to avoid this warning please ???
import UIKit
import SwiftUI
struct FirstResponderTextFiels: UIViewRepresentable {
#Binding var text: String
let placeholder: String
#Binding var showKeyboard: Bool
// Create the coordinator
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
#Binding var showKeyboard: Bool
var becameFirstResponder = false
init(text: Binding<String>, showKeyboard: Binding<Bool>) {
self._text = text
self._showKeyboard = showKeyboard
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, showKeyboard: $showKeyboard)
}
// Create the textfield
func makeUIView(context: Context) -> some UIView {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if context.coordinator.showKeyboard {
uiView.becomeFirstResponder()
context.coordinator.showKeyboard = false
}
}
}
The warning message
After review the code I found if I remove this part of code it has no effect and it just work fine
func updateUIView(_ uiView: UIViewType, context: Context) {
if context.coordinator.showKeyboard {
uiView.becomeFirstResponder()
context.coordinator.showKeyboard = false // <--- Remove it
}
}

SwiftUI - making a textfield inside a view be the first responder on touch to that view

On a view I have something like this:
TextFieldUsername()
this shows something like
So, this view shows an icon and the textfield username.
Below that, I have another one for the password.
Making that username field in focus is unnecessarily hard. The textfield is not small, but making the username field to focus is a matter of tapping on the exact position and perhaps you have to tap 2 or 3 times to make it happen.
I would like to make the whole TextFieldUsername() tappable or to increase the hit area of that textfield. I would like better to make the whole thing tappable and once tapped, make its textfield in focus.
This is TextFieldUsername
struct TextFieldUsername: View {
#State var username:String
var body: some View {
HStack {
Image(systemName: "person.crop.circle")
.renderingMode(.template)
.foregroundColor(.black)
.opacity(0.3)
.fixedSize()
TextField(TextFieldUsernameStrings.username, text: $username)
.textFieldStyle(PlainTextFieldStyle())
.textContentType(.username)
.autocapitalization(.none)
}
}
}
Is that possible in SwiftUI without using any external library like introspect?
Using a custom TextField like the one from Matteo Pacini
you can do something like this:
struct CustomTextField1: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField1>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField1.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField1>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
struct ContentView : View {
#State var text: String = ""
#State var isEditing = false
var body: some View {
CustomTextField1(text: $text, isFirstResponder: isEditing)
.frame(width: 300, height: 50)
.background(Color.red)
.onTapGesture {
isEditing.toggle()
}
}
}
It's a bit complex, but should get the work done. As for a pure SwiftUI answer, it's currently unavailable.

SwiftUI, tvOS, UIViewControllerRepresentable and a focus issue

I'm trying to implement search functionality in the tvOS SwiftUI app. I'm using UISearchController as the most straight forward solution to do this. I've wrapped it inside SearchView which conforms to UIViewControllerRepresentable. The problem is, that it looks like the focus engine refuses to focus on a part of a view controller UI - UISearchBar that is wrapped. I can type the search query from my Mac, inside the simulator, to verify that the search works, but of course, it's not the real thing.
I've tried to add the .focusable() modifier into a SearchView, but it didn't help.
Also tried to implement shouldUpdateFocus, preferredFocusEnvironments and didUpdateFocus callbacks inside my custom subclasses of UISearchController and UISearchContainerViewController but those are not called at all.
I think I'm missing something very straightforward here.
Here is the code for the SearchView:
struct SearchView: UIViewControllerRepresentable {
#Binding var text: String
typealias UIViewControllerType = UISearchContainerViewController
typealias Context = UIViewControllerRepresentableContext<SearchView>
func makeUIViewController(context: Context) -> UIViewControllerType {
let controller = UISearchController(searchResultsController: context.coordinator)
controller.searchResultsUpdater = context.coordinator
return UISearchContainerViewController(searchController: controller)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> SearchView.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: UIViewController, UISearchResultsUpdating {
#Binding var text: String
init(text: Binding<String>) {
_text = text
super.init(nibName: nil, bundle: nil)
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
text = searchText
}
}
}
And the main ContentView (I've stripped some non-important code):
struct ContentView: View {
#State var model = ["aa", "ab", "bb", "bc", "cc", "dd", "ee"]
#State var searchQuery: String = ""
var body: some View {
SearchView(text: $searchQuery)
List {
ForEach(model.filter({ $0.hasPrefix(searchQuery) })) { item in
Text(item)
}
}
}
}
Finally, it can be done through the .searchable modifier, introduced in iOS 15 beta 1, no need for UISearchController wrappers anymore.
P.S. Got an official response from Apple on this bug (FB8974300) to also use .searchable modifier, so there is no fix for the older versions.

Cannot assign to property: '$text' is immutable

I wanted to make a custom textfield in SwiftUI to can handle first responder but I had this error in the code and struct is immutable I don't know what should I do?
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(txt: Binding<String>) {
self.$text = txt
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(txt: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
In beta 4, the implementation of property wrappers changed.
Until beta 3, this was valid:
self.$text = txt
In beta 4, it changed to:
self._text = txt
Check for the difference in implementation, in this other question I posted:
https://stackoverflow.com/a/57088052/7786555
And for more details:
https://stackoverflow.com/a/56975728/7786555