drawRect 无效上下文

drawRect Invalid Context

尝试根据从服务器上下载的值在 UIView 中绘制图表。

我有一个块成功地拉动了 start/end 点(我确实必须添加延迟以确保数组在开始之前具有值。我已经尝试移动 CGContextRef调度内外,但我仍然得到 'Invalid Context'.

我试过在不同的地方添加 [self setNeedsDisplay];,但没有成功。

代码如下:

- (void)drawRect:(CGRect)rect {

    // Drawing code

    // Array - accepts values from method
    float *values;

    UIColor * greenColor = [UIColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:1.0];
    UIColor * redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];

    // Call to method to run server query, get data, parse (TBXML), assign values to array
    // this is working - NSLog output shows proper values are downloaded and parsed...
    values = [self downloadData];

    // Get context
    CGContextRef context = UIGraphicsGetCurrentContext();
    NSLog (@"Context: %@", context);

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"Waiting for array to populate from URL/Parsing....");


    NSLog(@"length 1: %f", values[0]);
    NSLog(@"length 2: %f", values[1]);



    float starty = 100.0;
    float startleft = 25.0;

    CGContextSetLineWidth (context, 24.0);

    CGContextSetStrokeColorWithColor (context, greenColor.CGColor);

    CGContextMoveToPoint(context, startleft, starty);

    CGContextAddLineToPoint(context, values[0], starty);

    NSLog(@"Start/Stop Win values: %f", values[0]);

    CGContextStrokePath (context);

    starty = starty + 24.0;


    CGContextSetLineWidth (context, 24.0);

    CGContextSetStrokeColorWithColor (context, redColor.CGColor);

    CGContextMoveToPoint(context, startleft, starty);

    CGContextAddLineToPoint(context, values[1], starty);

    NSLog(@"Start/Stop Loss values: %f",  values[1]);

    CGContextStrokePath (context);

     */

    });

}

一些观察:

  1. 此无效上下文是您启动异步进程的结果,因此在调用 dispatch_after 块时,提供给 drawRect 的上下文不再存在并且您的异步调用的块没有上下文来描写这些行。

    但是视图不应该发起这个网络请求和解析。通常视图控制器(或者更好,一些其他网络控制器等)应该发起网络请求和解析。

  2. drawRect 用于在给定时刻渲染视图。如果还没有什么要渲染的,它应该立即 return。当数据可用时,您向视图提供进行渲染所需的数据并启动 setNeedsDisplay.

  3. 所以,一个常见的模式是在你的视图子类中有一个 属性,并为那个 属性 调用 setter setNeedsDisplay 给你。

  4. 与其启动异步请求并尝试在两秒(或任意时间)内使用数据,不如给 downloadData 一个完成处理程序块参数,该参数下载完成后调用,下载解析完成后立即触发更新。这避免了不必要的延迟(例如,如果您等待两秒钟,但在 0.5 秒内获得数据,为什么要等待比必要的时间更长;如果您想要两秒钟,但在 2.1 秒内获得数据,则可能没有任何数据可显示)。在下载和解析完成时启动视图的更新。

  5. 这个 float * 引用是一个局部变量,永远不会被填充。您的 downloadData 可能应该 return 上述完成处理程序中的必要数据。坦率地说,这种指向 C 数组的指针的概念无论如何都不是您应该在 Objective-C 中使用的模式。如果您的网络响应确实 return 只是两个浮点数,那么您应该将其传递给此视图,而不是 float *

  6. 注意,我已经用 UIKit 绘图替换了 CoreGraphics 代码。就个人而言,我倾向于更进一步,转向 CAShapeLayer 而根本没有 drawRect。但我不想对你说太多。但一般的想法是尽可能使用最高级别的抽象,没有必要为了像这样简单的事情而深入 CoreGraphics 的杂草。


这不太正确,因为我不太了解您的模型数据是什么,但让我们暂时假设它只是 return 一系列浮点值。所以你可能有这样的东西:

//  BarView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface BarView : UIView
@property (nonatomic, copy, nullable) NSArray <NSNumber *> *values;
@end

NS_ASSUME_NONNULL_END

//  BarView.m

#import "BarView.h"

@implementation BarView
    
- (void)drawRect:(CGRect)rect {
    if (!self.values) { return; }

    NSArray *colors = @[UIColor.greenColor, UIColor.redColor]; // we’re just alternating between red and green, but do whatever you want

    float y = 100.0;
    float x = 25.0;

    for (NSInteger i = 0; i < self.values.count; i++) {
        float value = [self.values[i] floatValue];
        UIBezierPath *path = [UIBezierPath bezierPath];
        path.lineWidth = 24;
        [colors[i % colors.count] setStroke];
        [path moveToPoint:CGPointMake(x, y)];
        [path addLineToPoint:CGPointMake(x + value, y)];
        [path stroke];

        y += 24;
    }
}

- (void)setValues:(NSArray<NSNumber *> *)values {
    _values = [values copy];
    [self setNeedsDisplay];
}
@end

请注意,这不会执行任何网络请求。它只是渲染提供给它的任何值。 values 的 setter 将为我们触发 setNeedsDisplay

然后

//  ViewController.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface ViewController : UIViewController

- (void)download:(void (^)(NSArray <NSNumber *> * _Nullable, NSError * _Nullable))completion;

@end

NS_ASSUME_NONNULL_END

//  ViewController.m

#import "ViewController.h"
#import "BarView.h"

@interface ViewController ()
@property (nonatomic, weak) IBOutlet BarView *barView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self download:^(NSArray <NSNumber *> *values, NSError *error) {
        if (error) {
            NSLog(@"%@", error);
            return;
        }

        self.barView.values = values;
    }];
}

- (void)download:(void (^)(NSArray <NSNumber *> *, NSError *))completion {
    NSURL *url = [NSURL URLWithString:@"..."];
    [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // parse the data here

        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
            return;
        }

        NSArray *values = ...

        // when done, call the completion handler
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(values, nil);
        });
    }] resume];
}

@end

现在,我将由您来构建 NSArray of NSNumber 值,因为这是一个完全独立的问题。虽然将此 network/parsing 代码移出视图并移入视图控制器会更好一些,但它可能甚至不属于那里。您可能有另一个对象专用于执行网络请求 and/or 解析结果。但是,同样,这可能超出了这个问题的范围。

但希望这能说明这个想法:从执行网络请求或解析数据的业务中获取视图。让它只渲染提供的任何数据。

产生: