Pull to refresh
15
0
Александр Терентьев @at_nil

Инженер

Send message

Спасибо за ответы!

То ничего не сработает, потому что в случае с UIHostingController апдейт все еще будет произведен в конце ранлупа когда выполнится CATransaction.flush(), а наша Transaction будет проигнорирована.

Возможно, поможет вызвать `hostingController.view.layoutIfNeeded()` явно внутри транзакции с выключенными анимациями:

CATransaction.begin()
CATransaction.disableAnimations()
UIView.disableAnimations()
hostingController.rootView = /* New File Content */
hostingController.view.layoutIfNeeded() // может помочь
CATransaction.commit()

Я сейчас проверил в Playground (правда на CALayer):

CATransaction.begin()
CATransaction.disableActions()
view.setNeedsLayout()
view.sublayer.frame = view.bounds
CATransaction.commit() // !!! в рамках этого вызова !!!

class View: UIView {
        class Layer: CALayer {
            var isRedColor = false
            
            override func add(_ anim: CAAnimation, forKey key: String?) {
                super.add(anim, forKey: key) // !!! попадаю сюда !!!
            }
            
            override func layoutSublayers() {
                super.layoutSublayers()
                
                backgroundColor = isRedColor ? UIColor.green.cgColor :  UIColor.red.cgColor // через эту строчку
            }
        }
        
        lazy var sublayer: Layer = {
            let layer = Layer()
            self.layer.addSublayer(layer)
            return layer
        }()
        
        override func layoutSubviews() {
            super.layoutSubviews()
            sublayer.setNeedsLayout()
        }
        
    }

Я предполагал, что такое может быть, так как помню примеры отчасти похожей проблемы, когда нужно было внутри блока UIView.animate {} (UIView.animate — обёртка над созданием транзакции + включение actions для CALayer, у которых delegateUIView) явно вызывать layoutIfNeeded.
То есть, если я ничего не упускаю, изменения в рамках commit одной транзакции в эту же транзакцию не попадают (или точнее не подхватывают её свойства отключения/включения Actions).

То есть, моё предположение, что приведённое решение не сработало, даже если SwiftUI всё-таки подвязан CATransaction.commit, имею ввиду происходит внутри layoutSubviews у какой-то UIView, которая ассоциирована со SwiftUI, она просто не подхватывала флаг отключения анимаций, так как этот layout происходит в CATransaction.commit, а не между begin и commit этой (!) же транзакции.
Возможно, это даже сделано специально инженерами Apple, чтобы анимировалось (или наоборот не анимировалось) явно только то, что написано в блоке транзакциии (между begin и commit)/ анимации (в блоке UIView.animate)

Возможно у вас была эта же проблема, когда пробовали явно обернуть в Transaction обновление ячейки, но не уверен, так как не знаю, делает ли SwiftUI layout ассоциированных UIView (и всех изменений) через стандартный цикл транзакции ( а именно layoutSubviews контейнера / ассоциированной UIView) или же у него свой цикл обновлений, похожий на все UIView/CALayer, но инициированный не CATransaction.commit, а чем-то другим (какой-то своей запускалкой в итерации Runloop).

Но, в любом случае, это ручной "пинок" layout. Если в вашем решении со сбросом состояния нет доп layout, то можно и не пробовать то, что я описал

Спасибо за статью.
Хочется кое-что уточнить, а кое с чем поспорить, простите.

Ну и хуже, чем легаси модуль на Obj-C, уж точно не получится.

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

Из-за декларативной парадигмы SwiftUI не может похвастаться подобным.

Не соглашусь, отсутствие выноса расчётов layout в фон — это особенность не декларативного подхода в целом, а одной из его реализаций (SwiftUI).

... SwiftUI обычно леайутит view в конце ранлупа Main Thread.

Хочется уточить, что это не особенность поведения SwiftUI, а норма для UIKit и скорее даже для CoreAnimation, так как именно в конце итерации вызывается CATransaction.flush, что подготавливает (layout, render, decodeImages, и т д) и отправляет все накопленные изменения на RenderServer. Так что использование `layoutIfNeeded` — удар по производительности для любого решения, основанного на CoreAnimation (включая UIKit), не только для SwiftUI. И, так как это поведение по умолчанию (если я ничего не упускаю), хотелось бы понять, что именно имеется ввиду в этой фразе (а именно почему переиспользований одной ячейки больше одного? обрабатывается несколько "бизнес-сигналов" изменений за один кадр?):

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

Также, если честно, я не увидел на экране сложный UI, где был бы необходим расчёт размеров в фоне. В первую очередь про это говорит статичный размер элементов. Из не константного размера вижу только текст, но он везде выглядит очень коротким, и от его размера не зависит "окружение"

Заранее спасибо за ответ!

Спасибо! Я смотрел эти доклады несколько месяцев назад. У меня было ощущение, что коллеги из Apple просто упростили и абстрактно формализовали существующий процесс рендеринга в этом видео. Но надо будет проверить. Спасибо)

Так получилось, что после вопроса друга (сейчас этот вопрос иногда звучит на собеседованиях) "почему в iOS некоторые анимации работают, даже если остановить процесс в дебаггере" меня заинтересовало разобраться в деталях всех этапов формирования и обновления кадров на iOS. У меня есть ощущения недосказанности или неточности, которые хочется подсвеитить.

1) Описание Offscreen Rendering

Кажется, здесь RenderServer подменяется понятием GPU. Если не ошибаюсь, до offscreen rendering ещё никакой GPU не используется. CoreAnimation работает на CPU, все необходимые дорисовки слоёв с использованием СoreGraphics происходят на CPU (если не говорить о слоях, которые напрямую используют Metal или OpenGL). То есть, если оперировать шагами из статьи, offscreen rendering происходит на этапе Render Prepare до Render Execute.

2) Изображения и GPU

Как только GPU выполняет отрисовку изображений, то это готово к отображению для следующего VSYNC

О каких изображениях идёт речь? О картинках? О контенте (CALayer.contents, bitmap) слоёв?

Картинки, которые требуют декомпрессии (WebP, JPEG) обрабаыватываются нашим приложением. И могут вообще служить причиной тормозов UI потока (CATransaction.commit), если не было предварительной декомпрессии.

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

Contents слоёв тоже рисуется на CPU, в UI-потоке в нашем приложении: например, буффер пикселей текста, это можно по CALayer.contents увидеть для стандартных View, рисующих текст. Ну и использования drawInContext или drawRect как раз являются примером рисования на CPU.

Или же тут всё-таки речь о итоговом отображении экрана?

Ну и:

Если вы пытаетесь нарисовать изображение, размер которого превышает максимальный размер текстуры, поддерживаемый GPU (обычно 2048 × 2048 или 4096 × 4096, в зависимости от устройства), для предварительной обработки изображения необходимо использовать CPU. 

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

Даже в статье есть отдельный пункт про декодинг изображений в фоне, акутальность которого как раз обусловлена тем, что картинки по умолчанию проходят финальную подготовку в UI-потоке прямо перед отправкой слоя на RenderServer.

Из таких аспектов возникает ощущение неточностей и несогласованностей внутри статьи, когда предложение из одного пункта, кажется, противоречит информации из другого пунта.

3) Не понятно, что такое "лайут реквест"

Может возникнуть ощущение, что это какой-то механизм для синхронизации layout или ещё какой-то системный callback. Хочется понять, о чём всё-таки речь? Любой вызовCATransaction.commit? Любой вызов layoutIfNeeded()? Или же что-то другое?

4) Vsync

VSYNC — это дедлайн для каждой фазы нашего рендер лупа

Точный ответ можно узнать, конечно, только из кода CoreAnimation, но всё же не похоже, чтобы VSYNC был дедлайном для каждой описанной в статье фазы подготовки изображения.

Согласен, что есть какая-то логичная отсечка, когда обновляется сам физический дисплей, остальное, думаю, всё условно. Например, что-то намекающее на Vsync, я видел только в виде даты CADisplayLink.targetTimestamp. Но, следует помнить, что CADisplayLink — это почти просто таймер, имеющий специализированный интерфейс для удобной работы с формированием кадров (а точнее транзакций).

Я не уверен, что как минимум для процесса нашего приложения не попадение в рамки 1/fps между CADisplayLink.timestamp и CADisplayLink.targetTimestamp обязательно приведёт к залипанию кадра, если CoreAnimation всё-таки успеет за оставшееся время (уже меньше 1/fps) обработать эту транзакцию.

Также, если верить старым докладам Apple, RenderServer не ждёт никаких синхронизаций кадров, а просто обрабатывает прилетающие транзакции. Которых мы иногда закидываем больше, чем 1 на кадр (надо вспомнить, где в коде мы сами создаём CATransaction.begin/CATransaction.commit):

WWDC 2014 Session 419. Advanced Graphics and Animations for iOS Apps.

Возможно, за 8 лет с того WWDC что-то поменялось в CoreAnimation, но что-то мне кажется, что вряд ли. Надо будет проверить через Instruments Metal System Trace, там видна и синхронизация дисплея и то, что делают другие процессы. Я немного рассказывал про Render pipeline, работу UIKit+CoreAnimation и инструменты в своём докладе: https://habr.com/ru/company/vk/blog/481626/

Спасибо:)

Интересный сценарий. Так как это новая логика: новое бизнес-требование, здесь можно подумать над какой-то регистрацией идентификаторов контента. Причём, в таком случае, идентификаторы могут даже не полностью совпадать с множеством типов, если какие-то сущности отвечают за контент разного происхождения, которое хочется разделять, и тогда лучше, наверно, отделить реализацию идентификаторов фильтрации от "мета-информации" - сущностей элементов

Но и паттерн-матчинг множества типов тоже может подойти. Тут, как и везде, уже надо плясать от задачи и проекта

Я ожидал чего-то подобного, публикуя доклад на тему про проектирование с громким заголовком и мыслями и болями, которые в голове крутились давно :)

Спасибо) хотелось идти от примеров, чтобы проиллюстрировать то, что я считаю проблемами

Спасибо! Почитаю

Старался идти по примерам. Досадно, что кажется путано:(

конечно, могут быть ветвеления, ведь тут всё зависит от реализации диспетчиризации

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

Простите, если мой ответ показался грубым, но я действительно хотел бы увидеть те плюсы, которые enum и другие перечисления могут принести в систему, поэтому и спрашиваю так, в чем их ценности. Да и во всём докладе я делаю упор именно на избавлении от переборов и проверок, а не просто на борьбе против enum. Если в использовании enum есть преимущества, я хотел о них узнать. А пока enum, к сожалению, мне кажется инструментом, который легко ведёт к коду, как к перебору вариантов, и этой статьёй я пытаюсь подтолкнуть коллег к более осмысленному использованию перечислений и переборов:(

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

кажется, вы просто предлагете запрятывание переборов. а в чём плюс? чтобы в коде был соблазн сделать больше переборов списков элементов как в

// профит

[<<FeedItem>>].compactMap(\.post) // -> [Post]
[<<FeedItem>>].compactMap(\.clip) // -> [Clip] 

?

для меня это всё выглядит как "костыли", так как везде распрораняется знание о том, а какие элементы ленты бывают, и какие их детали. из-за того, что везде по коду лезет это знание, отсюда возникают места, которые как раз вычленяют необходимые типы, как вы предложили:

// "секретный" ингредиент в виде небольшого шаблонного кода
 
var post: Post? {
  	guard case let .post(post) = self else { return nil }
    return post
}
  
var clip: Clip? {
  	guard case let .clip(clip) = self else { return nil }
    return clip
}

Для меня это всё кажется, как фиксы или костыли, чтобы прикрыть симптомы неуместного использования enum

И ещё вы, кажется, предлагаете код, который я в статье везде помечаю, как нелогичный:

case banPost(FeedItem) // почему здесь закладывается хранение всех элементов, а не постов?

Вот только я действительно не понимаю ценности этих финтов, которые, как по мне, только упрощают написание неэффективного негибкого переплетённого между сценариями кода. И статья как раз о том, как можно избавиться от этого кода и оперировать только абстракциями, а знание деталей инкапсулировать в код детелаей, не размазывая по проекту

Пример с Reuse Pool я привёл, чтобы показать, что можно по-другому реализовывать dequeue и не приводить никакие типы)

Не обязательно прокидывать через парсер - можно сделать аргументом render (такой пример есть в статье)

Если удобнее использовать стандартное как есть - можно использовать его, я не заставляю отказываться от этого) для меня главное поделиться идеей, что бывает и по-другому

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

даже в функциональном программировании увидел, что есть абстрактные типы данных: обобщенные алгебраические типы данных. Если я правильно понял, по сути, это как раз аналог интерфейсов. За переход к которым я высказываюсь

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

И тут не так важно, будет функциональный стиль или ещё какой-то, я высказываюсь за построение кода так, чтобы в нём находились по минимуму или вообще отсутствовали любые сопоставления и проверки значений. И просто переход к фунцикональному программированию может не избавить от написания кода, который будет постоянно в своих кусочках что-то сопоставлять или проверять, а не просто выполнять то, что задекларировано и собрано заранее

Если вы имеете ввиду под "научиться извлекать ассоциированные значения не привнося шаблоны паттерн матчинга" оперирование сущностями через абстракции (интерфейсы), тогда это как раз то, что я пытаюсь донести)

Справедливое замечание. У меня как-то возник даже такой пример: в компании есть анкеты. И можно сделать так, что по отдельным вопросам будут журналы для подписей или какой-то другой инфы от команд (для меня это аналог перебора вариантов enum в функции), а можно так, что будут анкеты со всеми вопросами для разных команд. Тут уже ближе к реализации интерфейса/протокола/trait.

Кажется, что при добавлении команд использовать второе проще.

Также кажется, что для добавления вопросов проще использовать первое. Но! Это невозможно легко распараллелить без конфликтов, так как есть необходимость изменения общего журнала разными командами

вполне возможно, посмотрю, спасибо)

если код аккуратно разделён на слои, когда в одном месте не пытаемся на несколько уровней вниз реализовать асинхронные операции, callback hell не должно возникнуть.

про сценарий с Either не понял:( речь про список каких-то Future идёт? можно реализовать callback, где будем записывать значение в future. Или о каком сценарии речь?

"получатель не сможет проигнорировать ошибку" — можно сделать обязательным наличием onError обработчика.

По сути, я описываю переход от традиционного вызова функций, которые что-то возвращают, к асинхронному стилю с обработчиками и инкапсулированным ветвлением.

Если слушателю обязательно надо прокинуть ошибку выше, и при этом почему-то не преобразовав её в ошибки уровня своей логики — он может обявить для внешнего слушателя (или супервизора из реактивных шаблонов проектирования) обязательность регистрации обработчика onError того же типа, что требует слой ниже.

Или я ошибочно понял сценарий?

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Works in
Registered
Activity