I want to disable the option of copy/paste from my Textfield in SwiftUI. How to achieve that?
This works for me:
Using UITextField conforms to UIViewRepresentable.
import SwiftUI
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
Text(text)
UITextFieldViewRepresentable(text: $text) // Using it
.frame(height: 44)
.border(.red)
}
}
}
// MARK: UITextFieldViewRepresentable
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var text: String
typealias UIViewType = ProtectedTextField
func makeUIView(context: Context) -> ProtectedTextField {
let textField = ProtectedTextField()
textField.delegate = context.coordinator
return textField
}
// From SwiftUI to UIKit
func updateUIView(_ uiView: ProtectedTextField, context: Context) {
uiView.text = text
}
// From UIKit to SwiftUI
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 ?? ""
}
}
}
// Custom TextField with disabling paste action
class ProtectedTextField: UITextField {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
Use UIViewRepresentable class make wrapper class like this.
import SwiftUI
struct CustomeTextField: View {
#State var textStr = ""
var body: some View {
VStack(spacing: 10) {
Text("This is textfield:")
.font(.body)
.foregroundColor(.gray)
TextFieldWrapperView(text: self.$textStr)
.background(Color.gray)
.frame(width: 200, height: 50)
}
.frame(height: 40)
}
}
struct TextFieldWrapperView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> TFCoordinator {
TFCoordinator(self)
}
}
extension TextFieldWrapperView {
func makeUIView(context: UIViewRepresentableContext<TextFieldWrapperView>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
class TFCoordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapperView
init(_ textField: TextFieldWrapperView) {
self.parent = textField
}
func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UIResponderStandardEditActions.paste(_:)) {
return false
}
return canPerformAction(action: action, withSender: sender)
}
}
Related
So I want to implement a custom control as a UIViewRepresentable which correctly handles focus using an #FocusState binding.
So I want to be able to manage the focus like so:
struct MyControl: UIViewRepresentable { ... }
struct Container: View {
#FocusState var hasFocus: Bool = false
var body: some View {
VStack {
MyControl()
.focused($hasFocus)
Button("Focus my control") {
hasFocus = true
}
}
}
}
What do I have to implement in MyControl to have it respond to the focus state properly? Is there a protocol or something which must be implemented?
Disclaimer: the solution is not suitable for full custom controls. For these cases, you can try to pass the FocusState as binding: let isFieldInFocus: FocusState<Int?>.Binding
In my case, I have wrapped UITextView. In order to set the focus, I only used .focused($isFieldInFocus).
Some of the information can be obtained through the property wrapper(#Environment(\.)), but this trick does not work with focus.
struct ContentView: View {
#FocusState var isFieldInFocus: Bool
#State var text = "Test message"
#State var isDisabled = false
var body: some View {
VStack {
Text("Focus for UITextView")
.font(.headline)
AppTextView(text: $text)
.focused($isFieldInFocus)
.disabled(isDisabled)
.frame(height: 200)
HStack {
Button("Focus") {
isFieldInFocus = true
}
.buttonStyle(.borderedProminent)
Button("Enable/Disable") {
isDisabled.toggle()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AppTextView: UIViewRepresentable {
#Binding var text: String
#Environment(\.isEnabled) var isEnabled: Bool
#Environment(\.isFocused) var isFocused: Bool // doesn't work
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: .body)
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
print("isEnabled", isEnabled, "isFocused", isFocused)
}
class Coordinator : NSObject, UITextViewDelegate {
private var parent: AppTextView
init(_ textView: AppTextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I am making a SwiftUI Button from a custom UIButton class called UIPillButton. Here is the code for creating the SwiftUI button:
Button(action: {
print("Button tapped")
}) {
PillButton()
.padding(.top)
}
Here is my class for creating a SwiftUI view called PillButton which converts my custom UIButton over:
struct PillButton: UIViewRepresentable {
var ntPillButton = NTPillButton(type: .filled, title: "Start Test")
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject {
var parent: PillButton
init(_ pillButton: PillButton) {
self.parent = pillButton
super.init()
}
}
func makeUIView(context: UIViewRepresentableContext<PillButton>) -> UIView {
let view = UIView()
view.addSubview(ntPillButton)
NSLayoutConstraint.activate([
ntPillButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
ntPillButton.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PillButton>) {}
}
The issue is that the Button is not running the action if I tap on the PillButton itself. It only works if I select the buffer (padding) space above the PillButton. How can I make it so I can use the custom PillButton class as a normal SwiftUI button?
It is not clear what is NTPillButton, but if it is subclass of UIButton then the following demo of generic approach (using base UIButton) should clear and applicable.
Tested with Xcode 11.4 / iOS 13.4
The below gives this simple usage
PillButton(title: "Start Test") {
print("Button tapped")
}
.frame(maxWidth: .infinity) // << screen-wide
.padding(.top)
so now PillButton demo itself:
struct PillButton: UIViewRepresentable {
let title: String
let action: () -> ()
var ntPillButton = UIButton()//NTPillButton(type: .filled, title: "Start Test")
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject {
var parent: PillButton
init(_ pillButton: PillButton) {
self.parent = pillButton
super.init()
}
#objc func doAction(_ sender: Any) {
self.parent.action()
}
}
func makeUIView(context: Context) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(self.title, for: .normal)
button.addTarget(context.coordinator, action: #selector(Coordinator.doAction(_ :)), for: .touchDown)
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {}
}
I'm trying to use UITextField in SwiftUI.
I've created a custom struct for UITextField and made a Bindable String, which holds the input text.
When I'm trying to pass the input text back to the viewModel, it doesn't work.
For the sake of comparing, I placed SwiftUI Textfield in the code and it works
Could someone please help?
import SwiftUI
final class RegistrationViewViewModel: ObservableObject {
#Published var firstName: String = "" {
didSet {
print(oldValue)
}
willSet {
print(newValue)
}
}
#Published var lastName: String = "" {
didSet {
print(oldValue)
}
willSet {
print(newValue)
}
}
func saveData() {
print("Saving...", firstName, lastName)
}
}
struct RegistrationView: View {
#ObservedObject var viewModel = RegistrationViewViewModel()
var body: some View {
VStack(spacing: 50) {
TextField("Last name", text: $viewModel.lastName)
.frame(width: 300, height: 35, alignment: .center)
.border(Color(.systemGray))
CustomTextField(inputText: $viewModel.firstName, placeholder: "First name")
.frame(width: 300, height: 35, alignment: .center)
.border(Color(.systemGray))
Button(action: {
self.viewModel.saveData()
}) {
Text("Submit")
}
}
}
}
struct CustomTextField: UIViewRepresentable {
var inputText: Binding<String>
var placeholder: String
func makeUIView(context: Context) -> UITextField {
let textfield = UITextField(frame: .zero)
textfield.delegate = context.coordinator
textfield.placeholder = placeholder
return textfield
}
func updateUIView(_ uiView: UITextField, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ parent: CustomTextField) {
self.parent = parent
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}
try to set your updateUIView function as below:
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = inputText
}
I know how to create a UIViewRepresentable in SwiftUI in order to enable first responder functionality for a TextField:
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (!uiView.isFirstResponder) {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
My problem is attempting to "re-focus" this custom text field. So while this text field DOES get focused when my ContentView is initialized, I want to know how I can re-focus this text field programmatically, AFTER it has lost focus.
Here is my ContentView:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
// ???
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
Here is what I've tried. I thought maybe I could create a #State variable which can control the FirstResponderTextField, so I went ahead and changed my structs as follows:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
#State var isFocused: Bool = true
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1, isFocused: $isFocused)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
self.isFocused = true
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
#Binding var isFocused: Bool
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (isFocused) {
uiView.becomeFirstResponder()
isFocused = false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
It does not appear to be working. I mean, it works when I first click the button, but stops working afterwards.
Also, I am now getting this warning:
Modifying state during view update, this will cause undefined behaviour.
Is it possible to create a UIViewRepresentable that can be re-focused whenever I want?
You should not be setting the isFocused to false in your FirstResponderTextField.
Instead, in FirstResponderTextField, observe the value changes in the isFocused binding, and set your control to being first responder or not accordingly.
I created a solution for you with an ObserableObject corresponding to the TextFieldState in this gist on github
For future reference, this is what I changed:
instead of #State var isFocused: Bool = true in ContentView, use #ObservedObject var textFieldState = TextFieldState()
have a property #ObservedObject var state: TextFieldState in FirstResponderTextField
simply do this in FirstResponderTextField.updateUIView:
if state.isFirstResponder {
uiView.becomeFirstResponder()
}
Here is my code:
struct CustomTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let uiView = UITextField()
uiView.delegate = context.coordinator
return uiView
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ CustomTextField: CustomTextField) {
self.parent = CustomTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
Building doesn't cause the warning, but using CustomTextField in the simulator does.
Here is the warning:
Modifying state during view update, this will cause undefined behavior.
It is showing on this line:
parent.text = textField.text ?? ""
How do I get rid of this warning? What am I doing wrong?
Try to perform modification asynchronously:
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.parent.text = self.textField.text ?? ""
}
}