Как стать автором
Обновить

SwiftUI на iOS 14: преодолевая баги и улучшая перформанс в 3 раза

Уровень сложностиСредний
Время на прочтение19 мин
Количество просмотров5.9K
Всего голосов 27: ↑26 и ↓1+29
Комментарии16

Комментарии 16

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

При чем тут язык программирования и среда( и прочее) разработки? Да и ios 14 устарела, даже на древнем iPhone SE стоит 15.4. Читаешь как экшен, борьба за сотые доли миллисекунд, глюки как головы гидры, крутяк(хотя на первый взгляд кажется, что проблема была сначала зачем-то создана, а потом героически решена, наверное, пока еще что нибудь не вылезет). А потом запускаешь приложение Яндекса умный дом, и не дождавшись его открытия за 10(а то и больше) секунд, идешь и выключаешь свет вручную.

Ставить молча минусы это прикольно, написали бы, с чем не согласны )). С тем что ios 15 не ставится на устройства выпущенные в 2015-начале 2016 годах. Это 9 лет. Согласно данным компании, на начало 2024 года, ios 15 и ниже, установлены на менее 4% устройств(про ios 14 и говорить нечего)…

Я поставил минус за три момента

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

  2. Устаревшая iOS 14 на текущий момент поддерживается разработчиками, как было указано в самом начале. Это значит, что есть ненулевое число пользователей, для которых приложение стало работать быстрее - именно об этом написано в статье. Упоминание, что версия iOS устарела можно было заменить на "Солнце садится на западе", равное по ценности твоему очевидное утверждение.

  3. Ты указал, что согласно данным компании iOS 15 установлено на менее 4% устройств. Значит ли это, что можно эти данные экстраполировать на аудиторию приложения, о котором статья? Можно попробовать. Но ты не учел, что раскладка по девайсам в среднем по больнице вообще не равна раскладке аудитории конкретного приложения. Чем позже вышло - тем меньше аудитория старых версий, обратное тоже справедливо. Еще один твой аргумент в воздух.

Этого достаточно?

Не совсем, поясню, то что ios 14 поддерживается, это хорошо, но насколько я понял из статьи , было принято решение внедрить визуальные улучшения на «новом фреймворке» в эту устаревшую версию ios и именно это решение ускорили в «3 раза». Насколько это решение работает быстрее старого, не совсем понятно. Насчет приложения «умный дом», над которым автор и команда не работает, согласен, не корректный пример. Просто задело, что в статье рассказывается про допуски по времени, про микро и миллисекунды, а тут на другие порядки никто внимания не обращает. Компания одна, требования к одному приложению видны(в статье можно прочитать), неужели настолько разный подход и отношение?

Можно вместо ответа на вопрос

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

я просто прикреплю скрин с Википедии?

Короче понятно, «к пуговицам претензии есть?». А вы не думаете, что для абсолютного большинства есть Яндекс, этакий монолит со своими стандартами и качеством, практически единые требования при приеме на работу, позиционирование компании мирового топ-уровня и такой разный результат )).

Насчет основной темы статьи, ответил выше, можете и дальше интерполировать и натягивать сову на глобус, о огромном процентном отношении пользователей диска с ios 14, которым нужна другая визуализация )). Сама статья хорошая, работа проделана серьезная, применение решений из статьи очень ограничено.

было принято решение внедрить визуальные улучшения на «новом фреймворке» в эту устаревшую версию ios и именно это решение ускорили в «3 раза».

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

А стали работать быстрее мы независимо от версии iOS, в частности, все тесты проводились на iPhone 15 Pro Max и iOS 17.4.1.

Спасибо за ответ, теперь гораздо понятней.

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

Скорее принятый подход. У всех решений есть свои плюсы и минусы. 

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

Со SwiftUI это делать куда проще, но и здесь есть свои недостатки, главный из которых это более трудоемкая поддержка старых версий iOS.

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

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

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

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

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

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

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

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

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

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

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

Спасибо за вопросы, постараюсь ответить на все по порядку:

Не соглашусь, отсутствие выноса расчётов 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 будет проигнорирована.

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

То ничего не сработает, потому что в случае с 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, то можно и не пробовать то, что я описал

Мы начали примерно с того же кода с форсированием лейаутов, но это было слишком медленно.

Второй идеей было форсить 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
      }
    }
  }
}

спасибо что поделились, прошел почти через то же самое)
было бы еще интересно узнать, как в итоге реализовали лейаут для SwiftUI ячеек? в моем кейсе это было основной проблемой, так как хостинг контроллер часто возвращал некорректные значения для высоты (особенно при скролле, когда ячейка проходила через safe area коллекции)

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

Но вообще safe area для него и правда больная тема, и если она не нужна, то ее можно отключить:

if #available(iOS 16.4, *) {
  hostingController.safeAreaRegions = []
} else {
  hostingController._disableSafeArea = true
}

Зарегистрируйтесь на Хабре, чтобы оставить комментарий