Teabyte

Mobile app development for iOS and Swift

Use UITextView in SwiftUI

2020-04-10

With iOS 14 Apple introduced a native way to work with multiline editable text. You can find the documentation here. If you need to work with Code that is pre iOS 14 the following approach can still be used.

Before starting this post I hope that everyone who is reading this article is healthy and stays it !

It has been quite a while since my last blog post. End of the last year there was quite a lot of work to do in my company and now with the recent pandemic I couldn't find enough time to neither work on some of my side projects nor to write some posts on my blog.

Luckily I found some free time in the last weeks to get a first look on SwiftUI. In this post I want to highlight how we can use UIKit components, that haven't been ported to yet, in SwiftUI. I want to show that creating an View that wraps a UITextView component. Maybe this will be become obsolete when Apple announces new additions to SwiftUI at WWDC 2020. Until then I hope this small article provides some nice insights for fellow SwiftUI developers.

Currently I am working on a small side project which is completely written in SwiftUI. For this I needed a multiline scrollable text input component. Since the only component which is natively available in SwiftUI, TextField, is only supporting one line of text and is not scrollable, it was necessary to somehow use UITextView within SwiftUI.

Fortunately this is rather easy to accomplish by using the UIViewRepresentable protocol. This protocol helps us to use UIKit components in SwiftUI.

Features

Before we dive into the code, I first want to highlight what my TextView component should do:

  • Multiline input
  • Scrollable
  • Close keyboard by pressing a done button in a UIToolbar on top of the keyboard

Implementation

At first we create a new struct which conforms to UIViewRepresentable. This defines the view which are going to use within other SwiftUI views.

struct TextView: UIViewRepresentable {
    @Binding var text: String
 
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        return textView
    }
 
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }
}

To conform to the protocol we need to implement two methods: makeUIView and updateUIView. The first one creates an UIView instance that needs to be displayed. The latter simply updates the displayed UIView instance. In addition to the two methods we have added a @Binding var text: String which represents the text that is entered into the text view. This would already be enough to use our TextView in any SwiftUI of our choice but it wouldn't do much. We are missing two main features:

  • The binding is not updated when the text inside the UITextView instance changes
  • The UIToolbar to close the keyboard when the user is done is still missing

For the first point we need to implement a so called Coordinator class. You can think of the coordinator as something like a delegate to the UIView instance.

struct TextView: UIViewRepresentable {
    @Binding var text: String
 
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
 
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        return textView
    }
 
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }
 
    class Coordinator: NSObject, UITextViewDelegate {
 
        var parent: TextView
 
        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }
 
        func textViewDidChange(_ textView: UITextView) {
            // This populates the updated text value back to the SwiftUI world through the binding
            self.parent.text = textView.text
        }
    }
}

With the help of the coordinator class we can react to the textViewDidChange event and change the binding of the TextView accordingly. You can become a lot more creative here and implement a lot more delegate methods if necessary.

The last thing which is missing is the UIToolbar implementation. For that we are creating a small extension on UITextView and using it inside the makeUIView function of TextView.

extension UITextView {
 
    func addDoneButton(title: String, target: Any, selector: Selector) {
 
        let toolBar = UIToolbar(frame: CGRect(x: 0.0,
                                              y: 0.0,
                                              width: UIScreen.main.bounds.size.width,
                                              height: 44.0))
        let flexible = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let barButton = UIBarButtonItem(title: title, style: .plain, target: target, action: selector)
        toolBar.setItems([flexible, barButton], animated: false)
        self.inputAccessoryView = toolBar
    }
 
    @objc func doneButtonTapped(button: UIBarButtonItem) {
        self.resignFirstResponder()
    }
}
 
struct TextView: UIViewRepresentable {
    ...
     func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        textView.addDoneButton(title: "Done", target: textView, selector: #selector(textView.doneButtonTapped(button:)))
        return textView
    }
    ...
}
 

Use it

Now we can finally use an UITextView right in SwiftUI without any further changes. The only thing which we need to care of, is that the TextView needs a dedicated height, but this can be easily set with the .frame() modifier. In the snippet below we are setting the height to be fixed at 200 pts and the width to fill all the available space.

struct ContentView: View {
    @State var text: String = ""
 
    var body: some View {
        TextView(text: self.$text)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: 200)
    }
 
}
 

Conclusion

We've seen, that with the help of UIViewRepresentable we can bridge the gap between missing UIKit components and SwiftUI, until all components have been natively ported by Apple itself, rather easily.

I hope that this post helps to give you a glimpse about what you can do to enhance SwiftUI with missing components from UIKit.

Hope to see you soon for the next article and stay healthy in this rough times!