
С выходом iOS 26 Apple наконец-то обновила UI. Интерфейсы стали «стеклянными»: физически корректное преломление, жирная хроматическая аберрация, глубокий объем. Мне очень нравится как это выглядит, хотя в использовании, довольно часто, есть вопросы с удобством.
Но есть проблема: этот API (как и все другие новые от Apple) доступен только в новейшей системе. Если ваше приложение должно поддерживать iOS 18, 17 или (не дай бог) 14, то там всё по-старому: UIVisualEffectView, который блюрит фон.
Звучит как вызов и я захотел написать свой Liquid Glass на Metal + SwiftUI для iOS 16, который визуально будет 1-в-1 как нативный в iOS 26. Казалось бы, задача простая: пиши/найди/вайбкодь шейдер, и готово.
Спойлер: шейдеры оказались самой легкой частью (спасибо готовым решениям и LLM). А вот попытка заставить это работать в 120 фпс, без системных привилегий, уперлась в знаменитую «безопасность» системы iOS. О том, как я боролся с закрытым API, рекурсией и почему Apple может делать это быстро, а мы - нет, читайте под катом.
Часть 1. Шейдеры: Франкенштейн и LLM
Сразу признаюсь: я не силён в графике и не пишу шейдеры за кофе. Но для этой задачи это и не пригодилось.
Визуальную часть я собрал как конструктор. Нашел примеры на Shadertoy, вносил изменения и просил LLM обяснить что присходит в шейдерах и зачем.
В итоге мы получили рецепт того самого «жидкого» стекла:
Blur: Использую
MPSImageGaussianBlurдля размытия на GPU. Это дает то самое «маслянистое», густое размытие без пикселей (в идеале нужно делать blur на шейдере, но этот компромисс для меня проще).SDF Refraction: Шейдер вычисляет signed distance field для скругленных углов и использует его градиент для имитации преломления света. Края объектов под стеклом красиво искажаются, давая эффект настоящего стекла.
Псевдослучайный дизеринг: Добавляю процедурный шум для устранения бандинга на градиентах.
Вот примерный фрагмент логики (Metal), отвечающий за тот самый эффект дисперсии:
// Вычисляем SDF для скругленных углов
float sdf = boxSDF(centered, halfBoxSize - uniforms.cornerRadius);
// Получаем градиент для эффекта рефракции
float2 gradient = getGradient(centered, halfBoxSize, uniforms.cornerRadius);
float2 refractOffset = computeRefractOffset(gradient, sdf);
// Читаем размытую текстуру фона со смещением
half4 blurred = getBlurredColor(backgroundTexture, s, uv + refractOffset, blurLevel);
// Добавляем дизеринг для устранения бандинга
float2 noise = randomVec2(in.position.xy);
blurred.rgb += (noise.x - 0.5) * 0.02;С самим шейдером проблем не было вообще. GPU iPhone (даже старых) перемалывает эту математику не напрягаясь. Проблемы начались там, где я их не ждал (кого я обманываю) - на этапе подачи данных в этот шейдер.
Часть 2. Запретный плод: CABackdropLayer
Чтобы стекло работало не жидко, шейдеру нужно знать, что находится под ним.
Как это делает Apple в iOS 26? У них есть CABackdropLayer. По сути, это прямой доступ к буферу рендера композитора. Система просто говорит: «Вот тебе кусок уже отрисованного экрана, делай жидкое стекло». Это происходит на уровне железа, мгновенно и без затрат CPU.
Буфер рендера композитора - это финальная картинка всех слоёв UI, которую система собирает перед выводом на экран.
Композитор (Window Server в iOS/macOS) - это системный процесс, который смешивает все эти слои в один кадр. У него уже есть готовая картинка слоёв в памяти GPU.
Можем ли мы использовать это?
Технически - конечно. По факту - nope. Достать приватный класс через Runtime - дело пяти минут. Я пробовал, это работает идеально: 120 FPS, полное соответствие нативному поведению новой iOS.
В чем подвох?
App Store Review. Использование приватных API - это гарантированный реджект в 99% случаев. Если вы делаете приложение in-house, то берите приватный API и кайфуйте. Но для App Store придется идти «легальным» путем.
Часть 3. «Легальный» путь и смерть производительности

Раз нам нельзя читать буфер экрана напрямую (как это делает система), остается один вариант: делать скриншоты области под стеклом.
И вот тут начинается боль, нет даже больше - боооль. Схема работы:
CPU: Принудительно рендерим иерархию View под стеклом в картинку через
layer.render(in:)GPU: Применяем Gaussian blur через Metal Performance Shaders
GPU: Шейдер накладывает рефракцию и финальную красоту
Узкое место здесь - пункт №1. Шейдер + MPS blur отрабатывают очень быстро 0.5-2ms. А вот создание скриншота - это тяжелая операция. В iOS (даже старых версий) просто нет публичного API, чтобы сказать «дай мне текстуру того, что сейчас на экране» без оверхеда.
Попытка №1: drawHierarchy(afterScreenUpdates: true)
Самый честный метод. Захватывает всё: и SwiftUI тонкости, и UIKit.
Результат: Лаги. Метод синхронный. Пока он думает, интерфейс стоит. FPS падает до 15-20 при активном скролле.
Попытка №2: layer.render(in: context)
Работает быстрее и именно это используется в финальной версии.
Результат: Это суррогат. Он не захватывает некоторые системные эффекты и сложную анимацию (например не захватывает SwiftUI blur 😶🌫️). Но это лучшее, что есть из публичных API.
Получается парадокс: 90% времени кадра уходит не на красивый эффект (ради чего всё затевалось), а на бюрократию - попытку легально получить пиксели экрана.
Часть 4. Рекурсия (Bonus Track)
Еще один забавный момент - эффект «зеркального коридора».
Когда я делаю снимок экрана, в кадр попадает само мое стекло. На следующем кадре шейдер применяет эффект к снимку со стеклом. Получается бесконечная рекурсия, превращающая UI в кашу.
Пришлось писать костыли:
func captureSnapshot(for glassView: UIView) -> CGImage? {
// ... подготовка
let originalAlpha = glassView.layer.opacity
// Пробовал isHidden = true, вместо opacity = 0.0 - по тестам получилось медленнее
glassView.layer.opacity = 0.0 // Спрячься
targetView.layer.render(in: ctx.cgContext) // Сфоткайся
glassView.layer.opacity = originalAlpha // Покажись
return image.cgImage
}Это работает, но добавляет еще больше накладных расходов и риск мерцания, если не синхронизировать с CADisplayLink идеально (у меня получилось не идеально 🫡).
Берите, пользуйтесь (Open Source)
Несмотря на все ограничения системы, результат того стоит - визуально эффект не отличить от нативного. Я оформил все наработки в готовую библиотеку LiquidGlass. Вам не придется самим возиться с Metal-шейдерами, настройкой Mipmaps и борьбой с рекурсией.
Подключайте, смотрите код, кидайте пулл-реквесты, если найдете способ оптимизировать захват экрана:
https://github.com/BarredEwe/LiquidGlass
Итог
Я добился того, чего хотел (но какой ценой?): визуально на iOS 14 эффект выглядит один в один как на iOS 26. Шейдеры + LLM творят чудеса.
Но работающего в любых случаях решения для динамического контента на публичных API у меня не получилось из-за закрытости системы.
Apple сделал амазинг в новой iOS, но не дала нам инструментов, чтобы воспроизвести это качество на старых версиях легальными методами. Мы вынуждены гонять данные по кругу GPU -> CPU -> GPU, когда они уже лежат в памяти видеокарты.
P.S. Еще больше графики и AI
Если вам интересны подобные эксперименты на стыке визуала и кода, заглядывайте ко мне в Telegram-канал Prefire_iOS
Я там часто публикую то, что не тянет на полноценную статью, но выглядит залипательно. Например:
•❄️ Динамические снежинки на экране - еще один проект с шейдерами, который превращает рабочий стол в зимнюю сказку.
•🐱 Тамагочи с мозгами LLM - котик, который живет у вас на экране, но вместо скриптов им управляет локальная нейронка (Private LLM). С ним можно болтать, и он действительно понимает контекст.
https://t.me/prefire_ios - подписывайтесь, там уютно и технологично.
Если кто-то знает легальный способ получить доступ к Backdrop-буферу без слайд-шоу на старых iOS - пишите в комменты, буду рад!