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
Related
I bridge UIKit with SwiftUI as follows:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
init(language: Binding<String>, text: Binding<String>) {
self._language = language
self._text = text
}
func makeUIView(context: Context) -> UITextField {
let textField = getTextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
}
private func getTextField() -> UITextField {
let textField = WordTextField(frame: .zero)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
class WordTextField: UITextField {
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
}
And call it as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: $foreignThing)
This works fine in some parts of my app. In other parts, I need a text field which calls a method when the user taps the enter key after entering text. It's written like this:
TextField("", text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I tried implementing the above with UITextFieldViewRepresenatable as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I'm getting 'compiler couldn't type check this expression in reasonable time' error. I think I've narrowed it down to not implementing .onCommit:{} in my UITextFieldViewRepresentable()
If this is the problem, then I'd like to know how .onCommit:{} can be implemented in UITextFieldViewRepresentable().
There are a few mistakes in the UIViewRepresentable implementation:
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
This text binding will be out of date when the View is recreated, so change it to:
func makeCoordinator() -> Coordinator {
return Coordinator()
}
You can store the textField inside the coordinator, make it a lazy, set delegate self, then return it from the make func, eg..
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField // lazy property that sets delegate self.
}
In update, you need to make use of the new value of the binding, e.g.
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
// use the new binding
context.coordinator.textDidChange = { newText in
text = newText
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: UITextField = {
let textField = UITextField()
textField.delegate = self
return textField
}()
var textDidChange: ((String) -> Void)?
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text)
}
}
You'll see something similar in PaymentButton.swift in Apple's Fruta sample.
Perhaps a simpler way would be this (but I've not tested it yet):
// update
context.coordinator.textBinding = _text
// Coordinator
var textBinding: Binding<String>?
// textViewDidChange
textBinding?.wrappedValue = textField.text
I'm trying to delete all text inside a Text Field when a button to the right of the text field is tapped. When I tap the clear button it behaves as though the text has been cleared. It shows the placeholder text which is set to be shown when the Text Field is cleared. However, the text is still showing 'underneath' the placeholder text. I can delete the typed in text by using the delete key on the keyboard, one character at a time, but clearing all at once should be a quicker option.
I have a UIViewRepresentable for a TextField as below. The reason I'm using a UIViewRepresentable is because I need the keyboard language layout to change languages as required by the user. SwiftUI doesn't currently support this:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
var onCommit: (() -> Bool)
init(language: Binding<String>, text: Binding<String>, onCommit: #escaping() -> Bool = { return true }) {
self._language = language
self._text = text
self.onCommit = onCommit
}
func makeUIView(context: Context) -> WordTextField {
let textField = WordTextField(onCommit: onCommit)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func updateUIView(_ uiView: WordTextField, context: Context) {
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
}
class WordTextField: UITextField, UITextFieldDelegate {
var textDidChange: ((String) -> Void)?
var onCommit: (() -> Bool)
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
init(textDidChange: ( (String) -> Void)? = nil, onCommit: #escaping () -> Bool = { return true }, language: String? = nil) {
self.textDidChange = textDidChange
self.onCommit = onCommit
self.language = language
super.init(frame: .zero)
self.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text ?? "")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return onCommit()
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
I call it as below:
#State private var userAnswer: String = ""
#State private var keyboardLanguage: String = ""
#State private var hintColour: Color = .green
#State private var hintDisabled: Bool = false
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0.allowedCharacters(string: $0)
self.enableHint()
}), onCommit: {
checkAnswer()
return true
})
.showClearButton(userAnswer: $userAnswer, hintDisabled: $hintDisabled, hintColour: $hintColour)
The view modifier .showClearButton does not clear the userAnswer text when tapped. Tapping the keyboard delete button clears userAnswer one character at a time.
The view modifier code is below:
extension View {
func showClearButton(userAnswer: Binding<String>, hintDisabled: Binding<Bool>, hintColour: Binding<Color>) -> some View {
self.modifier(TextFieldClearButton(text: userAnswer, hintDisabled: hintDisabled, hintColour: hintColour))
}
}
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
#Binding var hintDisabled: Bool
#Binding var hintColour: Color
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = ""; hintDisabled = false; hintColour = Color.systemBlue },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.gray))
.imageScale(.large)
.frame(width: 44, height: 44, alignment: .trailing)
}
)
}
}
}
}
You forgot to set the new text on the UITextField which in the case of the clear button is an empty string, fix like this:
func updateUIView(_ uiView: WordTextField, context: Context) {
// set the new text
uiView.text = text
// you also forgot to set the new onCommit
uiView.onCommit = onCommit
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
Also, you could benefit from a Coordinator here, it works like this:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.textDidChange = {
text = $0
}
uiView.onCommit = onCommit
uiView.text = text // maybe could check it is different first
if uiView.language != self.language {
uiView.language = self.language
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: {
let textField = UITextField()
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}()
// your closure properties
// all your delegate methods
}
I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable.
I am not sure how to change the background colour/image of a UIBarButtonItem to show that the underline attribute button is selected. The UIBarButtonItem is inside of a UIToolbar, which is inside of a UITextView. I need the underline button to be a different colour when either the selected text contains the underline attribute, or if the typingAttributes contains the underline attribute (because the underline button has been selected).
Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the NSMutableAttributeString with no attributes")
var body: some View {
WYSIWYG(outerMutableString: $mutableString)
}
}
struct WYSIWYG: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = outerMutableString
context.coordinator.stringDidChange = { string in
outerMutableString = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
private let fontSize: CGFloat = 32.0
// var to check if the underline button has been pressed
private var underlineIsSelected: Bool = false
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: fontSize)
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]
toolBar.sizeToFit()
textView.inputAccessoryView = toolBar
return textView
}()
var stringDidChange: ((NSMutableAttributedString) -> ())?
func textViewDidChange(_ textView: UITextView) {
stringDidChange?(textView.textStorage)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var list: [NSAttributedString.Key: Any] = [:]
if underlineIsSelected { list[.underlineStyle] = NSUnderlineStyle.single.rawValue }
textView.typingAttributes = list
stringDidChange?(textView.textStorage)
return true
}
func textViewDidChangeSelection(_ textView: UITextView) { }
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
if (isActive(key: .underlineStyle)) {
textView.textStorage.removeAttribute(
.underlineStyle,
range: range)
} else {
textView.textStorage.addAttribute(
.underlineStyle,
value: NSUnderlineStyle.single.rawValue,
range: range)
}
stringDidChange?(textView.textStorage)
}
underlineIsSelected.toggle()
}
// func to check if the selected part of the NSMutableAttributedString contains the attribute key
func isActive(key: NSAttributedString.Key) -> Bool {
var range = textView.selectedRange
if range.length > 0 {
return (textView.textStorage.attribute(
key,
at: range.location,
longestEffectiveRange: &range,
in: range) != nil) ? true : false
}
return false
}
}
}
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
}
}
I have created a SwiftUI TextView based on a UITextView using UIViewRepresentable (s. code below). Displaying text in Swiftui works OK.
But now I need to access internal functions of UITextView from my model. How do I call e.g. UITextView.scrollRangeToVisible(_:) or access properties like UITextView.isEditable ?
My model needs to do these modifications based on internal model states.
Any ideas ? Thanks
(p.s. I am aware of TextEditor in SwiftUI, but I need support for iOS 13!)
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
You can use something like configurator callback pattern, like
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
var configurator: ((UITextView) -> ())? // << here !!
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
// alternat is to call this function in makeUIView, which is called once,
// and the store externally to send methods directly.
configurator?(myTextView) // << here !!
}
// ... other code
}
and use it in your SwiftUI view like
TextView(...) { uiText in
uiText.isEditing = some
}
Note: depending on your scenarios it might be additional conditions need to avoid update cycling, not sure.