Який найбільш надійний спосіб змусити 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];

Ура, Дуг

Відповіді:


193

Гарантований, надійний спосіб примусити себе UIViewвідтворити це [myView setNeedsDisplay]. Якщо у вас виникли проблеми з цим, ви, швидше за все, зіткнетесь з одним із цих питань:

  • Ви телефонуєте до того, як фактично отримаєте дані, або ваш -drawRect:щось надмірно кешує.

  • Ви очікуєте, що представлення буде залучено в той момент, коли ви називаєте цей метод. Навмисно немає жодного способу вимагати "намалювати зараз цю саму секунду" за допомогою системи нанесення какао. Це призведе до порушення всієї системи композиційного перегляду, продуктивності сміття і, ймовірно, створить всілякі артефакти. Є лише способи сказати, «це потрібно зробити в наступному циклі жеребкування».

Якщо вам потрібна "деяка логіка, малювання, ще якась логіка", тоді вам потрібно покласти "ще трохи логіки" в окремий метод і викликати його, використовуючи -performSelector:withObject:afterDelay:із затримкою 0. Це додасть "ще трохи логіки" після наступний цикл жеребкування Дивіться це питання на прикладі такого типу коду та випадку, коли він може знадобитися (хоча зазвичай краще шукати інші рішення, якщо це можливо, оскільки це ускладнює код).

Якщо ви не думаєте, що все витягується, поставте точку перерви -drawRect:і подивіться, коли вам дзвонять. Якщо ви дзвоните -setNeedsDisplay, але -drawRect:не отримуєте виклик у наступному циклі подій, тоді зануріться у свою ієрархію перегляду та переконайтесь, що ви не намагаєтесь десь перехитрити. Надмірна кмітливість - перша причина поганого малювання в моєму досвіді. Коли ти думаєш, що ти найкраще знаєш, як обманути систему робити те, що ти хочеш, ти зазвичай змушуєш її робити саме те, чого не хочеш.


Роб, ось мій список. 1) Чи є у мене дані? Так. Я створюю представлення у методі - hidSpinner - викликається головним потоком із з'єднанняDidFinishLoading: таким чином: [self performSelectorOnMainThread: @selector (hidSpinner) withObject: nil waitUntilDone: NO]; 2) Мені не потрібно малювати моментально. Мені просто це потрібно. Сьогодні Наразі це абсолютно випадково і поза моїм контролем. [myView setNeedsDisplay] є абсолютно ненадійним. Я навіть зайшов так далеко, щоб викликати [myView setNeedsDisplay] в viewDidAppear:. Нутін '. Какао мене просто ігнорує. Божевілля !!
dugla

1
Я виявив, що при такому підході часто виникає затримка між setNeedsDisplayі фактичним викликом drawRect:. Хоча надійність виклику тут, я б не називав це "найбільш надійним" рішенням - надійне рішення для малювання теоретично повинно робити все малювання безпосередньо перед поверненням до клієнтського коду, який вимагає нічиї.
Сліпп Д. Томпсон

1
Нижче я прокоментував вашу відповідь з більш детальною інформацією. Намагання змусити систему малювання вийти з ладу методами виклику, документація прямо говорить, що не викликати, не є надійною. У кращому випадку це невизначена поведінка. Швидше за все, це погіршить продуктивність та якість малювання. У гіршому випадку він може зайти в тупик або розбитися.
Роб Нап'єр

1
Якщо вам потрібно намалювати перед поверненням, намалюйте контекст зображення (який працює так, як полотно, яке багато людей очікують). Але не намагайтеся хитрувати систему композиції. Він призначений для забезпечення високоякісної графіки дуже швидко з мінімальними витратами на ресурси. Частиною цього є об'єднання кроків малювання в кінці циклу запуску.
Роб Нап'єр

1
@RobNapier Я не розумію, чому ти стаєш такою оборонною. Перевірте ще раз; немає жодного порушення API (хоча це було очищено на основі вашої пропозиції, я б стверджував, що називати власний метод не є порушенням), а також шансу на тупик або збій. Це також не "фокус"; він використовує набір CALayerNeedsDisplay / displayIfNeed звичайним чином. Крім того, я вже досить довго використовую його для малювання з Quartz способом, паралельним GLKView / OpenGL; це виявляється безпечним, стабільним та швидшим, ніж рішення, яке ви перерахували. Якщо ви мені не вірите, спробуйте. Тут вам нема чого втрачати.
Сліпп Д. Томпсон

52

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

Сподіваюся, це допоможе.


Це точно була моя помилка. У мене було враження, що я працюю на головній нитці, але це не було, поки я NSLogged NSThread.isMainThread не зрозумів, що є кутовий випадок, коли я НЕ вносить зміни шару на основну нитку. Дякуємо, що врятували мене від виривання волосся!
Містер Т

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()
}

Ціль-С

/// 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є 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()
}

Ціль-С

/// 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)
}

Ціль-С

/// 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()метод підкаже CALayerдо displayIfNeeded(), який синхронно передзвонить в наш draw(_:,in:)і зробить малюнок internalDraw(_:), оновивши візуальне з тим, що втягнуто в контекст, перш ніж рухатися далі.


Цей підхід схожий на вищезгаданий @ RobNapier, але має перевагу displayIfNeeded()додаткового виклику setNeedsDisplay(), що робить його синхронним.

Це можливо, тому CALayerщо експонуйте більше функціональних можливостей малювання, ніж UIViewу нас - шари нижчого рівня, ніж представлення, і явно розроблені для малюнка, що легко налаштовується в макеті, і (як і багато речей у какао) призначені для гнучкого використання ( як батьківський клас, або як делегатор, або як міст до інших систем малювання, або просто самостійно). Правильне використання CALayerDelegateпротоколу робить все це можливим.

Більш детальну інформацію про налаштування CALayers можна отримати в розділі Налаштування об'єктів шару в Посібнику з основних анімаційних програм .


Зверніть увагу, що в документації drawRect:прямо написано: "Ніколи не слід безпосередньо називати цей метод". Крім того, CALayer displayпрямо говорить "Не закликайте цей метод безпосередньо". Якщо ви хочете синхронно малювати шари contentsбезпосередньо, немає необхідності порушувати ці правила. Ви можете просто намалювати шар на contentsбудь-який час (навіть на фонових нитках). Просто для цього додайте підшар. Але це інакше, ніж розміщення його на екрані, якому потрібно дочекатися правильного часу композиції.
Роб Нап'єр

Я кажу про додавання підшару, оскільки документи також попереджають про змішання безпосередньо з шаром UIView contents("Якщо об'єкт шару прив'язаний до об'єкта перегляду, вам слід уникати прямого встановлення вмісту цього властивості. Взаємодія між видами та шарами зазвичай призводить до на думку, що замінює вміст цього ресурсу під час наступного оновлення. ") Я не особливо рекомендую цей підхід; передчасне малювання підриває продуктивність та якість малювання. Але якщо вам це потрібно з якихось причин, то contentsяк це отримати.
Роб Нап'єр

@RobNapier Point, отриманий при виклику drawRect:безпосередньо. Не потрібно було демонструвати цю техніку, і це було виправлено.
Сліпп Д. Томпсон

@RobNapier Щодо contentsтехніки, яку ви пропонуєте… звучить привабливо. Я спробував щось подібне спочатку, але не міг змусити його працювати, і виявив, що вищезгадане рішення є набагато меншим кодом, без причин не бути таким же виконавцем. Однак якщо у вас є робоче рішення для contentsпідходу, мені було б цікаво його прочитати (немає жодної причини, чому у вас не може бути двох відповідей на це питання, правда?)
Slipp D. Thompson,

@RobNapier Примітка. Це не має нічого спільного з CALayer display. Це просто власний публічний метод у підкласі UIView, як і те, що робиться в GLKView (інший підклас UIView, і єдиний, про який я знаю в Apple, що вимагає функціонування ЗАРАЗ! ).
Сліпп Д. Томпсон

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.

Сподіваюся, це допоможе тому, хто там загубився, як я.


0

Ну, я знаю, що це може бути великою зміною або навіть не підходить для вашого проекту, але ви вважали, що не будете виконувати поштовх, поки у вас вже немає даних ? Таким чином вам потрібно буде лише намалювати погляд лише один раз, і досвід користувача також буде кращим - поштовх буде рухатися вже завантаженим.

Те, як ви це робите, полягає в тому, що UITableView didSelectRowAtIndexPathви асинхронно запитуєте дані. Щойно ви отримаєте відповідь, ви вручну виконайте повідомлення про передачу даних та передайте дані вашому viewController в prepareForSegue. Тим часом ви можете показати якийсь показник активності, для простого перегляду індикатора завантаження https://github.com/jdg/MBProgressHUD

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.