drawRect Invalid Context

扶醉桌前 提交于 2020-03-23 08:03:40

问题


Trying to draw a graph in UIView from values pulled down from a server.

I have a block that is successfully pulling the start/end points (I did have to add the delay to make sure the array had the values before commencing. I've tried moving the CGContextRef both inside and outside the dispatch but I still get 'Invalid Context'.

I have tried adding [self setNeedsDisplay]; at various places without luck.

Here's the code:

- (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:


A couple of observations:

  1. This invalid context is a result that you’re initiating an asynchronous process, so by the time the dispatch_after block is called, the context supplied to drawRect no longer exists and your asynchronously called block has no context in which to stroke these lines.

    But the view shouldn’t be initiating this network request and parsing. Usually the view controller (or better, some other network controller or the like) should be initiating the network request and the parsing.

  2. The drawRect is for rendering the view at a given moment in time. If there’s nothing to render yet, it should just return immediately. When the data is available, you supply the view the data necessary to do the rendering and initiate the setNeedsDisplay.

  3. So, a common pattern would be to have a property in your view subclass, and have the setter for that property call setNeedsDisplay for you.

  4. Rather than initiating the asynchronous request and trying to use the data in two seconds (or any arbitrary amount of time), you instead give your downloadData a completion handler block parameter, which it calls when the download is done, and trigger the updating immediately as soon as the download and parse is done. This avoids unnecessary delays (e.g. if you wait two seconds, but get data in 0.5 seconds, why wait longer than necessary; if you want two seconds, but get data in 2.1 seconds, you risk not having any data to show). Initiate the update of the view exactly when the download and parse is done.

  5. This float * reference is a local variable and will never get populated. Your downloadData probably should return the necessary data in the aforementioned completion handler. Frankly, this notion of a pointer to a C array is not a pattern you should be using in Objective-C, anyway. If your network response really returns just two floats, that’s what you should be passing to this view, not a float *.

  6. Note, I’ve replaced the CoreGraphics code with UIKit drawing. Personally, I’d be inclined to go further and move to CAShapeLayer and not have a drawRect at all. But I didn’t want to throw too much at you. But the general idea is to use the highest level of abstraction as you can and there’s no need to get into the weeds of CoreGraphics for something as simple as this.


This isn’t going to be quite right as I don’t really understand what your model data is, but let’s assume for a second it’s just returning a series of float values. So you might have something like:

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

And

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

Note, this isn’t doing any network requests. It’s just rendering whatever values have been supplied to it. And the setter for values will trigger setNeedsDisplay for us.

Then

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

And

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

Now, I’ll leave it up to you to build the NSArray of NSNumber values, as that’s a completely separate question. And while moving this network/parsing code out of the view and into the view controller is a little better, it probably doesn’t even belong there. You might have another object dedicated to performing network requests and/or parsing results. But, again, that’s probably beyond the scope of this question.

But hopefully this illustrates the idea: Get the view out of the business of performing network requests or parsing data. Have it just render whatever data was supplied.

That yields:



来源:https://stackoverflow.com/questions/60555159/drawrect-invalid-context

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!