Lazy Var
443 subscribers
2 videos
2 links
iOS-разработка, эксперименты и нестандартные решения.

Автор: @walkbeat
Download Telegram
Channel created
This media is not supported in your browser
VIEW IN TELEGRAM
_UIPortalView - репликация view без копирования и снапшотов

Сталкивались с ситуацией, когда необходимо показать содержимое одной view в другом месте экрана? Например, живая миниатюра контента (PiP), отражение, размытый backdrop или копия view для hero-transition.
Вариантов обычно два:
1. Дублировать объект - создать полную копию с вложенной иерархией, синхронизировать состояние, управлять жизненным циклом.
2. Рендерить snapshot каждый кадр через drawHierarchy или snapshotView.
Оба варианта жизнеспособны, но первый даёт усложнение, а второй нагрузку на CPU.

У Apple есть приватное API _UIPortalView, которое решает эту задачу на уровне системного композитора.
В iOS 26 _UIPortalView стал частью _UILiquidLensView - приватного компонента, который реализует Liquid Glass.



Как это работает?
_UIPortalView это UIView, чей backing layer CAPortalLayer. CAPortalLayer хранит ссылку (sourceLayerRenderId) на source layer в render tree и говорит render server композитить содержимое source в позиции портала. _UIPortalView добавляет UIKit-уровень: sourceView вместо sourceLayerRenderId, matchesPosition, matchesAlpha и другие свойства.


Как получить доступ?
Класс приватный - доступ через runtime:

Class cls = NSClassFromString(@"_UIPortalView");
UIView *portal = [[cls alloc] init];



Доступные свойства
sourceView — vew-источник, контент которого отображает портал
matchesPosition — привязка к позиции source в координатах окна
matchesTransform — наследует transform от source
matchesAlpha — наследует alpha от source
hidesSourceView — скрывает source, но порталы его отображают
allowsHitTesting — разрешает hit testing на портале
allowsBackdropGroups — поддержка CABackdropLayer групп (blur/vibrancy)
forwardsClientHitTestingToSourceView — пробрасывает тачи с портала на source


Как работает matchesPosition?
matchesPosition = true - портал показывает фрагмент source, совпадающий по позиции в координатах окна. Как отверстие в стене, за которой спрятан source (именно так работает эффект градиентного фона у баблов сообщений в Telegram - каждый бабл показывает свой кусок одного общего градиента)
matchesPosition = false - портал показывает source от (0, 0). Управлять видимой областью можно через bounds.origin портала (сдвиг viewport) или через frame.origin портала внутри clipped-контейнера.


Перформанс
Поскольку рендеринг происходит в render server, создание порталов практически не увеличивает нагрузку на CPU.
Но есть нюансы при большом количестве порталов - GPU накладывает свои ограничения, но об этом расскажу отдельно.

Когда попробовал порталировать AVPlayerLayer, столкнулся с тем, что в портале отображался застывший кадр - портал обновлялся только при изменении layer-свойства в source subtree.

Причина: AVPlayerLayer.contents обновляется через IOSurface swap. CAPortalLayer подписан на изменения layer tree (transform, opacity, sublayers), а не на IOSurface content changes внутри source subtree.
Workaround - infinite анимация на source sublayer, которая поднимает dirty flag каждый кадр.

Спасибо @eleev за пояснение.


В следующих статьях расскажу подробней про ограничения, производительность и хитрые способы применения.
🔥3710
Media is too big
VIEW IN TELEGRAM
В прошлой статье разобрали _UIPortalView - способ отобразить содержимое view без копирования и снапшотов.
А теперь перейдём к практике. Соберём Warp-эффект - объёмный загиб краёв view на порталах и CATransform3D.
Подобный warp-effect используется в Telegram для панели выбора реакции.



Идея
Берём четверть окружности, делим на N частей - это наши сегменты. Каждый сегмент - отдельный _UIPortalView, который показывает свою полоску контента. Расставляем их в 3D-пространстве вдоль дуги - и плоский view превращается в объёмный цилиндрический загиб.


Как устроен сегмент?
Каждый сегмент - UIView с порталом внутри (matchesPosition = false). Портал показывает весь source целиком от точки (0, 0), а сегмент обрезает его через clipsToBounds. Чтобы показать нужный фрагмент, сдвигаем frame портала внутри сегмента.


Геометрия дуги
Загиб моделируется четвертью окружности (от 90° до 0°). Делим дугу на N равных углов - получаем N хорд. Каждая хорда - один сегмент варпа. Чем больше N - тем ближе к гладкой кривой, но больше порталов.
Каждый сегмент описывается тремя параметрами: угол наклона (для CATransform3DRotate), длина (высота сегмента) и координата на дуге (для CATransform3DTranslate):

func computeSegments(arcHeight: CGFloat) -> [Segment] {
let step = CGFloat.pi / 2 / CGFloat(segmentCount)
return (0..<segmentCount).map { i in
let a = CGFloat.pi / 2 - CGFloat(i + 1) * step
let end = CGPoint(x: cos(a), y: sin(a))
let start = CGPoint(x: cos(a + step), y: sin(a + step))
let dx = end.x - start.x
let dy = end.y - start.y
return Segment(angle: atan2(dy, dx), length: hypot(dx, dy) * arcHeight, arcPoint: start)
}
}



CATransform3D сегментов
Трансформ каждого сегмента - это три операции в строгом порядке:

var t = CATransform3DIdentity
t.m34 = 1 / perspective
t = CATransform3DTranslate(t, 0, sign * seg.arcPoint.x * arcHeight, (1 - seg.arcPoint.y) * arcHeight)
t = CATransform3DRotate(t, -sign * seg.angle, 1, 0, 0)

1. t.m34 = 1 / perspective - включаем перспективу.
CATransform3D - матрица 4x4. Элемент m34 отвечает за перспективное деление: при проецировании 3D-координат на экран каждая точка делится на 1 + z * m34. Чем дальше объект по оси Z - тем сильнее он уменьшается.
Значение 1/50 даёт резкую перспективу, как широкоугольный объектив; 1/250 - умеренную; 1/1000 - почти ортографическую проекцию.

2. CATransform3DTranslate - двигаем сегмент в его точку на дуге: по Y вдоль поверхности, по Z в глубину экрана.
3. CATransform3DRotate - наклоняем вокруг оси X на угол хорды, чтобы сегмент лёг по касательной.


Заключение
Чтобы элементы плавно растворялись у краёв, а не обрезались - добавил edge-эффекты для глубины: градиентный blur на краях варпа, полупрозрачный overlay для затемнения загиба и fade mask на контенте.

Весь код - чистый UIKit без зависимостей. Выложил в открытый репозиторий: https://github.com/Oscarworld/WarpEffect
1🔥392🗿2