iOS: использование URIiew 'drawRect:' против делегата его слоя 'drawLayer: inContext:' - PullRequest
71 голосов
/ 12 февраля 2011

У меня есть класс, который является подклассом UIView. Я могу рисовать вещи внутри представления либо путем реализации метода drawRect, либо путем реализации drawLayer:inContext:, который является методом делегата CALayer.

У меня два вопроса:

  1. Как решить, какой подход использовать? Есть ли варианты использования для каждого из них?
  2. Если я реализую drawLayer:inContext:, он вызывается (а drawRect нет, по крайней мере, насколько можно судить по установке точки останова), даже если я не назначаю свое представление как CALayer делегировать с помощью:

    [[self layer] setDelegate:self];

    почему вызывается метод делегата, если мой экземпляр не определен как делегат слоя? и какой механизм предотвращает вызов drawRect, если вызывается drawLayer:inContext:?

Ответы [ 7 ]

72 голосов
/ 12 февраля 2011

Как решить, какой подход использовать? Есть ли варианты использования для каждого из них?

Всегда используйте drawRect: и никогда не используйте UIView в качестве делегата чертежа для любого CALayer.

Почему метод делегата вызывается, если мой экземпляр не определен как делегат слоя? и какой механизм предотвращает вызов drawRect, если вызывается drawLayer:inContext:?

Каждый экземпляр UIView является делегатом чертежа для своей поддержки CALayer. Вот почему [[self layer] setDelegate:self];, похоже, ничего не делал. Это избыточно. Метод drawRect: фактически является методом делегирования чертежа для слоя вида. Внутренне, UIView реализует drawLayer:inContext:, где он делает что-то свое и затем вызывает drawRect:. Вы можете увидеть это в отладчике:

drawRect: stacktrace

Вот почему drawRect: никогда не вызывался, когда вы реализовали drawLayer:inContext:. Именно поэтому вам никогда не следует реализовывать какие-либо методы делегирования CALayer в пользовательском подклассе UIView. Вы также никогда не должны делать какой-либо вид делегата рисунка для другого слоя. Это вызовет все виды дурачества.

Если вы используете drawLayer:inContext:, потому что вам нужен доступ к CGContextRef, вы можете получить его изнутри вашего drawRect:, позвонив по номеру UIGraphicsGetCurrentContext().

44 голосов
/ 08 февраля 2012

drawRect следует применять только в случае крайней необходимости.Реализация по умолчанию drawRect включает ряд интеллектуальных оптимизаций, таких как интеллектуальное кэширование рендеринга представления.Переопределение обходит все эти оптимизации.Это плохо.Эффективное использование методов рисования слоев почти всегда превосходит пользовательские drawRect.Apple часто использует UIView в качестве делегата для CALayer - фактически каждый UIView является делегатом своего слоя .Вы можете увидеть, как настроить рисование слоя внутри UIView в нескольких примерах Apple, включая (в настоящее время) ZoomingPDFViewer.

Хотя использование drawRect является обычным явлением, эта практика не приветствуется, поскольку, по крайней мере,2002/2003, IIRC.По этому пути осталось не так много веских причин.

Усовершенствованная оптимизация производительности на iPhone OS (слайд 15)

Основные сведения об анимации

Общее представление о рендеринге UIKit

Технические вопросы и ответы QA1708: Улучшение производительности рисования изображений на iOS

Просмотр руководства по программированию: Оптимизация вида чертежа

12 голосов
/ 03 февраля 2012

Вот примеры Sample ZoomingPDFViewer от Apple:

-(void)drawRect:(CGRect)r
{

    // UIView uses the existence of -drawRect: to determine if it should allow its CALayer
    // to be invalidated, which would then lead to the layer creating a backing store and
    // -drawLayer:inContext: being called.
    // By implementing an empty -drawRect: method, we allow UIKit to continue to implement
    // this logic, while doing our real drawing work inside of -drawLayer:inContext:

}

-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    ...
}
8 голосов
/ 17 марта 2016

Используете ли вы drawLayer(_:inContext:) или drawRect(_:) (или оба) для пользовательского кода рисования, зависит от того, нужен ли вам доступ к текущему значению свойства слоя во время его анимации.

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

drawRect(_:)

Если вам не нужно получать доступ к текущему значению свойства слоя / вида во время его анимации, вы можете просто использовать drawRect(_:), чтобы выполнить свой собственный рисунок.Все будет отлично работать.

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

drawLayer(_:inContext:)

Скажем, например, что вы хотите использовать backgroundColor в своем пользовательском коде рисования:

override func drawRect(rect: CGRect) {
    let colorForCustomDrawing = self.layer.backgroundColor
    // your custom drawing code
}

Когда вы протестируете свой код, вы заметите, что backgroundColor не возвращает правильное (то есть текущее) значение, пока анимация находится в полете.Вместо этого он возвращает окончательное значение (т. Е. Значение для завершения анимации).

Чтобы получить значение current во время анимации, необходимо получить доступ к backgroundColorlayer параметр передан в drawLayer(_:inContext:).И вы также должны нарисовать параметр context .

Очень важно знать, что self.layer представления и параметр layer переданы в drawLayer(_:inContext:)не всегда один и тот же слой!Последний может быть копией первого с частичной анимацией, уже примененной к его свойствам. Таким образом, вы можете получить доступ к правильным значениям свойств анимации в полете.

Теперь чертеж работает, как и ожидалось:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}

Но есть две новые проблемы: setNeedsDisplay() и несколько свойств, таких как backgroundColor и opaque, больше не работают для вашего вида.UIView больше не переадресовывает вызовы и не переключается на свой собственный слой.

setNeedsDisplay() делает что-то, только если ваше представление реализует drawRect(_:).Не имеет значения, действительно ли функция что-то делает, но UIKit использует ее, чтобы определить, выполняете ли вы пользовательское рисование или нет.

Свойства, вероятно, больше не работают, потому что собственная реализация UIView drawLayer(_:inContext:) больше не вызывается.

Так что решение довольно простое.Просто вызовите реализацию суперкласса drawLayer(_:inContext:) и реализуйте пустую drawRect(_:):

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}

Сводка

Используйте drawRect(_:), пока у вас нетпроблема в том, что свойства возвращают неправильные значения во время анимации:

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

Используйте drawLayer(_:inContext:) и drawRect(_:), если вам нужно получить доступ к текущему значению свойств вида / слоя, пока они находятсяанимированные:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}
7 голосов
/ 12 февраля 2011

В iOS перекрытие между видом и его слоем очень велико.По умолчанию представление является делегатом своего уровня и реализует метод уровня drawLayer:inContext:.Насколько я понимаю, drawRect: и drawLayer:inContext: более или менее эквивалентны в этом случае.Возможно, реализация по умолчанию drawLayer:inContext: звонков drawRect: или drawRect: вызывается, только если drawLayer:inContext: не реализован вашим подклассом.

Как решить, какой подход использовать?Есть ли варианты использования для каждого из них?

Это не имеет значения.Чтобы следовать соглашению, я обычно использовал бы drawRect: и резервировал бы использование drawLayer:inContext:, когда мне фактически нужно рисовать пользовательские подслои, которые не являются частью представления.

2 голосов
/ 27 июля 2011

В документации Apple есть следующее: «Существуют и другие способы предоставления содержимого представления, например, непосредственная установка содержимого нижележащего слоя, но переопределение метода drawRect: является наиболее распространенным.техника. "

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

Делегат слоя UIView направлен на UIView.Однако UIView ведет себя по-разному в зависимости от того, реализован ли метод drawRect: или нет.Например, если вы устанавливаете свойства непосредственно на слой (например, цвет фона или радиус угла), эти значения перезаписываются, если у вас есть метод drawRect: - даже если он полностью пуст (то есть даже не вызывает super).

0 голосов
/ 30 апреля 2019

Для представлений со слоями с пользовательским содержимым вы должны продолжать переопределять методы представления для рисования.Представление на уровне слоя автоматически делает себя делегатом своего уровня и реализует необходимые методы делегата, и вы не должны изменять эту конфигурацию.Вместо этого вы должны реализовать свой метод drawRect: метод для рисования вашего контента. Руководство по программированию базовой анимации

...