it-swarm-ru.tech

iOS: использование URIiew 'drawRect:' против делегата его слоя 'drawLayer: inContext:'

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

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

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

    [[self layer] setDelegate:self];

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

71
Itamar Katz

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

Всегда используйте 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().

72
Nathan Eror

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

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

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

Основные основы анимации

Понимание рендеринга UIKit

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

View Programming Guide: Оптимизация вида чертежа

44
quellish

Вот примеры 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
{
    ...
}
12
Dennis Fan

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

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

drawRect(_:)

Если вам не нужно получать доступ к текущему значению свойства layer/view во время его анимации, вы можете просто использовать 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 не возвращает правильное (то есть текущее) значение, пока анимация находится в полете. Вместо этого он возвращает окончательное значение (то есть значение для завершения анимации).

Чтобы получить текущее значение во время анимации, необходимо получить доступ к backgroundColor параметра layer передано 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 использует это, чтобы определить, делаете ли вы пользовательский рисунок или нет.

Вероятно, свойства больше не работают, потому что собственная реализация UIViewdrawLayer(_: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.
}
8
fluidsonic

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

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

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

7
Ole Begemann

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

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

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

2
jamie

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

0
Singer Quincy