Подтвердить что ты не робот

Как просто ждать какого-либо макета в iOS?

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

Только UIKit.

view.addItemsA()
view.addItemsB()
view.addItemsC()

Скажем, на iPhone 6s

Для каждого из них требуется одна секунда для создания UIKit.

Это произойдет:

один большой шаг

ОНИ ОТВЕРЖДАЮТ ВСЕ. Чтобы повторить, экран просто зависает в течение 3 секунд, в то время как UIKit выполняет огромную работу. Затем все они появляются сразу.

Но позвольте сказать, что я хочу, чтобы это произошло:

введите описание изображения здесь

ОНИ ПРОДОЛЖАЮТСЯ. Экран просто висит в течение 1 секунды, в то время как UIKit строит один. Кажется. Он снова висит, когда он строит следующий. Кажется. И так далее.

(Примечание "одна секунда" - просто простой пример для ясности. См. конец этого сообщения для более полного примера.)

Как вы это делаете в iOS?

Вы можете попробовать следующее. Кажется, что это не работает.

view.addItemsA()

view.setNeedsDisplay()
view.layoutIfNeeded()

view.addItemsB()

Вы можете попробовать следующее:

 view.addItemsA()
 view.setNeedsDisplay()
 view.layoutIfNeeded()_b()
 delay(0.1) { self._b() }
}

func _b() {
 view.addItemsB()
 view.setNeedsDisplay()
 view.layoutIfNeeded()
 delay(0.1) { self._c() }...
  • Обратите внимание, что если значение слишком мало - , этот подход просто и, очевидно, ничего не делает. UIKit просто будет работать. (Что еще он мог бы сделать?). Если значение слишком велико, оно бессмысленно.

  • Обратите внимание, что в настоящее время (iOS10), , если я не ошибаюсь: если вы попробуете этот трюк с трюком нулевой задержки, он работает в лучшем случае беспорядочно. (Как вы, вероятно, ожидаете.)

Отключить цикл запуска...

view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()

RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()

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

(т.е. Apple UIKit теперь достаточно сложна, чтобы смазать работу UIKit за пределами этого "трюка".)

Мысль: есть ли способ, в UIKit, получить обратный вызов, когда он, в основном, нарисовал все просмотренные вами взгляды? Есть ли другое решение?

Кажется, что одно из решений... поместит subviews в контроллеры, поэтому вы получаете обратный вызов "didAppear" и отслеживаете их. Это кажется инфантильным, но, может быть, это единственный шаблон? Будет ли это действительно работать? (Только одна проблема: я не вижу никакой гарантии, что didAppear гарантирует, что все подзаголовки будут нарисованы.)


Если это еще не ясно...

Пример повседневного использования:

• Скажите, что возможно семь разделов.

• Скажем, каждый из них обычно занимает от 0,01 до 0,20 для построения UIKit (в зависимости от того, какую информацию вы показываете).

• Если вы просто "пусть все идет в один удар" , это будет нормально или приемлемо (общее время, скажем от 0,05 до 0,15)... но...

• часто возникает утомительная пауза для пользователя, поскольку появляется "новый экран". (.1 до .5 или хуже).

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

4b9b3361

Ответ 1

Окно-сервер имеет окончательный контроль над тем, что появляется на экране. iOS отправляет обновления только серверу окон при выполнении текущего CATransaction. Чтобы это произошло, когда это необходимо, iOS регистрирует CFRunLoopObserver для активности .beforeWaiting в цикле выполнения основного потока. После обработки события (предположительно, вызывая в ваш код) цикл выполнения вызывает наблюдателя до того, как он ждет следующего события. Наблюдатель фиксирует текущую транзакцию, если она есть. Завершение транзакции включает в себя запуск макета, пропуск отображения (в котором вы вызываете методы drawRect) и отправку обновленного макета и содержимого на оконный сервер.

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

Один из способов сделать это - позвонить CATransaction.flush(). Разумным случаем использовать CATransaction.flush() является то, когда вы хотите поместить новый CALayer на экран, и вы хотите, чтобы у него была анимация сразу. Новый CALayer не будет отправлен на оконный сервер до тех пор, пока транзакция не будет выполнена, и вы не сможете добавить к нему анимацию, пока она не появится на экране. Итак, вы добавляете слой в иерархию слоев, вызываете CATransaction.flush(), а затем добавляете анимацию на уровень.

Вы можете использовать CATransaction.flush, чтобы получить необходимый эффект. Я не рекомендую этот, но вот код:

@IBOutlet var stackView: UIStackView!

@IBAction func buttonWasTapped(_ sender: Any) {
    stackView.subviews.forEach { $0.removeFromSuperview() }
    for _ in 0 ..< 3 {
        addSlowSubviewToStack()
        CATransaction.flush()
    }
}

func addSlowSubviewToStack() {
    let view = UIView()
    // 300 milliseconds of "work":
    let endTime = CFAbsoluteTimeGetCurrent() + 0.3
    while CFAbsoluteTimeGetCurrent() < endTime { }
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

И вот результат:

CATransaction.flush demo

Проблема с вышеупомянутым решением заключается в том, что он блокирует основной поток, вызывая Thread.sleep. Если ваш основной поток не отвечает на события, пользователь не только разочаровывается (потому что ваше приложение не реагирует на ее прикосновения), но в конечном итоге iOS решит, что приложение зависает и убивает его.

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

Итак, вот лучший, рекомендуемый способ добавления subviews:

@IBOutlet var betterButton: UIButton!

@IBAction func betterButtonWasTapped(_ sender: Any) {
    betterButton.isEnabled = false
    stackView.subviews.forEach { $0.removeFromSuperview() }
    addViewsIfNeededWithoutBlocking()
}

private func addViewsIfNeededWithoutBlocking() {
    guard stackView.arrangedSubviews.count < 3 else {
        betterButton.isEnabled = true
        return
    }
    self.addSubviewToStack()
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
        self.addViewsIfNeededWithoutBlocking()
    }
}

func addSubviewToStack() {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

И вот результат (идентичный):

DispatchQueue asyncAfter demo

Ответ 2

TL;DR

Включение ожидающих изменений пользовательского интерфейса на сервере рендеринга с помощью CATransaction.flush() или разделить работу на несколько фреймов с помощью CADisplayLink (пример кода ниже).


Резюме

Есть ли способ, в UIKit, получить обратный вызов, когда он нарисовал все просмотренные вами представления?

Нет

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

Есть ли другое решение?

Да, iOS может обрабатывать изменения только один раз за кадр, но ваше приложение не является тем, что делает этот рендеринг. Процесс оконного сервера.
Ваше приложение делает свой макет и рендеринг, а затем фиксирует его изменения на свой layerTree на сервере рендеринга. Он будет делать это автоматически в конце runloop, или вы можете принудительно отправить незавершенные транзакции на сервер визуализации CATransaction.flush().

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

Возможные решения

Это интересующая вас часть.

1: сделайте как можно больше фоновой очереди так, как сможете, и повысите производительность.
Серьезно iPhone 7 является третьим самым мощным компьютером (не телефоном) в моем доме, только избитым моим игровым ПК и Macbook Pro. Это быстрее, чем каждый другой компьютер в моем доме. Для отображения пользовательского интерфейса приложений не требуется пауза в 3 секунды.

2: скрытые ожидания CAT-транзакций
EDIT: Как указано rob mayoff, вы можете заставить CoreAnimation отправить ожидающие изменения в сервер рендеринга, вызвав CATransaction.flush()

addItems1()
CATransaction.flush()
addItems2()
CATransaction.flush()
addItems3()

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

Однако вам следует попытаться избежать явного вызова вызова flush. Предоставляя флеш выполнять во время runloop...... и транзакции и анимации, которые работают от транзакции к транзакции, будут продолжать функционировать.

Однако заголовочный файл CATransaction включает эту цитату, которая, по-видимому, подразумевает, что даже если им это не нравится, это официально поддерживается.

В некоторых случаях (т.е. никакой цикл цикла или цикл цикла не блокируется) может потребоваться использование явных транзакций для получения своевременных обновлений дерева рендеринга.

Документация для Apple - "Лучшая документация для + [CATransaction flush ]" .

3: dispatch_after()
Просто задержите код до следующей runloop. dispatch_async(main_queue) не будет работать, но вы можете использовать dispatch_after() без задержки.

addItems1()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
  addItems2()

  DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
    addItems3()
  }
}

Вы упомянули в своем ответе, что это больше не работает для вас. Тем не менее, он отлично работает в тестовой Swift Playground и примере приложения iOS, которое я включил с этим ответом.

4: Используйте CADisplayLink
CADisplayLink вызывается один раз для каждого кадра и позволяет гарантировать выполнение только одной операции для каждого кадра, гарантируя, что экран сможет обновляться между операциями.

DisplayQueue.sharedInstance.addItem {
  addItems1()
}
DisplayQueue.sharedInstance.addItem {
  addItems2()
}
DisplayQueue.sharedInstance.addItem {
  addItems3()
}

Требуется, чтобы этот вспомогательный класс работал (или похожим).

// A queue of item that you want to run one per frame (to allow the display to update in between)
class DisplayQueue {
  static let sharedInstance = DisplayQueue()
  init() {
    displayLink = CADisplayLink(target: self, selector: #selector(displayLinkTick))
    displayLink.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
  }
  private var displayLink:CADisplayLink!
  @objc func displayLinkTick(){
    if let _ = itemQueue.first {
      itemQueue.remove(at: 0)() // Remove it from the queue and run it
      // Stop the display link if it not needed
      displayLink.isPaused = (itemQueue.count == 0)
    }
  }
  private var itemQueue:[()->()] = []
  func addItem(block:@escaping ()->()) {
    displayLink.isPaused = false // It needed again
    itemQueue.append(block) // Add the closure to the queue
  }
}

5: вызовите runloop напрямую.
Мне это не нравится из-за возможности бесконечного цикла. Но, признаюсь, это маловероятно. Я также не уверен, что это официально поддерживается, или инженер Apple собирается прочитать этот код и выглядеть в ужасе.

// Runloop (seems to work ok, might lead to infitie recursion if used too frequently in the codebase)
addItems1()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems2()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems3()

Это должно работать, если только (хотя и не реагирует на события runloop) вы делаете что-то еще, чтобы блокировать вызов runloop от завершения, поскольку CATransaction отправляется на оконный сервер в конце runloop.


Пример кода

Демонстрационный проект Xcode и игровая площадка Xcode (Xcode 8.2, Swift 3)


Какой вариант использовать?

Мне нравятся решения DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) и CADisplayLink лучшие. Тем не менее, DispatchQueue.main.asyncAfter не гарантирует, что он будет работать на следующей отметке runloop, чтобы вы не захотели доверять ей?

CATransaction.flush() заставит вас изменить настройки пользовательского интерфейса на сервер рендеринга, и это использование, по-видимому, соответствует комментариям Apple к классу, но содержит некоторые предупреждения.

В некоторых случаях (т.е. никакой цикл цикла или цикл цикла не блокируется) может потребоваться использование явных транзакций для получения своевременных обновлений дерева рендеринга.


Подробное объяснение

Остальная часть этого ответа - это предыстория того, что происходит внутри UIKit, и объясняет, почему исходные ответы на попытки использовать view.setNeedsDisplay() и view.layoutIfNeeded() ничего не сделали.


Обзор макета и рендеринга UIKit

CADisplayLink полностью не связан с UIKit и runloop.

Не совсем. iOS UI - это графический процессор, созданный как 3D-игра. И пытается сделать как можно меньше. Поэтому многие вещи, такие как макет и рендеринг, не происходят, когда что-то меняется, но когда это необходимо. Вот почему мы называем setNeedsLayout не макетными представлениями. Каждый кадр может меняться несколько раз. Тем не менее, iOS будет пытаться вызывать только layoutSubviews один раз за кадр, а не 10 раз setNeedsLayout может быть вызван.

Тем не менее, очень много происходит на процессоре (макет, -drawRect: и т.д.), так как все это сочетается.

Обратите внимание, что это все упрощено и пропускает множество вещей, таких как CALayer, фактически являющийся реальным объектом просмотра, который показывает на экране не UIView и т.д.

Каждый UIView можно рассматривать как растровое изображение, текстуру изображения/графического процессора. Когда экран отображается, графический процессор объединяет иерархию представления в полученный в результате фрейм, который мы видим. Он формирует представления, превращая текстуры subviews поверх предыдущих просмотров в готовый рендер, который мы видим на экране (аналогично игре).

Это то, что позволило iOS иметь такой плавный и легко анимированный интерфейс. Чтобы анимировать представление по экрану, ему не нужно ничего переписывать. На следующем кадре, который просматривает текстуру, просто составлен в несколько другом месте на экране, чем раньше. Ни он, ни мнение, что он был в избытке, чтобы их содержимое было перезаписано.

В прошлом общий совет по производительности обычно заключался в том, чтобы сократить количество просмотров в иерархии представлений, полностью отображая ячейки представления в drawRect:. Этот совет должен был сделать шаг компостирования GPU быстрее на ранних устройствах iOS. Тем не менее, графический процессор настолько быстро работает на современных устройствах iOS, что сейчас больше не беспокоит.

LayoutSubviews и DrawRect

-setNeedsLayout аннулирует текущий макет представления и отмечает его как необходимый макет.
-layoutIfNeeded передаст представление, если у него нет допустимого макета

-setNeedsDisplay будет отмечать виды, которые необходимо перерисовать. Ранее мы говорили, что каждый вид визуализируется в текстуру/изображение представления, которое можно перемещать и манипулировать с помощью графического процессора без необходимости перерисовывать. Это вызовет его перерисовку. Чертеж выполняется путем вызова -drawRect: на ЦПУ и, следовательно, медленнее, чем возможность полагаться на графический процессор, который может выполнять большинство кадров.

И важно отметить, что эти методы не делают. Методы компоновки ничего не делают визуально. Хотя если для представлений contentMode установлено значение redraw, изменение кадра просмотров может привести к аннулированию визуализации представлений (trigger -setNeedsDisplay).

Вы можете попробовать следующее в течение всего дня. Кажется, это не работает:

view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsC()
view.setNeedsDisplay()
view.layoutIfNeeded()

Из того, что мы узнали, ответ должен быть очевидным, почему это не работает сейчас. view.layoutIfNeeded() ничего не делает, кроме пересчета кадров его подзонов.
view.setNeedsDisplay() просто помечает представление так, как требуется перерисовка в следующий раз. UIKit просматривает иерархию представлений, обновляя текстуры просмотра для отправки на GPU. Однако, это не влияет на подсмотры, которые вы пытались добавить.

В вашем примере view.addItemsA() добавлено 100 подменю. Это отдельные несвязанные слои/текстуры на GPU, пока GPU не объединит их в следующий фреймбуфер. Единственное исключение - если у CALayer shouldRasterize установлено значение true. В этом случае он создает отдельную текстуру для представления, а ее подматривает и визуализирует (думает на графическом процессоре) представление, и он подглядывает в одну текстуру, эффективно кэшируя композицию, которую он должен будет выполнять каждый кадр. Это имеет преимущество в производительности, когда не нужно составлять все его подпрограммы в каждом кадре. Тем не менее, если вид или его просмотры часто меняются (например, во время анимации), это будет штраф за производительность, так как это приведет к аннулированию кешированной текстуры, часто требующей ее перерисовывания (аналогично частому вызову -setNeedsDisplay).


Теперь любой инженер игры просто сделает это...

view.addItemsA()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsB()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsC()

Теперь, действительно, это работает.

Но почему это работает?

Теперь -setNeedsLayout и -setNeedsDisplay не запускают ретрансляцию или перерисовку, а вместо этого просто отмечают вид как необходимый. По мере того, как UIKit готовится к рендерингу следующего кадра, он вызывает представления с недопустимыми текстурами или макетами для перерисовки или ретрансляции. После того, как все будет готово, он отправляет команду GPU в композицию и отображает новый кадр.

Итак, основной цикл запуска в UIKit, вероятно, выглядит примерно так.

-(void)runloop
{
    //... do touch handling and other events, etc...

    self.windows.recursivelyCheckLayout() // effectively call layoutIfNeeded on everything
    self.windows.recursivelyDisplay() // call -drawRect: on things that need it
    GPU.recompositeFrame() // render all the layers into the frame buffer for this frame and displays it on screen
}

Итак, вернемся к исходному коду.

view.addItemsA() // Takes 1 second
view.addItemsB() // Takes 1 second
view.addItemsC() // Takes 1 second

Итак, почему все 3 изменения появляются сразу после 3 секунд вместо одного за раз на 1 секунду?

Хорошо, если этот бит кода запущен в результате нажатия кнопки или аналогичного, он выполняет синхронную блокировку основного потока (поток UIKit требует внесения изменений пользовательского интерфейса) и поэтому блокирует цикл выполнения в строке 1, четная обрабатывающая часть. Фактически, вы делаете, чтобы первая строка метода runloop возвращалась за 3 секунды.

Однако мы определили, что макет не будет обновляться до строки 3, отдельные представления не будут отображаться до тех пор, пока строка 4 и никакие изменения не будут отображаться на экране до последней строки метода runloop, строка 5.

Причина, по которой вручную запускается runloop, заключается в том, что вы в основном вставляете вызов методу runloop(). Ваш метод работает в результате вызова из функции runloop

-runloop()
   - events, touch handling, etc...
      - addLotsOfViewsPressed():
         -addItems1() // blocks for 1 second
         -runloop()
         |   - events and touch handling
         |   - layout invalid views
         |   - redraw invalid views
         |   - tell GPU to composite and display a new frame
         -addItem2() // blocks for 1 second
         -runloop()
         |   - events // hopefully nothing massive like addLotsOfViewsPressed()
         |   - layout
         |   - drawing
         |   - GPU render new frame
         -addItems3() // blocks for 1 second
  - relayout invalid views
  - redraw invalid views
  - GPU render new frame

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


КОНЕЦ


Ниже приведено только уточнение.


Дополнительная информация о том, что здесь происходит


CADisplayLink и NSRunLoop

Если я не ошибаюсь, KH кажется, что в принципе вы считаете, что "цикл запуска" (то есть: этот: RunLoop.current) - это CADisplayLink.

Runloop и CADisplayLink - это не одно и то же. Но CADisplayLink подключается к runloop для работы.
Я немного оговорился раньше (в чате), когда я сказал, что NSRunLoop называет CADisplayLink каждым тиком, это не так. Насколько я понимаю, NSRunLoop - это в основном цикл while (1), который заключается в том, чтобы сохранить поток в действии, обрабатывать события и т.д. Чтобы избежать скольжения, я собираюсь попытаться процитировать экстенсивно из собственной документации Apples для следующих бит.

Запуск цикла очень похож на его название. Это цикл, в который входит поток, и используется для запуска обработчиков событий в ответ на входящие события. Ваш код предоставляет управляющие операторы, используемые для реализации фактической части цикла цикла выполнения, другими словами, ваш код предоставляет цикл while или for, который управляет циклом запуска. В вашем цикле вы используете объект цикла цикла для "запуска" кода обработки событий, который принимает события и вызывает установленные обработчики.
Анатомия цикла запуска - Руководство по программированию потоков - developer.apple.com

CADisplayLink использует NSRunLoop и должен быть добавлен к одному, но отличается. Чтобы процитировать заголовочный файл CADisplayLink:

"Если не будет приостановлено, он будет запускать каждый vsync до удаления".
От: func add(to runloop: RunLoop, forMode mode: RunLoopMode)

И из документации свойств preferredFramesPerSecond.

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

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

Знакомство с сервером рендеринга

Если вам случится блокировать поток, это не имеет никакого отношения к тому, как работает UIKit.

Не совсем. Причина, по которой мы должны касаться только UIViews из основного потока, состоит в том, что UIKit не является потокобезопасным и работает на основном потоке. Если вы блокируете основной поток, вы заблокировали поток, на который выполняется UIKit.

Будет ли UIKit работать "как вы говорите" {... "отправить сообщение, чтобы остановить видеокадры. сделайте всю нашу работу! Отправьте еще одно сообщение, чтобы снова начать видео!" }

Это не то, что я говорю.

Или работает ли "как я говорю" {... то есть, как и в обычном программировании, сделайте столько, сколько сможете, пока кадры не закончатся - о нет, это заканчивается! - подождите, пока следующий кадр! сделайте больше... "}

Это не то, как работает UIKit, и я не понимаю, как это было возможно, без принципиального изменения его архитектуры. Как это означает наблюдать за окончанием кадра?

Как обсуждалось в разделе "Обзор макета и рендеринга UIKit" моего ответа, UIKit пытается не работать заранее. -setNeedsLayout и -setNeedsDisplay можно назвать столько раз за кадр, сколько хотите. Они только аннулируют макет и визуализацию рендеринга, если он уже был признан недействительным, что кадр, а второй вызов ничего не делает. Это означает, что если 10 изменений все недействительны, то макет представления UIKit по-прежнему должен оплачивать только однократное вычисление раскладки (если вы не использовали -layoutIfNeeded между вызовами -setNeedsLayout).

То же самое относится к -setNeedsDisplay. Хотя, как обсуждалось ранее, ни одно из них не относится к тому, что появляется на экране. layoutIfNeeded обновляет кадр представления, а displayIfNeeded обновляет текстуру рендеринга, но это не связано с тем, что появляется на экране. Представьте, что каждый UIView имеет переменную UIImage, которая представляет собой хранилище поддержки (фактически на CALayer, или ниже, и не является UIImage, но это иллюстрация). Повторное изменение этого вида просто обновляет UIImage. Но UIImage - это всего лишь данные, а не графические изображения на экране, пока что-то не нарисовано на экране.

Итак, как сделать UIView на экране?

Раньше я писал псевдокод UIKits main render runloop. До сих пор в моих ответах я игнорировал значительную часть UIKit, но не все это работает внутри вашего процесса. Удивительное количество вещей UIKit, связанных с отображением вещей, на самом деле происходит в процессе сервера рендеринга, а не в ваших приложениях. Сервер сервера/окна рендеринга был SpringBoard (пользовательский интерфейс для домашнего экрана) до iOS 6 (с тех пор BackBoard и FrontBoard поглотили множество SpringBoards более функциональными функциями, связанными с ОС, в результате чего он больше фокусировался на главном пользовательском интерфейсе операционной системы. экран/блокировка экрана/центр уведомлений/центр управления/переключатель приложений/и т.д.).

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

-(void)runloop
{
    //... do touch handling and other events, etc...

    UIWindow.allWindows.layoutIfNeeded() // effectively call layoutIfNeeded on everything
    UIWindow.allWindows.recursivelyDisplay() // call -drawRect: on things that need to be rerendered

    // Sends all the changes to the render server process to actually make these changes appear on screen
    // CATransaction.flush() which means:
    CoreAnimation.commit_layers_animations_to_WindowServer()
} 

Это имеет смысл, одно замораживание приложения iOS не должно блокировать все устройство. На самом деле мы можем продемонстрировать это на iPad с двумя приложениями, работающими бок о бок. Когда мы заставляем замораживать, другое не затрагивается.

Это два пустых шаблона приложений, которые я создал, и вставлял один и тот же код в оба. Оба должны иметь текущее время в метке в середине экрана. Когда я нажимаю кнопку "заморозить", он вызывает sleep(1) и зависает от приложения. Все останавливается. Но iOS в целом все в порядке. Другие приложения, центр управления, центр уведомлений и т.д. Все это не подвержено влиянию.

Будет ли UIKit работать "как вы говорите" {... "отправить сообщение, чтобы остановить видеокадры. сделайте всю нашу работу! Отправьте еще одно сообщение, чтобы снова начать видео!" }

В приложении нет команды UIKit stop video frames, потому что ваше приложение вообще не имеет контроля над экраном. Экран будет обновляться с 60FPS, используя любой фрейм, который дает сервер окон. Окно-сервер будет создавать новый кадр для отображения на 60FPS, используя последние известные позиции, текстуры и деревья слоев, с которыми ваше приложение дало ему работать.
Когда вы замораживаете основной поток в своем приложении, строка CoreAnimation.commitLayersAnimationsToWindowServer(), которая работает последней (после вашего дорогого кода add lots of views), блокируется и не запускается. В результате, даже если есть изменения, сервер окон еще не был отправлен, и поэтому он просто продолжает использовать последнее состояние, отправленное для вашего приложения.

Анимация - это еще одна часть UIKit, которая заканчивается процессом, на сервере окон. Если перед sleep(1) в этом примере приложения мы начнем анимацию UIView, мы увидим, что она запустится, тогда метка замерзнет и перестанет обновляться (поскольку sleep() запущен). Однако, несмотря на то, что основной поток приложений заморожен, анимация будет продолжаться независимо.

func freezePressed() {
    var newFrame = animationView.frame
    newFrame.origin.y = 600

    UIView.animate(withDuration: 3, animations: { [weak self] in
        self?.animationView.frame = newFrame
    })

    // Wait for the animation to have a chance to start, then try to freeze it
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        NSLog("Before freeze");
        sleep(2) // block the main thread for 2 seconds
        NSLog("After freeze");
    }
}

Это результат:

На самом деле мы можем пойти лучше.

Если мы изменим метод freezePressed() на это.

func freezePressed() {
    var newFrame = animationView.frame
    newFrame.origin.y = 600

    UIView.animate(withDuration: 4, animations: { [weak self] in
        self?.animationView.frame = newFrame
    })

    // Wait for the animation to have a chance to start, then try to freeze it
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
        // Do a lot of UI changes, these should completely change the view, cancel its animation and move it somewhere else
        self?.animationView.backgroundColor = .red
        self?.animationView.layer.removeAllAnimations()
        newFrame.origin.y = 0
        newFrame.origin.x = 200
        self?.animationView.frame = newFrame
        sleep(2) // block the main thread for 2 seconds, this will prevent any of the above changes from actually taking place
    }
}

Теперь без вызова sleep(2) анимация будет работать в течение 0,2 секунды, после чего она будет отменена, и представление будет перемещено в другую часть экрана другого цвета. Тем не менее, вызов сна блокирует основной поток в течение 2 секунд, что означает, что ни одно из этих изменений не отправляется на оконный сервер до тех пор, пока большая часть пути через анимацию не будет.

И только для подтверждения здесь результат с вычеркнутой строкой sleep().

Это, надеюсь, объяснит, что происходит. Эти изменения похожи на UIViews, которые вы добавляете в свой вопрос. Они помещаются в очередь, чтобы быть включенными в следующее обновление, но поскольку вы блокируете основной поток, отправляя так много за один раз, вы прекращаете отправку сообщения, которое будет включать их в следующий кадр. Следующий кадр не блокируется, iOS создаст новый фрейм, в котором будут показаны все обновления, полученные от SpringBoard, и другое приложение iOS. Но поскольку ваше приложение по-прежнему блокирует свой основной поток, iOS не получает никаких обновлений из вашего приложения и поэтому не будет показывать никаких изменений (если у него нет изменений, например анимаций, уже поставленных на очередь на оконном сервере).

Итак, чтобы суммировать

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

Ответ 3

Это своего рода решение. Но это не инженерное дело.

Собственно, да. Добавляя задержку, вы делаете именно то, что, как вы сказали, хотите сделать: вы разрешаете выполнить runloop и выполняете макет, и как только это будет сделано, снова зайдите в основной поток. Это, по сути, одно из моих основных видов использования delay. (Возможно, вы даже сможете использовать delay нуля.)

Ответ 4

3 метода, которые могут работать ниже. Первое, что я мог бы заставить его работать, если subview также добавляет контроллер, если он не находится непосредственно в контроллере представления. Второй - это пользовательский вид:) Кажется, вам интересно, когда layoutSubviews закончен в представлении для меня. Этот непрерывный процесс - это то, что замерзает дисплей из-за 1000 subviews плюс последовательно. В зависимости от вашей ситуации вы можете добавить представление childviewcontroller и отправить уведомление, когда viewDidLayoutSubviews() закончено, но я не знаю, соответствует ли это вашему прецеденту. Я тестировал с 1000 подписчиков на просмотр viewcontroller и работал. В этом случае задержка 0 будет делать именно то, что вы хотите. Вот рабочий пример.

 import UIKit
 class TrackingViewController: UIViewController {

    var layoutCount = 0
    override func viewDidLoad() {
        super.viewDidLoad()

          // Add a bunch of subviews
    for _ in 0...1000{
        let view = UIView(frame: self.view.bounds)
        view.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        view.backgroundColor = UIColor.green
        self.view.addSubview(view)
    }

}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("Called \(layoutCount)")
    if layoutCount == 1{
        //finished because first call was an emptyview
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }
    layoutCount += 1
} }

Затем в вашем основном контроллере просмотра, который вы добавляете subviews, вы можете это сделать.

import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 0
    var count = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.finishedLayoutAddAnother), name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4, execute: {
            //add first view
            self.finishedLayoutAddAnother()
        })
    }

    deinit {
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    func finishedLayoutAddAnother(){
        print("We are finished with the layout of last addition and we are displaying")
        addView()
    }

    func addView(){
        // we keep adding views just to cause
        print("Fired \(Date())")
        if count < 100{
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {

                // let test = TestSubView(frame: CGRect(x: self.view.bounds.midX - 50, y: y, width: 50, height: 20))
                let trackerVC = TrackingViewController()
                trackerVC.view.frame = CGRect(x: self.view.bounds.midX - 50, y: self.y, width: 50, height: 20)
                trackerVC.view.backgroundColor = UIColor.red
                self.view.addSubview(trackerVC.view)
                trackerVC.didMove(toParentViewController: self)
                self.y += 30
                self.count += 1
            })
        }
    }
}

Или существует ДАЛЕЕ более сумасшедший путь и, вероятно, лучший способ. Создайте свой собственный взгляд, который в некотором смысле сохраняет свое время и перезванивает, когда хорошо не отбрасывать кадры. Это неполировано, но будет работать.

завершение gif

import UIKit
class CompletionView: UIView {
    private var lastUpdate : TimeInterval = 0.0
    private var checkTimer : Timer!
    private var milliSecTimer : Timer!
    var adding = false
    private var action : (()->Void)?
    //just for testing
    private var y : CGFloat = 0
    private var x : CGFloat = 0
    //just for testing
    var randomColors = [UIColor.purple,UIColor.gray,UIColor.green,UIColor.green]


    init(frame: CGRect,targetAction:(()->Void)?) {
        super.init(frame: frame)
        action = targetAction
        adding = true
        for i in 0...999{
            if y > bounds.height - bounds.height/100{
                y -= bounds.height/100
            }

            let v = UIView(frame: CGRect(x: x, y: y, width: bounds.width/10, height: bounds.height/100))
            x += bounds.width/10
            if i % 9 == 0{
                x = 0
                y += bounds.height/100
            }

            v.backgroundColor = randomColors[Int(arc4random_uniform(4))]
            self.addSubview(v)
        }

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func milliSecCounting(){
        lastUpdate += 0.001
    }

    func checkDate(){
        //length of 1 frame
        if lastUpdate >= 0.003{
            checkTimer.invalidate()
            checkTimer = nil
            milliSecTimer.invalidate()
            milliSecTimer = nil
            print("notify \(lastUpdate)")
            adding = false
            if let _ = action{
                self.action!()
            }
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        lastUpdate = 0.0
        if checkTimer == nil && adding == true{
            checkTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(CompletionView.checkDate), userInfo: nil, repeats: true)
        }

        if milliSecTimer == nil && adding == true{
             milliSecTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(CompletionView.milliSecCounting), userInfo: nil, repeats: true)
        }
    }
}


import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 30
    override func viewDidLoad() {
        super.viewDidLoad()
        // Wait 3 seconds to give the sim time
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
            [weak self] in
            self?.addView()
        })
    }

    var count = 0
    func addView(){
        print("starting")
        if count < 20{
            let completionView = CompletionView(frame: CGRect(x: 0, y: self.y, width: 100, height: 100), targetAction: {
                [weak self] in
                self?.count += 1
                self?.addView()
                print("finished")
            })
            self.y += 105
            completionView.backgroundColor = UIColor.blue
            self.view.addSubview(completionView)
        }
    }
}

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

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
  //code})

Ответ 5

Используйте NSNotification для достижения необходимого эффекта.

Сначала зарегистрируйте наблюдателя в главном представлении и создайте обработчик наблюдателя.

Затем инициализируйте все эти объекты A, B, C... в отдельном потоке (например, фон), например, self performSelectorInBackground

Затем - отправьте уведомление из подпунктов, а последнее - performSelectorOnMainThread, чтобы добавить subview в желаемом порядке с необходимыми задержками.
Чтобы ответить на вопросы в комментариях, скажем, у вас есть UIViewController, который был показан на экране. Этот объект - не точка обсуждения, и вы можете решить, куда поместить код, контролируя внешний вид. Код предназначен для объекта UIViewController (поэтому он сам). View - некоторый объект UIView, рассматриваемый как родительский вид. ViewN - одно из подзонов. Его можно масштабировать позже.

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(handleNotification:) 
    name:@"ViewNotification"
    object:nil];

Это зарегистрировало наблюдателя, необходимого для связи между потоками.   ViewN * V1 = [[ViewN alloc] init]; Здесь - могут быть выделены subviews - пока не показаны.

- (void) handleNotification: (id) note {
    ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
    [self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}

Этот обработчик позволяет получать сообщения и помещать объект UIView в родительское представление. Выглядит странно, но дело в том, что вам нужно выполнить метод addSubview по основному потоку. performSelectorOnMainThread позволяет начать добавлять subview в основной поток, не блокируя выполнение приложения.

Теперь - мы создаем метод, который помещает subviews на экран.

-(void) sendToScreen: (id) obj {
    NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}

Этот метод будет отправлять уведомления из любого потока, отправляя объект как элемент NSDictionary с именем ViewArrived.
И, наконец, мнения, которые необходимо добавить с задержкой в ​​3 секунды:

-(void) initViews {
    ViewN * V1 = [[ViewN alloc] init];
    ViewN * V2 = [[ViewN alloc] init];
    ViewN * V3 = [[ViewN alloc] init];
    [self performSelector: @selector(sendToScreen:) withObject: V1 afterDelay: 3.0];
    [self performSelector: @selector(sendToScreen:) withObject: V2 afterDelay: 6.0];
    [self performSelector: @selector(sendToScreen:) withObject: V3 afterDelay: 9.0];
}

Это не единственное решение. Также можно управлять надзорами родительского представления, считая свойство NSArray subviews.
В любом случае вы можете запускать метод initViews всякий раз, когда вам нужно, и даже в фоновом потоке, и он позволяет управлять внешним видом, механизм performSelector позволяет избежать блокировки выполнения потока.