SwiftUI is great for creating views for your iPhone application, but sometimes you need UIKit to fill some gaps. UIViewRepresentable provides a way to bridge the gap between UIKit and SwiftUI by wrapping a UIKit view and allowing you to use that view in SwiftUI.
I recently wrote about bringing Writing Tools to SwiftUI, but as mentioned in that tutorial, SwiftUI is limited and only brings a small part of the available features. The only feature available to a TextEditor in regards to writing tools is .writingToolsBehavior.
In this tutorial, we’ll look at how to bring a UIKit view into SwiftUI.
To accomplish this we’re going to need to use UIViewRepresentable so that we can bring in the UITextView.
Lets start with the most basic implementation that brings a UITextView into a SwiftUI view:
import SwiftUI
struct AITextView: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Enter text...")
AITextView()
}
}
}
We declare a struct called AITextView that conforms to UIViewRepresentable. This protocol has two required methods, one called makeUIView and the other called updateUIView.
We can return a UITextView in makeUIView.
In our ContentView, we can use this new UIViewRepresentable view as we would any other view (see line 18).
As it stands, this will function. You can write text in the AITextView and use the Apple Intelligence writing tools although it doesn’t communicate with the SwiftUI view.
import SwiftUI
struct AITextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 18)
textView.text = text
textView.delegate = context.coordinator
if #available(iOS 18.0, *) {
textView.writingToolsBehavior = .complete
}
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if #available(iOS 18.0, *) {
uiView.writingToolsBehavior = .complete
}
uiView.text = text
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: AITextView
init(_ parent: AITextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
struct ContentView: View {
@State private var text = "Type something here..."
var body: some View {
VStack {
Text("Enter text:")
.font(.headline)
AITextView(text: $text)
.frame(height: 200)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray, lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
Text("You wrote:")
.font(.subheadline)
.padding(.top)
Text(text)
.padding()
.frame(maxWidth: .infinity)
.background(Color(.systemGray6))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.padding()
}
}
#Preview {
ContentView()
}
In this next example, we’ve added more to it so that you can see communication happen between the SwiftUI view and UIKit view.
On line 4 we add a variable called text that uses the @Binding property wrapper. The source of truth for this is in the ContentView on line 45. Anything we enter on line 45 will show in the UITextView.
In makeUIView starting line 6 we have adjusted the text by changing the font and we set the text of the view to be the text variable. We also set the delegate here, but more on that part to come.
Line 13 we set the writingToolsBehaviour to complete if we’re on iOS 18+.
In the first example we returned just a UITextView, but in this version we create the text view, adjust it as needed, and then return the modified view.
In the updateUIView method on line 19 onwards, we also set the writingToolsBehaviour, and then we set the uiView.text to text.
Line 27 we make a coordinator. A coordinator is used to act as delegate and returns the Coordinator class which is declared on line 31 onwards.
The Coordinator class in this case conforms to NSObject and also UITextViewDelegate which means any delegate methods available in those classes can be implemented here.
Each time something happens in the editor, textViewDidChange will be called and we can get the text of the textView and set the parent.Text as needed. The parents text, the @Binding, is then updated and the SwiftUI view is also updated. Line 10 is where we set the delegate to the coordinator.
Line 34 is fairly boilerplate in this example.
On the ContentView side, text is updated and the view refreshes.
Doing it this way establishes a two-way binding.
Lets now take a look at adding some Writing Tools delegates. Given that they are part of the UITextView, we can simply add them into the Coordinator:
class Coordinator: NSObject, UITextViewDelegate {
var parent: AITextView
init(_ parent: AITextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewWritingToolsWillBegin(_ textView: UITextView) {
print("stop syncing")
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
print("start syncing")
}
}
You can addjust the Coordinator class to include two new delegates:
textViewWritingToolsWillBegin and textViewWritingToolsDidEnd.
In the case of writing tools, you may be dealing with text that needs to be saved somewhere. These two delegate methods could be used to inform your storage solution to temporarily not save until the writing tools have complted what they are doing.
If you use the writing tools in the app now, you’ll see the stop and start syncing messages show in the console.
As always, please reachout in the comments.
Leave a Reply
You must be logged in to post a comment.