SwiftUIMiddleTechnical

Как интегрировать 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, включая производительность, память и сопровождение.

Sources

Related topics