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
}
}
}
Related
I'm using a wrapper around UISearchBar and I'm seeing a different behavior when passing a #Published property into this SearchBar wrapper versus a TextField.
Both are updating the #ObservedObject var query = Query() class #Published var input property as expected but only the TextField is then updating the SearchSheet view. I would like for the view to be updated when input has been changed in SearchBar similarly to how it is updated from TextField.
Edit: I've updated my question to include the ContentView where it looks like this issue is specific to when the sheet is called from a Button in a NavigationBarItem.
struct ContentView: View {
#State var showingSearch = false
var body: some View {
NavigationView {
VStack {
Text("Hello World")
}
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}
)
}
}
}
class Query: ObservableObject {
#Published var input = "" {
didSet {
// Called as expected in both cases but only TextField updates the SearchSheet view.
}
}
}
struct SearchSheet: View {
#ObservedObject var query = Query()
var body: some View {
VStack {
// Does not update the SearchSheet view. I would like it understand why and how to update it.
SearchBar(text: $query.input, placeholder: "Search")
// Does update the SearchSheet view.
TextField("Search", text: $query.input)
Text("\(query.input)")
}
}
}
import SwiftUI
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
struct SearchBar_Previews: PreviewProvider {
#State private static var text = ""
static var previews: some View {
SearchBar(text: $text, placeholder: "Search")
}
}
Moving the display of the sheet out from navigationBarItems resolves the issue. At the time I believe this is a bug.
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
)
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}
I'm trying to observe/get some of SwiftUI's List scrolling attributes by wrapping/injection of UIScrollView using UIViewRepresentable.
I'm getting inconsistent behavior with bindings.
Pressing the buttons and changing values depending on if the button is in the parent vs child view has different results.
The bindings from my ObservableObject ScrollInfo class and the UIViewRepresentable start fine, but then break, unless the whole screen is refreshed and makeUIView runs again (like changing to a different tab).
Is there a way to force the UIViewRepresentable to run makeUIView again on a binding update? Or something that will fix this?
I'd like for isScrolling values to be updated and working all the time.
I set up a test to change the colors of the circles to red if the user is dragging the scrollview down. It works initially but stops if I update a value from the ObservableObject in the parent view.
Screenshots of Test from code below
Bindings keep working with bottom button press (updating ObservableObject) in child view
Bindings break with top button press (updating ObservableObject) in parent view
// Parent View
import SwiftUI
struct ContentView: View {
#ObservedObject var scrollInfo:ScrollInfo = ScrollInfo()
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded = true;
})
{
Text("REFRESH")
}
TestView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// Child View
import SwiftUI
struct TestView: View {
#State var test1:String = "Test1"
#ObservedObject var scrollInfo:ScrollInfo = ScrollInfo()
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded.toggle()
}) {
Text("REFRESH")
}
List{
VStack {
VStack{
Text(String( self.scrollInfo.contentLoaded))
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
} .padding(.bottom,620).padding(.top,20)
.background(
ListScrollingHelper(scrollInfo: self.scrollInfo)// injection
)
}.padding(.top,4).onAppear(perform:{
})
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
// ScrollInfo Class
class ScrollInfo: ObservableObject {
#Published var isScrolling:Bool = false
#Published var text:String = "Blank"
#Published var contentLoaded:Bool = false
init()
{
print( "scrollInfo init")
}
deinit {
print("scrollInfo denit")
}
}
// UIViewRepresentable
final class ListScrollingHelper: UIViewRepresentable {
var scrollInfo:ScrollInfo
#Published var scrollView: UIScrollView?
init( scrollInfo:ScrollInfo) {
print("init UIViewRepresentable listscrolling helper")
self.scrollInfo = scrollInfo
}
func makeUIView(context: Context) -> UIView {
//self.uiScrollView.delegate = context.coordinator
print("makeview")
return UIView()
//return self.uiScrollView // managed by SwiftUI, no overloads
}
func catchScrollView(for view: UIView) {
print("checking for scrollview")
if nil == scrollView {
scrollView = view.enclosingScrollView()
if(scrollView != nil)
{
print("scrollview found")
}
}
}
func updateUIView(_ uiView: UIView, context: Context) {
catchScrollView(for: uiView)
if(scrollView != nil)
{
scrollView!.delegate = context.coordinator
}
print("updatingUIView")
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject,UIScrollViewDelegate {
var parent: ListScrollingHelper
init(_ listScrollingHelper: ListScrollingHelper) {
self.parent = listScrollingHelper
print("init coordinator")
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// DispatchQueue.main.async {
if(!self.parent.scrollInfo.isScrolling)
{
self.parent.scrollInfo.isScrolling = true
//self.parent.scrollInfo.text = "scroll"
// }
}
print("start scroll")
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(self.parent.scrollInfo.isScrolling && !decelerate)
{
self.parent.scrollInfo.isScrolling = false
}
print("end scroll")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if(self.parent.scrollInfo.isScrolling)
{
self.parent.scrollInfo.isScrolling = false
}
print("end scroll")
}
deinit
{
print("deinit coordinator")
}
}
deinit {
print("deinit UIViewRepresentable listscrolling helper")
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
EDIT - WORKAROUND
I wasn't able to get it working with the ObservableObject or EnvironmentObject, but I was able to get it working with #State and #Binding, although it's a limited amount of info passed back. (ScrollInfo is still there only to use to testing changing a parent ObservableObject)
Hope this helps someone else!
import SwiftUI
struct TestView: View {
#State var isScrolling:Bool = false;
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded.toggle()
}) {
Text("REFRESH")
}
List{
VStack {
VStack{
Text("isScrolling")
Text(String(self.isScrolling))
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
} .padding(.bottom,620).padding(.top,20).background( ListScrollingHelper(isScrolling: self.$isScrolling))
}.padding(.top,4)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
final class ListScrollingHelper: UIViewRepresentable {
#Binding var isScrolling:Bool
private var scrollView: UIScrollView?
init(isScrolling:Binding<Bool>)
{
self._isScrolling = isScrolling
}
func makeUIView(context: Context) -> UIView {
return UIView()
}
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func updateUIView(_ uiView: UIView, context: Context) {
catchScrollView(for: uiView)
if(scrollView != nil)
{
scrollView!.delegate = context.coordinator
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject,UIScrollViewDelegate {
var parent: ListScrollingHelper
init(_ listScrollingHelper: ListScrollingHelper) {
self.parent = listScrollingHelper
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if(!self.parent.isScrolling)
{
self.parent.isScrolling = true
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(self.parent.isScrolling && !decelerate)
{
self.parent.isScrolling = false
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if(self.parent.isScrolling)
{
self.parent.isScrolling = false
}
}
deinit
{
}
}
deinit {
}
}
class ScrollInfo: ObservableObject {
#Published var isScrolling:Bool = false
#Published var text:String = "Blank"
#Published var contentLoaded:Bool = false
}
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 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)
}
}
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()
}