iOS 12, Xcode 10: UIView setNeedsDisplay(_:) кажется неработающим

После обновления до Xcode 10 я понял, что подпрограмма draw(_ rect: CGRect) моего пользовательского UIView (класс, производный от UIView) в моем приложении вызывалась с неправильным rect. Действительно, он всегда вызывается с rect, являющимся полным кадром базового UIView, вместо rect, определяемого setNeedsDisplay(_ rect: CGRect).

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

import Foundation
import UIKit
import PlaygroundSupport

class CustomView: UIView {
    override func draw(_ rect: CGRect) {
        print("rect = \(rect)")
    }
}

let customView = CustomView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 200.0, height: 200.0)))
PlaygroundPage.current.liveView = customView
print("test")
customView.setNeedsDisplay(CGRect(origin: CGPoint.zero, size: CGSize(width: 100.0, height: 100.0)))

Результат, который я получаю,

прямоугольник = (0,0, 0,0, 200,0, 200,0)
проверка
прямоугольник = (0,0, 0,0, 200,0, 200,0)

Первый напечатанный вывод для rect - это стандартная полная перерисовка представления, но второй после печати "test" приводит к проблеме. Выходные данные получаются в результате перерисовки из-за вызова customView.setNeedsDisplay перед этим и должны представлять собой меньший указанный прямоугольник (0.0, 0.0, 100.0, 100.0).

Итак, мои очевидные вопросы:

  • Можете ли вы воспроизвести это поведение?
  • Я упускаю что-то очевидное?
  • Это ошибка?

person Wizard    schedule 04.10.2018    source источник


Ответы (2)


Я тестировал это в Xcode 9, 10 и 10.1.

Поведение определенно изменилось между iOS 11 и iOS 12/12.1.

В документации или заголовочном файле нет указаний на то, что это было сделано намеренно.

Похоже на ошибку для меня.

person Ashley Mills    schedule 04.10.2018
comment
Я не согласен, что это ошибка. Это изменение в поведении, но я не могу найти никакой документации, указывающей, что аргумент setNeedsDisplay(_:) будет использоваться для следующего вызова draw(_:). - person Mats; 04.10.2018
comment
Документация, безусловно, подразумевает это. Для setNeedsDisplay(_:): помечает указанный прямоугольник приемника как требующий перерисовки, а для draw(_:): во время последующих операций рисования прямоугольник может определять только часть вашего вида. - person Pete Morris; 04.10.2018
comment
Я думаю, что это абсолютно необходимая функциональность setNeedsDisplay(_ rect: CGRect). Я также думаю, что это предполагаемый способ обновления частичных представлений. Конечно, можно было вызвать draw напрямую, но это нарушило бы шаблон проектирования обновления. - person Wizard; 04.10.2018
comment
Возможно, стоит спросить на форумах разработчиков Apple и посмотреть, ответит ли кто-нибудь из Apple. - person Ashley Mills; 04.10.2018
comment
Разместил его на форумах разработчиков Apple. Давайте посмотрим. - person Wizard; 04.10.2018
comment
Это тоже сбило меня с толку, поэтому я добавил ответ ниже, который объясняет причину и намерение на случай, если кто-то все еще ищет полный ответ. - person Jon Hocking; 05.04.2019

На самом деле это сделано намеренно с новой функцией динамического резервного хранилища iOS 12.

Что такое резервный магазин

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

Если бы вы рисовали изображение в градациях серого, но память была выделена для широкой цветовой гаммы, это привело бы к большому количеству пустой выделенной памяти (оттенки серого занимают меньше места, чем RGBA). Чтобы обойти это, функция динамического резервного хранилища работает, рисуя все содержимое представления, а ЗАТЕМ определяя, сколько памяти ему нужно, вместо того, чтобы предполагать, что все с самого начала нуждается в широкой цветовой поддержке.

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

Как это обойти

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

Вы можете выбрать три варианта, которые относятся к оттенкам серого, RGBA 8 бит и RGBA 16 бит (широкий цвет).

поэтому просто позвоните:

layer.contentsFormat = .RGBA16Float

и ваш setNeedsDisplay(_ rect: CGRect) снова начнет работать как положено

Вы можете прочитать об этом свойстве здесь: https://developer.apple.com/documentation/quartzcore/calayer/1792104-contentsformat

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

https://developer.apple.com/videos/play/wwdc2018/219/?time=1451

person Jon Hocking    schedule 05.04.2019