optional
dev
import Blog

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
import SwiftUI

struct CustomTextField: UIViewRepresentable {
  func makeUIView(context: Context) -> UITextField {
    return UITextField()
  }
  
  func updateUIView(_ textField: UITextField, context: Context) {}
}

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
import SwiftUI

struct ContentView: View {
  var body: some View {
    CustomTextField()
      .frame(width: 150, height: 30)
      .background(Color.gray)
  }
}

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
  
  func makeUIView(context: Context) -> UITextField {
    let textField = UITextField()
    NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                           object: nil,
                                           queue: .main) { _ in
      text = textField.text ?? ""
    }
    return textField
  }
  
  func updateUIView(_ textField: UITextField, context: Context) {}
  
  static func dismantleUIView(_ 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
import SwiftUI

struct ContentView: View {
  @State private var text: String = ""
  
  var body: some View {
    CustomTextField(text: $text)
      .frame(width: 150, height: 30)
      .background(Color.gray)
      .onChange(of: text) { _ in
        print("onChange text = \(text)")
      }
  }
}

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
import SwiftUI

struct ContentView: View {
  @State private var text = ""
  
  var body: some View {
    CustomTextField(text: $text)
      .frame(width: 150, height: 30)
      .background(Color.gray)
      .onChange(of: text) { _ in
        if text == "ABC" {
          text = ""
        }
      }
  }
}

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
func updateUIView(_ 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
  
  func makeUIView(context: Context) -> UITextField {
    let textField = UITextField()
    NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                           object: nil,
                                           queue: .main) { _ in
      text = textField.text ?? ""
    }
    return textField
  }
  
  func updateUIView(_ 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

  func makeUIView(context: Context) -> UITextField {
    let textField = UITextField()
    textField.delegate = context.coordinator
    return textField
  }

  func updateUIView(_ textField: UITextField, context: Context) {
    textField.text = text
    if firstResponder && 

!

textField.isFirstResponder { textField.becomeFirstResponder() } else if

!

firstResponder && textField.isFirstResponder { textField.resignFirstResponder() } } func makeCoordinator() -> Coordinator { return Coordinator(text: $text) } final class Coordinator: NSObject, UITextFieldDelegate { @Binding var text: String init(text: Binding<String>) { _text = text } func textField(_ 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 
// value of type true; can use get or set on it
$foo
// a getter to a @Binding that mirrors the value of its equivalent @State, get only
_foo
// the @State itself, mutable in classes, but only mutable in init for structs

Assuming we tie that @State to a binding @Binding var foo: Bool

1
2
3
 bar 
// the underlying value, which is true; can use get or set on it
$bar
// a getter to the @Binding itself; used for passing onto other parts of the code
_bar
// the @Binding itself, mutable in classes, but only mutable in init for structs

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!