Мы начали примерно с того же кода с форсированием лейаутов, но это было слишком медленно.
Второй идеей было форсить layout SwiftUI в prepareForReuse() при помощи вызова layoutIfNeeded(). Но это не лучшее решение, потому что тем самым мы вмешиваемся в процесс layout'а SwiftUI и ломаем все оптимизации, которые он делает под капотом, из-за чего получаем множество неожиданных фризов.
А позже пришли к более изящному и быстрому решению – модификации текущей транзакции на лету.
При помощи модификатора View.transaction(:) мы выключаем анимации для всех view в иерархии в нужные моменты времени. Поэтому решение и работает максимально быстро.
extension View {
func disabledAnimations(_ disabled: @escaping () -> Bool) -> some View {
self.transaction { (transaction: inout Transaction) in
if disabled() {
transaction.disablesAnimations = true
}
}
}
}
Спасибо за вопросы, постараюсь ответить на все по порядку:
Не соглашусь, отсутствие выноса расчётов layout в фон — это особенность не декларативного подхода в целом, а одной из его реализаций (SwiftUI).
Ага, я это и имел ввиду) Что в декларативной парадигме у тебя нет возможности повлиять на поведение системных компонентов. Как Apple их реализовали, так они и будут работать.
Также, если честно, я не увидел на экране сложный UI, где был бы необходим расчёт размеров в фоне.
А про размеры в фоне, это просто пример из головы. У нас и правда в лейаутах - нечего рассчитывать в фоне.
Хочется уточить, что это не особенность поведения SwiftUI, а норма для UIKit
Да, действительно UIKit и SwiftUI оба привязаны к транзакциями. Но здесь есть одно отличие, если UIKit только лейаутится (рендерится и т.д), то SwiftUI производит полный пересчет View.body у тех View, чей стейт менялся внутри CATransaction. При этом применит анимации между начальным значением стейта и последним, пропустив все промежуточные.
Из-за чего и происходит баг с ненужными анимациями. Если на примере, то последовательность примерно такая:
CATransaction.begin();
У ячейки был контент какого-то файла;
Мы заменили контент ячейки на плейсхолдер;
Затем обновили контент ячейки до актуального состояния нового файла;
CATransaction.flush();
Произошли анимации между контентом старого и нового файла. А плейсхолдер был проигнорирован как промежуточный.
И даже если мы напишем что-то в духе:
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
hostingController.rootView = /* New File Content */
}
То ничего не сработает, потому что в случае с UIHostingController апдейт все еще будет произведен в конце ранлупа когда выполнится CATransaction.flush(), а наша Transaction будет проигнорирована.
Скорее принятый подход. У всех решений есть свои плюсы и минусы.
Если бы мы остались на UIKit, то не нужно было бы сильно ухищряться с поддержкой старых версий, но при этом верстать красивый и качественный UI было бы гораздо сложнее. Для нас действительно важен хороший, стабильный UI.
Со SwiftUI это делать куда проще, но и здесь есть свои недостатки, главный из которых это более трудоемкая поддержка старых версий iOS.
Спасибо за фидбэк, я понял, что не сразу считывается, что перфоманс улучшился и на других версиях iOS, дополню это в тексте.
было принято решение внедрить визуальные улучшения на «новом фреймворке» в эту устаревшую версию ios и именно это решение ускорили в «3 раза».
Не совсем, если мы хотим сделать решение на новом фреймворке, то не можем просто так взять и не поддержать его одинаково хорошо на всех версиях iOS которые у нас есть.
А стали работать быстрее мы независимо от версии iOS, в частности, все тесты проводились на iPhone 15 Pro Max и iOS 17.4.1.
Кто-то пишет не использовать параметр cornerRadius. Применение viewLayer.cornerRadius может привести к offscreen rendering. Вместо этого можно использовать класс UIBezierPath
Судя по видео из источников (https://developer.apple.com/videos/play/tech-talks/10857/), использование .cornerRadius приводит к offscreen rendering, только если .masksToBounds == true, но это все равно эффективнее чем использовать маску.
Спасибо, что поделились! Интересно, как часто с таким подходом Вы встречаетесь с ложноположительными срабатываниями? Например, из-за того что SwiftUI может потерять ссылку на StateObject или создать его несколько раз?
Мы начали примерно с того же кода с форсированием лейаутов, но это было слишком медленно.
А позже пришли к более изящному и быстрому решению – модификации текущей транзакции на лету.
Спасибо за вопросы, постараюсь ответить на все по порядку:
Ага, я это и имел ввиду) Что в декларативной парадигме у тебя нет возможности повлиять на поведение системных компонентов. Как Apple их реализовали, так они и будут работать.
А про размеры в фоне, это просто пример из головы. У нас и правда в лейаутах - нечего рассчитывать в фоне.
Да, действительно UIKit и SwiftUI оба привязаны к транзакциями. Но здесь есть одно отличие, если UIKit только лейаутится (рендерится и т.д), то SwiftUI производит полный пересчет
View.body
у техView
, чей стейт менялся внутриCATransaction
. При этом применит анимации между начальным значением стейта и последним, пропустив все промежуточные.Из-за чего и происходит баг с ненужными анимациями. Если на примере, то последовательность примерно такая:
CATransaction.begin()
;У ячейки был контент какого-то файла;
Мы заменили контент ячейки на плейсхолдер;
Затем обновили контент ячейки до актуального состояния нового файла;
CATransaction.flush()
;Произошли анимации между контентом старого и нового файла. А плейсхолдер был проигнорирован как промежуточный.
И даже если мы напишем что-то в духе:
То ничего не сработает, потому что в случае с
UIHostingController
апдейт все еще будет произведен в конце ранлупа когда выполнитсяCATransaction.flush()
, а нашаTransaction
будет проигнорирована.Привет, у нас все ячейки в коллекции имеют фиксированный размер, поэтому мы просто задаем его
UIHostingController
.Но вообще safe area для него и правда больная тема, и если она не нужна, то ее можно отключить:
Скорее принятый подход. У всех решений есть свои плюсы и минусы.
Если бы мы остались на UIKit, то не нужно было бы сильно ухищряться с поддержкой старых версий, но при этом верстать красивый и качественный UI было бы гораздо сложнее. Для нас действительно важен хороший, стабильный UI.
Со SwiftUI это делать куда проще, но и здесь есть свои недостатки, главный из которых это более трудоемкая поддержка старых версий iOS.
Спасибо за фидбэк, я понял, что не сразу считывается, что перфоманс улучшился и на других версиях iOS, дополню это в тексте.
Не совсем, если мы хотим сделать решение на новом фреймворке, то не можем просто так взять и не поддержать его одинаково хорошо на всех версиях iOS которые у нас есть.
А стали работать быстрее мы независимо от версии iOS, в частности, все тесты проводились на iPhone 15 Pro Max и iOS 17.4.1.
Судя по видео из источников (https://developer.apple.com/videos/play/tech-talks/10857/), использование
.cornerRadius
приводит к offscreen rendering, только если.masksToBounds == true
, но это все равно эффективнее чем использовать маску.