Каков наиболее надежный способ заставить UIView перерисовываться?

117

У меня есть UITableView со списком элементов. Выбор элемента подталкивает viewController, который затем выполняет следующие действия. из метода viewDidLoad я запускаю URLRequest для данных, которые требуются для одного из моих подпредставлений - подкласса UIView с переопределенным drawRect. Когда данные поступают из облака, я начинаю строить свою иерархию представлений. рассматриваемому подклассу передаются данные, и его метод drawRect теперь имеет все необходимое для визуализации.

Но.

Поскольку я не вызываю drawRect явно - Cocoa-Touch обрабатывает это - у меня нет способа сообщить Cocoa-Touch, что я действительно, очень хочу, чтобы этот подкласс UIView отображался. Когда? Теперь было бы хорошо!

Я пробовал [myView setNeedsDisplay]. Иногда это срабатывает. Очень пятнистый.

Я борюсь с этим часами. Может ли кто-нибудь, кто предоставит мне надежный и надежный подход к принудительному повторному рендерингу UIView.

Вот фрагмент кода, который передает данные в представление:

// Create the subview
self.chromosomeBlockView = [[[ChromosomeBlockView alloc] initWithFrame:frame] autorelease];

// Set some properties
self.chromosomeBlockView.sequenceString     = self.sequenceString;
self.chromosomeBlockView.nucleotideBases    = self.nucleotideLettersDictionary;

// Insert the view in the view hierarchy
[self.containerView          addSubview:self.chromosomeBlockView];
[self.containerView bringSubviewToFront:self.chromosomeBlockView];

// A vain attempt to convince Cocoa-Touch that this view is worthy of being displayed ;-)
[self.chromosomeBlockView setNeedsDisplay];

Ура, Дуг

dugla
источник

Ответы:

193

Гарантированный и надежный способ принудительного UIViewповторного рендеринга - это [myView setNeedsDisplay]. Если у вас возникли проблемы с этим, вы, вероятно, столкнулись с одной из следующих проблем:

  • Вы вызываете его до того, как у вас действительно есть данные, или вы -drawRect:что-то чрезмерно кэшируете.

  • Вы ожидаете, что представление будет отрисовано в момент вызова этого метода. Нет намеренно никакого способа потребовать «рисовать прямо сейчас, в эту самую секунду» с использованием системы рисования Какао. Это нарушит всю систему компоновки представления, приведет к потере производительности и, вероятно, создаст всевозможные артефакты. Есть только способы сказать «это нужно нарисовать в следующем цикле рисования».

Если вам нужно «немного логики, рисовать, еще немного логики», тогда вам нужно поместить «еще немного логики» в отдельный метод и вызывать его, используя -performSelector:withObject:afterDelay:с задержкой 0. Это добавит «еще немного логики» после следующий цикл розыгрыша. См. Этот вопрос для примера такого кода и случая, когда он может понадобиться (хотя обычно лучше искать другие решения, если это возможно, поскольку это усложняет код).

Если вы не думаете, что дела идут плохо, поставьте точку останова -drawRect:и посмотрите, когда вам позвонят. Если вы звоните -setNeedsDisplay, но -drawRect:не получаете вызова в следующем цикле событий, то покопайтесь в своей иерархии представлений и убедитесь, что вы не пытаетесь где-то перехитрить. По моему опыту, чрезмерная сообразительность - причина №1 плохого рисования. Когда вы думаете, что лучше всего знаете, как заставить систему делать то, что вы хотите, вы обычно заставляете ее делать именно то, чего вы не хотите.

Роб Напье
источник
Роб, вот мой контрольный список. 1) Есть ли у меня данные? Да. Я создаю представление в методе hideSpinner, который вызывается в основном потоке из connectionDidFinishLoading: таким образом: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Мне не нужно рисовать сразу. Мне это просто нужно. Cегодня. В настоящее время это совершенно случайно и вне моего контроля. [myView setNeedsDisplay] совершенно ненадежен. Я даже зашел так далеко, что вызвал [myView setNeedsDisplay] в viewDidAppear :. Nuthin. Какао меня просто игнорирует. Maddening !!
dugla 01
1
Я обнаружил, что при таком подходе часто возникает задержка между setNeedsDisplayвызовом и фактическим вызовом drawRect:. Хотя надежность вызова здесь и здесь, я бы не назвал это «самым надежным» решением - надежное решение для рисования теоретически должно выполнять все рисование непосредственно перед возвратом к клиентскому коду, который требует рисования.
Слипп Д. Томпсон
1
Я прокомментировал ваш ответ более подробно. Попытка заставить систему рисования выйти из строя путем вызова методов, о которых в документации явно говорится, что не следует вызывать, не является надежным. В лучшем случае это неопределенное поведение. Скорее всего, это ухудшит производительность и качество рисования. В худшем случае он может зайти в тупик или выйти из строя.
Роб Нэпир
1
Если вам нужно нарисовать перед возвращением, рисуйте в контексте изображения (который работает так же, как и холст, которого ожидают многие люди). Но не пытайтесь обмануть систему композитинга. Он разработан для быстрого предоставления высококачественной графики с минимальными затратами ресурсов. Частично это объединяет шаги рисования в конце цикла выполнения.
Роб Нэпир
1
@RobNapier Я не понимаю, почему ты так защищаешься. Проверьте еще раз, пожалуйста; нет нарушения API (хотя он был очищен на основе вашего предложения, я бы сказал, что вызов собственного метода не является нарушением), ни шанса на тупик или сбой. Это тоже не «уловка»; он обычно использует setNeedsDisplay / displayIfNeeded CALayer. Более того, я уже довольно давно использую его, чтобы рисовать с помощью Quartz параллельно с GLKView / OpenGL; оно оказалось безопасным, стабильным и быстрым, чем указанное вами решение. Если вы мне не верите, попробуйте. Тебе здесь нечего терять.
Слипп Д. Томпсон
52

У меня возникла проблема с большой задержкой между вызовами setNeedsDisplay и drawRect: (5 секунд). Оказалось, что я вызвал setNeedsDisplay в другом потоке, чем основной поток. После перемещения этого вызова в основной поток задержка исчезла.

Надеюсь, это поможет.

Вернер Альтевишер
источник
Это определенно была моя ошибка. У меня создалось впечатление, что я работаю в основном потоке, но только после того, как я NSLogged NSThread.isMainThread понял, что есть угловой случай, когда я НЕ вносил изменения уровня в основной поток. Спасибо, что спас меня от выдергивания волос!
Mr. T
14

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

В своем подклассе UIView создайте displayNow()метод, который сообщает слою « установить курс для отображения », а затем « сделать его таким »:

стриж

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
public func displayNow()
{
    let layer = self.layer
    layer.setNeedsDisplay()
    layer.displayIfNeeded()
}

Objective-C

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
- (void)displayNow
{
    CALayer *layer = self.layer;
    [layer setNeedsDisplay];
    [layer displayIfNeeded];
}

Также реализуйте draw(_: CALayer, in: CGContext)метод, который будет вызывать ваш частный / внутренний метод рисования (который работает, поскольку каждый UIViewравен a CALayerDelegate) :

стриж

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
override func draw(_ layer: CALayer, in context: CGContext)
{
    UIGraphicsPushContext(context)
    internalDraw(self.bounds)
    UIGraphicsPopContext()
}

Objective-C

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self internalDrawWithRect:self.bounds];
    UIGraphicsPopContext();
}

И создайте свой собственный internalDraw(_: CGRect)метод вместе с отказоустойчивым draw(_: CGRect):

стриж

/// Internal drawing method; naming's up to you.
func internalDraw(_ rect: CGRect)
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
override func draw(_ rect: CGRect) {
    internalDraw(rect)
}

Objective-C

/// Internal drawing method; naming's up to you.
- (void)internalDrawWithRect:(CGRect)rect
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
- (void)drawRect:(CGRect)rect {
    [self internalDrawWithRect:rect];
}

А теперь просто звоните, myView.displayNow()когда вам действительно-действительно нужно отрисовать (например, из CADisplayLinkобратного вызова) . Наш displayNow()метод сообщит объекту CALayerto displayIfNeeded(), который синхронно обратится к нам draw(_:,in:)и выполнит рисование internalDraw(_:), обновляя визуал тем, что нарисовано в контексте, прежде чем двигаться дальше.


Этот подход аналогичен описанному выше в @ RobNapier, но имеет преимущество вызова displayIfNeeded()в дополнение к setNeedsDisplay(), что делает его синхронным.

Это возможно, потому что CALayers предоставляют больше возможностей рисования, чем UIViews, - слои являются более низкими, чем представления, и разработаны специально для целей высоконастраиваемого рисования в макете, и (как и многие вещи в Какао) спроектированы для гибкого использования ( как родительский класс, или как делегатор, или как мост к другим системам рисования, или просто самостоятельно). CALayerDelegateВсе это возможно благодаря правильному использованию протокола.

Более подробную информацию о возможности настройки CALayers можно найти в разделе « Настройка объектов слоя» Руководства по программированию базовой анимации .

Слипп Д. Томпсон
источник
Обратите внимание, что в документации для drawRect:явно сказано: «Вы никогда не должны вызывать этот метод напрямую». Кроме того, CALayer displayявно говорит: «Не вызывайте этот метод напрямую». Если вы хотите синхронно рисовать на слоях contentsнапрямую, нет необходимости нарушать эти правила. Вы можете просто рисовать на слое в contentsлюбое время (даже в фоновых потоках). Просто добавьте для этого подслой в представление. Но это отличается от того, чтобы вывести его на экран, когда нужно дождаться правильного времени композитинга.
Роб Нэпир
Я предлагаю добавить подслой, поскольку документы также предупреждают о возможном вмешательстве напрямую со слоем UIView contents(«Если объект слоя привязан к объекту представления, вам следует избегать установки содержимого этого свойства напрямую. Взаимодействие между представлениями и слоями обычно приводит к в представлении, заменяющем содержимое этого свойства во время последующего обновления. ") Я не особо рекомендую этот подход; Преждевременное рисование снижает производительность и качество рисования. Но если он вам нужен по какой-то причине, то contentsвот как это получить.
Роб Нэпир
@RobNapier Точка, полученная при drawRect:прямом вызове . Не было необходимости демонстрировать эту технику, и она была исправлена.
Слипп Д. Томпсон
@RobNapier Что касается contentsтехники, которую вы предлагаете ... звучит заманчиво. Сначала я пробовал что-то подобное, но не мог заставить его работать, и обнаружил, что указанное выше решение содержит гораздо меньше кода без каких-либо причин не быть таким же производительным. Однако, если у вас есть рабочее решение для этого contentsподхода, мне было бы интересно его прочитать (нет причин, по которым у вас не может быть двух ответов на этот вопрос, верно?)
Слипп Д. Томпсон,
@RobNapier Примечание: это не имеет ничего общего с CALayer display. Это просто настраиваемый общедоступный метод в подклассе UIView, точно такой же, как в GLKView (еще один подкласс UIView, и единственный известный мне Apple, который требует функциональности DRAW NOW! ).
Слипп Д. Томпсон
5

У меня была такая же проблема, и все решения от SO или Google у меня не работали. Обычно setNeedsDisplayэто работает, но когда это не так ...
Я пробовал вызывать setNeedsDisplayпредставление всеми возможными способами из всех возможных потоков и прочего - все равно безуспешно. Мы знаем, как сказал Роб, что

«это нужно нарисовать в следующем цикле рисования».

Но почему-то на этот раз не сыграла. И единственное решение, которое я нашел, - это вызвать его вручную через некоторое время, чтобы все, что блокирует розыгрыш, исчезло, например:

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 
                                        (int64_t)(0.005 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
    [viewToRefresh setNeedsDisplay];
});

Это хорошее решение, если вам не нужно очень часто перерисовывать представление. В противном случае, если вы занимаетесь движением (действием), обычно не возникает проблем с простым вызовом setNeedsDisplay.

Надеюсь, это поможет кому-то, кто там заблудился, как и я.

dreamzor
источник
0

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

Вы делаете это UITableView didSelectRowAtIndexPathасинхронно, запрашивая данные. Получив ответ, вы вручную выполняете переход и передаете данные вашему viewController в prepareForSegue. Между тем вы можете захотеть показать какой-нибудь индикатор активности, для простой проверки индикатора загрузки https://github.com/jdg/MBProgressHUD

Miki
источник