UIViewRepresentable
Tested with Xcode 12.3
With each passing year, we hope for more and more UI components to be added to SwiftUI, but even though the framework has been out since the 2019 edition of WWDC, there's still a few things that we miss dearly.
We'll look over UITextField, a component that I find myself bringing into SwiftUI projects over and over again. The default TextField offered doesn't have the ability to become or resign first responder. Deal breaker 🙅
Luckily, Apple has provided a way to bring a UIKit component and integrate it seamlessly into our SwiftUI code, using the UIViewRepresentable protocol, so let's dive right in!
1 2 3 4 5 6 7 8 9
When implementing the UIViewRepresentable protocol, we need to provide the implementation for 2 methods. The former manages the creation and setup of our UIKit component, while the latter deals with making sure the UIKit component stays up-to-date.
Replace the default ContentView and start using your new shiny SwiftUI view ✨
1 2 3 4 5 6 7 8 9
It isn't very useful right now since we're writing text into the void, so let's see how we can inform our ContentView of text changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import SwiftUI struct CustomTextField: UIViewRepresentable { @Binding var text: String funcmakeUIView(context: Context) -> UITextField { let textField = UITextField() NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: .main) { _ in text = textField.text ??""} return textField } funcupdateUIView(_ textField: UITextField, context: Context) {} static funcdismantleUIView(_ textField: UITextField, coordinator: Coordinator) { NotificationCenter.default.removeObserver(self) } }
The text binding will hold a reference to a value that was passed to our CustomTextField during initialization. Remember that since this is a struct, an init method is automatically generated.
Because we're adding our custom view as an observer, we also need to make sure that we perform the necessary clean-up. UIViewRepresentable protocol also supplies dismantleUIView for this purpose, which acts like a deinit.
To monitor our changes, there's 2 approaches we can take. The first one involves adding a modifier to our view and listening to changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
The second approach involves a view model that holds our text variable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import SwiftUI final class ViewModel: ObservableObject { init() {} @Published var text =""{ didSet { print("didSet text = \(text)") } } } struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { CustomTextField(text: $viewModel.text) .frame(width:150, height:30) .background(Color.gray) } }
In both example you may have noticed the $ sign. This is used to create a Binding for that value. It is similar to the idea of creating a pointer in other languages.
Couldn't we have used didSet in the first example? We could, but we would only get some of the updates. Because of how struct works in tandem with @State, didSet isn't aware of changes that are propagated by bindings. The only time didSet would trigger is when we would change the text variable from inside some methods like onAppear, onChange, etc.
So far, we've looked over keeping the SwiftUI component up-to-date. But what about keeping the UIKit component up-to-date? Let's say we would like to have our text field text reset when the user types "ABC".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
If we try to run it as it is right now, we would notice that even though we type "ABC" and it should reset, that's not what happens. This is where the updateUIView method comes into play. We just need to make one simple change to our custom view:
1 2 3
funcupdateUIView(_ textField: UITextField, context: Context) { textField.text = text }
We've successfully created the two way communication! If we type "AB", once the "C" is entered, the text will disappear.
So far so good. Next on the list, making our CustomTextField capable of becoming and resigning first responder. The simplest approach would be to have another binding in our custom view, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import SwiftUI struct CustomTextField: UIViewRepresentable { @Binding var text: String @Binding var firstResponder: Bool funcmakeUIView(context: Context) -> UITextField { let textField = UITextField() NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: .main) { _ in text = textField.text ??""} return textField } funcupdateUIView(_ textField: UITextField, context: Context) { textField.text = text if firstResponder &&!
textField.isFirstResponder { textField.becomeFirstResponder() } else if!
firstResponder && textField.isFirstResponder { textField.resignFirstResponder() } } }
Now everytime the value of firstResponder changes, the keyboard will appear / disappear based on that value. Create another @State variable, this time a boolean. Give it the initial value of true and pass it on to the initializer that is giving the compile error.
What if we wanted to use the delegate approach instead of the NotificationCenter approach? In an ideal world, we would simply make our custom type conform to UITextFieldDelegate, but doing so requires our type to also conform to NSObjectProtocol, which only classes can do.
This where the concept of Coordinator comes into play:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
import SwiftUI struct CustomTextField: UIViewRepresentable { @Binding var text: String @Binding var firstResponder: Bool funcmakeUIView(context: Context) -> UITextField { let textField = UITextField() textField.delegate = context.coordinator return textField } funcupdateUIView(_ textField: UITextField, context: Context) { textField.text = text if firstResponder &&!
textField.isFirstResponder { textField.becomeFirstResponder() } else if!
firstResponder && textField.isFirstResponder { textField.resignFirstResponder() } } funcmakeCoordinator() -> Coordinator { return Coordinator(text: $text) } final class Coordinator: NSObject, UITextFieldDelegate { @Binding var text: String init(text: Binding<String>) { _text = text } functextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { text = NSString(string: textField.text ??"") .replacingCharacters(in: range, with: string) return true } } }
The Coordinator is a class and therefore can conform to UITextFieldDelegate. It has the same binding as the parent struct. To assign that binding in the init method, the _ prefix is used.
As a quick recap, when we have a @State var foo = true
1 2 3
foo
Assuming we tie that @State to a binding @Binding var foo: Bool
1 2 3
bar
Is it done? It's done 🎉
As a final note, even though some SwiftUI modifiers work with UIViewRepresentable views, like .frame(...), .background(...), .padding(...), more internal ones such as .font(...), .foregroundColor(...) might not. My usual setup for UITextField is something like:
1 2 3 4
textField.autocapitalizationType = .none textField.font = .systemFont(ofSize:20) textField.textAlignment = .center textField.textColor = .blue
Thank you for taking the time to read and all the best on your SwiftUI journey!