Другое

Руководство по рендерингу внешних дисплеев iOS

Узнайте, как правильно настроить рендеринг внешних дисплеев iOS для соответствия нативному разрешению без черных полос. Полное руководство с примерами кода для настройки iPhone и внешнего экрана.

Как правильно настроить рендеринг внешнего дисплея iOS для соответствия разрешению внешнего дисплея как на iPhone, так и на внешнем экране?

Я столкнулся с проблемой, когда мой ExternalDesktopView рендерится с разрешением iPhone как на самом iPhone, так и на внешнем дисплее, создавая черные промежутки, похожие на зеркальное отображение iPhone. Функция setupExternalWindow в моем ExternalDisplayManager.swift, по-видимому, отвечает за это поведение.

Вот моя текущая реализация:

swift
private func setupExternalWindow(for screen: UIScreen) {
    removeExternalWindow()

    guard let mode = screen.currentMode else {
        print("⚠️ No screen mode found for external display.")
        return
    }

    let nativeSize = mode.size
    let scale = screen.nativeScale
    let logicalSize = CGSize(width: nativeSize.width / scale,
                             height: nativeSize.height / scale)

    print("""
    🖥️ External Display Info:
    - Native pixel size: \(nativeSize)
    - Logical size: \(logicalSize)
    - Scale: \(scale)
    """)

    
    let window = UIWindow(frame: screen.coordinateSpace.bounds)
    window.screen = screen
    window.backgroundColor = .black
    window.isHidden = true
    window.contentScaleFactor = scale

    
    let desktop = DesktopModelHolder.shared.desktopModel
    let cursor = CursorManager.shared

    let externalRoot = ExternalDesktopView()
        .environmentObject(desktop)
        .environmentObject(cursor)
        .ignoresSafeArea()
        .frame(width: logicalSize.width, height: logicalSize.height)
        .clipped()

    let host = UIHostingController(rootView: AnyView(externalRoot))
    host.view.backgroundColor = .black
    host.view.frame = window.bounds
    host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    host.view.contentScaleFactor = scale
    host.view.layer.contentsScale = scale
    host.view.transform = .identity

    
    host.additionalSafeAreaInsets = .zero
    host.view.layer.masksToBounds = true
    host.modalPresentationStyle = .fullScreen

    window.rootViewController = host

    
    DispatchQueue.main.async {
        self.externalWindow = window
        self.externalHostingController = host
        window.isHidden = false
        window.makeKeyAndVisible()
        host.view.layoutIfNeeded()
        CATransaction.flush()
        self.isExternalConnected = true

        print("External display is now rendered in TRUE FULLSCREEN.")
    }

    
    updateExternalPointer(from: cursor.mainPointerLocation)
}

Как можно изменить этот код, чтобы внешний дисплей рендерился с его родным разрешением без черных промежутков?

Чтобы правильно настроить рендеринг внешнего дисплея iOS и устранить черные промежутки при совпадении с родным разрешением внешнего дисплея, необходимо решить несколько ключевых аспектов в вашей функции setupExternalWindow. На основе результатов исследования вот как следует изменить ваш код:

Основные проблемы в текущей реализации связаны с обработкой пространства координат, компенсацией оверскана и размером представления. Вот исправленный подход:

swift
private func setupExternalWindow(for screen: UIScreen) {
    removeExternalWindow()
    
    // Устанавливаем компенсацию оверскана для удаления черных границ
    screen.overscanCompensation = .none
    
    guard let mode = screen.currentMode else {
        print("⚠️ Не найден режим экрана для внешнего дисплея.")
        return
    }
    
    let nativeSize = mode.size
    let scale = screen.nativeScale
    
    // Используем родные границы для точных размеров в пикселях
    let nativeBounds = screen.nativeBounds
    let logicalSize = CGSize(width: nativeSize.width / scale,
                            height: nativeSize.height / scale)
    
    print("""
    🖥️ Информация о внешнем дисплее:
    - Родной размер в пикселях: \(nativeSize)
    - Родные границы: \(nativeBounds)
    - Логический размер: \(logicalSize)
    - Масштаб: \(scale)
    """)
    
    // Используем родные границы для фрейма окна для обеспечения правильного выравнивания пикселей
    let window = UIWindow(frame: CGRect(x: 0, y: 0, width: nativeBounds.width, height: nativeBounds.height))
    window.screen = screen
    window.backgroundColor = .black
    window.isHidden = true
    window.contentScaleFactor = scale
    
    let desktop = DesktopModelHolder.shared.desktopModel
    let cursor = CursorManager.shared
    
    // Размер представления SwiftUI на основе родных размеров в пикселях
    let externalRoot = ExternalDesktopView()
        .environmentObject(desktop)
        .environmentObject(cursor)
        .ignoresSafeArea()
        .frame(width: nativeSize.width, height: nativeSize.height)
        .clipped()
    
    let host = UIHostingController(rootView: AnyView(externalRoot))
    host.view.backgroundColor = .black
    
    // Устанавливаем фрейм в соответствии с родными размерами в пикселях
    host.view.frame = CGRect(x: 0, y: 0, width: nativeSize.width, height: nativeSize.height)
    host.view.autoresizingMask = []
    host.view.contentScaleFactor = scale
    host.view.layer.contentsScale = scale
    host.view.transform = .identity
    
    host.additionalSafeAreaInsets = .zero
    host.view.layer.masksToBounds = true
    host.modalPresentationStyle = .fullScreen
    
    window.rootViewController = host
    
    DispatchQueue.main.async {
        self.externalWindow = window
        self.externalHostingController = host
        window.isHidden = false
        window.makeKeyAndVisible()
        
        // Принудительно выполняем компоновку и обеспечиваем правильный рендеринг
        host.view.layoutIfNeeded()
        CATransaction.flush()
        self.isExternalConnected = true
        
        print("Внешний дисплей теперь отображается в РОДНОМ РАЗРЕШЕНИИ без черных промежутков.")
    }
    
    updateExternalPointer(from: cursor.mainPointerLocation)
}

Объяснение ключевых изменений

1. Компенсация оверскана

swift
screen.overscanCompensation = .none

Это критически важно для устранения черных границ. Согласно документации Apple, компенсация оверскана может вызывать черные границы на внешних дисплеях, а установка ее в значение .none гарантирует, что ваш контент заполнит весь экран источник.

2. Использование родных границ

swift
let nativeBounds = screen.nativeBounds
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: nativeBounds.width, height: nativeBounds.height))

Использование UIScreen.nativeBounds вместо screen.coordinateSpace.bounds предоставляет фактические размеры дисплея в пикселях, обеспечивая точное выравнивание без промежутков источник.

3. Размер представления в родных пикселях

swift
.frame(width: nativeSize.width, height: nativeSize.height)
host.view.frame = CGRect(x: 0, y: 0, width: nativeSize.width, height: nativeSize.height)

Ваше представление SwiftUI и его хост-контроллер должны иметь размер на основе родных размеров в пикселях (nativeSize), а не логических координат, для обеспечения пиксельно-точного рендеринга на внешнем дисплее.

4. Правильная настройка масштабирования

swift
window.contentScaleFactor = scale
host.view.contentScaleFactor = scale
host.view.layer.contentsScale = scale

Последовательная установка коэффициента масштабирования во всех слоях гарантирует, что ваш контент будет рендериться с правильным разрешением для родного масштаба внешнего дисплея источник.

Дополнительные рекомендации

1. Обработка изменений режима экрана

swift
NotificationCenter.default.addObserver(
    forName: UIScreen.modeDidChangeNotification,
    object: screen,
    queue: .main
) { [weak self] _ in
    self?.setupExternalWindow(for: screen)
}

Это гарантирует, что ваш внешний дисплей будет правильно переconfigure’ся при изменении режима экрана.

2. Использование Display Link для синхронизации

swift
let displayLink = screen.displayLink(withTarget: self, selector: #selector(updateExternalDisplay))
displayLink.add(to: .main, forMode: .common)

Это помогает поддерживать правильную синхронизацию кадров между вашим приложением и внешним дисплеем источник.

3. Особенности пространства координат

Для представлений SwiftUI в частности, рассмотрите возможность использования:

swift
.coordinateSpace(name: "externalDisplay")

в иерархии представлений для правильной обработки преобразований координат между iPhone и внешним дисплеем.

Советы по устранению неполадок

Если проблемы все еще возникают:

  1. Проверьте дисплеи Retina: Убедитесь, что ваше приложение правильно обрабатывает высокоразрешенные внешние дисплеи, проверяя значение UIScreen.nativeScale.

  2. Тестируйте с разными внешними дисплеями: Некоторые дисплеи могут иметь уникальные конфигурации EDID, требующие специальной обработки.

  3. Мониторьте вывод в консоли: Отладочные операторы печати в измененном коде помогут вам проверить, что родные размеры и масштабирование рассчитываются правильно.

  4. Рассмотрите Metal/OpenGL: Если вы используете Metal или OpenGL, убедитесь, что ваши буферы рендеринга точно соответствуют разрешениям пикселей внешнего дисплея источник.


Содержание


Понимание проблем с разрешением внешнего дисплея

Черные промежутки, которые вы наблюдаете, возникают потому, что iOS по умолчанию применяет компенсацию оверскана к внешним дисплеям, аналогично тому, как работал TV-out на более ранних устройствах. Это создает безопасные зоны вокруг вашего контента, чтобы он не обрезался на старых дисплеях, но приводит к черным границам на современных дисплеях, которые могут отображать полный контент.

Согласно исследованиям, Apple использует эту компенсацию оверскана по умолчанию для HDMI-соединений, что объясняет, почему вы видите черные границы вокруг того, что должно быть полноэкранным внешним дисплеем источник.

Ключевые свойства дисплеев iOS

Для правильной настройки рендеринга внешнего дисплея вам необходимо понять эти важные свойства UIScreen:

  • UIScreen.nativeScale: Коэффициент масштабирования между точками и пикселями для экрана
  • UIScreen.nativeBounds: Границы экрана в пикселях
  • UIScreen.currentMode: Текущий режим разрешения дисплея
  • UIScreen.overscanCompensation: Свойство для настройки компенсации оверскана

Коэффициент родного масштабирования особенно важен, так как он указывает, сколько пикселей соответствует каждой точке в системе координат. Для внешних дисплеев это значение должно соответствовать физической плотности пикселей дисплея.

Правильная настройка окна

Настройка окна критически важна для рендеринга внешнего дисплея. Вот что нужно учитывать:

  1. Размеры фрейма: Используйте размеры в родных пикселях для фрейма окна
  2. Назначение экрана: Всегда устанавливайте свойство экрана окна на внешний UIScreen
  3. Коэффициент масштабирования: Применяйте родной коэффициент масштабирования ко всем соответствующим свойствам
  4. Пространство координат: Учитывайте преобразования пространства координат

Исследования показывают, что создание отдельных UIWindow объектов для внутренних и внешних дисплеев является рекомендуемым подходом источник.

Настройка представлений Swift для внешних дисплеев

При использовании SwiftUI с внешними дисплеями вам необходимо:

  1. Задавать размеры представлений в родных пикселях: Используйте размеры в родных пикселях, а не логические координаты
  2. Обрабатывать безопасные зоны: Правильно управляйте вставками безопасной зоны для внешних дисплеев
  3. Применять коэффициент масштабирования: Обеспечьте последовательное масштабирование во всей иерархии представлений
  4. Пространства координат: Используйте соответствующие преобразования пространства координат

Исследования показывают, что GeometryReader может быть полезен для определения размеров экрана, но для внешних дисплеев часто требуется более прямой контроль над размерами в пикселях источник.

Устранение распространенных проблем

Если вы продолжаете сталкиваться с проблемами:

  1. Черные промежутки: Проверьте настройки компенсации оверскана
  2. Неправильное масштабирование: Убедитесь, что значения nativeScale последовательно применяются
  3. Проблемы с компоновкой: Убедитесь, что фреймы представлений соответствуют размерам в родных пикселях
  4. Проблемы с производительностью: Рассмотрите использование display link для синхронизации

Исследования показывают, что внешние дисплеи могут поддерживать несколько разрешений, и iOS обычно предпочитает наивысшее поддерживаемое разрешение по умолчанию источник.

Дополнительные параметры настройки

Для более сложных настроек:

  1. Поддержка нескольких разрешений: Разрешите пользователям выбирать из доступных UIScreenMode
  2. Динамическое переключение режимов: Реагируйте на изменения разрешения с помощью наблюдателей уведомлений
  3. Пользовательское масштабирование: Реализуйте пользовательские алгоритмы масштабирования для определенных типов дисплеев
  4. Интеграция Metal/OpenGL: Используйте рендеринг на уровне пикселей для графически интенсивных приложений

Для приложений Metal исследования показывают, что MTKView автоматически поддерживает родной масштаб экрана, что может упростить реализацию источник.


Заключение

Чтобы правильно настроить рендеринг внешнего дисплея iOS без черных промежутков:

  1. Установите overscanCompensation в .none для устранения черных границ
  2. Используйте nativeBounds и nativeScale для точного рендеринга на основе пикселей
  3. Задавайте размеры представлений и окон в родных пикселях, а не в логических координатах
  4. Применяйте последовательное масштабирование ко всем слоям представления и свойствам
  5. Обрабатывайте изменения режима экрана с помощью соответствующих наблюдателей уведомлений

Ключевое понимание заключается в том, что внешние дисплеи требуют пиксельно-точной обработки координат, что отличается от точечной системы координат, используемой на экранах iPhone. Фокусируясь на размерах в родных пикселях и правильном масштабировании, вы можете добиться полноэкранного рендеринга внешнего дисплея без каких-либо черных промежутков.

Для дальнейшего изучения рассмотрите исследования техник рендеринга Metal и OpenGL для внешних дисплеев, так как они обеспечивают еще более точный контроль на уровне пикселей.


Источники

  1. Apple Developer - Отображение контента на внешнем дисплее
  2. Apple Developer - Технический вопрос-ответ QA1909: Поддержка родного масштаба экрана
  3. Stack Overflow - Как заставить iOS отображать экран с родным разрешением внешнего дисплея
  4. Ole Begemann - iPhone 6 Plus Pixel Peeping
  5. Форумы MacRumors - Учебник по черным границам TV Out
  6. InformIT - Использование внешних экранов
  7. Форумы разработчиков Apple - Поддержка внешнего дисплея в приложении iOS
  8. Stack Overflow - Принудительная настройка разрешения внешнего экрана iPhone
Авторы
Проверено модерацией
Модерация