有没有办法让 drawRect 现在工作?

Posted

技术标签:

【中文标题】有没有办法让 drawRect 现在工作?【英文标题】:Is there a way to make drawRect work right NOW? 【发布时间】:2011-01-19 19:32:47 【问题描述】:

如果你是drawRect的高级用户,你会知道drawRect当然不会真正运行,直到“所有处理完成”。

setNeedsDisplay 将视图标记为无效和操作系统,并且基本上等到所有处理完成。在您想要的常见情况下,这可能会令人愤怒:

一个视图控制器 1 启动某些功能 2 增量 3 创作出越来越复杂的艺术品和 4 在每个步骤中,您都设置了NeedsDisplay(错误!)5 直到所有工作完成 6

当然,当您执行上述 1-6 时,所发生的只是 drawRect 在第 6 步之后运行一次

您的目标是在第 5 点刷新视图。该怎么做?

【问题讨论】:

他不是在强制 UIView 重绘——他是在强制运行循环循环。游戏的不同阶段,而不是你通常应该在生产代码中做的事情(因为你会抢占你的代码来运行循环中的所有其他内容,包括计时器、套接字等。) 重新减速:如果您嵌套 UI 运行循环或 drawRects 而没有针对竞争条件进行特定处理,您最终可能会多次重绘相同的像素,甚至可能让旧的 UI 事件或像素更新覆盖更新的东西。 对不起,整个强制更新设计坏了。只需在后台处理数据并更新 (setNeedsDisplayInRect:) 屏幕的所需部分sometime - 即每 1/10 秒或当处理取得一些实际进展时。不要与框架抗争。 不要将解决方案作为问题的一部分发布; [改为发布答案](***.com/help/self-answer)。 【参考方案1】:

如果我正确理解您的问题,有一个简单的解决方案。在您长时间运行的例程中,您需要告诉当前运行循环在您自己处理的某些点处处理单个迭代(或更多运行循环)。例如,当您想要更新显示时。任何具有脏更新区域的视图都会在运行 runloop 时调用其 drawRect: 方法。

告诉当前的runloop处理一次迭代(然后返回给你......):

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

这是一个(低效)长时间运行例程的示例,带有相应的 drawRect - 每个都在自定义 UIView 的上下文中:

- (void) longRunningRoutine:(id)sender

    srand( time( NULL ) );

    CGFloat x = 0;
    CGFloat y = 0;

    [_path moveToPoint: CGPointMake(0, 0)];

    for ( int j = 0 ; j < 1000 ; j++ )
    
        x = 0;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = 0;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        x = self.bounds.size.width;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = self.bounds.size.height;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        [self setNeedsDisplay];
        [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
    

    [_path removeAllPoints];


- (void) drawRect:(CGRect)rect

    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor( ctx, [UIColor blueColor].CGColor );

    CGContextFillRect( ctx,  rect);

    CGContextSetStrokeColorWithColor( ctx, [UIColor whiteColor].CGColor );

    [_path stroke];

And here is a fully working sample demonstrating this technique.

通过一些调整,您可能可以调整它以使 UI 的其余部分(即用户输入)也响应。

更新(使用此技术的警告)

我只想说我同意这里其他人的大部分反馈,他们说这个解决方案(调用 runMode: 来强制调用 drawRect:) 不一定是个好主意。我已经回答了这个问题,我认为这是对所述问题的事实“这是如何”的答案,我不打算将其宣传为“正确的”架构。另外,我并不是说可能没有其他(更好的?)方法可以达到相同的效果——当然可能还有其他我不知道的方法。

更新(回答 Joe 的示例代码和性能问题)

您看到的性能下降是在绘图代码的每次迭代中运行 runloop 的开销,其中包括将图层渲染到屏幕以及 runloop 执行的所有其他处理,例如输入收集和处理.

一种选择可能是减少调用 runloop 的频率。

另一个选项可能是优化您的绘图代码。就目前而言(我不知道这是您的实际应用程序,还是只是您的示例......)您可以做一些事情来让它更快。我要做的第一件事是将所有 UIGraphicsGet/Save/Restore 代码移到循环之外。

但是,从架构的角度来看,我强烈建议您考虑此处提到的其他一些方法。我看不出为什么你不能在后台线程上构造你的绘图(算法不变),并使用计时器或其他机制来通知主线程以某种频率更新它的 UI,直到绘图完成。我认为参与讨论的大多数人都会同意这是“正确”的方法。

【讨论】:

嗨,汤姆——这太棒了。没有其他人提到过这个技巧——哇??!你是怎么知道的。我要彻底调查。真的,这正是在某些情况下需要做的......强制drawRect“立即”工作,即当你陷入运行循环时返回运行循环。 [NSDate date],太棒了...我必须测试一下。 @Joe - Tom 说得对,这不是技巧,而是对运行循环的简单操作。考虑到 NSRunLoop 的基本功能,它应该适用于 Mac 和 ios 上的所有操作系统版本。我和我认识的许多其他人都没有花太多时间在 NSRunLoop 上,所以它的某些元素我不知道也就不足为奇了。 Apple 在 WWDC 2010 Session 208 - Network Apps for iPhone OS, Part 2 中讨论了一些关于运行循环的内容,如果您对它们的更多使用方式感兴趣的话。 -1 :我讨厌投反对票,因为我实际上曾经尝试这样做。但我只是仔细检查以确保,Apple 的 DTS 不建议在 iOS 中嵌套 UI 运行循环,尤其是在 NSDefaultRunLoopMode 中。说可能会出现“古怪的问题”。 @hotpaw2 我不推荐使用这个。我正在回答这个问题。我认为这不值得一票否决。同意,有更好的方法来设计应用程序,放下手。问题是关于“是否可能”,是的,确实如此。 运行一次运行循环只提交当前通过更改视图和底层启动的隐式 CATransaction,您可以直接使用显式 CATransaction 来完成相同的事情,而不涉及运行循环。【参考方案2】:

用户界面的更新发生在当前通过运行循环的末尾。这些更新是在主线程上执行的,所以任何在主线程中长时间运行的事情(冗长的计算等)都会导致界面更新无法启动。此外,在主线程上运行一段时间的任何操作也会导致您的触摸处理无响应。

这意味着没有办法“强制”在主线程上运行的进程中的某个其他点进行 UI 刷新。 前面的陈述并不完全正确,正如 Tom 的回答显示。您可以允许运行循环在主线程上执行的操作中间完成。但是,这仍然可能会降低您的应用程序的响应能力。

一般来说,建议您将需要一段时间才能执行的任何操作移至后台线程,以便用户界面可以保持响应。但是,您希望对 UI 执行的任何更新都需要在主线程上完成。

也许在 Snow Leopard 和 iOS 4.0+ 下执行此操作的最简单方法是使用块,例如以下基本示例:

dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(queue, ^
    // Do some work
    dispatch_async(main_queue, ^
        // Update the UI
    );
);

上面的Do some work 部分可能是一个冗长的计算,或者是一个循环多个值的操作。在此示例中,UI 仅在操作结束时更新,但如果您希望在 UI 中持续跟踪进度,您可以将调度放置到主队列中您需要执行 UI 更新的地方。

对于较旧的操作系统版本,您可以手动或通过 NSOperation 中断后台线程。对于手动后台线程,您可以使用

[NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];

[self performSelectorInBackground:@selector(doWork) withObject:nil];

然后更新你可以使用的 UI

[self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];

请注意,我发现在处理连续进度条时,需要使用上一个方法中的 NO 参数来获得持续的 UI 更新。

This sample application 我为我的班级创建的说明了如何使用 NSOperations 和队列来执行后台工作,然后在完成后更新 UI。此外,我的Molecules 应用程序使用后台线程来处理新结构,并在此过程中更新状态栏。你可以下载源代码看看我是怎么做到的。

【讨论】:

@Joe - 我还记得这被记录在某处,但我无法在标准位置找到它。也许它在一次 WWDC 会谈中被提及。 UIKit 和 AppKit 肯定有不同的处理方式,因为 UIKit 提供了全新的开始,而且它在很大程度上基于 Core Animation。 @Joe - 正如我在 hotpaw2 的回答中评论的那样,我只能通过在 Core Animation 中使用刷新操作来快速更新 CALayers,但这会在其他地方导致不良影响。 @Brad Larson:你看到什么样的 CA 刷新工件? @Joe - 我相信将waitUntilDone: 设置为YES 的问题是,您的后台线程将在其执行中阻塞,直到主线程有机会让运行循环完成。这可能会导致您的处理线程停止,并且至少在我的一种情况下阻止了定期 UI 更新。您的处理线程不应该真正关心 UI 是否已完成更新,因此让它触发选择器并继续执行它正在做的事情是有意义的。【参考方案3】:

您可以在循环中重复执行此操作,它会正常工作,没有线程,不会弄乱运行循环等。

[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];

如果在循环之前已经存在隐式事务,您需要使用 [CATransaction commit] 提交它,然后才能工作。

【讨论】:

如果我在 setNeedsDisplay 之后执行 CATransaction 刷新,而不是 begin+commit,它适用于我(调用视图的 drawRect)。 请注意,我的 [ CATransaction flush ] 测试不是尝试重新输入相同的 drawRect 或 UI 处理程序,而是刷新单独的进度视图的 drawRect 并因此为其设置动画。这对我有用,即使在 UI 线程阻塞长计算内部也是如此。 @Joe Blow :您是否尝试以高于 drawRect 执行时间允许的帧速率进行更新?这可能是放缓的一个原因。尝试较低的 fps,更好地匹配 drawRect 完成所需的时间。例如除非自上次 drawRect 以来已经过去了这么多毫秒(16.7 的倍数),否则不要刷新。 @Joe Blow :还要注意(当前!)iPhone 和 iPad 有一个应用程序 CPU。因此,任何 Core Graphics 绘图、视图更新或 UI 运行循环处理所花费的时间都会占用时间,从而减慢所有其他处理。 如果您在 SimpleDrawingView 的第 17 行放置 [CATransaction commit] 它将关闭当前的隐式事务,那么我的答案将起作用。【参考方案4】:

为了尽快调用 drawRect(不一定是立即调用,因为操作系统可能仍会等到下一次硬件显示刷新等),应用程序应尽快使其 UI 运行循环空闲可能,通过退出 UI 线程中的任何和所有方法,并且在非零的时间内。

您可以在主线程中执行此操作,方法是将任何花费超过动画帧时间的处理切分成更短的块,并仅在短暂延迟后安排继续工作(因此 drawRect 可能会在间隙中运行),或者通过执行在后台线程中处理,定期调用 performSelectorOnMainThread 以某种合理的动画帧速率执行 setNeedsDisplay。

一种非 OpenGL 方法来立即更新显示(这意味着在下一次或三个硬件显示刷新时)是通过将可见的 CALayer 内容与您绘制的图像或 CGBitmap 交换。应用程序几乎可以随时将 Quartz 绘制到 Core Graphics 位图中。

新添加的答案:

请参阅下面 Brad Larson 的 cmets 和 Christopher Lloyd 对此处另一个答案的评论,作为通向此解决方案的提示。

[ CATransaction flush ];

将导致在已完成 setNeedsDisplay 请求的视图上调用 drawRect,即使刷新是从阻塞 UI 运行循环的方法内部完成的。

请注意,当阻塞 UI 线程时,需要刷新核心动画来更新不断变化的 CALayer 内容。因此,对于以动画形式显示进度的图形内容,它们最终可能是同一事物的形式。

上面新添加的答案的新添加注释:

刷新速度不要超过 drawRect 或动画绘制完成的速度,因为这可能会使刷新排队,导致奇怪的动画效果。

【讨论】:

@Joe - 现在退出处理程序,在它完成之前。我使用的方法是将我的长处理循环分解为状态机状态(来自算法 101),然后尽快退出例程,如果不是循环结束,则由具有一帧延迟的计时器调用后续状态.如果由于大量嵌套或递归而无法执行此操作,最好的办法是考虑在后台线程中进行所有处理。 您应该能够定期运行运行循环以更新 UI。这样做不会接受用户输入事件(根据我的经验),因此最好尽快将运行循环的控制权返回给系统。 根据我的经验,只有在更改内容后执行[CATransaction flush] 时,换出 CALayer 中的内容才会立即更新。但是,这可能会导致界面其他地方出现伪影。 @Joe Blow:是的。我有一个调用 setNeedsDisplay 的应用程序,并且仅在视图初始化时执行一个 drawRect。从那时起,所有动画都是通过移动 UIViews 并用 CGImageRefs 交换 CALayer 内容来完成的。没有drawRects。似乎为进度条等提供了相当平滑的帧速率,即使没有任何 CATransaction 刷新(但是应用程序确实很快返回到 UI 运行循环)。 @Brad Larson :我在我的 iPhone 上编写并运行了一个测试应用程序,该应用程序定期更新 CALayer 内容,该方法会阻塞 UI 线程数秒。是的,需要 CATransaction 刷新才能获得看似立即的更新(我使用 20 fps)。但是,也许是因为我所有的 CALayers 和子视图都不重叠,并且没有安排其他动画发生,我看到零伪影。 YMMV。【参考方案5】:

不怀疑这样做的智慧(你应该这样做),你可以这样做:

[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];

-setNeedsDisplay 会将视图标记为需要重绘。 -displayIfNeeded 将强制视图的支持层重绘,但前提是它已被标记为需要显示。

不过,我要强调的是,您的问题表明了一种可能需要重新设计的架构。在几乎极少数情况下,您应该永远不需要或不想强制视图立即重绘。 UIKit 并没有考虑到该用例,如果它有效,请认为自己很幸运。

【讨论】:

Delong :我在一个长时间运行的计算例程中尝试了这个,它阻塞了主 UI 线程,直到长时间计算完成后才导致调用 drawRect。 @Joe 当然布拉德的方法更有意义。我的帖子是严格回答“如何立即绘制视图”。 @Joe Blow :DDL 可能会说,由于 UIKit 在设计时并未考虑到这一点,因此该技术可能会在未来的某些与当前 API 文档完全一致的操作系统更新中严重失效。在您当前的测试设备上没有崩溃的应用是不够的。 @hotpaw2 正确。 UIKit 设计为在每次运行循环旋转时绘制一次视图,而不会下降到子旋转(自己旋转)。然而,这可能会在未来由 Apple 自行决定改变,因此编写一个依赖于实现细节的应用程序绝不是一个好主意。正确地做事是一个更好的主意。昂贵的东西在后台线程上进行。 UI 进入主线程。【参考方案6】:

您是否尝试过在辅助线程上进行繁重的处理并回调主线程来安排视图更新? NSOperationQueue 让这种事情变得非常容易。


将一组 NSURL 作为输入并异步下载它们的示例代码,并在每个完成并保存时通知主线程。

- (void)fetchImageWithURLs:(NSArray *)urlArray 
    [self.retriveAvatarQueue cancelAllOperations];
    self.retriveAvatarQueue = nil;

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

    for (NSUInteger i=0; i<[urlArray count]; i++) 
        NSURL *url = [urlArray objectAtIndex:i];

        NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(cacheImageWithIndex:andURL:)]];
        [inv setTarget:self];
        [inv setSelector:@selector(cacheImageWithIndex:andURL:)];
        [inv setArgument:&i atIndex:2];
        [inv setArgument:&url atIndex:3];

        NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
        [opQueue addOperation:invOp];
        [invOp release];
    

    self.retriveAvatarQueue = opQueue;
    [opQueue release];


- (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url 
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
    NSError *error = nil;

    // Save the file      
    if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) 
        DLog(@"Error saving file at %@", filePath);
    

    // Notifiy the main thread that our file is saved.
    [self performSelectorOnMainThread:@selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];


【讨论】:

是的,您发现无法以 500fps 刷新屏幕。 嗨 Jonathon - 不,这与我所说的完全无关。只需将 NSLogs 放入 drawRect 中并进行实验,以发现当前例程之后的“间隙”,操作系统将借此机会尝试绘制。【参考方案7】:

我意识到这是一个旧线程,但我想为给定问题提供一个干净的解决方案。

我同意其他发帖者的观点,即在理想情况下,所有繁重的工作都应该在后台线程中完成,但是有时这根本不可能,因为耗时的部分需要大量访问非线程安全的UIKit 提供的方法。就我而言,初始化我的 UI 非常耗时,而且我无法在后台运行任何东西,所以我最好的选择是在初始化期间更新进度条。

但是,一旦我们考虑到理想的 GCD 方法,解决方案实际上很简单。我们在后台线程中完成所有工作,将其划分为在主线程上同步调用的卡盘。将为每个卡盘运行运行循环,更新 UI 和任何进度条等。

- (void)myInit

    // Start the work in a background thread.
    dispatch_async(dispatch_get_global_queue(0, 0), ^

        // Back to the main thread for a chunk of code
        dispatch_sync(dispatch_get_main_queue(), ^
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        );

        // Next chunk
        dispatch_sync(dispatch_get_main_queue(), ^
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        );

        ...
    );

当然,这与 Brad 的技术基本相同,但他的回答并没有完全解决手头的问题 - 在定期更新 UI 的同时运行大量非线程安全代码。

【讨论】:

根据我的需要尝试使用它:SESSION 读取 URL,然后解析,然后创建标签和线条(使用 CGRect)。将 URL 获取和解析放在您的“//Back to main for a chunk”区域中,并将标签创建和线条绘制放在“更新进度”区域中(因为 UI 必须在 main 中完成。创建标签;获取 CGContext:无效上下文并且没有线条。【参考方案8】:

我认为,最完整的答案来自 Jeffrey Sambell 的博文 'Asynchronous Operations in iOS with Grand Central Dispatch',它对我有用! 它与上面 Brad 提出的解决方案基本相同,但在 OSX/IOS 并发模型方面进行了充分解释。

dispatch_get_current_queue 函数将返回当前队列 从中分派块和dispatch_get_main_queue 函数将返回您的 UI 正在运行的主队列。

dispatch_get_main_queue 函数对于更新 iOS 应用程序的 UI 作为 UIKit 方法不是线程安全的(有一些 例外),因此您为更新 UI 元素所做的任何调用都必须始终是 从主队列完成。

典型的 GCD 调用如下所示:

// Doing something on the main thread
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^

// Perform long running process   
dispatch_async(dispatch_get_main_queue(), ^
    // Update the UI   
    ); 
); 

// Continue doing other stuff on the  
// main thread while process is running.

这是我的工作示例(iOS 6+)。它使用AVAssetReader 类显示存储视频的帧:

//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];

dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^
    CMSampleBufferRef buffer;
    while ( [asset_reader status]==AVAssetReaderStatusReading )
    
        buffer = [asset_reader_output copyNextSampleBuffer];
        if (buffer!=nil)
        
            //The point is here: to use the main queue for actual UI operations
            dispatch_async(dispatch_get_main_queue(), ^
                // Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
                [self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
                CFRelease (buffer);
            );
        
    
);

此示例的第一部分可以在 Damian 的回答中找到 here。

【讨论】:

我正在尝试将上述解决方案应用于以下方面: NSURLSESSION to get XML data from server;解析;根据下载的数据绘制线条并创建标签。我有创建线(尝试了 Core Graphics/CGRect 和 Bezier 路径)并在 'dispatch_async (dispatch_get_main_queue ()' 部分下的“更新 UI 区域”中创建标签 - 标签出现,线条没有。我相信绘图代码是准确的 - 取自使用已弃用 URL 调用的应用程序的早期版本。【参考方案9】:

Joe -- 如果您愿意设置它,以便您的冗长处理都发生在 drawRect 内部,您可以让它工作。我刚刚写了一个测试项目。有用。请参阅下面的代码。

LengthyComputationTestAppDelegate.h:

#import <UIKit/UIKit.h>
@interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> 
    UIWindow *window;


@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

LengthComputationTestAppDelegate.m:

#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"

@implementation LengthyComputationTestAppDelegate

@synthesize window;


#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions     

    // Override point for customization after application launch.
    IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
    [self.window addSubview:ipv];
    [ipv release];
    [self.window makeKeyAndVisible];
    return YES;

增量器.h:

#import <Foundation/Foundation.h>

//singleton object
@interface Incrementer : NSObject 
    NSUInteger theInteger_;


@property (nonatomic) NSUInteger theInteger;

+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;

incrementer.m:

#import "Incrementer.h"

@implementation Incrementer

@synthesize theInteger = theInteger_;

static Incrementer *inc = nil;

-(void) increment 
    theInteger_++;


-(BOOL) finishedIncrementing 
    return (theInteger_>=100000000);


-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval 
    NSTimeInterval negativeTimeInterval = -1*timeInterval;
    NSDate *startDate = [NSDate date];
    while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
        [self increment];
    return self.theInteger;


-(id) init 
    if (self = [super init]) 
        self.theInteger = 0;
    
    return self;


#pragma mark --
#pragma mark singleton object methods

+ (Incrementer *) sharedIncrementer  
    @synchronized(self) 
        if (inc == nil) 
            inc = [[Incrementer alloc]init];        
        
    
    return inc;


+ (id)allocWithZone:(NSZone *)zone 
    @synchronized(self) 
        if (inc == nil) 
            inc = [super allocWithZone:zone];
            return inc;  // assignment and return on first allocation
        
    
    return nil; // on subsequent allocation attempts return nil


- (id)copyWithZone:(NSZone *)zone

    return self;


- (id)retain 
    return self;


- (unsigned)retainCount 
    return UINT_MAX;  // denotes an object that cannot be released


- (void)release 
    //do nothing


- (id)autorelease 
    return self;


@end

IncrementerProgressView.m:

#import "IncrementerProgressView.h"


@implementation IncrementerProgressView
@synthesize progressLabel = progressLabel_;
@synthesize nextUpdateTimer = nextUpdateTimer_;

-(id) initWithFrame:(CGRect)frame 
    if (self = [super initWithFrame: frame]) 
        progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
        progressLabel_.font = [UIFont systemFontOfSize:26];
        progressLabel_.adjustsFontSizeToFitWidth = YES;
        progressLabel_.textColor = [UIColor blackColor];
        [self addSubview:progressLabel_];
    
    return self;


-(void) drawRect:(CGRect)rect 
    [self.nextUpdateTimer invalidate];
    Incrementer *shared = [Incrementer sharedIncrementer];
    NSUInteger progress = [shared incrementForTimeInterval: 0.1];
    self.progressLabel.text = [NSString stringWithFormat:@"Increments performed: %d", progress];
    if (![shared finishedIncrementing])
        self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(@selector(setNeedsDisplay)) userInfo:nil repeats:NO];


- (void)dealloc 
    [super dealloc];


@end

【讨论】:

请,不。 -drawRect: 仅用于绘图。不要尝试将其用于其他用途。【参考方案10】:

关于原问题:

总之,你可以(A)背景大画幅,调用前台进行UI更新或(B)可以说有争议建议有四种“立即”方法不使用后台进程。对于有效的结果,运行演示程序。它对所有五种方法都有#defines。


汤姆·斯威夫特交替

Tom Swift 解释了非常简单地操纵运行循环的惊人想法。以下是触发运行循环的方式:

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

这是一项真正令人惊叹的工程。当然,在操作运行循环时应​​该非常小心,正如许多人指出的那样,这种方法仅限于专家。


然而,奇怪的问题出现了......

尽管许多方法有效,但它们实际上并不“有效”,因为您会在演示中清楚地看到一个奇怪的渐进式减速伪影。

滚动到我在下面粘贴的“答案”,显示控制台输出 - 您可以看到它是如何逐渐变慢的。

这是新的 SO 问题:Mysterious "progressive slowing" problem in run loop / drawRect

这里是演示应用的 V2...http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

你会看到它测试了所有五种方法,

#ifdef TOMSWIFTMETHOD
 [self setNeedsDisplay];
 [[NSRunLoop currentRunLoop]
      runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
#endif
#ifdef HOTPAW
 [self setNeedsDisplay];
 [CATransaction flush];
#endif
#ifdef LLOYDMETHOD
 [CATransaction begin];
 [self setNeedsDisplay];
 [CATransaction commit];
#endif
#ifdef DDLONG
 [self setNeedsDisplay];
 [[self layer] displayIfNeeded];
#endif
#ifdef BACKGROUNDMETHOD
 // here, the painting is being done in the bg, we have been
 // called here in the foreground to inval
 [self setNeedsDisplay];
#endif

您可以自己查看哪些方法有效,哪些无效。

您可以看到奇怪的“渐进式减速”。为什么会这样?

你可以看到有争议的 TOMSWIFT 方法,实际上响应性完全没有问题。随时点击响应。 (但仍然是奇怪的“渐进式减速”问题)

所以压倒性的事情是这种奇怪的“渐进式减速”:在每次迭代中,由于未知原因,循环所花费的时间都会减少。请注意,这适用于“正确”(背景外观)或使用“立即”方法之一。


实用的解决方案?

对于将来阅读的任何人,如果您由于“神秘的渐进式减速”而实际上无法使其在生产代码中工作...... Felz 和 Void 在另一个具体问题中各自提出了令人震惊的解决方案,希望它有帮助。

【讨论】:

以上是关于有没有办法让 drawRect 现在工作?的主要内容,如果未能解决你的问题,请参考以下文章

我想动画,事实上淡化,“内”drawRect

没有子类化的重载方法(特别是drawRect:)

将 svg 从 Inkscape 导出到 Corel Draw

iOS draw出各种图形

使用drawRect延迟加载UIView:绘图

DrawRect 内存问题