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);
*/
});
}
一些观察:
此无效上下文是您启动异步进程的结果,因此在调用 dispatch_after
块时,提供给 drawRect
的上下文不再存在并且您的异步调用的块没有上下文来描写这些行。
但是视图不应该发起这个网络请求和解析。通常视图控制器(或者更好,一些其他网络控制器等)应该发起网络请求和解析。
drawRect
用于在给定时刻渲染视图。如果还没有什么要渲染的,它应该立即 return。当数据可用时,您向视图提供进行渲染所需的数据并启动 setNeedsDisplay
.
所以,一个常见的模式是在你的视图子类中有一个 属性,并为那个 属性 调用 setter setNeedsDisplay
给你。
与其启动异步请求并尝试在两秒(或任意时间)内使用数据,不如给 downloadData
一个完成处理程序块参数,该参数下载完成后调用,下载解析完成后立即触发更新。这避免了不必要的延迟(例如,如果您等待两秒钟,但在 0.5 秒内获得数据,为什么要等待比必要的时间更长;如果您想要两秒钟,但在 2.1 秒内获得数据,则可能没有任何数据可显示)。在下载和解析完成时启动视图的更新。
这个 float *
引用是一个局部变量,永远不会被填充。您的 downloadData
可能应该 return 上述完成处理程序中的必要数据。坦率地说,这种指向 C 数组的指针的概念无论如何都不是您应该在 Objective-C 中使用的模式。如果您的网络响应确实 return 只是两个浮点数,那么您应该将其传递给此视图,而不是 float *
。
注意,我已经用 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 解析结果。但是,同样,这可能超出了这个问题的范围。
但希望这能说明这个想法:从执行网络请求或解析数据的业务中获取视图。让它只渲染提供的任何数据。
产生:
尝试根据从服务器上下载的值在 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);
*/
});
}
一些观察:
此无效上下文是您启动异步进程的结果,因此在调用
dispatch_after
块时,提供给drawRect
的上下文不再存在并且您的异步调用的块没有上下文来描写这些行。但是视图不应该发起这个网络请求和解析。通常视图控制器(或者更好,一些其他网络控制器等)应该发起网络请求和解析。
drawRect
用于在给定时刻渲染视图。如果还没有什么要渲染的,它应该立即 return。当数据可用时,您向视图提供进行渲染所需的数据并启动setNeedsDisplay
.所以,一个常见的模式是在你的视图子类中有一个 属性,并为那个 属性 调用 setter
setNeedsDisplay
给你。与其启动异步请求并尝试在两秒(或任意时间)内使用数据,不如给
downloadData
一个完成处理程序块参数,该参数下载完成后调用,下载解析完成后立即触发更新。这避免了不必要的延迟(例如,如果您等待两秒钟,但在 0.5 秒内获得数据,为什么要等待比必要的时间更长;如果您想要两秒钟,但在 2.1 秒内获得数据,则可能没有任何数据可显示)。在下载和解析完成时启动视图的更新。这个
float *
引用是一个局部变量,永远不会被填充。您的downloadData
可能应该 return 上述完成处理程序中的必要数据。坦率地说,这种指向 C 数组的指针的概念无论如何都不是您应该在 Objective-C 中使用的模式。如果您的网络响应确实 return 只是两个浮点数,那么您应该将其传递给此视图,而不是float *
。注意,我已经用 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 解析结果。但是,同样,这可能超出了这个问题的范围。
但希望这能说明这个想法:从执行网络请求或解析数据的业务中获取视图。让它只渲染提供的任何数据。
产生: