强制UIView重绘的最可靠方法是什么?


117

我有一个带有项目列表的UITableView。选择一个项目会推送一个viewController,然后继续执行以下操作。从方法viewDidLoad中,我触发了URL请求,以获取子视图中的on所需的数据-覆盖了drawRect的UIView子类。当数据从云到达时,我开始构建视图层次结构。有问题的子类将传递数据,并且它的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];

干杯,道格

Answers:


193

强制UIView重新渲染的有保证的坚决方式是[myView setNeedsDisplay]。如果您遇到问题,则可能会遇到以下问题之一:

  • 您是在实际拥有数据之前调用它,或者您-drawRect:正在过度缓存某些内容。

  • 您期望在调用此方法时可以绘制视图。故意没有办法使用Cocoa绘图系统要求“现在第二秒就绘图”。这将破坏整个视图合成系统,破坏性能,并可能创建各种伪像。只有办法说“需要在下一个绘制周期中绘制”。

如果您需要的是“一些逻辑,绘图,更多逻辑”,则需要将“更多逻辑”放在单独的方法中,并使用-performSelector:withObject:afterDelay:0延迟进行调用。这将在“逻辑”之后加上“更多逻辑”。下一个绘制周期。有关此类代码的示例以及可能需要使用此代码的情况,请参见此问题(尽管通常最好是寻找其他解决方案,因为这样会使代码复杂化)。

如果您认为事情没有进展-drawRect:,请在其中插入一个断点,看看何时被调用。如果您正在调用-setNeedsDisplay,但是-drawRect:在下一个事件循环中没有被调用,请深入查看视图层次结构,并确保不试图超越智能。在我的经验中,过分聪明是导致画图不好的主要原因之一。当您认为自己最了解如何诱使系统执行所需的操作时,通常可以使它完全执行所需的操作。


罗布,这是我的清单。1)我有数据吗?是。我在一个方法中创建视图-hideSpinner-从connectionDidFinishLoading在主线程上调用:因此:[self performSelectorOnMainThread:@selector(hideSpinner)withObject:nil waitUntilDone:NO]; 2)我不需要立即绘图。我只需要它。今天。目前,它是完全随机的,并且不受我的控制。[myView setNeedsDisplay]完全不可靠。我什至可以在viewDidAppear:中调用[myView setNeedsDisplay]。Nuthin'。可可根本不理我。疯了!
dugla

1
我发现使用这种方法时,通常会在setNeedsDisplay和的实际调用之间存在延迟drawRect:。尽管调用可靠性就在这里,但我不会将其称为“最可靠”的解决方案-从理论上讲,可靠的绘图解决方案应在返回要求绘图的客户端代码之前立即进行所有绘图。
斯利普·汤普森

1
我在下面对您的答案进行了评论,提供了更多详细信息。试图通过调用文档明确声明不调用的方法来迫使绘图系统运行不正常是不可靠的。充其量是未定义的行为。最有可能降低性能和图纸质量。在最坏的情况下,它可能会死锁或崩溃。
罗布·纳皮尔

1
如果需要在返回之前进行绘制,请绘制图像上下文(其工作原理与许多人期望的画布一样)。但是不要试图欺骗合成系统。它旨在以最少的资源开销快速提供高质量的图形。其中一部分是在运行循环结束时合并绘图步骤。
罗布·纳皮尔

1
@RobNapier我不明白你为什么变得如此防御。请再检查一次; 没有API违例(尽管已经根据您的建议进行了清理,我认为调用自己的方法不是违规),也没有死锁或崩溃的机会。这也不是一个“把戏”。它以正常方式使用CALayer的setNeedsDisplay / displayIfNeeded。此外,我已经使用了很长一段时间了,它以与GLKView / OpenGL平行的方式来绘制Quartz。事实证明,它比您列出的解决方案安全,稳定且速度更快。如果您不相信我,请尝试一下。您在这里不会失去任何东西。
斯利普·汤普森

52

我在调用setNeedsDisplay和drawRect之间有很大的延迟问题:(5秒)。原来,我在与主线程不同的线程中调用了setNeedsDisplay。将此调用移至主线程后,延迟消失了。

希望这个对你有帮助。


这绝对是我的错误。我给我的印象是我在主线程上运行,但是直到我NSLogged NSThread.isMainThread我才意识到有一个极端的情况,我没有在主线程上进行层更改。感谢您救我免于拔头发!
T先生

14

保证视图同步 返回之前(返回调用代码之前)的退款保证,钢筋混凝土实体方法是配置CALayerUIView子类的交互。

在您的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()
}

目标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之所以有效,是因为每个都是一个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()
}

目标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)
}

目标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 具有更多的绘图功能-图层比视图更底层,并且是为在布局内进行高度可配置的绘图而明确设计的,并且(像Cocoa中的许多事物一样)被设计为灵活使用(作为父类,或作为委托人,或作为与其他绘图系统的桥梁,或仅靠它们自己)。CALayerDelegate协议的正确使用使所有这些成为可能。

有关CALayers可配置性的更多信息,请参见《核心动画编程指南》的“ 设置图层对象”部分


请注意,用于的文档drawRect:明确指出“您永远不要直接自己调用此方法”。另外,CALayer display明确表示“请勿直接调用此方法”。如果要contents直接在图层上同步绘制,则无需违反这些规则。您可以contents随时在图层上绘制(甚至在后台线程上)。只需在视图中添加一个子层即可。但这不同于将其显示在屏幕上,后者需要等待正确的合成时间。
罗布·纳皮尔

我说要添加一个子层,因为文档还警告不要直接与UIView的层搞混contents(“如果该层对象绑定到视图对象,则应避免直接设置此属性的内容。通常会导致视图和层之间的相互作用在视图中,在后续更新期间替换此属性的内容。”)我不特别推荐这种方法;过早的绘图会损害性能和绘图质量。但是,如果由于某种原因需要它,那么contents如何获得它。
罗布·纳皮尔

@RobNapier点drawRect:直接调用。无需演示此技术,并且已修复。
斯利普D.汤普森

@RobNapier至于contents您所建议的技术……听起来很诱人。我最初尝试过类似的方法,但是无法使其正常工作,并且发现上述解决方案的代码少得多,没有理由不表现得那么出色。但是,如果您对此方法有一个可行的解决方案,那么contents我会对阅读它感兴趣(没有理由您不能对此问题有两个答案,对吗?)
Slipp D. Thompson

@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

我希望它能帮助像我一样迷路的人。


0

好吧,我知道这可能是一个很大的变化,甚至不适合您的项目,但是您是否考虑过在没有数据之前不执行推送?这样,您只需要绘制一次视图,用户体验也将更好-推送将移入已加载的视图。

执行此操作的方法是UITableView didSelectRowAtIndexPath异步请求数据。收到响应后,您可以手动执行segue并将数据传递到中的viewController prepareForSegue。同时,您可能想要显示一些活动指标,对于简单的加载指标,请检查https://github.com/jdg/MBProgressHUD

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.