Как интегрировать UIKit-компоненты в SwiftUI с помощью UIViewRepresentable?
UIViewRepresentable оборачивает UIKit-вид в SwiftUI через makeUIView (создание), updateUIView (синхронизация состояния) и опциональный Coordinator для делегатов. Используется для WKWebView, MKMapView и компонентов без SwiftUI-эквивалента.
UIViewRepresentable: интеграция UIKit в SwiftUI
Протокол UIViewRepresentable позволяет обернуть любой UIKit-вид (UIView) и использовать его в SwiftUI-иерархии. Это необходимо для компонентов, у которых нет SwiftUI-эквивалента: WKWebView, MKMapView, UITextView с расширенным функционалом, кастомные UIView.
Структура протокола
Протокол требует реализации двух обязательных методов и одного опционального:
makeUIView(context:)— создаёт UIView один раз при первом появлении.updateUIView(_:context:)— вызывается при каждом изменении SwiftUI-состояния, синхронизирует данные.makeCoordinator()— создаёт объект-посредник для делегатов и обратных вызовов (опционально).
Пример 1: WKWebView
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
@Binding var isLoading: Bool
func makeCoordinator() -> Coordinator {
Coordinator(isLoading: $isLoading)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// Загружаем только если URL изменился
if uiView.url != url {
uiView.load(URLRequest(url: url))
}
}
final class Coordinator: NSObject, WKNavigationDelegate {
@Binding var isLoading: Bool
init(isLoading: Binding<Bool>) {
_isLoading = isLoading
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isLoading = false
}
}
}
Пример 2: UITextField с фокусом
struct FocusableTextField: UIViewRepresentable {
@Binding var text: String
var placeholder: String
var isFocused: Bool
func makeUIView(context: Context) -> UITextField {
let field = UITextField()
field.placeholder = placeholder
field.addTarget(
context.coordinator,
action: #selector(Coordinator.textChanged),
for: .editingChanged
)
return field
}
func updateUIView(_ uiView: UITextField, context: Context) {
if uiView.text != text {
uiView.text = text
}
// Управляем фокусом из SwiftUI
if isFocused && !uiView.isFirstResponder {
uiView.becomeFirstResponder()
} else if !isFocused && uiView.isFirstResponder {
uiView.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(binding: $text)
}
final class Coordinator: NSObject {
var binding: Binding<String>
init(binding: Binding<String>) {
self.binding = binding
}
@objc func textChanged(_ sender: UITextField) {
binding.wrappedValue = sender.text ?? ""
}
}
}
Пример использования
struct ArticleView: View {
@State private var isLoading = false
var body: some View {
ZStack {
WebView(
url: URL(string: "https://talanto.work")!,
isLoading: $isLoading
)
if isLoading {
ProgressView()
}
}
}
}
Размер и Layout
SwiftUI передаёт предложенный размер через sizeThatFits. Если UIView имеет собственный intrinsicContentSize, SwiftUI учитывает его. Для принудительного размера используйте .frame() в SwiftUI или реализуйте sizeThatFits(_:uiView:context:):
func sizeThatFits(_ proposal: ProposedViewSize, uiView: WKWebView, context: Context) -> CGSize? {
CGSize(width: proposal.width ?? 300, height: 400)
}
Подводные камни
updateUIViewвызывается при каждом ре-рендере SwiftUI — без проверкиif uiView.text != textв UITextView сбрасывается курсор и состояние выделения.- Вызов
becomeFirstResponder()внутриmakeUIViewне работает — клавиатура не появится, потому что вид ещё не в иерархии. Делайте это вupdateUIView. - Не сохраняйте ссылку на родительскую структуру
UIViewRepresentableв Coordinator — структура пересоздаётся при каждом ре-рендере, старая ссылка устаревает. - Если UIView изменяет свой размер (например,
UITextViewпри наборе текста), SwiftUI не автоматически подстраивается безinvalidateIntrinsicContentSize(). - Автолайаут-констрейнты, добавленные к UIView внутри
makeUIView, могут конфликтовать с SwiftUI-layout — предпочитайтеtranslatesAutoresizingMaskIntoConstraints = true. - Тестирование UIViewRepresentable напрямую в unit-тестах затруднено — оборачивайте в UIHostingController или тестируйте через XCUITest.
- Утечки памяти возникают, если Coordinator держит сильную ссылку на UIView или родительский контроллер — используйте
weakтам, где это возможно.
Common mistakes
- Сводить «интегрировать UIKit-компоненты в SwiftUI с помощью
UIViewRepresentable» к синтаксису и не объяснять идентичность view. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-25.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «интегрировать UIKit-компоненты в SwiftUI с помощью
UIViewRepresentable» и подтверждает ее корректным примером. - Умеет связать ответ с инвалидация body, тестированием и отладкой на устройстве.
- Называет ограничения подхода swiftui-25, включая производительность, память и сопровождение.