-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
189: UIKit and SwiftUI Interop with UIViewRepresentable
So, you've been using SwiftUI for a while now, and it's great—until it isn't. There are moments where you'll realize a specific UIKit component just does something better, or maybe there's a legacy view you can't afford to rewrite. For me, this usually happens with UITextView. While SwiftUI has TextEditor, it's often too simplistic when you need fine-grained control over the scroll behavior or specific delegate callbacks.
Trying to drop a UIKit view into SwiftUI
I wanted to build a simple text input that tells me exactly when the user changes the text, but I want the precision of the UIKit delegate. My first instinct was to just try and drop a UITextView into my VStack. Of course, that doesn't work. The compiler screams at me because UITextView is a subclass of UIView, and SwiftUI views have to conform to the View protocol. They speak entirely different languages: one is imperative (do this, then that), and the other is declarative (it should look like this).
To bridge this, Apple gave us UIViewRepresentable. Let's see if we can get a basic text view on the screen.
struct MyTextView: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = .systemFont(ofSize: 18)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// This is where we update the view when SwiftUI state changes
}
}
I'll put this in my ContentView, and it works! I see the text box. But here's the problem: it's a black hole. I can type into it, but my SwiftUI state doesn't know anything is happening. It's an isolated island of UIKit inside my SwiftUI app.
Bridging the gap with the protocol
To make this useful, I need a binding. I want the text in the UITextView to sync with a @State variable in my parent view. I'll add a binding to the struct first.
struct MyTextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.text = text
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
Now, if I change the text variable from a button in SwiftUI, the UITextView updates. Great. But the reverse still isn't happening. Typing in the box doesn't update the binding. Why? Because UITextView communicates changes through a delegate (UITextViewDelegate), and SwiftUI has no idea how to "listen" to a delegate automatically.
The missing link: Getting data back
This is where the Coordinator comes in. Think of the Coordinator as the "translator." It's a class that lives alongside your representable view and handles the UIKit-style callbacks, then pushes those changes back into the SwiftUI state.
I'll add a makeCoordinator method. I'll make the Coordinator the delegate of the text view, and inside the textViewDidChange method, I'll update the binding.
struct MyTextView: UIViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator // Tell UIKit to talk to the coordinator
textView.text = text
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MyTextView
init(_ parent: MyTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text ?? ""
}
}
}
Taming the update loop
I ran this, and I immediately hit a classic bug: the cursor jumps to the end of the text every time I type a character. It's maddening.
Here is what's happening: I type a letter → textViewDidChange fires → the Coordinator updates the @Binding → SwiftUI detects a state change and triggers updateUIView → updateUIView sets uiView.text = text. Setting the text property on a UITextView resets the cursor position.
I only want to update the UIKit view if the text is actually different from what's already there. A simple if check in updateUIView fixes this entirely.
func updateUIView(_ uiView: UITextView, context: Context) {
if uiView.text != text {
uiView.text = text
}
}
Now it's smooth. I've successfully wrapped a UIKit view, handled its delegate callbacks via a Coordinator, and prevented a feedback loop. This pattern is exactly how you'll handle almost every UIKit-to-SwiftUI integration, whether it's a MKMapView, a WKWebView, or a complex UISearchBar.
📋 Practical Task
Building a Real-Time Character Counter with UITextView
Your goal is to create a SwiftUI view that uses UIViewRepresentable to wrap a UITextView. The view must implement the following requirements:
- Create a
UIViewRepresentablestruct calledCharacterCountTextViewthat takes aBinding<String>. - Implement a
Coordinatorthat conforms toUITextViewDelegateto update the binding whenever the text changes. - Ensure that
updateUIViewdoes not cause the cursor to jump by checking if the text is already equal to the binding value. - In your main
ContentView, display thisCharacterCountTextViewand aTextview below it that displays the current character count (e.g., "Characters: 42"). - Style the
UITextViewinsidemakeUIViewto have a gray border and a small amount of padding so it's clearly visible on the screen.
There are no comments for now.